Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 21 additions & 9 deletions contracts/StakingPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -41,7 +42,8 @@ contract StakingPool {
uint256 _minStakes,
uint256 _minerFeeRateBp,
uint256 _poolMaintainerFeeRateBp,
uint256 _maxStakers
uint256 _maxStakers,
uint256 _maturityTime
)
public
{
Expand All @@ -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;
Expand All @@ -60,6 +63,7 @@ contract StakingPool {
minerFeeRateBp = _minerFeeRateBp;
poolMaintainerFeeRateBp = _poolMaintainerFeeRateBp;
maxStakers = _maxStakers;
maturityTime = _maturityTime; // timestamp(seconds)
}

modifier onlyMiner() {
Expand Down Expand Up @@ -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) {
Expand Down
56 changes: 54 additions & 2 deletions test/StakingPool.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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];
Expand All @@ -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 () => {
Expand All @@ -47,6 +73,7 @@ contract('StakingPool', async (accounts) => {
minerFeeRateBp,
poolMaintainerFeeRateBp,
maxStakers,
maturityTime,
);
});

Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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);
Expand Down