├── .env.example ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .prettierignore ├── .solhint.json ├── .solhintignore ├── README.md ├── contracts ├── Staking.sol └── token │ └── StakeToken.sol ├── hardhat.config.ts ├── package.json ├── rm.txt ├── scripts └── deploy.ts ├── test └── staking.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | ETHERSCAN_API_KEY=ABC123ABC123ABC123ABC123ABC123ABC1 2 | GOERLI_URL=https://goerli.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161 3 | PRIVATE_KEY=0xabc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc1 4 | ADMIN_1=0x2Ca79e8e0dC91cf771660596FA435B94dC81aab3 5 | ADMIN_2=0xd37f0723dFe67072eCd8D04e1A7A835D62991Ecc 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | artifacts 3 | cache 4 | coverage 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: false, 4 | es2021: true, 5 | mocha: true, 6 | node: true, 7 | }, 8 | plugins: ["@typescript-eslint"], 9 | extends: [ 10 | "standard", 11 | "plugin:prettier/recommended", 12 | "plugin:node/recommended", 13 | ], 14 | parser: "@typescript-eslint/parser", 15 | parserOptions: { 16 | ecmaVersion: 12, 17 | }, 18 | rules: { 19 | "node/no-unsupported-features/es-syntax": [ 20 | "error", 21 | { ignores: ["modules"] }, 22 | ], 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | .env 4 | coverage 5 | coverage.json 6 | typechain 7 | 8 | #Hardhat files 9 | cache 10 | artifacts 11 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | hardhat.config.ts 2 | scripts 3 | test 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | artifacts 3 | cache 4 | coverage* 5 | gasReporterOutput.json 6 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "rules": { 4 | "compiler-version": ["error", "^0.8.0"], 5 | "func-visibility": ["warn", { "ignoreConstructors": true }] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.solhintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dynamic Staking on Ethereum 2 | 3 | For most common staking applications, the admin has to provide the estimated APY for the program for a certain period of time beforehand. With the dynamic staking approach, it calculates the APY dynamically for a particular stakeholder based on the number of stakeholders, their staked amount and the rewards which were added to the Staking contract address till that point of time. There is no lock-in time for the stakeholder's stake in this approach. Stakeholders can remove their stake at any point in time and can claim the rewards. Here the staking program is done for a dummy StakeToken(STK) which is an ERC20 token deployed on the Goerli network. 4 | 5 | The financial logic of the staking smart contract is to assign shares to each stakeholder and rewards are in proportion to the shares. Just like Mutual Funds derives the NAV(Net Asset Value) and it increases or decreases based on the shares and its asset value inside of it, the similar way the NAV in this case will be STK per share price which will increase as and when rewards are added to the staking program. 6 | 7 | By default the initial ratio will be set at 1:1, so 1 STK is equal to 1 share. Each user who stake at this ratio will receive an equal amount of shares for the number of STK she/he staked. During the month a reward will be sent to the Staking smart contract, which will alter the number of STK on the contract and by default alter the STK per share ratio. 8 | 9 | #### Example flow - 10 | 11 | 1. Initially the STK/share ratio will be 1. 12 | 2. `StakeholderA` stakes 1000 STK token at this point, so `StakeholderA` will receive 1000 shares. 13 | 3. Reward of 100 STK is deposited on the Staking contract address. 14 | 4. Now, the STK/share ratio gets increase to 1.1 (total STK / number of shares = 1100/1000) 15 | 5. `StakeholderB` stakes 1000 STK token at this point, so `StakeholderB` will receive 1000/1.1 ~ 909 shares 16 | 6. `StakeholderA` remove stake of 1000 STK at this point, so `StakeholderA` will receive 1000\*1.1 = 1100 STK. So, reward of `StakeholderA` is 1100-1000 = 100 STK 17 | 18 | > More detailed scenarios covering all the edge cases can be found here - 19 | > https://docs.google.com/spreadsheets/d/11yU9c4G4PJ50dzmtILRMYd5w_qO-qCa2cM0ZOWJXfsA/edit#gid=0 20 | 21 | #### Smart contract features 22 | 23 | 1. Upgradable smart contract 24 | 2. Role based acces control (RBAC) 25 | 3. Pausable smart contract 26 | 4. Refund STK to the stakeholders by admin in case of some issue 27 | 28 | ### Deployment 29 | 30 | ##### To deploy your own instance of StakeToken and Staking contract 31 | 32 | 1. Rename `.env.example` to `.env` and replace all the required values there 33 | 2. Run `npm run deploy` to deploy both the contracts. This will deploy the conract on the Goerli network. To change the network, you need to update the RPC url in `.env` and also in the `package.json` script. Also, make sure you have funds to pay for transaction fee on the wallet you mentioned in the `.env`. 34 | 35 | > The STK token and Staking contract is deployed on Goerli network at `0xD154805B317C83f61aB1744A0A0C931Bd318e50a` and `0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0` address respectively. 36 | > StakeToken - https://goerli.etherscan.io/address/0xD154805B317C83f61aB1744A0A0C931Bd318e50a 37 | > Staking - https://goerli.etherscan.io/address/0xB767f1030d239FF3d84d3369A37312C714740cC8 38 | 39 | ### Test 40 | 41 | 1. To run the unit test, use the below command 42 | 43 | ```sh 44 | npx hardhat test 45 | ``` 46 | -------------------------------------------------------------------------------- /contracts/Staking.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.6; 2 | 3 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 4 | import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; 5 | import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; 6 | import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; 7 | import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 8 | // https://docs.openzeppelin.com/contracts/4.x/api/proxy#transparent-vs-uups 9 | import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; 10 | 11 | contract Staking is 12 | Initializable, 13 | UUPSUpgradeable, 14 | AccessControlUpgradeable, 15 | PausableUpgradeable 16 | { 17 | using EnumerableSet for EnumerableSet.AddressSet; 18 | 19 | IERC20 private stkToken; 20 | EnumerableSet.AddressSet private stakeholders; 21 | 22 | struct Stake { 23 | uint256 stakedSTK; 24 | uint256 shares; 25 | } 26 | 27 | bytes32 private ADMIN_ROLE; 28 | uint256 private base; 29 | uint256 private totalStakes; 30 | uint256 private totalShares; 31 | bool private initialRatioFlag; 32 | 33 | mapping(address => Stake) private stakeholderToStake; 34 | 35 | event StakeAdded( 36 | address indexed stakeholder, 37 | uint256 amount, 38 | uint256 shares, 39 | uint256 timestamp 40 | ); 41 | event StakeRemoved( 42 | address indexed stakeholder, 43 | uint256 amount, 44 | uint256 shares, 45 | uint256 reward, 46 | uint256 timestamp 47 | ); 48 | 49 | modifier hasAdminRole() { 50 | require(hasRole(ADMIN_ROLE, msg.sender), "Caller is not an admin"); 51 | _; 52 | } 53 | modifier isInitialRatioNotSet() { 54 | require(!initialRatioFlag, "Initial Ratio has already been set"); 55 | _; 56 | } 57 | 58 | modifier isInitialRatioSet() { 59 | require(initialRatioFlag, "Initial Ratio has not yet been set"); 60 | _; 61 | } 62 | 63 | function initialize( 64 | address admin1, 65 | address admin2, 66 | address _stkToken 67 | ) public initializer { 68 | AccessControlUpgradeable.__AccessControl_init(); 69 | PausableUpgradeable.__Pausable_init(); 70 | 71 | ADMIN_ROLE = keccak256("ADMIN_ROLE"); 72 | 73 | // Set up roles 74 | _setupRole(ADMIN_ROLE, admin1); 75 | _setupRole(ADMIN_ROLE, admin2); 76 | 77 | stkToken = IERC20(_stkToken); 78 | base = 10**18; 79 | } 80 | 81 | function _authorizeUpgrade(address) internal override hasAdminRole {} 82 | 83 | function pauseContract() public hasAdminRole { 84 | _pause(); 85 | } 86 | 87 | function unPauseContract() public hasAdminRole { 88 | _unpause(); 89 | } 90 | 91 | function setInitialRatio(uint256 stakeAmount) 92 | public 93 | isInitialRatioNotSet 94 | hasAdminRole 95 | { 96 | require( 97 | totalShares == 0 && stkToken.balanceOf(address(this)) == 0, 98 | "Stakes and shares are non-zero" 99 | ); 100 | 101 | stakeholders.add(msg.sender); 102 | stakeholderToStake[msg.sender] = Stake({ 103 | stakedSTK: stakeAmount, 104 | shares: stakeAmount 105 | }); 106 | totalStakes = stakeAmount; 107 | totalShares = stakeAmount; 108 | initialRatioFlag = true; 109 | 110 | require( 111 | stkToken.transferFrom(msg.sender, address(this), stakeAmount), 112 | "STK transfer failed" 113 | ); 114 | 115 | emit StakeAdded(msg.sender, stakeAmount, stakeAmount, block.timestamp); 116 | } 117 | 118 | function createStake(uint256 stakeAmount) 119 | public 120 | whenNotPaused 121 | isInitialRatioSet 122 | { 123 | uint256 shares = (stakeAmount * totalShares) / 124 | stkToken.balanceOf(address(this)); 125 | 126 | require( 127 | stkToken.transferFrom(msg.sender, address(this), stakeAmount), 128 | "STK transfer failed" 129 | ); 130 | 131 | stakeholders.add(msg.sender); 132 | stakeholderToStake[msg.sender].stakedSTK += stakeAmount; 133 | stakeholderToStake[msg.sender].shares += shares; 134 | totalStakes += stakeAmount; 135 | totalShares += shares; 136 | 137 | emit StakeAdded(msg.sender, stakeAmount, shares, block.timestamp); 138 | } 139 | 140 | function removeStake(uint256 stakeAmount) public whenNotPaused { 141 | uint256 stakeholderStake = stakeholderToStake[msg.sender].stakedSTK; 142 | uint256 stakeholderShares = stakeholderToStake[msg.sender].shares; 143 | 144 | require(stakeholderStake >= stakeAmount, "Not enough staked!"); 145 | 146 | uint256 stakedRatio = (stakeholderStake * base) / stakeholderShares; 147 | uint256 currentRatio = (stkToken.balanceOf(address(this)) * base) / 148 | totalShares; 149 | uint256 sharesToWithdraw = (stakeAmount * stakeholderShares) / 150 | stakeholderStake; 151 | 152 | uint256 rewards = 0; 153 | 154 | if (currentRatio > stakedRatio) { 155 | rewards = (sharesToWithdraw * (currentRatio - stakedRatio)) / base; 156 | } 157 | 158 | stakeholderToStake[msg.sender].shares -= sharesToWithdraw; 159 | stakeholderToStake[msg.sender].stakedSTK -= stakeAmount; 160 | totalStakes -= stakeAmount; 161 | totalShares -= sharesToWithdraw; 162 | 163 | require( 164 | stkToken.transfer(msg.sender, stakeAmount + rewards), 165 | "STK transfer failed" 166 | ); 167 | 168 | if (stakeholderToStake[msg.sender].stakedSTK == 0) { 169 | stakeholders.remove(msg.sender); 170 | } 171 | 172 | emit StakeRemoved( 173 | msg.sender, 174 | stakeAmount, 175 | sharesToWithdraw, 176 | rewards, 177 | block.timestamp 178 | ); 179 | } 180 | 181 | function getStkPerShare() public view returns (uint256) { 182 | return (stkToken.balanceOf(address(this)) * base) / totalShares; 183 | } 184 | 185 | function stakeOf(address stakeholder) public view returns (uint256) { 186 | return stakeholderToStake[stakeholder].stakedSTK; 187 | } 188 | 189 | function sharesOf(address stakeholder) public view returns (uint256) { 190 | return stakeholderToStake[stakeholder].shares; 191 | } 192 | 193 | function rewardOf(address stakeholder) public view returns (uint256) { 194 | uint256 stakeholderStake = stakeholderToStake[stakeholder].stakedSTK; 195 | uint256 stakeholderShares = stakeholderToStake[stakeholder].shares; 196 | 197 | if (stakeholderShares == 0) { 198 | return 0; 199 | } 200 | 201 | uint256 stakedRatio = (stakeholderStake * base) / stakeholderShares; 202 | uint256 currentRatio = (stkToken.balanceOf(address(this)) * base) / 203 | totalShares; 204 | 205 | if (currentRatio <= stakedRatio) { 206 | return 0; 207 | } 208 | 209 | uint256 rewards = (stakeholderShares * (currentRatio - stakedRatio)) / base; 210 | 211 | return rewards; 212 | } 213 | 214 | function rewardForSTK(address stakeholder, uint256 stkAmount) 215 | public 216 | view 217 | returns (uint256) 218 | { 219 | uint256 stakeholderStake = stakeholderToStake[stakeholder].stakedSTK; 220 | uint256 stakeholderShares = stakeholderToStake[stakeholder].shares; 221 | 222 | require(stakeholderStake >= stkAmount, "Not enough staked!"); 223 | 224 | uint256 stakedRatio = (stakeholderStake * base) / stakeholderShares; 225 | uint256 currentRatio = (stkToken.balanceOf(address(this)) * base) / 226 | totalShares; 227 | uint256 sharesToWithdraw = (stkAmount * stakeholderShares) / 228 | stakeholderStake; 229 | 230 | if (currentRatio <= stakedRatio) { 231 | return 0; 232 | } 233 | 234 | uint256 rewards = (sharesToWithdraw * (currentRatio - stakedRatio)) / base; 235 | 236 | return rewards; 237 | } 238 | 239 | function getTotalStakes() public view returns (uint256) { 240 | return totalStakes; 241 | } 242 | 243 | function getTotalShares() public view returns (uint256) { 244 | return totalShares; 245 | } 246 | 247 | function getCurrentRewards() public view returns (uint256) { 248 | return stkToken.balanceOf(address(this)) - totalStakes; 249 | } 250 | 251 | function getTotalStakeholders() public view returns (uint256) { 252 | return stakeholders.length(); 253 | } 254 | 255 | function refundLockedSTK(uint256 from, uint256 to) public hasAdminRole { 256 | require(to <= stakeholders.length(), "Invalid `to` param"); 257 | uint256 s; 258 | 259 | for (s = from; s < to; s += 1) { 260 | totalStakes -= stakeholderToStake[stakeholders.at(s)].stakedSTK; 261 | 262 | require( 263 | stkToken.transfer( 264 | stakeholders.at(s), 265 | stakeholderToStake[stakeholders.at(s)].stakedSTK 266 | ), 267 | "STK transfer failed" 268 | ); 269 | 270 | stakeholderToStake[stakeholders.at(s)].stakedSTK = 0; 271 | } 272 | } 273 | 274 | function removeLockedRewards() public hasAdminRole { 275 | require(totalStakes == 0, "Stakeholders still have stakes"); 276 | 277 | uint256 balance = stkToken.balanceOf(address(this)); 278 | 279 | require(stkToken.transfer(msg.sender, balance), "STK transfer failed"); 280 | } 281 | } -------------------------------------------------------------------------------- /contracts/token/StakeToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.6; 2 | 3 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 4 | 5 | contract StakeToken is ERC20 { 6 | constructor(uint256 initialSupply) ERC20("StakeToken", "STK") { 7 | _mint(msg.sender, initialSupply); 8 | } 9 | } -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from "dotenv"; 2 | 3 | import { HardhatUserConfig, task } from "hardhat/config"; 4 | import "@nomiclabs/hardhat-etherscan"; 5 | import "@nomiclabs/hardhat-waffle"; 6 | import "@typechain/hardhat"; 7 | import "hardhat-gas-reporter"; 8 | import "solidity-coverage"; 9 | import "@openzeppelin/hardhat-upgrades"; 10 | import "@nomiclabs/hardhat-ethers"; 11 | 12 | dotenv.config(); 13 | 14 | 15 | const config: HardhatUserConfig = { 16 | solidity: "0.8.6", 17 | networks: { 18 | goerli: { 19 | url: process.env.GOERLI_URL || "", 20 | accounts: 21 | process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [], 22 | gas: 2100000, 23 | }, 24 | }, 25 | gasReporter: { 26 | enabled: process.env.REPORT_GAS !== undefined, 27 | currency: "USD", 28 | }, 29 | etherscan: { 30 | apiKey: process.env.ETHERSCAN_API_KEY, 31 | }, 32 | }; 33 | 34 | export default config; 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hardhat-project", 3 | "scripts": { 4 | "deploy": "npx hardhat run scripts/deploy.ts --network goerli" 5 | }, 6 | "devDependencies": { 7 | "@nomiclabs/hardhat-ethers": "^2.0.4", 8 | "@nomiclabs/hardhat-etherscan": "^2.1.8", 9 | "@nomiclabs/hardhat-waffle": "^2.0.2", 10 | "@openzeppelin/hardhat-upgrades": "^1.13.0", 11 | "@typechain/ethers-v5": "^7.2.0", 12 | "@typechain/hardhat": "^2.3.1", 13 | "@types/chai": "^4.3.0", 14 | "@types/mocha": "^9.1.0", 15 | "@types/node": "^12.20.42", 16 | "@typescript-eslint/eslint-plugin": "^4.33.0", 17 | "@typescript-eslint/parser": "^4.33.0", 18 | "chai": "^4.3.4", 19 | "dotenv": "^10.0.0", 20 | "eslint": "^7.32.0", 21 | "eslint-config-prettier": "^8.3.0", 22 | "eslint-config-standard": "^16.0.3", 23 | "eslint-plugin-import": "^2.25.4", 24 | "eslint-plugin-node": "^11.1.0", 25 | "eslint-plugin-prettier": "^3.4.1", 26 | "eslint-plugin-promise": "^5.2.0", 27 | "ethereum-waffle": "^3.4.0", 28 | "ethers": "^5.5.3", 29 | "hardhat": "^2.8.3", 30 | "hardhat-gas-reporter": "^1.0.7", 31 | "prettier": "^2.5.1", 32 | "prettier-plugin-solidity": "^1.0.0-beta.19", 33 | "solhint": "^3.3.6", 34 | "solidity-coverage": "^0.7.18", 35 | "ts-node": "^10.4.0", 36 | "typechain": "^5.2.0", 37 | "typescript": "^4.5.5" 38 | }, 39 | "dependencies": { 40 | "@openzeppelin/contracts": "^4.4.2", 41 | "@openzeppelin/contracts-upgradeable": "^4.4.2" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /rm.txt: -------------------------------------------------------------------------------- 1 | Wed 07/26/2023 16:39:26.04 2 | -------------------------------------------------------------------------------- /scripts/deploy.ts: -------------------------------------------------------------------------------- 1 | import { ethers, upgrades } from "hardhat"; 2 | 3 | async function main() { 4 | const {ADMIN_1, ADMIN_2} = process.env; 5 | const StakeToken = await ethers.getContractFactory("StakeToken"); 6 | const stk = await StakeToken.deploy("100000000000000000000000000000"); 7 | 8 | await stk.deployed(); 9 | 10 | console.log('StakeToken deployed to:', stk.address); 11 | 12 | const Staking = await ethers.getContractFactory("Staking"); 13 | const staking = await upgrades.deployProxy( 14 | Staking, 15 | [ 16 | ADMIN_1, 17 | ADMIN_2, 18 | stk.address 19 | ], 20 | { 21 | initializer: "initialize", 22 | kind: "uups", 23 | } 24 | ); 25 | 26 | await staking.deployed(); 27 | 28 | console.log("Staking deployed to:", staking.address); 29 | 30 | const approvalTx = await stk.approve(staking.address, ethers.utils.parseUnits('100')); 31 | console.log('approvalTx hash', approvalTx.hash); 32 | 33 | const setInitialRatioTx = await staking.functions.setInitialRatio(ethers.utils.parseUnits('100')); 34 | console.log('setInitialRatioTx hash', setInitialRatioTx.hash); 35 | } 36 | 37 | main().catch((error) => { 38 | console.error(error); 39 | process.exitCode = 1; 40 | }); 41 | -------------------------------------------------------------------------------- /test/staking.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { ethers, upgrades } from "hardhat"; 3 | import { Staking } from "../typechain/Staking"; 4 | import { Signer } from "ethers"; 5 | 6 | describe("Staking.sol", () => { 7 | let stakingInstance: any; 8 | let stkInstance: any; 9 | let admin: Signer; 10 | let owner: Signer; 11 | let staker1:Signer; 12 | let staker2:Signer; 13 | let staker3: Signer; 14 | let staker4:Signer; 15 | let staker5:Signer; 16 | 17 | beforeEach(async () => { 18 | [owner, staker1, staker2, staker3, staker4, staker5] = await ethers.getSigners(); 19 | admin = staker1; 20 | 21 | const stkFactory = await ethers.getContractFactory("StakeToken", owner); 22 | stkInstance = await stkFactory.deploy(ethers.utils.parseUnits("9999999999")); 23 | await stkInstance.deployed(); 24 | 25 | const Staking = await ethers.getContractFactory("Staking"); 26 | 27 | stakingInstance = (await upgrades.deployProxy( 28 | Staking, 29 | [ 30 | await owner.getAddress(), 31 | await admin.getAddress(), 32 | stkInstance.address, 33 | ], 34 | { 35 | initializer: "initialize", 36 | kind: "uups", 37 | } 38 | )) as Staking; 39 | 40 | await stkInstance.transfer(await staker1.getAddress(), ethers.utils.parseUnits('500')); 41 | await stkInstance.transfer(await staker2.getAddress(), ethers.utils.parseUnits('1000')); 42 | await stkInstance.transfer(await staker3.getAddress(), ethers.utils.parseUnits('500')); 43 | await stkInstance.transfer(await staker4.getAddress(), ethers.utils.parseUnits('500')); 44 | await stkInstance.transfer(await staker5.getAddress(), '1'); 45 | }); 46 | 47 | 48 | /* 49 | * Scenario covered in excel 50 | * https://docs.google.com/spreadsheets/d/11yU9c4G4PJ50dzmtILRMYd5w_qO-qCa2cM0ZOWJXfsA/edit#gid=0 51 | * Case includes - 52 | * Withdraw all stakes and re-stake 53 | * Withdraw partial stakes 54 | * Create stake on partially withdrawn stakes 55 | * Create and withdraw stakes when no rewards been added in between create and withdraw 56 | */ 57 | it('scenario1', async () => { 58 | let staker3Balance = 0; 59 | let staker4Balance = 0; 60 | let totalRewardsDistributed = 0; 61 | 62 | // Staker1 create a stake of 500 DTX 63 | await stkInstance.connect(staker1).approve( 64 | stakingInstance.address, 65 | ethers.utils.parseUnits('500') 66 | ); 67 | await stakingInstance.connect(staker1).setInitialRatio(ethers.utils.parseUnits('500'), ); 68 | 69 | // Staker2 create a stake of 1000 DTX 70 | await stkInstance.connect(staker2).approve( 71 | stakingInstance.address, 72 | ethers.utils.parseUnits('1000') 73 | ); 74 | await stakingInstance.connect(staker2).createStake(ethers.utils.parseUnits('1000')); 75 | 76 | // Staker3 create a stake of 500 DTX 77 | await stkInstance.connect(staker3).approve( 78 | stakingInstance.address, 79 | ethers.utils.parseUnits('500') 80 | ); 81 | await stakingInstance.connect(staker3).createStake(ethers.utils.parseUnits('500')); 82 | 83 | // Add platform rewards of 100 DTX 84 | await stkInstance.transfer( 85 | stakingInstance.address, 86 | ethers.utils.parseUnits('100') 87 | ); 88 | 89 | expect((await stakingInstance.getStkPerShare()).toString()).to.be.equal( 90 | '1050000000000000000' 91 | ); 92 | 93 | // Staker1 withdraws 500 DTX 94 | await stakingInstance.connect(staker1).removeStake(ethers.utils.parseUnits('500')); 95 | 96 | // totalRewardsDistributed += Number(ethers.utils.parseUnits('25')); 97 | 98 | expect((await stkInstance.balanceOf(await staker1.getAddress())).toString()).to.be.equal( 99 | ethers.utils.parseUnits('525') 100 | ); 101 | expect((await stakingInstance.stakeOf(await staker1.getAddress())).toString()).to.be.equal( 102 | '0' 103 | ); 104 | expect((await stakingInstance.sharesOf(await staker1.getAddress())).toString()).to.be.equal( 105 | '0' 106 | ); 107 | expect((await stakingInstance.getStkPerShare()).toString()).to.be.equal( 108 | '1050000000000000000' 109 | ); 110 | 111 | // Staker2 withdraws 1000 DTX 112 | await stakingInstance.connect(staker2).removeStake(ethers.utils.parseUnits('1000')); 113 | 114 | // totalRewardsDistributed += Number(ethers.utils.parseUnits('50')); 115 | 116 | expect((await stkInstance.balanceOf(await staker2.getAddress())).toString()).to.be.equal( 117 | ethers.utils.parseUnits('1050') 118 | ); 119 | expect((await stakingInstance.stakeOf(await staker2.getAddress())).toString()).to.be.equal( 120 | '0' 121 | ); 122 | expect((await stakingInstance.sharesOf(await staker2.getAddress())).toString()).to.be.equal( 123 | '0' 124 | ); 125 | expect((await stakingInstance.getStkPerShare()).toString()).to.be.equal( 126 | '1050000000000000000' 127 | ); 128 | 129 | // Scenario - staker4 stake and withdraw before the next reward 130 | // Staker4 stakes 500 DTX 131 | await stkInstance.connect(staker4).approve( 132 | stakingInstance.address, 133 | ethers.utils.parseUnits('500') 134 | ); 135 | await stakingInstance.connect(staker4).createStake(ethers.utils.parseUnits('500')); 136 | 137 | expect(parseInt(await stakingInstance.sharesOf(await staker4.getAddress()))).to.be.equal( 138 | 476190476190476200000 139 | ); 140 | 141 | // Staker4 withdraws 500 DTX 142 | await stakingInstance.connect(staker4).removeStake(ethers.utils.parseUnits('500')); 143 | 144 | staker4Balance += 500000000000000000000; 145 | // totalRewardsDistributed += Number(ethers.utils.parseUnits('0')); 146 | 147 | expect((await stkInstance.balanceOf(await staker4.getAddress())).toString()).to.be.equal( 148 | staker4Balance.toString() 149 | ); 150 | 151 | // Staker3 withdraws 250 DTX 152 | await stakingInstance.connect(staker3).removeStake(ethers.utils.parseUnits('250')); 153 | 154 | staker3Balance += 262500000000000000000; 155 | // totalRewardsDistributed += Number(ethers.utils.parseUnits('12.5')); 156 | 157 | expect((await stkInstance.balanceOf(await staker3.getAddress())).toString()).to.be.equal( 158 | '262500000000000000000' 159 | ); 160 | expect((await stakingInstance.stakeOf(await staker3.getAddress())).toString()).to.be.equal( 161 | '250000000000000000000' 162 | ); 163 | expect((await stakingInstance.sharesOf(await staker3.getAddress())).toString()).to.be.equal( 164 | '250000000000000000000' 165 | ); 166 | expect((await stakingInstance.getStkPerShare()).toString()).to.be.equal( 167 | '1050000000000000000' 168 | ); 169 | 170 | // Staker4 stakes 500 DTX 171 | await stkInstance.transfer(await staker4.getAddress(), ethers.utils.parseUnits('500')); 172 | await stkInstance.connect(staker4).approve( 173 | stakingInstance.address, 174 | ethers.utils.parseUnits('500') 175 | ); 176 | await stakingInstance.connect(staker4).createStake(ethers.utils.parseUnits('500')); 177 | 178 | expect((await stakingInstance.stakeOf(await staker4.getAddress())).toString()).to.be.equal( 179 | ethers.utils.parseUnits('500') 180 | ); 181 | expect((await stakingInstance.sharesOf(await staker4.getAddress())).toString()).to.be.equal( 182 | '476190476190476190476' 183 | ); 184 | expect((await stakingInstance.getStkPerShare()).toString()).to.be.equal( 185 | '1050000000000000000' 186 | ); 187 | 188 | // Add platform rewards of 100 DTX 189 | await stkInstance.transfer( 190 | stakingInstance.address, 191 | ethers.utils.parseUnits('100') 192 | ); 193 | 194 | expect((await stakingInstance.getStkPerShare()).toString()).to.be.equal( 195 | '1187704918032786885' 196 | ); 197 | 198 | // Staker3 stakes 500 DTX 199 | await stkInstance.transfer(await staker3.getAddress(), ethers.utils.parseUnits('500')); 200 | await stkInstance.connect(staker3).approve( 201 | stakingInstance.address, 202 | ethers.utils.parseUnits('500') 203 | ); 204 | await stakingInstance.connect(staker3).createStake(ethers.utils.parseUnits('500')); 205 | 206 | expect((await stakingInstance.stakeOf(await staker3.getAddress())).toString()).to.be.equal( 207 | ethers.utils.parseUnits('750') 208 | ); 209 | expect(parseInt(await stakingInstance.sharesOf(await staker3.getAddress()))).to.be.equal( 210 | 670979986197377400000 211 | ); 212 | expect((await stakingInstance.getStkPerShare()).toString()).to.be.equal( 213 | '1187704918032786885' 214 | ); 215 | 216 | // Staker4 withdraws 500 DTX 217 | await stakingInstance.connect(staker4).removeStake(ethers.utils.parseUnits('500')); 218 | 219 | // totalRewardsDistributed += 6.557377049e19; 220 | 221 | expect( 222 | parseInt(await stkInstance.balanceOf(await staker4.getAddress())) - staker4Balance 223 | ).to.be.equal(565573770491803340000); 224 | 225 | // Staker3 withdraws 500 DTX 226 | await stakingInstance.connect(staker3).removeStake(ethers.utils.parseUnits('500')); 227 | 228 | expect( 229 | parseInt(await stkInstance.balanceOf(await staker3.getAddress())) - staker3Balance 230 | ).to.be.equal(531284153005464500000); 231 | 232 | staker3Balance += 531284153005464500000; 233 | // totalRewardsDistributed += 3.128415301e19; 234 | 235 | // Staker3 withdraws 250 DTX 236 | await stakingInstance.connect(staker3).removeStake(ethers.utils.parseUnits('250')); 237 | // totalRewardsDistributed += 1.56420765e19; 238 | 239 | expect( 240 | parseInt(await stkInstance.balanceOf(await staker3.getAddress())) - staker3Balance 241 | ).to.be.equal(265642076502732180000); 242 | 243 | // expect(totalRewardsDistributed).to.be.equal( 244 | // Number(ethers.utils.parseUnits('200')) 245 | // ); 246 | }); 247 | 248 | /* 249 | * Stake amount less than base 10^18 DTX 250 | * Staker5 stakes and withdraws 1 wei DTX 251 | */ 252 | it('scenario2', async () => { 253 | // Staker1 create a stake of 500 DTX 254 | await stkInstance.connect(staker1).approve( 255 | stakingInstance.address, 256 | ethers.utils.parseUnits('500') 257 | ); 258 | await stakingInstance.connect(staker1).setInitialRatio(ethers.utils.parseUnits('500')); 259 | 260 | // Staker2 create a stake of 1000 DTX 261 | await stkInstance.connect(staker2).approve( 262 | stakingInstance.address, 263 | ethers.utils.parseUnits('1000') 264 | ); 265 | await stakingInstance.connect(staker2).createStake(ethers.utils.parseUnits('1000')); 266 | 267 | // Staker3 create a stake of 500 DTX 268 | await stkInstance.connect(staker3).approve( 269 | stakingInstance.address, 270 | ethers.utils.parseUnits('500') 271 | ); 272 | await stakingInstance.connect(staker3).createStake(ethers.utils.parseUnits('500')); 273 | 274 | // Staker5 create a stake of 1 wei DTX 275 | await stkInstance.connect(staker5).approve(stakingInstance.address, '1'); 276 | await stakingInstance.connect(staker5).createStake('1'); 277 | 278 | // Add platform rewards of 100 DTX 279 | await stkInstance.transfer( 280 | stakingInstance.address, 281 | ethers.utils.parseUnits('100') 282 | ); 283 | 284 | // Staker5 withdraws 1 wei DTX 285 | await stakingInstance.connect(staker5).removeStake('1'); 286 | 287 | // Staker1 withdraws 500 DTX 288 | await stakingInstance.connect(staker1).removeStake(ethers.utils.parseUnits('500')); 289 | 290 | expect((await stkInstance.balanceOf(await staker1.getAddress())).toString()).to.be.equal( 291 | ethers.utils.parseUnits('525') 292 | ); 293 | expect((await stakingInstance.stakeOf(await staker1.getAddress())).toString()).to.be.equal( 294 | '0' 295 | ); 296 | expect((await stakingInstance.sharesOf(await staker1.getAddress())).toString()).to.be.equal( 297 | '0' 298 | ); 299 | expect((await stakingInstance.getStkPerShare()).toString()).to.be.equal( 300 | '1050000000000000000' 301 | ); 302 | 303 | // Staker2 withdraws 1000 DTX 304 | await stakingInstance.connect(staker2).removeStake(ethers.utils.parseUnits('1000')); 305 | 306 | expect((await stkInstance.balanceOf(await staker2.getAddress())).toString()).to.be.equal( 307 | ethers.utils.parseUnits('1050') 308 | ); 309 | expect((await stakingInstance.stakeOf(await staker2.getAddress())).toString()).to.be.equal( 310 | '0' 311 | ); 312 | expect((await stakingInstance.sharesOf(await staker2.getAddress())).toString()).to.be.equal( 313 | '0' 314 | ); 315 | expect((await stakingInstance.getStkPerShare()).toString()).to.be.equal( 316 | '1050000000000000000' 317 | ); 318 | }); 319 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "outDir": "dist", 8 | "declaration": true 9 | }, 10 | "include": ["./scripts", "./test", "./typechain"], 11 | "files": ["./hardhat.config.ts"] 12 | } 13 | --------------------------------------------------------------------------------