diff --git a/contracts/StakingPool.sol b/contracts/StakingPool.sol index 63bce3f..243ab2c 100644 --- a/contracts/StakingPool.sol +++ b/contracts/StakingPool.sol @@ -21,6 +21,7 @@ contract StakingPool { string public adminContactInfo; uint256 public totalStakes; uint256 public maxStakers; + uint256 public maturityTime; // Miner fee rate in basis point. address public miner; @@ -41,7 +42,8 @@ contract StakingPool { uint256 _minStakes, uint256 _minerFeeRateBp, uint256 _poolMaintainerFeeRateBp, - uint256 _maxStakers + uint256 _maxStakers, + uint256 _maturityTime ) public { @@ -51,6 +53,7 @@ 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; minerContactInfo = _minerContactInfo; admin = _admin; @@ -60,6 +63,7 @@ contract StakingPool { minerFeeRateBp = _minerFeeRateBp; poolMaintainerFeeRateBp = _poolMaintainerFeeRateBp; maxStakers = _maxStakers; + maturityTime = _maturityTime; // timestamp(seconds) } modifier onlyMiner() { @@ -197,24 +201,32 @@ contract StakingPool { if (dividend == 0) { return; } + uint256 totalPaid = 0; uint256 feeRateBp = minerFeeRateBp + poolMaintainerFeeRateBp; + if (maturityTime + 246060 <= now) { + feeRateBp = 0; + } + 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); - uint256 feeForMaintainer = totalFee.sub(feeForMiner); - poolMaintainerFee = poolMaintainerFee.add(feeForMaintainer); - minerReward = minerReward.add(feeForMiner); - assert(balance >= totalStakes); + 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); + } + + assert(balance >= totalStakes.add(minerReward).add(poolMaintainerFee)); } function getDividend(uint256 balance) private view returns (uint256) { diff --git a/test/StakingPool.test.js b/test/StakingPool.test.js index e759d02..cd013cd 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]; @@ -34,6 +59,7 @@ contract('StakingPool', async (accounts) => { // None goes to maintainer. const poolMaintainerFeeRateBp = 0; const maxStakers = 16; + const maturityTime = 1893427200; // 2030-01-01 const minStakes = toWei(1); beforeEach(async () => { @@ -47,6 +73,7 @@ contract('StakingPool', async (accounts) => { minerFeeRateBp, poolMaintainerFeeRateBp, maxStakers, + maturityTime, ); }); @@ -235,7 +262,7 @@ 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%. // eslint-disable-next-line max-len - pool = await StakingPool.new(miner, minerContactInfo, admin, adminContactInfo, maintainer, minStakes, minerFeeRateBp, 1250, maxStakers); + pool = await StakingPool.new(miner, minerContactInfo, admin, adminContactInfo, maintainer, minStakes, minerFeeRateBp, 1250, maxStakers, maturityTime); await pool.sendTransaction(txGen(accounts[0], toWei(1))); await forceSend(pool.address, toWei(8), treasury); // Stakes should be calculated correctly. @@ -260,7 +287,7 @@ 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%. // eslint-disable-next-line max-len - pool = await StakingPool.new(miner, minerContactInfo, admin, adminContactInfo, maintainer, minStakes, minerFeeRateBp, 1250, maxStakers); + pool = await StakingPool.new(miner, minerContactInfo, admin, adminContactInfo, maintainer, minStakes, minerFeeRateBp, 1250, maxStakers, maturityTime); await forceSend(pool.address, toWei(10), treasury); // Miner should take 50/(50+12.5) * 10 = 8. @@ -271,6 +298,31 @@ contract('StakingPool', async (accounts) => { assert.equal((await pool.estimatePoolMaintainerFee()), toWei(2)); }); + it('should handle maturity time correctly', 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(); + }); + it('should update miner and admin contact infomation correctly', async () => { assert.equal(((await pool.minerContactInfo())), minerContactInfo); assert.equal(((await pool.adminContactInfo())), adminContactInfo);