From 958cdff02eaf34597a10f3b14e695813474e0b79 Mon Sep 17 00:00:00 2001 From: Vadim Date: Wed, 26 Aug 2020 17:21:03 +0300 Subject: [PATCH] Lock initial validator stake --- contracts/base/StakingAuRaBase.sol | 40 ++++++++++++++++++---- package-lock.json | 2 +- package.json | 2 +- test/BlockRewardAuRa.js | 6 ++++ test/StakingAuRa.js | 17 +++++++++ test/mockContracts/StakingAuRaBaseMock.sol | 4 +++ 6 files changed, 62 insertions(+), 9 deletions(-) diff --git a/contracts/base/StakingAuRaBase.sol b/contracts/base/StakingAuRaBase.sol index 1905ff95..36503d9a 100644 --- a/contracts/base/StakingAuRaBase.sol +++ b/contracts/base/StakingAuRaBase.sol @@ -29,9 +29,10 @@ contract StakingAuRaBase is UpgradeableOwned, IStakingAuRa { mapping(address => address[]) internal _stakerPools; mapping(address => mapping(address => uint256)) internal _stakerPoolsIndexes; mapping(address => mapping(address => mapping(uint256 => uint256))) internal _stakeAmountByEpoch; + mapping(address => uint256) internal _stakeInitial; // Reserved storage space to allow for layout changes in the future. - uint256[25] private ______gapForInternal; + uint256[24] private ______gapForInternal; /// @dev The limit of the minimum candidate stake (CANDIDATE_MIN_STAKE). uint256 public candidateMinStake; @@ -373,6 +374,7 @@ contract StakingAuRaBase is UpgradeableOwned, IStakingAuRa { address stakingAddress = _pools[i]; require(stakeAmount[stakingAddress][stakingAddress] == 0); _stake(stakingAddress, stakingAddress, stakingAmount); + _stakeInitial[stakingAddress] = stakingAmount; emit PlacedStake(stakingAddress, stakingAddress, stakingEpoch, stakingAmount); } @@ -504,6 +506,9 @@ contract StakingAuRaBase is UpgradeableOwned, IStakingAuRa { stakeAmountTotal[_poolStakingAddress] = newStakeAmountTotal; if (staker == _poolStakingAddress) { + // Initial validator cannot withdraw their initial stake + require(newStakeAmount >= _stakeInitial[staker]); + // The amount to be withdrawn must be the whole staked amount or // must not exceed the diff between the entire amount and `candidateMinStake` require(newStakeAmount == 0 || newStakeAmount >= candidateMinStake); @@ -691,13 +696,19 @@ contract StakingAuRaBase is UpgradeableOwned, IStakingAuRa { /// @param _staker The staker address that is going to withdraw. function maxWithdrawAllowed(address _poolStakingAddress, address _staker) public view returns(uint256) { address miningAddress = validatorSetContract.miningByStakingAddress(_poolStakingAddress); + bool isDelegator = _poolStakingAddress != _staker; - if (!_isWithdrawAllowed(miningAddress, _poolStakingAddress != _staker)) { + if (!_isWithdrawAllowed(miningAddress, isDelegator)) { return 0; } uint256 canWithdraw = stakeAmount[_poolStakingAddress][_staker]; + if (!isDelegator) { + // Initial validator cannot withdraw their initial stake + canWithdraw = canWithdraw.sub(_stakeInitial[_staker]); + } + if (!validatorSetContract.isValidatorOrPending(miningAddress)) { // The pool is not a validator and is not going to become one, // so the staker can only withdraw staked amount minus already @@ -723,8 +734,9 @@ contract StakingAuRaBase is UpgradeableOwned, IStakingAuRa { /// @param _staker The staker address that is going to order the withdrawal. function maxWithdrawOrderAllowed(address _poolStakingAddress, address _staker) public view returns(uint256) { address miningAddress = validatorSetContract.miningByStakingAddress(_poolStakingAddress); + bool isDelegator = _poolStakingAddress != _staker; - if (!_isWithdrawAllowed(miningAddress, _poolStakingAddress != _staker)) { + if (!_isWithdrawAllowed(miningAddress, isDelegator)) { return 0; } @@ -738,7 +750,15 @@ contract StakingAuRaBase is UpgradeableOwned, IStakingAuRa { // If the pool is an active or pending validator, the staker can order withdrawal // up to their total staking amount minus an already ordered amount // minus an amount staked during the current staking epoch - return stakeAmount[_poolStakingAddress][_staker].sub(stakeAmountByCurrentEpoch(_poolStakingAddress, _staker)); + + uint256 canOrder = stakeAmount[_poolStakingAddress][_staker]; + + if (!isDelegator) { + // Initial validator cannot withdraw their initial stake + canOrder = canOrder.sub(_stakeInitial[_staker]); + } + + return canOrder.sub(stakeAmountByCurrentEpoch(_poolStakingAddress, _staker)); } /// @dev Prevents sending tokens directly to the `StakingAuRa` contract address @@ -1090,14 +1110,20 @@ contract StakingAuRaBase is UpgradeableOwned, IStakingAuRa { require(_poolStakingAddress != address(0)); require(_amount != 0); - // How much can `staker` withdraw from `_poolStakingAddress` at the moment? + // How much can `_staker` withdraw from `_poolStakingAddress` at the moment? require(_amount <= maxWithdrawAllowed(_poolStakingAddress, _staker)); uint256 newStakeAmount = stakeAmount[_poolStakingAddress][_staker].sub(_amount); // The amount to be withdrawn must be the whole staked amount or - // must not exceed the diff between the entire amount and MIN_STAKE - uint256 minAllowedStake = (_poolStakingAddress == _staker) ? candidateMinStake : delegatorMinStake; + // must not exceed the diff between the entire amount and min allowed stake + uint256 minAllowedStake; + if (_poolStakingAddress == _staker) { + require(newStakeAmount >= _stakeInitial[_staker]); // initial validator cannot withdraw their initial stake + minAllowedStake = candidateMinStake; + } else { + minAllowedStake = delegatorMinStake; + } require(newStakeAmount == 0 || newStakeAmount >= minAllowedStake); stakeAmount[_poolStakingAddress][_staker] = newStakeAmount; diff --git a/package-lock.json b/package-lock.json index d90cd3ce..91757f9e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "posdao-contracts", - "version": "0.1.6", + "version": "0.1.7", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 535c2cf2..ca0abedb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "posdao-contracts", - "version": "0.1.6", + "version": "0.1.7", "description": "Smart contracts for POSDAO consensus", "main": "index.js", "scripts": { diff --git a/test/BlockRewardAuRa.js b/test/BlockRewardAuRa.js index 8498a14b..7318df78 100644 --- a/test/BlockRewardAuRa.js +++ b/test/BlockRewardAuRa.js @@ -343,6 +343,9 @@ contract('BlockRewardAuRa', async accounts => { accounts[2], accounts[3] ]); + + await setCurrentBlockNumber(stakingEpochEndBlock.add(new BN(1))); + for (let i = 0; i < validators.length; i++) { (await blockRewardAuRa.snapshotPoolValidatorStakeAmount.call(nextStakingEpoch, validators[i])).should.be.bignumber.equal( candidateMinStake @@ -350,6 +353,9 @@ contract('BlockRewardAuRa', async accounts => { (await blockRewardAuRa.snapshotPoolTotalStakeAmount.call(nextStakingEpoch, validators[i])).should.be.bignumber.equal( candidateMinStake.add(delegatorMinStake.mul(new BN(3))) ); + + const stakingAddress = accounts[4 + i]; + await stakingAuRa.orderWithdraw(stakingAddress, candidateMinStake, {from: stakingAddress}).should.be.rejectedWith(ERROR_MSG); } const validatorsToBeFinalized = (await validatorSetAuRa.validatorsToBeFinalized.call()).miningAddresses; diff --git a/test/StakingAuRa.js b/test/StakingAuRa.js index 27be26d6..1cc36190 100644 --- a/test/StakingAuRa.js +++ b/test/StakingAuRa.js @@ -2902,6 +2902,23 @@ contract('StakingAuRa', async accounts => { await stakingAuRa.withdraw(initialStakingAddresses[1], mintAmount.add(new BN(1)), {from: initialStakingAddresses[1]}).should.be.rejectedWith(ERROR_MSG); await stakingAuRa.withdraw(initialStakingAddresses[1], mintAmount, {from: initialStakingAddresses[1]}).should.be.fulfilled; }); + it('initial validator cannot withdraw initial stake', async () => { + (await stakingAuRa.candidateMinStake.call()).should.be.bignumber.equal(mintAmount.div(new BN(2))); + const zero = new BN(0); + const one = new BN(1); + const initialStake = mintAmount.sub(one); + const stakingAddress = initialStakingAddresses[1]; + await stakingAuRa.stake(stakingAddress, initialStake, { from: stakingAddress }).should.be.fulfilled; + await stakingAuRa.setInitialStake(stakingAddress, initialStake).should.be.fulfilled; + await stakingAuRa.stake(stakingAddress, one, { from: stakingAddress }).should.be.fulfilled; + (await stakingAuRa.stakeAmount.call(stakingAddress, stakingAddress)).should.be.bignumber.equal(mintAmount); + await stakingAuRa.withdraw(stakingAddress, one, { from: stakingAddress }).should.be.fulfilled; + (await stakingAuRa.stakeAmount.call(stakingAddress, stakingAddress)).should.be.bignumber.equal(initialStake); + await stakingAuRa.withdraw(stakingAddress, one, { from: stakingAddress }).should.be.rejectedWith(ERROR_MSG); + await stakingAuRa.withdraw(stakingAddress, initialStake, { from: stakingAddress }).should.be.rejectedWith(ERROR_MSG); + await stakingAuRa.setInitialStake(stakingAddress, zero).should.be.fulfilled; + await stakingAuRa.withdraw(stakingAddress, initialStake, { from: stakingAddress }).should.be.fulfilled; + }); it('should fail if withdraw already ordered amount', async () => { // Set `initiateChangeAllowed` boolean flag to `true` await validatorSetAuRa.setCurrentBlockNumber(1).should.be.fulfilled; diff --git a/test/mockContracts/StakingAuRaBaseMock.sol b/test/mockContracts/StakingAuRaBaseMock.sol index 12de4e8a..57540ae9 100644 --- a/test/mockContracts/StakingAuRaBaseMock.sol +++ b/test/mockContracts/StakingAuRaBaseMock.sol @@ -29,6 +29,10 @@ contract StakingAuRaBaseMock is StakingAuRaBase { _currentBlockNumber = _blockNumber; } + function setInitialStake(address _stakingAddress, uint256 _amount) public { + _stakeInitial[_stakingAddress] = _amount; + } + function setStakeAmountTotal(address _poolStakingAddress, uint256 _amount) public { stakeAmountTotal[_poolStakingAddress] = _amount; }