diff --git a/contracts/staking/stakeManager/StakeManager.sol b/contracts/staking/stakeManager/StakeManager.sol index 765fd68a7..f916fcd90 100644 --- a/contracts/staking/stakeManager/StakeManager.sol +++ b/contracts/staking/stakeManager/StakeManager.sol @@ -303,7 +303,6 @@ contract StakeManager is IStakeManager, StakeManagerStorage, Initializable { uint256 amount, address delegator ) external returns (bool) { - require(delegationEnabled, "Delegation is disabled"); require(validators[validatorId].contractAddress == msg.sender, "Invalid contract address"); return token.transferFrom(delegator, address(this), amount); } @@ -427,6 +426,11 @@ contract StakeManager is IStakeManager, StakeManagerStorage, Initializable { } function updateValidatorState(uint256 validatorId, int256 amount) public { + if (amount > 0) { + // deposit during shares purchase + require(delegationEnabled, "Delegation is disabled"); + } + require(validators[validatorId].contractAddress == msg.sender, "Invalid contract address"); updateTimeline(amount, 0, 0); } diff --git a/test/units/staking/ValidatorShare.test.js b/test/units/staking/ValidatorShare.test.js index b3a8f9034..25f44051f 100644 --- a/test/units/staking/ValidatorShare.test.js +++ b/test/units/staking/ValidatorShare.test.js @@ -6,8 +6,45 @@ import { buyVoucher, sellVoucher } from './ValidatorShareHelper.js' import { web3 } from '@openzeppelin/test-helpers/src/setup' const toWei = web3.utils.toWei +const ZeroAddr = '0x0000000000000000000000000000000000000000' -function shouldWithdrawReward({ initialBalance, validatorId, user, reward }) { +function shouldHaveCorrectStakes({ userTotalStaked, totalStaked }) { + it('must have correct total staked', async function() { + assertBigNumberEquality(await this.validatorContract.amountStaked(this.user), userTotalStaked) + }) + + it('validator state must have correct amount', async function() { + assertBigNumberEquality(await this.stakeManager.currentValidatorSetTotalStake(), totalStaked) + }) +} + +function shouldBuyShares({ shares, voucherValueExpected, totalStaked }) { + it('ValidatorShare must mint correct amount of shares', async function() { + await expectEvent.inTransaction(this.receipt.tx, ValidatorShare, 'Transfer', { + from: ZeroAddr, + to: this.user, + value: shares + }) + }) + + it('must emit ShareMinted', async function() { + await expectEvent.inTransaction(this.receipt.tx, StakingInfo, 'ShareMinted', { + validatorId: this.validatorId, + user: this.user, + amount: voucherValueExpected, + tokens: shares + }) + }) + + it('must emit StakeUpdate', async function() { + await expectEvent.inTransaction(this.receipt.tx, StakingInfo, 'StakeUpdate', { + validatorId: this.validatorId, + newAmount: totalStaked + }) + }) +} + +function shouldWithdrawReward({ initialBalance, validatorId, user, reward, checkBalance = true }) { if (reward > 0) { it('must emit Transfer', async function() { await expectEvent.inTransaction(this.receipt.tx, TestToken, 'Transfer', { @@ -26,9 +63,16 @@ function shouldWithdrawReward({ initialBalance, validatorId, user, reward }) { }) } - it('must have updated balance', async function() { - const balance = await this.stakeToken.balanceOf(user || this.user) - assertBigNumberEquality(balance, new BN(initialBalance).add(new BN(reward))) + if (checkBalance) { + it('must have updated balance', async function() { + const balance = await this.stakeToken.balanceOf(user || this.user) + assertBigNumberEquality(balance, new BN(initialBalance).add(new BN(reward))) + }) + } + + it('must have liquid rewards == 0', async function() { + let rewards = await this.validatorContract.getLiquidRewards(user || this.user) + assertBigNumberEquality('0', rewards) }) it('must have correct initialRewardPerShare', async function() { @@ -39,7 +83,6 @@ function shouldWithdrawReward({ initialBalance, validatorId, user, reward }) { } contract('ValidatorShare', async function() { - const ZeroAddr = '0x0000000000000000000000000000000000000000' const wei100 = toWei('100') async function slash(slashes = [], validators = [], proposer = wallets[1], nonce = 1) { @@ -215,43 +258,22 @@ contract('ValidatorShare', async function() { this.receipt = await buyVoucher(this.validatorContract, voucherValue, this.user, shares) }) - it('ValidatorShare must mint correct amount of shares', async function() { - await expectEvent.inTransaction(this.receipt.tx, ValidatorShare, 'Transfer', { - from: ZeroAddr, - to: this.user, - value: shares - }) + shouldBuyShares({ + voucherValueExpected, + shares, + totalStaked }) - it('must emit ShareMinted', async function() { - await expectEvent.inTransaction(this.receipt.tx, StakingInfo, 'ShareMinted', { - validatorId: this.validatorId, - user: this.user, - amount: voucherValueExpected, - tokens: shares - }) - }) - - it('must emit StakeUpdate', async function() { - await expectEvent.inTransaction(this.receipt.tx, StakingInfo, 'StakeUpdate', { - validatorId: this.validatorId, - newAmount: this.stakeAmount.add(new BN(totalStaked)) - }) + shouldHaveCorrectStakes({ + userTotalStaked, + totalStaked }) shouldWithdrawReward({ - reward: reward, - initialBalance: initialBalance, + initialBalance, + reward, validatorId: '1' }) - - it('must have correct total staked', async function() { - assertBigNumberEquality(await this.validatorContract.amountStaked(this.user), userTotalStaked) - }) - - it('validator state must have correct amount', async function() { - assertBigNumberEquality(await this.stakeManager.currentValidatorSetTotalStake(), this.stakeAmount.add(new BN(totalStaked))) - }) } describe('when Alice purchases voucher once', function() { @@ -261,7 +283,7 @@ contract('ValidatorShare', async function() { this.user = this.alice }) - testBuyVoucher(toWei('100'), toWei('100'), toWei('100'), toWei('100'), toWei('100'), '0', toWei('69900')) + testBuyVoucher(toWei('100'), toWei('100'), toWei('100'), toWei('200'), toWei('100'), '0', toWei('69900')) }) describe('when Alice purchases voucher with exact minSharesToMint', function() { @@ -270,7 +292,7 @@ contract('ValidatorShare', async function() { before(function() { this.user = this.alice }) - testBuyVoucher(wei100, wei100, wei100, wei100, wei100, '0', toWei('69900')) + testBuyVoucher(wei100, wei100, wei100, toWei('200'), wei100, '0', toWei('69900')) }) describe('when validator turns off delegation', function() { @@ -308,15 +330,15 @@ contract('ValidatorShare', async function() { }) describe('1st purchase', async function() { - testBuyVoucher(toWei('100'), toWei('100'), toWei('100'), toWei('100'), toWei('100'), '0', toWei('69900')) + testBuyVoucher(toWei('100'), toWei('100'), toWei('100'), toWei('200'), toWei('100'), '0', toWei('69900')) }) describe('2nd purchase', async function() { - testBuyVoucher(toWei('150'), toWei('150'), toWei('250'), toWei('250'), toWei('150'), '0', toWei('69750')) + testBuyVoucher(toWei('150'), toWei('150'), toWei('250'), toWei('350'), toWei('150'), '0', toWei('69750')) }) describe('3rd purchase', async function() { - testBuyVoucher(toWei('250'), toWei('250'), toWei('500'), toWei('500'), toWei('250'), '0', toWei('69500')) + testBuyVoucher(toWei('250'), toWei('250'), toWei('500'), toWei('600'), toWei('250'), '0', toWei('69500')) }) }) @@ -335,16 +357,16 @@ contract('ValidatorShare', async function() { describe('1st purchase', async function() { advanceCheckpointAfter() - testBuyVoucher(toWei('100'), toWei('100'), toWei('100'), toWei('100'), toWei('100'), '0', toWei('69900')) + testBuyVoucher(toWei('100'), toWei('100'), toWei('100'), toWei('200'), toWei('100'), '0', toWei('69900')) }) describe('2nd purchase', async function() { advanceCheckpointAfter() - testBuyVoucher(toWei('150'), toWei('150'), toWei('250'), toWei('250'), toWei('150'), toWei('4500'), toWei('69750')) + testBuyVoucher(toWei('150'), toWei('150'), toWei('250'), toWei('350'), toWei('150'), toWei('4500'), toWei('69750')) }) describe('3rd purchase', async function() { - testBuyVoucher(toWei('250'), toWei('250'), toWei('500'), toWei('500'), toWei('250'), '6428571428571428571429', toWei('74000')) + testBuyVoucher(toWei('250'), toWei('250'), toWei('500'), toWei('600'), toWei('250'), '6428571428571428571429', toWei('74000')) }) }) @@ -356,7 +378,7 @@ contract('ValidatorShare', async function() { this.user = this.alice }) - testBuyVoucher(toWei('100'), toWei('100'), toWei('100'), toWei('100'), toWei('100'), '0', toWei('69900')) + testBuyVoucher(toWei('100'), toWei('100'), toWei('100'), toWei('200'), toWei('100'), '0', toWei('69900')) }) describe('when Bob stakes 1st time', function() { @@ -364,7 +386,7 @@ contract('ValidatorShare', async function() { this.user = this.bob }) - testBuyVoucher(toWei('100'), toWei('100'), toWei('100'), toWei('200'), toWei('100'), '0', toWei('69900')) + testBuyVoucher(toWei('100'), toWei('100'), toWei('100'), toWei('300'), toWei('100'), '0', toWei('69900')) }) describe('when Alice stakes 2nd time', function() { @@ -372,7 +394,7 @@ contract('ValidatorShare', async function() { this.user = this.alice }) - testBuyVoucher(toWei('200'), toWei('200'), toWei('300'), toWei('400'), toWei('200'), '0', toWei('69700')) + testBuyVoucher(toWei('200'), toWei('200'), toWei('300'), toWei('500'), toWei('200'), '0', toWei('69700')) }) describe('when Bob stakes 2nd time', function() { @@ -380,7 +402,7 @@ contract('ValidatorShare', async function() { this.user = this.bob }) - testBuyVoucher(toWei('200'), toWei('200'), toWei('300'), toWei('600'), toWei('200'), '0', toWei('69700')) + testBuyVoucher(toWei('200'), toWei('200'), toWei('300'), toWei('700'), toWei('200'), '0', toWei('69700')) }) }) @@ -399,7 +421,7 @@ contract('ValidatorShare', async function() { this.user = this.alice }) - testBuyVoucher(toWei('100'), toWei('100'), toWei('100'), toWei('100'), toWei('100'), '0', toWei('69900')) + testBuyVoucher(toWei('100'), toWei('100'), toWei('100'), toWei('200'), toWei('100'), '0', toWei('69900')) }) describe('when Bob stakes 1st time', function() { @@ -408,7 +430,7 @@ contract('ValidatorShare', async function() { this.user = this.bob }) - testBuyVoucher(toWei('100'), toWei('100'), toWei('100'), toWei('200'), toWei('100'), '0', toWei('69900')) + testBuyVoucher(toWei('100'), toWei('100'), toWei('100'), toWei('300'), toWei('100'), '0', toWei('69900')) }) describe('when Alice stakes 2nd time', function() { @@ -417,7 +439,7 @@ contract('ValidatorShare', async function() { this.user = this.alice }) - testBuyVoucher(toWei('200'), toWei('200'), toWei('300'), toWei('400'), toWei('200'), toWei('7500'), toWei('69700')) + testBuyVoucher(toWei('200'), toWei('200'), toWei('300'), toWei('500'), toWei('200'), toWei('7500'), toWei('69700')) }) describe('when Bob stakes 2nd time', function() { @@ -425,7 +447,7 @@ contract('ValidatorShare', async function() { this.user = this.bob }) - testBuyVoucher(toWei('200'), toWei('200'), toWei('300'), toWei('600'), toWei('200'), toWei('4800'), toWei('69700')) + testBuyVoucher(toWei('200'), toWei('200'), toWei('300'), toWei('700'), toWei('200'), toWei('4800'), toWei('69700')) }) }) @@ -447,6 +469,18 @@ contract('ValidatorShare', async function() { assertBigNumberEquality(rate.div(new BN(100)), diff) }) }) + + describe('when locked', function() { + deployAliceAndBob() + + before(async function() { + await this.stakeManager.testLockShareContract(this.validatorId, true) + }) + + it('reverts', async function() { + await expectRevert(buyVoucher(this.validatorContract, toWei('100'), this.alice, toWei('100')), 'locked') + }) + }) }) describe('exchangeRate', function() { @@ -605,14 +639,14 @@ contract('ValidatorShare', async function() { } } - function testSellVoucher({ returnedStake, reward, initialBalance, validatorId, user, minClaimAmount }) { + function testSellVoucher({ returnedStake, reward, initialBalance, validatorId, user, minClaimAmount, userTotalStaked, totalStaked }) { if (minClaimAmount) { it('must sell voucher with slippage', async function() { this.receipt = await sellVoucher(this.validatorContract, this.user, minClaimAmount) }) } else { it('must sell voucher', async function() { - this.receipt = await sellVoucher(this.validatorContract, user, minClaimAmount) + this.receipt = await sellVoucher(this.validatorContract, user) }) } @@ -626,6 +660,11 @@ contract('ValidatorShare', async function() { }) shouldWithdrawReward({ initialBalance, validatorId, user, reward }) + + shouldHaveCorrectStakes({ + userTotalStaked, + totalStaked + }) } describe('when Alice sells voucher', function() { @@ -636,7 +675,9 @@ contract('ValidatorShare', async function() { reward: toWei('18000'), initialBalance: new BN(0), validatorId: '1', - user: Alice + user: Alice, + userTotalStaked: toWei('0'), + totalStaked: toWei('100') }) }) @@ -651,7 +692,9 @@ contract('ValidatorShare', async function() { reward: toWei('18000'), initialBalance: new BN(0), validatorId: '1', - user: Alice + user: Alice, + userTotalStaked: toWei('0'), + totalStaked: toWei('50') }) }) @@ -664,7 +707,9 @@ contract('ValidatorShare', async function() { initialBalance: new BN(0), validatorId: '1', user: Alice, - minClaimAmount: aliceStake + minClaimAmount: aliceStake, + userTotalStaked: toWei('0'), + totalStaked: toWei('100') }) }) @@ -682,7 +727,9 @@ contract('ValidatorShare', async function() { reward: toWei('18000'), initialBalance: new BN(0), validatorId: '1', - user: Alice + user: Alice, + userTotalStaked: toWei('0'), + totalStaked: toWei('100') }) }) @@ -693,6 +740,24 @@ contract('ValidatorShare', async function() { await expectRevert(this.validatorContract.sellVoucher(toWei('100.00001'), { from: this.user }), 'Too much slippage') }) }) + + describe('when locked', function() { + before(doDeployAndBuyVoucherForAlice) + + before(async function() { + await this.stakeManager.testLockShareContract(this.validatorId, true) + }) + + testSellVoucher({ + returnedStake: aliceStake, + reward: toWei('18000'), + initialBalance: new BN(0), + validatorId: '1', + user: Alice, + userTotalStaked: toWei('0'), + totalStaked: toWei('100') + }) + }) }) describe('withdrawRewards', function() { @@ -966,14 +1031,45 @@ contract('ValidatorShare', async function() { runWithdrawRewardsTest([ { stake: { user: Alice, label: 'Alice', amount: new BN(toWei('100')) } }, { checkpoints: 1 }, - { withdraw: { user: Alice, label: 'Alice', expectedReward: '4500000000000000000000', initialBalance: new BN(0) } }, + { withdraw: { user: Alice, label: 'Alice', expectedReward: toWei('4500'), initialBalance: new BN(0) } }, { withdraw: { user: Alice, label: 'Alice', expectedReward: '0' } } ]) }) + + describe('when locked', function() { + before(doDeploy) + + before(async function() { + const amount = toWei('100') + await this.stakeToken.mint( + Alice, + amount + ) + await this.stakeToken.approve(this.stakeManager.address, amount, { + from: Alice + }) + await buyVoucher(this.validatorContract, amount, Alice) + await checkPoint([this.validatorUser], this.rootChainOwner, this.stakeManager) + await this.stakeManager.testLockShareContract(this.validatorId, true) + }) + + it('must withdraw rewards', async function() { + this.receipt = await this.validatorContract.withdrawRewards({ + from: Alice + }) + }) + + shouldWithdrawReward({ + initialBalance: new BN(0), + validatorId: '1', + user: Alice, + reward: toWei('4500') + }) + }) }) describe('restake', function() { - describe('when Alice restakes', function() { + function prepareForTest({ skipCheckpoint } = {}) { before(doDeploy) before(async function() { this.user = wallets[2].getChecksumAddressString() @@ -987,21 +1083,23 @@ contract('ValidatorShare', async function() { }) await buyVoucher(this.validatorContract, this.stakeAmount, this.user) - await checkPoint([this.validatorUser], this.rootChainOwner, this.stakeManager) - this.totalStaked = new BN(this.stakeAmount) - }) + this.shares = await this.validatorContract.balanceOf(this.user) - it('must have initalRewardPerShare == 0', async function() { - const userRewardPerShare = await this.validatorContract.initalRewardPerShare(this.user) - assertBigNumberEquality('0', userRewardPerShare) + if (!skipCheckpoint) { + await checkPoint([this.validatorUser], this.rootChainOwner, this.stakeManager) + } }) + } - it('must have correct liquid rewards', async function() { - this.totalStaked = this.totalStaked.add(new BN(toWei('4500'))) + describe('when Alice restakes', function() { + const voucherValueExpected = new BN(toWei('4500')) + const reward = new BN(toWei('4500')) + const userTotalStaked = new BN(toWei('4600')) + const shares = new BN(toWei('4500')) + const totalStaked = new BN(toWei('4700')) + const initialBalance = new BN(toWei('100')) - let rewards = await this.validatorContract.getLiquidRewards(this.user) - assertBigNumberEquality(rewards, toWei('4500')) - }) + prepareForTest() it('must restake', async function() { this.receipt = await this.validatorContract.restake({ @@ -1009,49 +1107,68 @@ contract('ValidatorShare', async function() { }) }) - it('must have correct initalRewardPerShare', async function() { - const currrentRewardPerShare = await this.validatorContract.rewardPerShare() - const userRewardPerShare = await this.validatorContract.initalRewardPerShare(this.user) - assertBigNumberEquality(currrentRewardPerShare, userRewardPerShare) + shouldBuyShares({ + voucherValueExpected, + userTotalStaked, + totalStaked, + shares, + reward, + initialBalance }) - it('must have liquid rewards == 0', async function() { - let rewards = await this.validatorContract.getLiquidRewards(this.user) - assertBigNumberEquality('0', rewards) + shouldWithdrawReward({ + reward: '0', // we need only partial test here, reward is not really claimed + initialBalance, + checkBalance: false, + validatorId: '1' }) it('must emit DelegatorRestaked', async function() { await expectEvent.inTransaction(this.receipt.tx, StakingInfo, 'DelegatorRestaked', { validatorId: this.validatorId, - totalStaked: this.totalStaked + totalStaked: userTotalStaked }) }) }) describe('when no liquid rewards', function() { - before(doDeploy) - before(async function() { - this.user = wallets[2].getChecksumAddressString() + prepareForTest({ skipCheckpoint: true }) - await this.stakeToken.mint( - this.user, - this.stakeAmount + it('reverts', async function() { + await expectRevert(this.validatorContract.restake({ from: this.user }), 'Too small rewards to restake') + }) + }) + + describe('when staking manager delegation is disabled', function() { + prepareForTest() + + before('disable delegation', async function() { + await this.governance.update( + this.stakeManager.address, + this.stakeManager.contract.methods.setDelegationEnabled(false).encodeABI() ) - await this.stakeToken.approve(this.stakeManager.address, this.stakeAmount, { - from: this.user - }) + }) - await buyVoucher(this.validatorContract, this.stakeAmount, this.user) + it('reverts', async function() { + await expectRevert(this.validatorContract.restake({ from: this.user }), 'Delegation is disabled') + }) + }) + + describe('when locked', function() { + prepareForTest() + + before(async function() { + await this.stakeManager.testLockShareContract(this.validatorId, true) }) it('reverts', async function() { - await expectRevert(this.validatorContract.restake({ from: this.user }), 'Too small rewards to restake') + await expectRevert(this.validatorContract.restake({ from: this.user }), 'locked') }) }) }) describe('unstakeClaimTokens', function() { - describe('when Alice unstakes right after voucher sell', function() { + function prepareForTest({ skipSell, skipBuy } = {}) { before(doDeploy) before(async function() { this.user = wallets[2].getChecksumAddressString() @@ -1067,13 +1184,21 @@ contract('ValidatorShare', async function() { this.totalStaked = this.stakeAmount }) - before('buy', async function() { - await buyVoucher(this.validatorContract, this.stakeAmount, this.user) - }) + if (!skipBuy) { + before('buy', async function() { + await buyVoucher(this.validatorContract, this.stakeAmount, this.user) + }) + } - before('sell', async function() { - await sellVoucher(this.validatorContract, this.user) - }) + if (!skipSell) { + before('sell', async function() { + await sellVoucher(this.validatorContract, this.user) + }) + } + } + + describe('when Alice unstakes right after voucher sell', function() { + prepareForTest() before('checkpoint', async function() { let currentEpoch = await this.stakeManager.currentEpoch() @@ -1097,6 +1222,41 @@ contract('ValidatorShare', async function() { amount: this.stakeAmount }) }) + + shouldHaveCorrectStakes({ + userTotalStaked: '0', + totalStaked: toWei('100') + }) + }) + + describe('when Alice claims too early', function() { + prepareForTest() + + it('reverts', async function() { + await expectRevert(this.validatorContract.unstakeClaimTokens({ + from: this.user + }), 'Incomplete withdrawal period') + }) + }) + + describe('when Alice claims with 0 shares', function() { + prepareForTest({ skipSell: true }) + + it('reverts', async function() { + await expectRevert(this.validatorContract.unstakeClaimTokens({ + from: this.user + }), 'Incomplete withdrawal period') + }) + }) + + describe('when Alice didn\'t buy voucher', function() { + prepareForTest({ skipSell: true, skipBuy: true }) + + it('reverts', async function() { + await expectRevert(this.validatorContract.unstakeClaimTokens({ + from: this.user + }), 'Incomplete withdrawal period') + }) }) })