diff --git a/contracts/staking/stakeManager/StakeManager.sol b/contracts/staking/stakeManager/StakeManager.sol index dd4382f2f..80b1057cd 100644 --- a/contracts/staking/stakeManager/StakeManager.sol +++ b/contracts/staking/stakeManager/StakeManager.sol @@ -190,6 +190,14 @@ contract StakeManager is Governance Methods */ + function setMinBidStakeFraction(uint256 fraction) public onlyGovernance { + minBidStakeFraction = fraction; + } + + function setBidCooldown(uint256 cooldown) public onlyGovernance { + bidCooldown = cooldown; + } + function setDelegationEnabled(bool enabled) public onlyGovernance { delegationEnabled = enabled; } @@ -451,7 +459,7 @@ contract StakeManager is bytes memory signerPubkey ) public onlyWhenUnlocked { require(currentValidatorSetSize() < validatorThreshold, "no more slots"); - require(amount >= minDeposit, "not enough deposit"); + require(amount >= validatorState.amount.mul(minBidStakeFraction).div(MIN_BID_PRECISION), "not enough deposit"); _transferAndTopUp(user, msg.sender, heimdallFee, amount); _stakeFor(user, amount, acceptDelegation, signerPubkey); } diff --git a/contracts/staking/stakeManager/StakeManagerExtension.sol b/contracts/staking/stakeManager/StakeManagerExtension.sol index c74dbcca4..373f65a67 100644 --- a/contracts/staking/stakeManager/StakeManagerExtension.sol +++ b/contracts/staking/stakeManager/StakeManagerExtension.sol @@ -38,9 +38,9 @@ contract StakeManagerExtension is StakeManagerStorage, Initializable, StakeManag ); uint256 _currentEpoch = currentEpoch; - uint256 _replacementCoolDown = replacementCoolDown; + uint256 cooldown = replacementCoolDown; // when dynasty period is updated validators are in cooldown period - require(_replacementCoolDown == 0 || _replacementCoolDown <= _currentEpoch, "Cooldown period"); + require(cooldown == 0 || cooldown <= _currentEpoch, "Cooldown period"); // (auctionPeriod--dynasty)--(auctionPeriod--dynasty)--(auctionPeriod--dynasty) // if it's auctionPeriod then will get residue smaller then auctionPeriod // from (CurrentPeriod of validator )%(auctionPeriod--dynasty) @@ -57,9 +57,17 @@ contract StakeManagerExtension is StakeManagerStorage, Initializable, StakeManag perceivedStake = perceivedStake.add(validators[validatorId].delegatedAmount); Auction storage auction = validatorAuction[validatorId]; + + // do not allow bidding too often + cooldown = lastBidTimestamp[msg.sender]; + require(cooldown == 0 || cooldown < block.timestamp, "bid too often"); + uint256 currentAuctionAmount = auction.amount; - perceivedStake = Math.max(perceivedStake, currentAuctionAmount); + perceivedStake = Math.max( + validatorState.amount.mul(minBidStakeFraction).div(MIN_BID_PRECISION), + Math.max(perceivedStake, currentAuctionAmount) + ); require(perceivedStake < amount, "Must bid higher"); require(token.transferFrom(msg.sender, address(this), amount), "Transfer failed"); @@ -74,6 +82,7 @@ contract StakeManagerExtension is StakeManagerStorage, Initializable, StakeManag auction.user = msg.sender; auction.acceptDelegation = _acceptDelegation; auction.signerPubkey = _signerPubkey; + lastBidTimestamp[msg.sender] = block.timestamp + bidCooldown; logger.logStartAuction(validatorId, currentValidatorAmount, amount); } diff --git a/contracts/staking/stakeManager/StakeManagerStorage.sol b/contracts/staking/stakeManager/StakeManagerStorage.sol index 6b448a838..1cec59363 100644 --- a/contracts/staking/stakeManager/StakeManagerStorage.sol +++ b/contracts/staking/stakeManager/StakeManagerStorage.sol @@ -51,6 +51,7 @@ contract StakeManagerStorage is GovernanceLockable, RootChainable { uint256 constant REWARD_PRECISION = 10**25; uint256 internal constant INCORRECT_VALIDATOR_ID = 2**256 - 1; uint256 internal constant INITIALIZED_AMOUNT = 1; + uint256 constant CHK_REWARD_PRECISION = 100; IERC20 public token; address public registry; diff --git a/contracts/staking/stakeManager/StakeManagerStorageExtension.sol b/contracts/staking/stakeManager/StakeManagerStorageExtension.sol index dca5cd233..c615f7756 100644 --- a/contracts/staking/stakeManager/StakeManagerStorageExtension.sol +++ b/contracts/staking/stakeManager/StakeManagerStorageExtension.sol @@ -1,12 +1,13 @@ pragma solidity 0.5.17; contract StakeManagerStorageExtension { + uint256 public constant MIN_BID_PRECISION = 10000; // down to 0.0001% fractions + address public eventsHub; uint256 public rewardPerStake; address public extensionCode; address[] public signers; - uint256 constant CHK_REWARD_PRECISION = 100; uint256 public prevBlockInterval; // how much less reward per skipped checkpoint, 0 - 100% uint256 public rewardDecreasePerCheckpoint; @@ -14,4 +15,10 @@ contract StakeManagerStorageExtension { uint256 public maxRewardedCheckpoints; // increase / decrease value for faster or slower checkpoints, 0 - 100% uint256 public checkpointRewardDelta; + // do not prevent bidding for some time to incentivize early bidding + uint256 public bidCooldown; + // fraction of the total stake acting as a minimum for auction bidding, + uint256 public minBidStakeFraction; + // tracks when auction bid happened for the user + mapping(address => uint256) public lastBidTimestamp; } diff --git a/test/helpers/deployer.js b/test/helpers/deployer.js index a3bab0702..8b8c1a02f 100644 --- a/test/helpers/deployer.js +++ b/test/helpers/deployer.js @@ -171,6 +171,20 @@ class Deployer { stakeManager.contract.methods.updateCheckpointRewardParams(val1, val2, val3).encodeABI() ) } + + stakeManager.setBidCooldown = (cooldown) => { + return governance.update( + stakeManager.address, + stakeManager.contract.methods.setBidCooldown(cooldown).encodeABI() + ) + } + + stakeManager.setMinBidStakeFraction = (fraction) => { + return governance.update( + stakeManager.address, + stakeManager.contract.methods.setMinBidStakeFraction(fraction).encodeABI() + ) + } } async deployStakeManager(wallets) { diff --git a/test/units/staking/stakeManager/StakeManager.Staking.js b/test/units/staking/stakeManager/StakeManager.Staking.js index a753b86b0..aa943f024 100644 --- a/test/units/staking/stakeManager/StakeManager.Staking.js +++ b/test/units/staking/stakeManager/StakeManager.Staking.js @@ -6,6 +6,7 @@ import { } from '../../../helpers/utils.js' import { expectEvent, expectRevert, BN } from '@openzeppelin/test-helpers' import { wallets, walletAmounts, freshDeploy, approveAndStake } from '../deployment' +const { toWei } = web3.utils module.exports = function(accounts) { let owner = accounts[0] @@ -17,7 +18,7 @@ module.exports = function(accounts) { let _aproveAmount = aproveAmount || walletAmounts[user].amount let _stakeAmount = stakeAmount || walletAmounts[user].stakeAmount - await approveAndStake.call(this, { wallet, stakeAmount: _stakeAmount, approveAmount: _aproveAmount, noMinting, signer }) + return approveAndStake.call(this, { wallet, stakeAmount: _stakeAmount, approveAmount: _aproveAmount, noMinting, signer }) } } @@ -76,7 +77,7 @@ module.exports = function(accounts) { }) } - function testStake(user, userPubkey, amount, stakeAmount, validatorId, fee) { + function testStake({ user, userPubkey, amount, stakeAmount, validatorId, fee, totalStake }) { before('Approve', async function() { this.user = user this.fee = new BN(fee || this.defaultHeimdallFee) @@ -116,7 +117,7 @@ module.exports = function(accounts) { it('must have correct total staked balance', async function() { const stake = await this.stakeManager.currentValidatorSetTotalStake() - assertBigNumberEquality(stake, stakeAmount) + assertBigNumberEquality(stake, totalStake || stakeAmount) }) it(`must have validatorId == ${validatorId}`, async function() { @@ -174,21 +175,21 @@ module.exports = function(accounts) { describe('when stakes first time', function() { const amounts = walletAmounts[wallets[1].getAddressString()] - testStake( - wallets[1].getChecksumAddressString(), - wallets[1].getPublicKeyString(), - amounts.amount, - amounts.stakeAmount, - 1 - ) + testStake({ + user: wallets[1].getChecksumAddressString(), + userPubkey: wallets[1].getPublicKeyString(), + amount: amounts.amount, + stakeAmount: amounts.stakeAmount, + validatorId: 1 + }) }) describe('when stakes again', function() { testStakeRevert( wallets[1].getChecksumAddressString(), wallets[1].getPublicKeyString(), - web3.utils.toWei('200'), - web3.utils.toWei('200') + toWei('200'), + toWei('200') ) }) }) @@ -213,13 +214,13 @@ module.exports = function(accounts) { testStakeRevert( wallets[2].getChecksumAddressString(), wallets[2].getPublicKeyString(), - web3.utils.toWei('250'), - web3.utils.toWei('150') + toWei('250'), + toWei('150') ) }) describe('when reStakes while on going auction', function() { it('when auction is active', async function() { - let auctionBid = web3.utils.toWei('10000') + let auctionBid = toWei('10000') const auctionUser = wallets[4].getAddressString() await this.stakeToken.mint(auctionUser, auctionBid) await this.stakeToken.approve(this.stakeManager.address, auctionBid, { @@ -245,21 +246,21 @@ module.exports = function(accounts) { describe('when user stakes', function() { const amounts = walletAmounts[wallets[3].getAddressString()] - testStake( - wallets[3].getChecksumAddressString(), - wallets[3].getPublicKeyString(), - amounts.amount, - amounts.stakeAmount, - 1 - ) + testStake({ + user: wallets[3].getChecksumAddressString(), + userPubkey: wallets[3].getPublicKeyString(), + amount: amounts.amount, + stakeAmount: amounts.stakeAmount, + validatorId: 1 + }) }) describe('when other user stakes beyond validator threshold', function() { testStakeRevert( wallets[4].getChecksumAddressString(), wallets[4].getPublicKeyString(), - web3.utils.toWei('100'), - web3.utils.toWei('100'), + toWei('100'), + toWei('100'), true ) }) @@ -272,7 +273,7 @@ module.exports = function(accounts) { const _wallets = [wallets[1], wallets[2], wallets[3]] let expectedValidatorId = 1 for (const wallet of _wallets) { - await doStake(wallet, { approveAmount: web3.utils.toWei('100'), stakeAmount: web3.utils.toWei('100') }).call(this) + await doStake(wallet, { approveAmount: toWei('100'), stakeAmount: toWei('100') }).call(this) const validatorId = await this.stakeManager.getValidatorId(wallet.getAddressString()) assertBigNumberEquality(expectedValidatorId, validatorId) @@ -284,14 +285,14 @@ module.exports = function(accounts) { describe('stake with heimdall fee', function() { before(freshDeploy) - testStake( - wallets[0].getChecksumAddressString(), - wallets[0].getPublicKeyString(), - web3.utils.toWei('200'), - web3.utils.toWei('150'), - 1, - web3.utils.toWei('50') - ) + testStake({ + user: wallets[0].getChecksumAddressString(), + userPubkey: wallets[0].getPublicKeyString(), + amount: toWei('200'), + stakeAmount: toWei('150'), + validatorId: 1, + fee: toWei('50') + }) }) describe('when Alice stakes, change signer and stakes with old signer', function() { @@ -314,6 +315,33 @@ module.exports = function(accounts) { await expectRevert(doStake(wallets[3], { signer: AliceWallet.getPublicKeyString() }).call(this), 'Invalid signer') }) }) + + describe('minimum deposit', function() { + const MinBidFraction = '1000' // 10% + const CorrectDeposit = toWei('100.1') + before(freshDeploy) + before(doStake(wallets[1], { aproveAmount: toWei('1001'), stakeAmount: toWei('1000') })) + before(async function() { + await this.stakeManager.setMinBidStakeFraction(MinBidFraction) + }) + + testStakeRevert( + wallets[2].getChecksumAddressString(), + wallets[2].getPublicKeyString(), + toWei('100'), + toWei('1'), + true + ) + + testStake({ + user: wallets[2].getChecksumAddressString(), + userPubkey: wallets[2].getPublicKeyString(), + amount: toWei('200'), + stakeAmount: CorrectDeposit, + validatorId: '2', + totalStake: new BN(toWei('1000')).add(new BN(CorrectDeposit)) + }) + }) }) describe('unstake', function() { @@ -454,7 +482,7 @@ module.exports = function(accounts) { }) it('when unstakes during auction', async function() { - const amount = web3.utils.toWei('1200') + const amount = toWei('1200') const auctionUser = wallets[4].getAddressString() await this.stakeToken.mint(auctionUser, amount) @@ -580,7 +608,7 @@ module.exports = function(accounts) { const Alice = wallets[2] const Bob = wallets[3] const Eve = wallets[4] - const stakeAmount = web3.utils.toWei('100') + const stakeAmount = toWei('100') before('Alice stake', doStake(Alice, { noMinting: true, stakeAmount })) before('Bob stake', doStake(Bob, { noMinting: true, stakeAmount })) @@ -606,13 +634,13 @@ module.exports = function(accounts) { }) it('must have correct reward', async function() { - assertBigNumberEquality(this.reward, web3.utils.toWei('3000')) + assertBigNumberEquality(this.reward, toWei('3000')) }) it('must emit ClaimRewards', async function() { await expectEvent.inTransaction(this.receipt.tx, StakingInfo, 'ClaimRewards', { validatorId: this.validatorId, - amount: web3.utils.toWei('3000'), + amount: toWei('3000'), totalAmount: await this.stakeManager.totalRewardsLiquidated() }) }) @@ -635,13 +663,13 @@ module.exports = function(accounts) { }) it('must have correct reward', async function() { - assertBigNumberEquality(this.reward, web3.utils.toWei('3000')) + assertBigNumberEquality(this.reward, toWei('3000')) }) it('must emit ClaimRewards', async function() { await expectEvent.inTransaction(this.receipt.tx, StakingInfo, 'ClaimRewards', { validatorId: this.validatorId, - amount: web3.utils.toWei('3000'), + amount: toWei('3000'), totalAmount: await this.stakeManager.totalRewardsLiquidated() }) }) @@ -666,21 +694,21 @@ module.exports = function(accounts) { it('Eve must have correct rewards', async function() { const validatorId = await this.stakeManager.getValidatorId(Eve.getAddressString()) this.reward = await this.stakeManager.validatorReward(validatorId) - assertBigNumberEquality(this.reward, web3.utils.toWei('12000')) + assertBigNumberEquality(this.reward, toWei('12000')) }) }) }) }) describe('restake', function() { - const initialStake = web3.utils.toWei('1000') + const initialStake = toWei('1000') const initialStakers = [wallets[0], wallets[1]] function doDeploy(acceptDelegation) { return async function() { await prepareForTest(8, 8).call(this) - const checkpointReward = new BN(web3.utils.toWei('10000')) + const checkpointReward = new BN(toWei('10000')) await this.governance.update( this.stakeManager.address, @@ -710,7 +738,7 @@ module.exports = function(accounts) { this.validatorReward = checkpointReward.mul(new BN(100 - proposerBonus)).div(new BN(100)).mul(new BN(auctionPeriod - currentEpoch)) this.validatorId = '1' this.user = initialStakers[0].getAddressString() - this.amount = web3.utils.toWei('100') + this.amount = toWei('100') await this.stakeToken.mint(this.user, this.amount) await this.stakeToken.approve(this.stakeManager.address, this.amount, { diff --git a/test/units/staking/stakeManager/StakeManager.test.js b/test/units/staking/stakeManager/StakeManager.test.js index 53e54801f..17ad04002 100644 --- a/test/units/staking/stakeManager/StakeManager.test.js +++ b/test/units/staking/stakeManager/StakeManager.test.js @@ -186,11 +186,11 @@ contract('StakeManager', async function(accounts) { this.stakeManager.contract.methods.setStakingToken(this.stakeToken.address).encodeABI() ) - await this.stakeToken.mint(this.stakeManager.address, web3.utils.toWei('10000000')) + await this.stakeToken.mint(this.stakeManager.address, toWei('10000000')) this.validatorId = '1' this.validatorUser = wallets[0] - this.stakeAmount = new BN(web3.utils.toWei('100')) + this.stakeAmount = new BN(toWei('100')) await approveAndStake.call(this, { wallet: this.validatorUser, stakeAmount: this.stakeAmount, acceptDelegation: true }) @@ -199,7 +199,7 @@ contract('StakeManager', async function(accounts) { this.user = wallets[2].getChecksumAddressString() - const approveAmount = web3.utils.toWei('20000') + const approveAmount = toWei('20000') await this.stakeToken.mint( this.user, approveAmount @@ -252,7 +252,7 @@ contract('StakeManager', async function(accounts) { describe('after commision rate changed', function() { it('Alice must purchase voucher', async function() { - await buyVoucher(this.validatorContract, web3.utils.toWei('100'), this.user) + await buyVoucher(this.validatorContract, toWei('100'), this.user) }) it('1 checkpoint must be commited', async function() { @@ -260,7 +260,7 @@ contract('StakeManager', async function(accounts) { }) it('liquid rewards must be correct', async function() { - assertBigNumberEquality(await this.validatorContract.getLiquidRewards(this.user), web3.utils.toWei('2250')) + assertBigNumberEquality(await this.validatorContract.getLiquidRewards(this.user), toWei('2250')) }) }) }) @@ -306,21 +306,21 @@ contract('StakeManager', async function(accounts) { await buyVoucher(this.validatorContract, this.stakeAmount, this.user) }) // get 25% of checkpoint rewards - testAfterComissionChange(web3.utils.toWei('2250'), '100') + testAfterComissionChange(toWei('2250'), '100') }) testCommisionRate('50', '100') describe('after commision rate changed', function() { // get 0% of checkpoint rewards - testAfterComissionChange(web3.utils.toWei('9000'), '100') + testAfterComissionChange(toWei('9000'), '100') }) testCommisionRate('100', '0') describe('after commision rate changed', function() { // get only 50% of checkpoint rewards - testAfterComissionChange(web3.utils.toWei('13500'), '100') + testAfterComissionChange(toWei('13500'), '100') }) }) @@ -352,7 +352,7 @@ contract('StakeManager', async function(accounts) { describe('updateValidatorDelegation', function() { let staker = wallets[1] - let stakeAmount = web3.utils.toWei('100') + let stakeAmount = toWei('100') function doDeploy(acceptDelegation) { before('Fresh deploy', freshDeploy) @@ -454,9 +454,9 @@ contract('StakeManager', async function(accounts) { describe('proposer bonus must be rewarded to the proposer without distribution to the delegators', function() { const delegator = wallets[1].getChecksumAddressString() const stakers = [ - { wallet: wallets[2], stake: new BN(web3.utils.toWei('100')) }, - { wallet: wallets[4], stake: new BN(web3.utils.toWei('100')) }, - { wallet: wallets[3], stake: new BN(web3.utils.toWei('100')) } + { wallet: wallets[2], stake: new BN(toWei('100')) }, + { wallet: wallets[4], stake: new BN(toWei('100')) }, + { wallet: wallets[3], stake: new BN(toWei('100')) } ] const signers = stakers.map(x => x.wallet) @@ -491,9 +491,9 @@ contract('StakeManager', async function(accounts) { const validatorWallet = wallets[2] const validatorId = '1' const stakers = [ - { wallet: validatorWallet, stake: new BN(web3.utils.toWei('200')) }, - { wallet: wallets[4], stake: new BN(web3.utils.toWei('100')) }, - { wallet: wallets[3], stake: new BN(web3.utils.toWei('200')) } + { wallet: validatorWallet, stake: new BN(toWei('200')) }, + { wallet: wallets[4], stake: new BN(toWei('100')) }, + { wallet: wallets[3], stake: new BN(toWei('200')) } ] const signers = stakers.map(x => x.wallet) @@ -518,9 +518,9 @@ contract('StakeManager', async function(accounts) { const validatorWallet = wallets[4] const validatorId = '2' const stakers = [ - { wallet: wallets[2], stake: new BN(web3.utils.toWei('200')) }, - { wallet: validatorWallet, stake: new BN(web3.utils.toWei('100')) }, - { wallet: wallets[3], stake: new BN(web3.utils.toWei('200')) } + { wallet: wallets[2], stake: new BN(toWei('200')) }, + { wallet: validatorWallet, stake: new BN(toWei('100')) }, + { wallet: wallets[3], stake: new BN(toWei('200')) } ] const signers = stakers.map(x => x.wallet) @@ -543,9 +543,9 @@ contract('StakeManager', async function(accounts) { describe('when validator signs twice and sends his 2nd signature out of order', function() { let stakers = [ - { wallet: wallets[2], stake: new BN(web3.utils.toWei('100')) }, - { wallet: wallets[4], stake: new BN(web3.utils.toWei('100')) }, - { wallet: wallets[3], stake: new BN(web3.utils.toWei('1000')) } + { wallet: wallets[2], stake: new BN(toWei('100')) }, + { wallet: wallets[4], stake: new BN(toWei('100')) }, + { wallet: wallets[3], stake: new BN(toWei('1000')) } ] const signers = stakers.map(x => x.wallet) @@ -566,8 +566,8 @@ contract('StakeManager', async function(accounts) { describe('when validators sign several times', function() { const stakers = [ - { wallet: wallets[2], stake: new BN(web3.utils.toWei('100')) }, - { wallet: wallets[3], stake: new BN(web3.utils.toWei('200')) } + { wallet: wallets[2], stake: new BN(toWei('100')) }, + { wallet: wallets[3], stake: new BN(toWei('200')) } ] const signers = stakers.map(x => x.wallet) @@ -595,7 +595,7 @@ contract('StakeManager', async function(accounts) { for (let i = 0; i < 100; ++i) { stakers.push({ wallet: w[i], - stake: new BN(web3.utils.toWei('1')) + stake: new BN(toWei('1')) }) } @@ -607,8 +607,8 @@ contract('StakeManager', async function(accounts) { describe('when 2 validators stakes, block interval 1, 1 epoch', function() { const stakers = [ - { wallet: wallets[2], stake: new BN(web3.utils.toWei('100')) }, - { wallet: wallets[3], stake: new BN(web3.utils.toWei('200')) } + { wallet: wallets[2], stake: new BN(toWei('100')) }, + { wallet: wallets[3], stake: new BN(toWei('200')) } ] prepareToTest(stakers) @@ -638,9 +638,9 @@ contract('StakeManager', async function(accounts) { describe('when 3 validators stake', function() { let stakers = [ - { wallet: wallets[2], stake: new BN(web3.utils.toWei('100')) }, - { wallet: wallets[3], stake: new BN(web3.utils.toWei('200')) }, - { wallet: wallets[4], stake: new BN(web3.utils.toWei('300')) } + { wallet: wallets[2], stake: new BN(toWei('100')) }, + { wallet: wallets[3], stake: new BN(toWei('200')) }, + { wallet: wallets[4], stake: new BN(toWei('300')) } ] function runTests(checkpointBlockInterval, blockInterval, epochs, expectedRewards) { @@ -780,14 +780,14 @@ contract('StakeManager', async function(accounts) { describe('when 3 validators stake but only 1 signs', function() { let stakers = [ - { wallet: wallets[2], stake: new BN(web3.utils.toWei('1000')) }, - { wallet: wallets[3], stake: new BN(web3.utils.toWei('100')) }, - { wallet: wallets[4], stake: new BN(web3.utils.toWei('100')) } + { wallet: wallets[2], stake: new BN(toWei('1000')) }, + { wallet: wallets[3], stake: new BN(toWei('100')) }, + { wallet: wallets[4], stake: new BN(toWei('100')) } ] prepareToTest(stakers, 1) testCheckpointing(stakers, [stakers[0].wallet], 1, 1, { - [stakers[0].wallet.getAddressString()]: web3.utils.toWei('7500'), + [stakers[0].wallet.getAddressString()]: toWei('7500'), [stakers[1].wallet.getAddressString()]: '0', [stakers[2].wallet.getAddressString()]: '0' }) @@ -795,13 +795,13 @@ contract('StakeManager', async function(accounts) { describe('when 7 validators stake but only 1 signs', function() { let stakers = [ - { wallet: wallets[2], stake: new BN(web3.utils.toWei('10000')) }, - { wallet: wallets[3], stake: new BN(web3.utils.toWei('100')) }, - { wallet: wallets[4], stake: new BN(web3.utils.toWei('100')) }, - { wallet: wallets[5], stake: new BN(web3.utils.toWei('100')) }, - { wallet: wallets[6], stake: new BN(web3.utils.toWei('100')) }, - { wallet: wallets[7], stake: new BN(web3.utils.toWei('100')) }, - { wallet: wallets[8], stake: new BN(web3.utils.toWei('100')) } + { wallet: wallets[2], stake: new BN(toWei('10000')) }, + { wallet: wallets[3], stake: new BN(toWei('100')) }, + { wallet: wallets[4], stake: new BN(toWei('100')) }, + { wallet: wallets[5], stake: new BN(toWei('100')) }, + { wallet: wallets[6], stake: new BN(toWei('100')) }, + { wallet: wallets[7], stake: new BN(toWei('100')) }, + { wallet: wallets[8], stake: new BN(toWei('100')) } ] prepareToTest(stakers, 1) @@ -819,7 +819,7 @@ contract('StakeManager', async function(accounts) { describe('when payload is invalid', function() { beforeEach(freshDeploy) beforeEach('Prepare to test', async function() { - this.amount = new BN(web3.utils.toWei('200')) + this.amount = new BN(toWei('200')) this.wallets = [wallets[2]] this.voteData = 'dummyData' this.stateRoot = utils.bufferToHex(utils.keccak256('stateRoot')) @@ -877,7 +877,7 @@ contract('StakeManager', async function(accounts) { }) describe('with votes', function() { - const amount = new BN(web3.utils.toWei('200')) + const amount = new BN(toWei('200')) async function feeCheckpointWithVotes(validatorId, start, end, votes, _sigPrefix, proposer) { let tree = await buildTreeFee(this.validators, this.accumulatedFees, this.checkpointIndex) @@ -929,7 +929,7 @@ contract('StakeManager', async function(accounts) { describe('Deploying and staking with 4 validators...', async function() { const AliceValidatorId = 1 - const firstFeeToClaim = new BN(web3.utils.toWei('25')) + const firstFeeToClaim = new BN(toWei('25')) beforeEach(function() { this.trees = [] @@ -1042,7 +1042,7 @@ contract('StakeManager', async function(accounts) { async function doDeploy() { await freshDeploy.call(this) - const amount = web3.utils.toWei('200') + const amount = toWei('200') for (const wallet of w) { await approveAndStake.call(this, { wallet, stakeAmount: amount }) } @@ -1086,8 +1086,8 @@ contract('StakeManager', async function(accounts) { { wallet: wallets[5] }, { wallet: wallets[3] } ], [wallets[5], wallets[0]], 1, 1, { - [wallets[5].getAddressString()]: web3.utils.toWei('4500'), - [wallets[3].getAddressString()]: web3.utils.toWei('4500') + [wallets[5].getAddressString()]: toWei('4500'), + [wallets[3].getAddressString()]: toWei('4500') }) }) @@ -1338,7 +1338,7 @@ contract('StakeManager', async function(accounts) { async function doDeploy() { await freshDeploy.call(this) - this.amount = new BN(web3.utils.toWei('200')) + this.amount = new BN(toWei('200')) this.totalStaked = new BN(0) for (const wallet of _wallets) { @@ -1435,8 +1435,8 @@ contract('StakeManager', async function(accounts) { describe('topUpForFee', function() { const wallet = wallets[1] const validatorUser = wallet.getChecksumAddressString() - const amount = web3.utils.toWei('200') - const fee = new BN(web3.utils.toWei('50')) + const amount = toWei('200') + const fee = new BN(toWei('50')) async function doDeploy() { await freshDeploy.call(this) @@ -1507,7 +1507,7 @@ contract('StakeManager', async function(accounts) { runTopUpTests(async function() { await doDeploy.call(this) - const mintAmount = web3.utils.toWei('10000') + const mintAmount = toWei('10000') await this.stakeToken.mint(user, mintAmount) await this.stakeToken.approve(this.stakeManager.address, new BN(mintAmount), { from: user @@ -1558,7 +1558,7 @@ contract('StakeManager', async function(accounts) { }) describe('claimFee', function() { - const amount = new BN(web3.utils.toWei('200')) + const amount = new BN(toWei('200')) async function feeCheckpoint(validatorId, start, end, proposer) { let tree = await buildTreeFee(this.validators, this.accumulatedFees, this.checkpointIndex) @@ -1674,9 +1674,9 @@ contract('StakeManager', async function(accounts) { describe('when Alice topups once and claims 2 times', async function() { const AliceValidatorId = 1 - const totalFee = new BN(web3.utils.toWei('100')) - const firstFeeToClaim = new BN(web3.utils.toWei('25')) - const secondFeeToClaim = new BN(web3.utils.toWei('100')) + const totalFee = new BN(toWei('100')) + const firstFeeToClaim = new BN(toWei('25')) + const secondFeeToClaim = new BN(toWei('100')) before(function() { this.trees = [] @@ -1737,7 +1737,7 @@ contract('StakeManager', async function(accounts) { before(function() { this.user = this.validatorsWallets[AliceValidatorId].getChecksumAddressString() this.validatorsCount = 2 - this.fee = new BN(web3.utils.toWei('50')) + this.fee = new BN(toWei('50')) this.claimedFee = this.fee this.topUpFeeFor = { [this.user]: this.fee @@ -1768,8 +1768,8 @@ contract('StakeManager', async function(accounts) { // If i want to be able to withdraw fee from previous checkpoint - should i commit previous tree root? describe.skip('when Alice top ups 2 times with different values', function() { const AliceValidatorId = 1 - const firstFee = new BN(web3.utils.toWei('50')) - const secondFee = new BN(web3.utils.toWei('30')) + const firstFee = new BN(toWei('50')) + const secondFee = new BN(toWei('30')) describe('when topup', function() { before(function() { @@ -1826,7 +1826,7 @@ contract('StakeManager', async function(accounts) { describe('reverts', function() { beforeEach(function() { this.validatorsCount = 2 - this.fee = new BN(web3.utils.toWei('50')) + this.fee = new BN(toWei('50')) this.validatorId = 1 }) @@ -1877,64 +1877,182 @@ contract('StakeManager', async function(accounts) { }) describe('startAuction', function() { - const _initialStakers = [wallets[1], wallets[2]] - const initialStakeAmount = web3.utils.toWei('200') + const InitialStakers = [wallets[1], wallets[2]] + const InitialStakeAmount = toWei('200') + const AliceAndBobBalance = toWei('100000') + + const Alice = wallets[3] + const Bob = wallets[4] + + const AliceBidAmount = toWei('1200') + const BobBidAmount = toWei('1250') + const BidCooldown = 600 // 10 minutes async function doDeploy() { await prepareForTest(8, 10).call(this) - for (const wallet of _initialStakers) { - await approveAndStake.call(this, { wallet, stakeAmount: initialStakeAmount }) + for (const wallet of InitialStakers) { + await approveAndStake.call(this, { wallet, stakeAmount: InitialStakeAmount }) } // cooldown period let auctionPeriod = (await this.stakeManager.auctionPeriod()).toNumber() let currentEpoch = (await this.stakeManager.currentEpoch()).toNumber() for (let i = currentEpoch; i <= auctionPeriod + (await this.stakeManager.dynasty()).toNumber(); i++) { - await checkPoint(_initialStakers, this.rootChainOwner, this.stakeManager) + await checkPoint(InitialStakers, this.rootChainOwner, this.stakeManager) } - this.amount = web3.utils.toWei('500') - await this.stakeToken.approve(this.stakeManager.address, this.amount, { - from: wallets[3].getAddressString() + // prepare Alice nad Bob to bidding + await this.stakeToken.mint(Alice.getAddressString(), AliceAndBobBalance) + await this.stakeToken.approve(this.stakeManager.address, AliceAndBobBalance, { + from: Alice.getAddressString() + }) + + await this.stakeToken.mint(Bob.getAddressString(), AliceAndBobBalance) + await this.stakeToken.approve(this.stakeManager.address, AliceAndBobBalance, { + from: Bob.getAddressString() }) + + this.aliceOldBalance = await this.stakeToken.balanceOf(Alice.getAddressString()) + this.bobOldBalance = await this.stakeToken.balanceOf(Bob.getAddressString()) + + this.validatorId = '1' + this.initialStakeAmount = InitialStakeAmount + + await this.stakeManager.setBidCooldown(BidCooldown) } describe('Alice and Bob bid', function() { - const Alice = wallets[3] - const Bob = wallets[4] + before('deploy', doDeploy) - let aliceBidAmount = web3.utils.toWei('1200') - let bobBidAmount = web3.utils.toWei('1250') + describe('when Alice bids', function() { + before(function() { + this.userOldBalance = this.aliceOldBalance + }) + testStartAuction(Alice.getChecksumAddressString(), Alice.getPrivateKeyString(), AliceBidAmount) + }) - before('deploy', doDeploy) + describe('when Bob bids', function() { + before(function() { + this.userOldBalance = this.bobOldBalance + }) + + testStartAuction(Bob.getChecksumAddressString(), Bob.getPublicKeyString(), BobBidAmount) + + it('Alice must get her bid back', async function() { + const currentBalance = await this.stakeToken.balanceOf(Alice.getAddressString()) + assertBigNumberEquality(this.aliceOldBalance, currentBalance) + }) + }) + }) + + describe('when Alice and Bob participate in the active bidding', function() { + describe('when Alice start the auction', function() { + before('deploy', doDeploy) + before('initial bids', async function() { + await this.stakeManager.startAuction(this.validatorId, AliceBidAmount, false, Alice.getPrivateKeyString(), { + from: Alice.getChecksumAddressString() + }) + await this.stakeManager.startAuction(this.validatorId, BobBidAmount, false, Bob.getPrivateKeyString(), { + from: Bob.getChecksumAddressString() + }) + }) + + describe('when Alice outbids Bob immediately', function() { + it('reverts', async function() { + await expectRevert( + this.stakeManager.startAuction(this.validatorId, new BN(BobBidAmount).add(new BN('1')), false, Alice.getPrivateKeyString(), { + from: Alice.getChecksumAddressString() + }), + 'bid too often' + ) + }) + }) + }) + }) + + describe('when Alice tries to start and confirm at the last epoch of the auction', async function() { + const amount = toWei('300') + const validatorId = 1 + + before(doDeploy) before(async function() { - await this.stakeToken.mint(Alice.getAddressString(), aliceBidAmount) - await this.stakeToken.approve(this.stakeManager.address, aliceBidAmount, { - from: Alice.getAddressString() + await this.stakeManager.advanceEpoch(1) + await this.stakeManager.startAuction(validatorId, amount, false, wallets[3].getPublicKeyString(), { + from: wallets[3].getAddressString() + }) + await this.stakeToken.approve(this.stakeManager.address, toWei('1'), { + from: wallets[3].getAddressString() }) + }) - await this.stakeToken.mint(Bob.getAddressString(), bobBidAmount) - await this.stakeToken.approve(this.stakeManager.address, bobBidAmount, { - from: Bob.getAddressString() + describe('when confirming auction too soon', function() { + it('reverts', async function() { + await expectRevert(this.stakeManager.confirmAuctionBid( + this.validatorId, + toWei('1'), + { + from: wallets[3].getAddressString() + } + ), 'Not allowed before auctionPeriod') }) + }) - this.userOldBalance = await this.stakeToken.balanceOf(Alice.getAddressString()) - this.bobOldBalance = await this.stakeToken.balanceOf(Bob.getAddressString()) + describe('when confirming auction on time', function() { + before(async function() { + await this.stakeManager.advanceEpoch(1) + }) - this.validatorId = '1' - this.initialStakeAmount = initialStakeAmount + it('should confirm auction', async function() { + await this.stakeManager.confirmAuctionBid( + this.validatorId, + toWei('1'), + { + from: wallets[3].getAddressString() + } + ) + }) + + it('should become validator', async function() { + assert.ok(!(await this.stakeManager.isValidator(this.validatorId))) + }) }) + }) - describe('when Alice bids', function() { - testStartAuction(Alice.getChecksumAddressString(), Alice.getPrivateKeyString(), aliceBidAmount) + describe('Bob uses a minimum bid', function() { + const TargetValidatorId = '3' + const MinBidFraction = '1000' // 10% + + before('deploy', doDeploy) + before(async function() { + // add 1 small validator + await approveAndStake.call(this, { wallet: wallets[3], stakeAmount: toWei('1') }) + + let auctionPeriod = (await this.stakeManager.auctionPeriod()).toNumber() + let currentEpoch = (await this.stakeManager.currentEpoch()).toNumber() + for (let i = currentEpoch; i <= auctionPeriod + (await this.stakeManager.dynasty()).toNumber(); i++) { + await checkPoint(InitialStakers, this.rootChainOwner, this.stakeManager) + } + + await this.stakeManager.setMinBidStakeFraction(MinBidFraction) }) - describe('when Bob bids', function() { - testStartAuction(Bob.getChecksumAddressString(), Bob.getPublicKeyString(), bobBidAmount) + describe('when bid hasn\'t reached a minimum total stake fraction', function() { + it('revert', async function() { + await expectRevert(this.stakeManager.startAuction(TargetValidatorId, toWei('2'), false, wallets[4].getPrivateKeyString(), { + from: wallets[4].getAddressString() + }), 'Must bid higher') + }) + }) - it('Alice must get her bid back', async function() { - const currentBalance = await this.stakeToken.balanceOf(Alice.getAddressString()) - assertBigNumberEquality(this.userOldBalance, currentBalance) + describe('when bid is a minimum fraction of total stake', function() { + it('should bid', async function() { + const { amount: totalStake } = await this.stakeManager.validatorState() + const minBidFractionPrecision = await this.stakeManager.MIN_BID_PRECISION() + const bid = new BN(totalStake).mul(new BN(MinBidFraction)).div(new BN(minBidFractionPrecision)) + + await this.stakeManager.startAuction(TargetValidatorId, bid.add(new BN('1')), false, wallets[4].getPrivateKeyString(), { + from: wallets[4].getAddressString() + }) }) }) }) @@ -1942,70 +2060,45 @@ contract('StakeManager', async function(accounts) { describe('reverts', function() { beforeEach('deploy', doDeploy) + const amount = toWei('300') + it('when bid during non-auction period', async function() { let auctionPeriod = await this.stakeManager.auctionPeriod() await this.stakeManager.advanceEpoch((auctionPeriod).toNumber()) - await expectRevert(this.stakeManager.startAuction(1, this.amount, false, wallets[3].getPrivateKeyString(), { + await expectRevert(this.stakeManager.startAuction(1, amount, false, wallets[3].getPrivateKeyString(), { from: wallets[3].getAddressString() }), 'Invalid auction period') }) - it('when trying to start and confirm in last epoch', async function() { - this.validatorId = 1 - await this.stakeManager.advanceEpoch(1) - await this.stakeManager.startAuction(this.validatorId, this.amount, false, wallets[3].getPublicKeyString(), { - from: wallets[3].getAddressString() - }) - await this.stakeToken.approve(this.stakeManager.address, web3.utils.toWei('1'), { - from: wallets[3].getAddressString() - }) - await expectRevert(this.stakeManager.confirmAuctionBid( - this.validatorId, - web3.utils.toWei('1'), - { - from: wallets[3].getAddressString() - } - ), 'Not allowed before auctionPeriod') - await this.stakeManager.advanceEpoch(1) - await this.stakeManager.confirmAuctionBid( - this.validatorId, - web3.utils.toWei('1'), - { - from: wallets[3].getAddressString() - } - ) - assert.ok(!(await this.stakeManager.isValidator(this.validatorId))) - }) - it('when bid during replacement cooldown', async function() { await this.governance.update( this.stakeManager.address, this.stakeManager.contract.methods.updateDynastyValue('7').encodeABI() ) - await expectRevert(this.stakeManager.startAuction(1, this.amount, false, wallets[3].getPrivateKeyString(), { + await expectRevert(this.stakeManager.startAuction(1, amount, false, wallets[3].getPrivateKeyString(), { from: wallets[3].getAddressString() }), 'Cooldown period') }) it('when bid on unstaking validator', async function() { - await this.stakeManager.unstake(1, { from: _initialStakers[0].getAddressString() }) - await expectRevert(this.stakeManager.startAuction(1, this.amount, false, wallets[3].getPrivateKeyString(), { + await this.stakeManager.unstake(1, { from: InitialStakers[0].getAddressString() }) + await expectRevert(this.stakeManager.startAuction(1, amount, false, wallets[3].getPrivateKeyString(), { from: wallets[3].getAddressString() }), 'Invalid validator for an auction') }) it('when restake on unstaking validator', async function() { - await this.stakeManager.unstake(1, { from: _initialStakers[0].getAddressString() }) - await expectRevert(this.stakeManager.restake(1, this.amount, false, { - from: _initialStakers[0].getAddressString() + await this.stakeManager.unstake(1, { from: InitialStakers[0].getAddressString() }) + await expectRevert(this.stakeManager.restake(1, amount, false, { + from: InitialStakers[0].getAddressString() }), 'No restaking') }) // since all the rewards are given in unstake already it('Must not transfer any rewards after unstaking', async function() { - await this.stakeManager.unstake(1, { from: _initialStakers[0].getAddressString() }) - const receipt = await this.stakeManager.withdrawRewards(1, { from: _initialStakers[0].getAddressString() }) + await this.stakeManager.unstake(1, { from: InitialStakers[0].getAddressString() }) + const receipt = await this.stakeManager.withdrawRewards(1, { from: InitialStakers[0].getAddressString() }) await expectEvent.inTransaction(receipt.tx, StakingInfo, 'ClaimRewards', { validatorId: '1', amount: '0', @@ -2014,13 +2107,13 @@ contract('StakeManager', async function(accounts) { }) it('when validatorId is invalid', async function() { - await expectRevert.unspecified(this.stakeManager.startAuction(0, this.amount, false, wallets[3].getPrivateKeyString(), { + await expectRevert.unspecified(this.stakeManager.startAuction(0, amount, false, wallets[3].getPrivateKeyString(), { from: wallets[3].getAddressString() })) }) it('when bid is too low', async function() { - await expectRevert(this.stakeManager.startAuction(1, web3.utils.toWei('100'), false, wallets[3].getPrivateKeyString(), { + await expectRevert(this.stakeManager.startAuction(1, toWei('100'), false, wallets[3].getPrivateKeyString(), { from: wallets[3].getAddressString() }), 'Must bid higher') }) @@ -2029,8 +2122,8 @@ contract('StakeManager', async function(accounts) { describe('confirmAuctionBid', function() { const initialStakers = [wallets[1], wallets[2]] - const bidAmount = new BN(web3.utils.toWei('1200')) - const initialStakeAmount = web3.utils.toWei('200') + const bidAmount = new BN(toWei('1200')) + const initialStakeAmount = toWei('200') function doDeploy(skipAuctionPeriod = true) { return async function() { @@ -2044,7 +2137,7 @@ contract('StakeManager', async function(accounts) { await approveAndStake.call(this, { wallet, stakeAmount: initialStakeAmount }) } - this.amount = web3.utils.toWei('500') + this.amount = toWei('500') await this.stakeToken.approve(this.stakeManager.address, this.amount, { from: wallets[3].getAddressString() }) @@ -2077,7 +2170,7 @@ contract('StakeManager', async function(accounts) { } describe('when last auctioner is not validator', function() { - const heimdallFee = web3.utils.toWei('100') + const heimdallFee = toWei('100') function prepareToTest() { before(async function() { @@ -2116,7 +2209,7 @@ contract('StakeManager', async function(accounts) { describe('when validator has more stake then last bid', function() { prepareToTest() before(async function() { - let restakeAmount = web3.utils.toWei('10000') + let restakeAmount = toWei('10000') await this.stakeToken.mint(this.prevValidatorAddr, restakeAmount) await this.stakeToken.approve(this.stakeManager.address, restakeAmount, { from: this.prevValidatorAddr @@ -2202,8 +2295,8 @@ contract('StakeManager', async function(accounts) { const validatorUserAddr = wallets[4].getChecksumAddressString() const auctionValidatorAddr = wallets[5].getChecksumAddressString() const auctionValidatorPubKey = wallets[5].getPublicKeyString() - const stakeAmount = web3.utils.toWei('1250') - const bidAmount = web3.utils.toWei('2555') + const stakeAmount = toWei('1250') + const bidAmount = toWei('2555') function doDeploy() { return async function() { @@ -2217,7 +2310,7 @@ contract('StakeManager', async function(accounts) { before('fresh deploy', doDeploy()) before(async function() { - await this.stakeToken.mint(this.stakeManager.address, web3.utils.toWei('1000000'))// rewards amount + await this.stakeToken.mint(this.stakeManager.address, toWei('1000000'))// rewards amount await approveAndStake.call(this, { wallet: validatorUser, stakeAmount, acceptDelegation: true }) @@ -2287,8 +2380,8 @@ contract('StakeManager', async function(accounts) { describe('stopAuctions', function() { const initialStakers = [wallets[1], wallets[2]] - const stakeAmount = web3.utils.toWei('1250') - const bidAmount = web3.utils.toWei('1350') + const stakeAmount = toWei('1250') + const bidAmount = toWei('1350') const bidder = wallets[3].getChecksumAddressString() const bidderPubKey = wallets[3].getPublicKeyString() @@ -2296,7 +2389,7 @@ contract('StakeManager', async function(accounts) { await prepareForTest(8, 10).call(this) for (const wallet of initialStakers) { - await approveAndStake.call(this, { wallet, stakeAmount, approveAmount: web3.utils.toWei('12500') }) + await approveAndStake.call(this, { wallet, stakeAmount, approveAmount: toWei('12500') }) } } @@ -2337,11 +2430,11 @@ contract('StakeManager', async function(accounts) { describe('stake migration', function() { const initialStakers = [wallets[1], wallets[2], wallets[3], wallets[4], wallets[5], wallets[6], wallets[7], wallets[8], wallets[9]] - const stakeAmount = web3.utils.toWei('1250') + const stakeAmount = toWei('1250') const stakeAmountBN = new BN(stakeAmount) - const delegationAmount = web3.utils.toWei('150') + const delegationAmount = toWei('150') const delegationAmountBN = new BN(delegationAmount) - const migrationAmount = web3.utils.toWei('100') + const migrationAmount = toWei('100') const migrationAmountBN = new BN(migrationAmount) async function prepareForTest() {