From cb000f24051d8fc8dbf1b23416ee2af47a778319 Mon Sep 17 00:00:00 2001 From: jishankai Date: Fri, 3 Jul 2020 20:19:30 +0800 Subject: [PATCH 1/5] Add maturity time --- contracts/StakingPool.sol | 18 ++++++++---- test/StakingPool.test.js | 58 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 69 insertions(+), 7 deletions(-) diff --git a/contracts/StakingPool.sol b/contracts/StakingPool.sol index b5cd50f..4449131 100644 --- a/contracts/StakingPool.sol +++ b/contracts/StakingPool.sol @@ -19,6 +19,7 @@ contract StakingPool { address public admin; uint256 public totalStakes; uint256 public maxStakers; + uint256 public maturityTime; // Miner fee rate in basis point. address public miner; @@ -35,7 +36,8 @@ contract StakingPool { address _poolMaintainer, uint256 _minerFeeRateBp, uint256 _poolMaintainerFeeRateBp, - uint256 _maxStakers + uint256 _maxStakers, + uint256 _maturityTime ) public { @@ -45,12 +47,14 @@ contract StakingPool { _minerFeeRateBp + _poolMaintainerFeeRateBp <= MAX_BP, "Fee rate should be in basis point." ); + require(_maturityTime > now, "Maturity time should be later than now."); miner = _miner; admin = _admin; poolMaintainer = _poolMaintainer; minerFeeRateBp = _minerFeeRateBp; poolMaintainerFeeRateBp = _poolMaintainerFeeRateBp; maxStakers = _maxStakers; + maturityTime = _maturityTime; // timestamp(seconds) } function poolSize() public view returns (uint256) { @@ -169,10 +173,14 @@ contract StakingPool { totalStakes = totalStakes.add(totalPaid); uint256 totalFee = dividend.sub(totalPaid); - uint256 feeForMiner = totalFee.mul(minerFeeRateBp).div(feeRateBp); - uint256 feeForMaintainer = totalFee.sub(feeForMiner); - poolMaintainerFee = poolMaintainerFee.add(feeForMaintainer); - minerReward = minerReward.add(feeForMiner); + if (maturityTime + 24*60*60 > now) { // One extra day for miner's operation + uint256 feeForMiner = totalFee.mul(minerFeeRateBp).div(feeRateBp); + minerReward = minerReward.add(feeForMiner); + uint256 feeForMaintainer = totalFee.sub(feeForMiner); + poolMaintainerFee = poolMaintainerFee.add(feeForMaintainer); + } else { + poolMaintainerFee = poolMaintainerFee.add(totalFee); + } assert(balance >= totalStakes); } diff --git a/test/StakingPool.test.js b/test/StakingPool.test.js index 80345f7..b9a21e1 100644 --- a/test/StakingPool.test.js +++ b/test/StakingPool.test.js @@ -9,6 +9,7 @@ require('chai').use(require('chai-as-promised')).should(); const revertError = 'VM Exception while processing transaction: revert'; const toWei = i => web3.utils.toWei(String(i)); const gasPriceMax = 0; +const web3SendAsync = promisify(web3.currentProvider.send); function txGen(from, value) { return { @@ -21,6 +22,30 @@ async function forceSend(target, value, from) { await selfDestruct.forceSend(target); } +let snapshotId; + +async function addDaysOnEVM(days) { + const seconds = days * 3600 * 24; + await web3SendAsync({ + jsonrpc: '2.0', method: 'evm_increaseTime', params: [seconds], id: 0, + }); + await web3SendAsync({ + jsonrpc: '2.0', method: 'evm_mine', params: [], id: 0, + }); +} + +function snapshotEVM() { + return web3SendAsync({ + jsonrpc: '2.0', method: 'evm_snapshot', id: Date.now() + 1, + }).then(({ result }) => { snapshotId = result; }); +} + +function revertEVM() { + return web3SendAsync({ + jsonrpc: '2.0', method: 'evm_revert', params: [snapshotId], id: Date.now() + 1, + }); +} + contract('StakingPool', async (accounts) => { let pool; const miner = accounts[9]; @@ -32,6 +57,7 @@ contract('StakingPool', async (accounts) => { // None goes to maintainer. const poolMaintainerFeeRateBp = 0; const maxStakers = 16; + const maturityTime = 1893427200; // 2030-01-01 beforeEach(async () => { pool = await StakingPool.new( @@ -41,6 +67,7 @@ contract('StakingPool', async (accounts) => { minerFeeRateBp, poolMaintainerFeeRateBp, maxStakers, + maturityTime, ); }); @@ -213,7 +240,8 @@ contract('StakingPool', async (accounts) => { it('should handle maintainer fee correctly', async () => { // Start a new pool where the pool takes 12.5% while the miner takes 50%. - pool = await StakingPool.new(miner, admin, maintainer, minerFeeRateBp, 1250, maxStakers); + // eslint-disable-next-line max-len + pool = await StakingPool.new(miner, admin, maintainer, minerFeeRateBp, 1250, maxStakers, maturityTime); await pool.sendTransaction(txGen(accounts[0], toWei(1))); await forceSend(pool.address, toWei(8), treasury); // Stakes should be calculated correctly. @@ -237,7 +265,8 @@ contract('StakingPool', async (accounts) => { it('should handle no staker case with pool maintainer', async () => { // Start a new pool where the pool takes 12.5% while the miner takes 50%. - pool = await StakingPool.new(miner, admin, maintainer, minerFeeRateBp, 1250, maxStakers); + // eslint-disable-next-line max-len + pool = await StakingPool.new(miner, admin, maintainer, minerFeeRateBp, 1250, maxStakers, maturityTime); await forceSend(pool.address, toWei(10), treasury); // Miner should take 50/(50+12.5) * 10 = 8. @@ -247,4 +276,29 @@ contract('StakingPool', async (accounts) => { assert.equal((await pool.poolMaintainerFee()), 0); assert.equal((await pool.estimatePoolMaintainerFee()), toWei(2)); }); + + it('should handle maturity time correctluy', async () => { + await pool.sendTransaction(txGen(accounts[0], toWei(42))); + await forceSend(pool.address, toWei(8), treasury); + // State has not been updated. + let minerReward = await pool.minerReward(); + assert.equal(minerReward, 0); + await pool.withdrawStakes(toWei(10)); + // 50% of coinbase rewards goes to miner. + minerReward = await pool.minerReward(); + assert.equal(minerReward, toWei(4)); + + // After ten years. + await snapshotEVM(); + await addDaysOnEVM(3650); + await forceSend(pool.address, toWei(8), treasury); + // State has not been updated. + minerReward = await pool.minerReward(); + assert.equal(minerReward, toWei(4)); + await pool.withdrawStakes(toWei(10)); + // Rewards for miner shouldn't be updated. + minerReward = await pool.minerReward(); + assert.equal(minerReward, toWei(4)); + await revertEVM(); + }); }); From 1cf551fa0b1a057407b5eacaa73b21e0de8d678d Mon Sep 17 00:00:00 2001 From: jishankai Date: Mon, 6 Jul 2020 15:05:02 +0800 Subject: [PATCH 2/5] Distribute all rewards to stakers after maturity --- contracts/StakingPool.sol | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/contracts/StakingPool.sol b/contracts/StakingPool.sol index 4449131..142d207 100644 --- a/contracts/StakingPool.sol +++ b/contracts/StakingPool.sol @@ -160,26 +160,31 @@ contract StakingPool { if (dividend == 0) { return; } - uint256 feeRateBp = minerFeeRateBp + poolMaintainerFeeRateBp; - uint256 stakerPayout = dividend.mul(MAX_BP - feeRateBp).div(MAX_BP); - uint256 totalPaid = 0; - for (uint256 i = 0; i < stakers.length; i++) { - StakerInfo storage info = stakerInfo[stakers[i]]; - uint256 toPay = stakerPayout.mul(info.stakes).div(totalStakes); - totalPaid = totalPaid.add(toPay); - info.stakes += toPay; - } - - totalStakes = totalStakes.add(totalPaid); - - uint256 totalFee = dividend.sub(totalPaid); if (maturityTime + 24*60*60 > now) { // One extra day for miner's operation + uint256 feeRateBp = minerFeeRateBp + poolMaintainerFeeRateBp; + uint256 stakerPayout = dividend.mul(MAX_BP - feeRateBp).div(MAX_BP); + uint256 totalPaid = 0; + for (uint256 i = 0; i < stakers.length; i++) { + StakerInfo storage info = stakerInfo[stakers[i]]; + uint256 toPay = stakerPayout.mul(info.stakes).div(totalStakes); + totalPaid = totalPaid.add(toPay); + info.stakes = info.stakes.add(toPay); + } + + totalStakes = totalStakes.add(totalPaid); + + uint256 totalFee = dividend.sub(totalPaid); uint256 feeForMiner = totalFee.mul(minerFeeRateBp).div(feeRateBp); minerReward = minerReward.add(feeForMiner); uint256 feeForMaintainer = totalFee.sub(feeForMiner); poolMaintainerFee = poolMaintainerFee.add(feeForMaintainer); } else { - poolMaintainerFee = poolMaintainerFee.add(totalFee); + for (uint256 i = 0; i < stakers.length; i++) { + StakerInfo storage info = stakerInfo[stakers[i]]; + uint256 toPay = dividend.mul(info.stakes).div(totalStakes); + info.stakes = info.stakes.add(toPay); + } + totalStakes = totalStakes.add(dividend); } assert(balance >= totalStakes); } From e2731489ef5512acef6ef65c463d094f8df85244 Mon Sep 17 00:00:00 2001 From: jishankai Date: Tue, 7 Jul 2020 15:53:31 +0800 Subject: [PATCH 3/5] Use totalPaid --- contracts/StakingPool.sol | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/contracts/StakingPool.sol b/contracts/StakingPool.sol index 9890534..41da1e0 100644 --- a/contracts/StakingPool.sol +++ b/contracts/StakingPool.sol @@ -193,10 +193,10 @@ contract StakingPool { if (dividend == 0) { return; } + uint256 totalPaid = 0; if (maturityTime + 24*60*60 > now) { // One extra day for miner's operation uint256 feeRateBp = minerFeeRateBp + poolMaintainerFeeRateBp; uint256 stakerPayout = dividend.mul(MAX_BP - feeRateBp).div(MAX_BP); - uint256 totalPaid = 0; for (uint256 i = 0; i < stakers.length; i++) { StakerInfo storage info = stakerInfo[stakers[i]]; uint256 toPay = stakerPayout.mul(info.stakes).div(totalStakes); @@ -204,8 +204,6 @@ contract StakingPool { info.stakes = info.stakes.add(toPay); } - totalStakes = totalStakes.add(totalPaid); - uint256 totalFee = dividend.sub(totalPaid); uint256 feeForMiner = totalFee.mul(minerFeeRateBp).div(feeRateBp); minerReward = minerReward.add(feeForMiner); @@ -215,10 +213,11 @@ contract StakingPool { for (uint256 i = 0; i < stakers.length; i++) { StakerInfo storage info = stakerInfo[stakers[i]]; uint256 toPay = dividend.mul(info.stakes).div(totalStakes); + totalPaid = totalPaid.add(toPay); info.stakes = info.stakes.add(toPay); } - totalStakes = totalStakes.add(dividend); } + totalStakes = totalStakes.add(totalPaid); assert(balance >= totalStakes); } From af74f495ef85f688867cff1dd4300713119066ad Mon Sep 17 00:00:00 2001 From: jishankai Date: Mon, 13 Jul 2020 14:33:11 +0800 Subject: [PATCH 4/5] Simplify code --- contracts/StakingPool.sol | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/contracts/StakingPool.sol b/contracts/StakingPool.sol index 41da1e0..3ae8927 100644 --- a/contracts/StakingPool.sol +++ b/contracts/StakingPool.sol @@ -194,29 +194,29 @@ contract StakingPool { return; } uint256 totalPaid = 0; - if (maturityTime + 24*60*60 > now) { // One extra day for miner's operation - uint256 feeRateBp = minerFeeRateBp + poolMaintainerFeeRateBp; - uint256 stakerPayout = dividend.mul(MAX_BP - feeRateBp).div(MAX_BP); - for (uint256 i = 0; i < stakers.length; i++) { - StakerInfo storage info = stakerInfo[stakers[i]]; - uint256 toPay = stakerPayout.mul(info.stakes).div(totalStakes); - totalPaid = totalPaid.add(toPay); - info.stakes = info.stakes.add(toPay); - } + uint256 feeRateBp = minerFeeRateBp + poolMaintainerFeeRateBp; + if (maturityTime + 246060 <= now) { + feeRateBp = 0; + } + + uint256 stakerPayout = dividend.mul(MAX_BP - feeRateBp).div(MAX_BP); + for (uint256 i = 0; i < stakers.length; i++) { + StakerInfo storage info = stakerInfo[stakers[i]]; + uint256 toPay = stakerPayout.mul(info.stakes).div(totalStakes); + totalPaid = totalPaid.add(toPay); + info.stakes = info.stakes.add(toPay); + } + if (feeRateBp != 0) { uint256 totalFee = dividend.sub(totalPaid); + // For miner uint256 feeForMiner = totalFee.mul(minerFeeRateBp).div(feeRateBp); minerReward = minerReward.add(feeForMiner); + // For pool maintainer uint256 feeForMaintainer = totalFee.sub(feeForMiner); poolMaintainerFee = poolMaintainerFee.add(feeForMaintainer); - } else { - for (uint256 i = 0; i < stakers.length; i++) { - StakerInfo storage info = stakerInfo[stakers[i]]; - uint256 toPay = dividend.mul(info.stakes).div(totalStakes); - totalPaid = totalPaid.add(toPay); - info.stakes = info.stakes.add(toPay); - } } + totalStakes = totalStakes.add(totalPaid); assert(balance >= totalStakes); } From f5985f63b4406531e8502ba415a29fe34b98c6ed Mon Sep 17 00:00:00 2001 From: jishankai Date: Tue, 21 Jul 2020 12:20:18 +0800 Subject: [PATCH 5/5] Fix assert --- contracts/StakingPool.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/StakingPool.sol b/contracts/StakingPool.sol index 71828a4..243ab2c 100644 --- a/contracts/StakingPool.sol +++ b/contracts/StakingPool.sol @@ -214,6 +214,7 @@ contract StakingPool { totalPaid = totalPaid.add(toPay); info.stakes = info.stakes.add(toPay); } + totalStakes = totalStakes.add(totalPaid); if (feeRateBp != 0) { uint256 totalFee = dividend.sub(totalPaid); @@ -225,8 +226,7 @@ contract StakingPool { poolMaintainerFee = poolMaintainerFee.add(feeForMaintainer); } - totalStakes = totalStakes.add(totalPaid); - assert(balance >= totalStakes); + assert(balance >= totalStakes.add(minerReward).add(poolMaintainerFee)); } function getDividend(uint256 balance) private view returns (uint256) {