Last active
June 21, 2022 12:56
-
-
Save BlockmanCodes/6449a85e1de59235a01523f81ef9ff51 to your computer and use it in GitHub Desktop.
Staking dapp
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
async function main() { | |
[signer1, signer2] = await ethers.getSigners(); | |
const Staking = await ethers.getContractFactory('Staking', signer1); | |
staking = await Staking.deploy({ | |
value: ethers.utils.parseEther('10') | |
}); | |
console.log("Staking contract deployed to:", staking.address, "by", signer1.address) | |
const provider = waffle.provider; | |
let data; | |
let transaction; | |
let receipt; | |
let block; | |
let newUnlockDate; | |
data = { value: ethers.utils.parseEther('0.5') } | |
transaction = await staking.connect(signer2).stakeEther(30, data) | |
data = { value: ethers.utils.parseEther('1') } | |
transaction = await staking.connect(signer2).stakeEther(180, data) | |
data = { value: ethers.utils.parseEther('1.75') } | |
transaction = await staking.connect(signer2).stakeEther(180, data) | |
data = { value: ethers.utils.parseEther('5') } | |
transaction = await staking.connect(signer2).stakeEther(90, data) | |
receipt = await transaction.wait() | |
block = await provider.getBlock(receipt.blockNumber) | |
newUnlockDate = block.timestamp - (60 * 60 * 24 * 100) | |
await staking.connect(signer1).changeUnlockDate(3, newUnlockDate) | |
data = { value: ethers.utils.parseEther('1.75') } | |
transaction = await staking.connect(signer2).stakeEther(180, data) | |
receipt = await transaction.wait() | |
block = await provider.getBlock(receipt.blockNumber) | |
newUnlockDate = block.timestamp - (60 * 60 * 24 * 100) | |
await staking.connect(signer1).changeUnlockDate(4, newUnlockDate) | |
} | |
// npx hardhat run --network localhost scripts/1_deploy.js | |
main() | |
.then(() => process.exit(0)) | |
.catch((error) => { | |
console.error(error); | |
process.exit(1); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"name": "client", | |
"version": "0.1.0", | |
"private": true, | |
"dependencies": { | |
"@testing-library/jest-dom": "^5.16.4", | |
"@testing-library/react": "^13.3.0", | |
"@testing-library/user-event": "^13.5.0", | |
"bootstrap": "^5.1.3", | |
"ethers": "^5.6.9", | |
"react": "^18.2.0", | |
"react-bootstrap-icons": "^1.8.4", | |
"react-dom": "^18.2.0", | |
"react-scripts": "5.0.1", | |
"web-vitals": "^2.1.4" | |
}, | |
"scripts": { | |
"start": "react-scripts start", | |
"build": "react-scripts build", | |
"test": "react-scripts test", | |
"eject": "react-scripts eject" | |
}, | |
"eslintConfig": { | |
"extends": [ | |
"react-app", | |
"react-app/jest" | |
] | |
}, | |
"browserslist": { | |
"production": [ | |
">0.2%", | |
"not dead", | |
"not op_mini all" | |
], | |
"development": [ | |
"last 1 chrome version", | |
"last 1 firefox version", | |
"last 1 safari version" | |
] | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useState } from 'react'; | |
const StakeModal = props => { | |
const { | |
onClose, | |
stakingLength, | |
stakingPercent, | |
setAmount, | |
stakeEther, | |
} = props | |
return ( | |
<> | |
<div className="modal-class" onClick={props.onClose}> | |
<div className="modal-content" onClick={e => e.stopPropagation()}> | |
<div className="modal-body"> | |
<h2 className="titleHeader">Stake Ether</h2> | |
<div className="row"> | |
<div className="col-md-9 fieldContainer"> | |
<input | |
className="inputField" | |
placeholder="0.0" | |
onChange={e => props.setAmount(e.target.value)} | |
/> | |
</div> | |
<div className="col-md-3 inputFieldUnitsContainer"> | |
<span>ETH</span> | |
</div> | |
</div> | |
<div className="row"> | |
<h6 className="titleHeader stakingTerms">{stakingLength} days @ {stakingPercent} APY</h6> | |
</div> | |
<div className="row"> | |
<div | |
onClick={() => stakeEther()} | |
className="orangeButton" | |
> | |
Stake | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</> | |
) | |
} | |
export default StakeModal |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require("@nomiclabs/hardhat-waffle"); | |
/** | |
* @type import('hardhat/config').HardhatUserConfig | |
*/ | |
module.exports = { | |
solidity: { | |
version: "0.8.0", | |
}, | |
paths: { | |
artifacts: "./client/src/artifacts", | |
} | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"name": "staking", | |
"version": "1.0.0", | |
"description": "", | |
"main": "index.js", | |
"scripts": { | |
"test": "echo \"Error: no test specified\" && exit 1" | |
}, | |
"keywords": [], | |
"author": "", | |
"license": "ISC", | |
"devDependencies": { | |
"hardhat": "^2.9.9" | |
}, | |
"dependencies": { | |
"@nomiclabs/hardhat-ethers": "^2.0.6", | |
"@nomiclabs/hardhat-waffle": "^2.0.3", | |
"chai": "^4.3.6", | |
"ethereum-waffle": "^3.4.4", | |
"ethers": "^5.6.9", | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.App { | |
background-color: #fff; | |
text-align: center; | |
min-height: 100vh; /* required for full page background */ | |
} | |
#root { | |
min-height: 100vh; /* required for full page background */ | |
} | |
.appBody { | |
height: 100%; | |
margin-top: 80px; | |
} | |
/*////////////////////////////////////////////////////////////////////////////*/ | |
/* Nav Bar */ | |
.navButton { | |
float: left; | |
display: inline-block; | |
margin-left: 24px; | |
padding: 5px; | |
color: #1168f1; | |
font-size: 24px; | |
} | |
.connectButton { | |
float: right; | |
margin-right: 24px; | |
background-color: #1168f1; | |
border-radius: 4px; | |
padding: 5px 15px; | |
color: #f1f1f3; | |
font-size: 24px; | |
cursor: pointer; | |
} | |
.connectButton:hover { | |
background-color: #f90; | |
} | |
.navBar { | |
padding-top: 18px; | |
overflow: hidden; | |
padding-bottom: 10px; | |
border-bottom: 1px solid rgba(17, 104, 241, 0.08); | |
} | |
/*////////////////////////////////////////////////////////////////////////////*/ | |
/* Market */ | |
.marketContainer { | |
background-color: #1168f1; | |
width: 620px; | |
height: 170px; | |
margin: 0 auto; | |
top: 50%; | |
border-radius: 24px; | |
padding: 8px; | |
} | |
.subContainer { | |
margin-bottom: 20px; | |
} | |
.marketOption { | |
display: flex; | |
margin-left: 22px; | |
} | |
.optionData { | |
margin-left: 10px; | |
color: #fff; | |
font-weight: 700; | |
} | |
.optionPercent { | |
display: block; | |
font-size: 24px; | |
font-weight: 1000; | |
color: #f90; | |
} | |
.logoImg { | |
background-color: #fff; | |
border-radius: 100px; | |
margin-bottom: 14px; | |
margin-right: 14px; | |
width: 28px; | |
height: 28px; | |
} | |
.marketHeader { | |
color: #fff; | |
font-size: 32px; | |
font-weight: 800; | |
} | |
.hoverButton:hover { | |
background: #f90; | |
cursor: pointer; | |
} | |
.hoverButton:active { | |
/* box-shadow: 0 0 0 white; */ | |
box-shadow: inset; | |
} | |
/*////////////////////////////////////////////////////////////////////////////*/ | |
/* Assets */ | |
.assetContainer { | |
background-color: #1168f1; | |
width: 620px; | |
min-height: 225px; | |
margin: 0 auto; | |
top: 50%; | |
border-radius: 24px; | |
padding: 6px; | |
padding: 8px; | |
margin-top: 50px; /* space between sections */ | |
color: #fff; | |
font-weight: 500; | |
} | |
.columnHeaders { | |
font-weight: 800; | |
color: #f90; | |
} | |
.stakedLogoImg { | |
background-color: #fff; | |
border-radius: 100px; | |
margin-bottom: 14px; | |
margin-right: 14px; | |
width: 18px; | |
height: 18px; | |
} | |
/*////////////////////////////////////////////////////////////////////////////*/ | |
/* Glyphs */ | |
.glyphContainer { | |
background-color: #fff; | |
display: inline-block; | |
width: 60px; | |
height: 60px; | |
border-radius: 12px; | |
} | |
.glyph { | |
font-size: 34px; | |
} | |
/******************************************************************************/ | |
/* Stake Modal */ | |
.modal-class { | |
background-color: rgba(0,0,0,0.5); | |
position: fixed; | |
left: 0; | |
top: -400px; | |
right: 0px; | |
bottom: 0; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
color: rgb(86, 90, 105); | |
} | |
.modal-content { | |
background-color: #fff; | |
width: 300px !important; | |
padding: 0px 15px; | |
background-color: rgb(237, 238, 242) !important; | |
border: 1px solid rgb(206, 208, 217) !important; | |
border-radius: 12px !important; | |
} | |
.fieldContainer { | |
padding-left: 0px !important; | |
} | |
.inputField { | |
padding-left: 10px; | |
border-radius: 36px; | |
border: 1px solid #65cdee; | |
height: 2rem; | |
margin: 10px 0; | |
width: 100%; | |
} | |
.inputFieldUnitsContainer { | |
padding-left: 0px !important; | |
padding-top: 14px; | |
} | |
.orangeButton { | |
width: 100%; | |
height: 45px; | |
line-height: 45px; | |
font-size: 24px; | |
color: #fff; | |
border-radius: 20px; | |
background-color: #dd2f81; | |
cursor: pointer | |
} | |
.orangeMiniButton { | |
width: 100%; | |
height: 25px; | |
line-height: 25px; | |
font-size: 12px; | |
color: #000; | |
border-radius: 20px; | |
background-color: #fff; | |
cursor: pointer | |
} | |
.orangeMiniButton:hover { | |
color: #fff; | |
background-color: #f90;; | |
} | |
.stakingTerms { | |
color: blue; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import './App.css'; | |
import react, { useEffect, useState } from 'react'; | |
import { ethers } from 'ethers'; | |
import artifact from './artifacts/contracts/Staking.sol/Staking.json' | |
import NavBar from './components/NavBar' | |
import StakeModal from './components/StakeModal' | |
import { Bank, PiggyBank, Coin } from 'react-bootstrap-icons' | |
const CONTRACT_ADDRESS = '0x5FbDB2315678afecb367f032d93F642f64180aa3' | |
function App() { | |
// general | |
const [provider, setProvider] = useState(undefined) | |
const [signer, setSigner] = useState(undefined) | |
const [contract, setContract] = useState(undefined) | |
const [signerAddress, setSignerAddress] = useState(undefined) | |
// assets | |
const [assetIds, setAssetIds] = useState([]) | |
const [assets, setAssets] = useState([]) | |
// staking | |
const [showStakeModal, setShowStakeModal] = useState(false) | |
const [stakingLength, setStakingLength] = useState(undefined) | |
const [stakingPercent, setStakingPercent] = useState(undefined) | |
const [amount, setAmount] = useState(0) | |
// helpers | |
const toWei = ether => ethers.utils.parseEther(ether) | |
const toEther = wei => ethers.utils.formatEther(wei) | |
useEffect(() => { | |
const onLoad = async () => { | |
const provider = await new ethers.providers.Web3Provider(window.ethereum) | |
setProvider(provider) | |
const contract = await new ethers.Contract( | |
CONTRACT_ADDRESS, | |
artifact.abi | |
) | |
setContract(contract) | |
} | |
onLoad() | |
}, []) | |
const isConnected = () => signer !== undefined | |
const getSigner = async () => { | |
provider.send("eth_requestAccounts", []) | |
const signer = provider.getSigner() | |
return signer | |
} | |
const getAssetIds = async (address, signer) => { | |
const assetIds = await contract.connect(signer).getPositionIdsForAddress(address) | |
return assetIds | |
} | |
const calcDaysRemaining = (unlockDate) => { | |
const timeNow = Date.now() / 1000 | |
const secondsRemaining = unlockDate - timeNow | |
return Math.max( (secondsRemaining / 60 / 60 / 24).toFixed(0), 0) | |
} | |
const getAssets = async (ids, signer) => { | |
const queriedAssets = await Promise.all( | |
ids.map(id => contract.connect(signer).getPositionById(id)) | |
) | |
queriedAssets.map(async asset => { | |
const parsedAsset = { | |
positionId: asset.positionId, | |
percentInterest: Number(asset.percentInterest) / 100, | |
daysRemaining: calcDaysRemaining( Number(asset.unlockDate) ), | |
etherInterest: toEther(asset.weiInterest), | |
etherStaked: toEther(asset.weiStaked), | |
open: asset.open, | |
} | |
setAssets(prev => [...prev, parsedAsset]) | |
}) | |
} | |
const connectAndLoad = async () => { | |
const signer = await getSigner(provider) | |
setSigner(signer) | |
const signerAddress = await signer.getAddress() | |
setSignerAddress(signerAddress) | |
const assetIds = await getAssetIds(signerAddress, signer) | |
setAssetIds(assetIds) | |
getAssets(assetIds, signer) | |
} | |
const openStakingModal = (stakingLength, stakingPercent) => { | |
setShowStakeModal(true) | |
setStakingLength(stakingLength) | |
setStakingPercent(stakingPercent) | |
} | |
const stakeEther = () => { | |
const wei = toWei(amount) | |
const data = { value: wei } | |
contract.connect(signer).stakeEther(stakingLength, data) | |
} | |
const withdraw = positionId => { | |
contract.connect(signer).closePosition(positionId) | |
} | |
return ( | |
<div className="App"> | |
<div> | |
<NavBar | |
isConnected={isConnected} | |
connect={connectAndLoad} | |
/> | |
</div> | |
<div className="appBody"> | |
<div className="marketContainer"> | |
<div className="subContainer"> | |
<span> | |
<img className="logoImg" src="eth-logo.webp"/> | |
</span> | |
<span className="marketHeader">Ethereum Market</span> | |
</div> | |
<div className="row"> | |
<div className="col-md-4"> | |
<div onClick={() => openStakingModal(30, '7%')} className="marketOption"> | |
<div className="glyphContainer hoverButton"> | |
<span className="glyph"> | |
<Coin /> | |
</span> | |
</div> | |
<div className="optionData"> | |
<span>1 Month</span> | |
<span className="optionPercent">7%</span> | |
</div> | |
</div> | |
</div> | |
<div className="col-md-4"> | |
<div onClick={() => openStakingModal(90, '10%')} className="marketOption"> | |
<div className="glyphContainer hoverButton"> | |
<span className="glyph"> | |
<Coin /> | |
</span> | |
</div> | |
<div className="optionData"> | |
<span>3 Months</span> | |
<span className="optionPercent">10%</span> | |
</div> | |
</div> | |
</div> | |
<div className="col-md-4"> | |
<div onClick={() => openStakingModal(180, '12%')} className="marketOption"> | |
<div className="glyphContainer hoverButton"> | |
<span className="glyph"> | |
<Coin /> | |
</span> | |
</div> | |
<div className="optionData"> | |
<span>6 Months</span> | |
<span className="optionPercent">12%</span> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div className="assetContainer"> | |
<div className="subContainer"> | |
<span className="marketHeader">Staked Assets</span> | |
</div> | |
<div> | |
<div className="row columnHeaders"> | |
<div className="col-md-2">Assets</div> | |
<div className="col-md-2">Percent Interest</div> | |
<div className="col-md-2">Staked</div> | |
<div className="col-md-2">Interest</div> | |
<div className="col-md-2">Days Remaining</div> | |
<div className="col-md-2"></div> | |
</div> | |
</div> | |
<br /> | |
{assets.length > 0 && assets.map((a, idx) => ( | |
<div className="row"> | |
<div className="col-md-2"> | |
<span> | |
<img className="stakedLogoImg" src="eth-logo.webp" /> | |
</span> | |
</div> | |
<div className="col-md-2"> | |
{a.percentInterest} % | |
</div> | |
<div className="col-md-2"> | |
{a.etherStaked} | |
</div> | |
<div className="col-md-2"> | |
{a.etherInterest} | |
</div> | |
<div className="col-md-2"> | |
{a.daysRemaining} | |
</div> | |
<div className="col-md-2"> | |
{a.open ? ( | |
<div onClick={() => withdraw(a.positionId)} className="orangeMiniButton">Withdraw</div> | |
) : ( | |
<span>closed</span> | |
)} | |
</div> | |
</div> | |
))} | |
</div> | |
</div> | |
{showStakeModal && ( | |
<StakeModal | |
onClose={() => setShowStakeModal(false)} | |
stakingLength={stakingLength} | |
stakingPercent={stakingPercent} | |
amount={amount} | |
setAmount={setAmount} | |
stakeEther={stakeEther} | |
/> | |
)} | |
</div> | |
); | |
} | |
export default App; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react'; | |
import ReactDOM from 'react-dom/client'; | |
import './index.css'; | |
import App from './App'; | |
import 'bootstrap/dist/css/bootstrap.css'; | |
const root = ReactDOM.createRoot(document.getElementById('root')); | |
root.render( | |
<React.StrictMode> | |
<App /> | |
</React.StrictMode> | |
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
pragma solidity ^0.8.0; | |
contract Staking { | |
address public owner; | |
struct Position { | |
uint positionId; | |
address walletAddress; | |
uint createdDate; | |
uint unlockDate; | |
uint percentInterest; | |
uint weiStaked; | |
uint weiInterest; | |
bool open; | |
} | |
Position position; | |
uint public currentPositionId; | |
mapping(uint => Position) public positions; | |
mapping(address => uint[]) public positionIdsByAddress; | |
mapping(uint => uint) public tiers; | |
uint[] public lockPeriods; | |
constructor() payable { | |
owner = msg.sender; | |
currentPositionId = 0; | |
tiers[30] = 700; | |
tiers[90] = 1000; | |
tiers[180] = 1200; | |
lockPeriods.push(30); | |
lockPeriods.push(90); | |
lockPeriods.push(180); | |
} | |
function stakeEther(uint numDays) external payable { | |
require(tiers[numDays] > 0, "Mapping not found"); | |
positions[currentPositionId] = Position( | |
currentPositionId, | |
msg.sender, | |
block.timestamp, | |
block.timestamp + (numDays * 1 days), | |
tiers[numDays], | |
msg.value, | |
calculateInterest(tiers[numDays], numDays, msg.value), | |
true | |
); | |
positionIdsByAddress[msg.sender].push(currentPositionId); | |
currentPositionId += 1; | |
} | |
function calculateInterest(uint basisPoints, uint numDays, uint weiAmount) private pure returns(uint) { | |
return basisPoints * weiAmount / 10000; // 700 / 10000 => 0.07 | |
} | |
function modifyLockPeriods(uint numDays, uint basisPoints) external { | |
require(owner == msg.sender, "Only owner may modify staking periods"); | |
tiers[numDays] = basisPoints; | |
lockPeriods.push(numDays); | |
} | |
function getLockPeriods() external view returns(uint[] memory) { | |
return lockPeriods; | |
} | |
function getInterestRate(uint numDays) external view returns(uint) { | |
return tiers[numDays]; | |
} | |
function getPositionById(uint positionId) external view returns(Position memory) { | |
return positions[positionId]; | |
} | |
function getPositionIdsForAddress(address walletAddress) external view returns(uint[] memory) { | |
return positionIdsByAddress[walletAddress]; | |
} | |
function changeUnlockDate(uint positionId, uint newUnlockDate) external { | |
require(owner == msg.sender, "Only owner may modify staking periods"); | |
positions[positionId].unlockDate = newUnlockDate; | |
} | |
function closePosition(uint positionId) external { | |
require(positions[positionId].walletAddress == msg.sender, 'Only position creator may modifiy position'); | |
require(positions[positionId].open == true, 'Position is closed'); | |
positions[positionId].open = false; | |
if(block.timestamp > positions[positionId].unlockDate) { | |
uint amount = positions[positionId].weiStaked + positions[positionId].weiInterest; | |
payable(msg.sender).call{value: amount}(""); | |
} else { | |
payable(msg.sender).call{value: positions[positionId].weiStaked}(""); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const { expect } = require("chai"); | |
describe('Staking', function () { | |
beforeEach(async function() { | |
[signer1, signer2] = await ethers.getSigners(); | |
Staking = await ethers.getContractFactory('Staking', signer1); | |
staking = await Staking.deploy({ | |
value: ethers.utils.parseEther('10') | |
}); | |
}); | |
describe('deploy', function () { | |
it('should set owner', async function() { | |
expect(await staking.owner()).to.equal(signer1.address) | |
}) | |
it('sets up tiers and lockPeriods', async function () { | |
expect(await staking.lockPeriods(0)).to.equal(30) | |
expect(await staking.lockPeriods(1)).to.equal(90) | |
expect(await staking.lockPeriods(2)).to.equal(180) | |
expect(await staking.tiers(30)).to.equal(700) | |
expect(await staking.tiers(90)).to.equal(1000) | |
expect(await staking.tiers(180)).to.equal(1200) | |
}) | |
}) | |
describe('stakeEther', function () { | |
it('transfers ether', async function () { | |
const provider = waffle.provider; | |
let contractBalance; | |
let signerBalance; | |
const transferAmount = ethers.utils.parseEther('2.0') | |
contractBalance = await provider.getBalance(staking.address) | |
signerBalance = await signer1.getBalance() | |
const data = { value: transferAmount } | |
const transaction = await staking.connect(signer1).stakeEther(30, data); | |
const receipt = await transaction.wait() | |
const gasUsed = receipt.gasUsed.mul(receipt.effectiveGasPrice) | |
// test the change in signer1's ether balance | |
expect( | |
await signer1.getBalance() | |
).to.equal( | |
signerBalance.sub(transferAmount).sub(gasUsed) | |
) | |
// test the change in contract's ether balance | |
expect( | |
await provider.getBalance(staking.address) | |
).to.equal( | |
contractBalance.add(transferAmount) | |
) | |
}) | |
it('adds a position to positions', async function () { | |
const provider = waffle.provider; | |
let position; | |
const transferAmount = ethers.utils.parseEther('1.0') | |
position = await staking.positions(0) | |
expect(position.positionId).to.equal(0) | |
expect(position.walletAddress).to.equal('0x0000000000000000000000000000000000000000') | |
expect(position.createdDate).to.equal(0) | |
expect(position.unlockDate).to.equal(0) | |
expect(position.percentInterest).to.equal(0) | |
expect(position.weiStaked).to.equal(0) | |
expect(position.weiInterest).to.equal(0) | |
expect(position.open).to.equal(false) | |
expect(await staking.currentPositionId()).to.equal(0) | |
data = { value: transferAmount } | |
const transaction = await staking.connect(signer1).stakeEther(90, data); | |
const receipt = await transaction.wait() | |
const block = await provider.getBlock(receipt.blockNumber) | |
position = await staking.positions(0) | |
expect(position.positionId).to.equal(0) | |
expect(position.walletAddress).to.equal(signer1.address) | |
expect(position.createdDate).to.equal(block.timestamp) | |
expect(position.unlockDate).to.equal(block.timestamp + (86400 * 90)) | |
expect(position.percentInterest).to.equal(1000) | |
expect(position.weiStaked).to.equal(transferAmount) | |
expect(position.weiInterest).to.equal( ethers.BigNumber.from(transferAmount).mul(1000).div(10000) ) | |
expect(position.open).to.equal(true) | |
expect(await staking.currentPositionId()).to.equal(1) | |
}) | |
it('adds address amd positionId to positionIdsByAddress', async function () { | |
const transferAmount = ethers.utils.parseEther('0.5') | |
const data = { value: transferAmount } | |
await staking.connect(signer1).stakeEther(30, data) | |
await staking.connect(signer1).stakeEther(30, data) | |
await staking.connect(signer2).stakeEther(90, data) | |
expect(await staking.positionIdsByAddress(signer1.address, 0)).to.equal(0) | |
expect(await staking.positionIdsByAddress(signer1.address, 1)).to.equal(1) | |
expect(await staking.positionIdsByAddress(signer2.address, 0)).to.equal(2) | |
}) | |
}) | |
describe('modifyLockPeriods', function() { | |
describe('owner', function () { | |
it('should create a new lock period', async function () { | |
await staking.connect(signer1).modifyLockPeriods(100, 999); | |
expect(await staking.tiers(100)).to.equal(999); | |
expect(await staking.lockPeriods(3)).to.equal(100); | |
}) | |
it('should modify an existing lock period', async function () { | |
await staking.connect(signer1).modifyLockPeriods(30, 150); | |
expect(await staking.tiers(30)).to.equal(150); | |
}) | |
}) | |
describe('non-owner', function () { | |
it('reverts', async function () { | |
expect( | |
staking.connect(signer2).modifyLockPeriods(100, 999) | |
).to.be.revertedWith( | |
'Only owner may modify staking periods' | |
) | |
}) | |
}) | |
}) | |
describe('getLockPeriods', function() { | |
it('returns all lock periods', async () => { | |
const lockPeriods = await staking.getLockPeriods() | |
expect( | |
lockPeriods.map(v => Number(v._hex)) | |
).to.eql( | |
[30,90,180] | |
) | |
}) | |
}) | |
describe('getInterestRate', function () { | |
it('returns the interest rate for a specific lockPeriod', async () => { | |
const interestRate = await staking.getInterestRate(30) | |
expect(interestRate).to.equal(700) | |
}) | |
}) | |
describe('getPositionById', function() { | |
it('returns data about a specific position, given a positionId', async () => { | |
const provider = waffle.provider; | |
const transferAmount = ethers.utils.parseEther('5') | |
const data = { value: transferAmount } | |
const transaction = await staking.connect(signer1).stakeEther(90, data) | |
const receipt = transaction.wait() | |
const block = await provider.getBlock(receipt.blockNumber) | |
const position = await staking.connect(signer1.address).getPositionById(0) | |
expect(position.positionId).to.equal(0) | |
expect(position.walletAddress).to.equal(signer1.address) | |
expect(position.createdDate).to.equal(block.timestamp) | |
expect(position.unlockDate).to.equal(block.timestamp + (86400 * 90)) | |
expect(position.percentInterest).to.equal(1000) | |
expect(position.weiStaked).to.equal(transferAmount) | |
expect(position.weiInterest).to.equal( ethers.BigNumber.from(transferAmount).mul(1000).div(10000) ) | |
expect(position.open).to.equal(true) | |
}) | |
}) | |
describe('getPositionIdsForAddress', function () { | |
it('returns a list of positionIds created by a specific address', async () => { | |
let data; | |
let transaction; | |
data = { value: ethers.utils.parseEther('5')} | |
transaction = await staking.connect(signer1).stakeEther(90, data); | |
data = { value: ethers.utils.parseEther('10')} | |
transaction = await staking.connect(signer1).stakeEther(90, data); | |
const positionIds = await staking.getPositionIdsForAddress(signer1.address) | |
expect( | |
positionIds.map(p => Number(p)) | |
).to.eql( | |
[0,1] | |
) | |
}) | |
}) | |
describe('changeUnlockDate', function() { | |
describe('owner', function() { | |
it('changes the unlockDate', async () => { | |
const data = { value: ethers.utils.parseEther('8') } | |
const transaction = await staking.connect(signer2).stakeEther(90, data) | |
const positionOld = await staking.getPositionById(0) | |
const newUnlockDate = positionOld.unlockDate - (86400 * 500) | |
await staking.connect(signer1).changeUnlockDate(0, newUnlockDate) | |
const positionNew = await staking.getPositionById(0) | |
expect( | |
positionNew.unlockDate | |
).to.be.equal( | |
positionOld.unlockDate - (86400 * 500) | |
) | |
}) | |
}) | |
describe('non-owner', function() { | |
it('reverts', async () => { | |
const data = { value: ethers.utils.parseEther('8') } | |
const transaction = await staking.connect(signer2).stakeEther(90, data) | |
const positionOld = await staking.getPositionById(0) | |
const newUnlockDate = positionOld.unlockDate - (86400 * 500) | |
expect( | |
staking.connect(signer2).changeUnlockDate(0, newUnlockDate) | |
).to.be.revertedWith( | |
'Only owner may modify staking periods' | |
) | |
}) | |
}) | |
}) | |
describe('closePosition', function() { | |
describe('after unlock date', function () { | |
it('transfers principal and interest', async () => { | |
let transaction; | |
let receipt; | |
let block; | |
const provider = waffle.provider; | |
const data = { value: ethers.utils.parseEther('8') } | |
transaction = await staking.connect(signer2).stakeEther(90, data) | |
receipt = transaction.wait() | |
block = await provider.getBlock(receipt.blockNumber) | |
const newUnlockDate = block.timestamp - (86400 * 100) | |
await staking.connect(signer1).changeUnlockDate(0, newUnlockDate) | |
const position = await staking.getPositionById(0) | |
const signerBalanceBefore = await signer2.getBalance() | |
transaction = await staking.connect(signer2).closePosition(0) | |
receipt = await transaction.wait() | |
const gasUsed = receipt.gasUsed.mul(receipt.effectiveGasPrice) | |
const signerBalanceAfter = await signer2.getBalance() | |
expect( | |
signerBalanceAfter | |
).to.equal( | |
signerBalanceBefore | |
.sub(gasUsed) | |
.add(position.weiStaked) | |
.add(position.weiInterest) | |
) | |
}) | |
}) | |
describe('before unlock date', function () { | |
it('transfers only principal', async () => { | |
let transaction; | |
let receipt; | |
let block; | |
const provider = waffle.provider; | |
const data = { value: ethers.utils.parseEther('5') } | |
transaction = await staking.connect(signer2).stakeEther(90, data) | |
receipt = transaction.wait() | |
block = await provider.getBlock(receipt.blockNumber) | |
const position = await staking.getPositionById(0) | |
const signerBalanceBefore = await signer2.getBalance() | |
transaction = await staking.connect(signer2).closePosition(0) | |
receipt = await transaction.wait() | |
const gasUsed = receipt.gasUsed.mul(receipt.effectiveGasPrice) | |
const signerBalanceAfter = await signer2.getBalance() | |
expect( | |
signerBalanceAfter | |
).to.equal( | |
signerBalanceBefore | |
.sub(gasUsed) | |
.add(position.weiStaked) | |
) | |
}) | |
}) | |
}) | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment