From e24692639210d2b04659ca5db7b4989abcab9195 Mon Sep 17 00:00:00 2001 From: Arth Patel Date: Wed, 21 Oct 2020 16:50:37 +0530 Subject: [PATCH 1/3] chg: add user param to _withdrawAndTransferReward and _buyShares --- .../staking/validatorShare/ValidatorShare.sol | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/contracts/staking/validatorShare/ValidatorShare.sol b/contracts/staking/validatorShare/ValidatorShare.sol index fffff4972..f76d378bf 100644 --- a/contracts/staking/validatorShare/ValidatorShare.sol +++ b/contracts/staking/validatorShare/ValidatorShare.sol @@ -157,8 +157,8 @@ contract ValidatorShare is IValidatorShare, ERC20NonTransferable, OwnableLockabl } function _buyVoucher(uint256 _amount, uint256 _minSharesToMint) internal returns(uint256) { - _withdrawAndTransferReward(); - uint256 amountToDeposit = _buyShares(_amount, _minSharesToMint); + _withdrawAndTransferReward(msg.sender); + uint256 amountToDeposit = _buyShares(_amount, _minSharesToMint, msg.sender); require(stakeManager.delegationDeposit(validatorId, amountToDeposit, msg.sender), "deposit failed"); return amountToDeposit; } @@ -181,7 +181,7 @@ contract ValidatorShare is IValidatorShare, ERC20NonTransferable, OwnableLockabl uint256 liquidReward = _withdrawReward(msg.sender); require(liquidReward >= minAmount, "Too small rewards to restake"); - uint256 amountRestaked = _buyShares(liquidReward, 0); + uint256 amountRestaked = _buyShares(liquidReward, 0, msg.sender); if (liquidReward > amountRestaked) { // return change to the user require(stakeManager.transferFunds(validatorId, liquidReward - amountRestaked, msg.sender), "Insufficent rewards"); @@ -194,16 +194,16 @@ contract ValidatorShare is IValidatorShare, ERC20NonTransferable, OwnableLockabl return amountRestaked; } - function _buyShares(uint256 _amount, uint256 _minSharesToMint) private onlyWhenUnlocked returns(uint256) { + function _buyShares(uint256 _amount, uint256 _minSharesToMint, address user) private onlyWhenUnlocked returns(uint256) { require(delegation, "Delegation is disabled"); uint256 rate = exchangeRate(); uint256 precision = _getRatePrecision(); uint256 shares = _amount.mul(precision).div(rate); require(shares >= _minSharesToMint, "Too much slippage"); - require(unbonds[msg.sender].shares == 0, "Ongoing exit"); + require(unbonds[user].shares == 0, "Ongoing exit"); - _mint(msg.sender, shares); + _mint(user, shares); // clamp amount of tokens in case resulted shares requires less tokens than anticipated _amount = _amount.sub(_amount % rate.mul(shares).div(precision)); @@ -212,7 +212,7 @@ contract ValidatorShare is IValidatorShare, ERC20NonTransferable, OwnableLockabl stakeManager.updateValidatorState(validatorId, int256(_amount)); StakingInfo logger = stakingLogger; - logger.logShareMinted(validatorId, msg.sender, _amount, shares); + logger.logShareMinted(validatorId, user, _amount, shares); logger.logStakeUpdate(validatorId); return _amount; @@ -236,7 +236,7 @@ contract ValidatorShare is IValidatorShare, ERC20NonTransferable, OwnableLockabl uint256 shares = claimAmount.mul(precision).div(rate); require(shares <= maximumSharesToBurn, "too much slippage"); - _withdrawAndTransferReward(); + _withdrawAndTransferReward(msg.sender); _burn(msg.sender, shares); stakeManager.updateValidatorState(validatorId, -int256(claimAmount)); @@ -264,18 +264,18 @@ contract ValidatorShare is IValidatorShare, ERC20NonTransferable, OwnableLockabl return liquidRewards; } - function _withdrawAndTransferReward() private returns(uint256) { - uint256 liquidRewards = _withdrawReward(msg.sender); + function _withdrawAndTransferReward(address user) private returns(uint256) { + uint256 liquidRewards = _withdrawReward(user); if (liquidRewards > 0) { - require(stakeManager.transferFunds(validatorId, liquidRewards, msg.sender), "Insufficent rewards"); - stakingLogger.logDelegatorClaimRewards(validatorId, msg.sender, liquidRewards); + require(stakeManager.transferFunds(validatorId, liquidRewards, user), "Insufficent rewards"); + stakingLogger.logDelegatorClaimRewards(validatorId, user, liquidRewards); } return liquidRewards; } function withdrawRewards() public { - uint256 rewards = _withdrawAndTransferReward(); + uint256 rewards = _withdrawAndTransferReward(msg.sender); require(rewards >= minAmount, "Too small rewards amount"); } From beea18d776651ce95bdd833b79f3989d17a5a26c Mon Sep 17 00:00:00 2001 From: Arth Patel Date: Thu, 22 Oct 2020 15:44:06 +0530 Subject: [PATCH 2/3] new: function to migrate delegation --- .../staking/stakeManager/StakeManager.sol | 6 ++++++ .../validatorShare/IValidatorShare.sol | 4 ++++ .../staking/validatorShare/ValidatorShare.sol | 21 +++++++++++++++++++ 3 files changed, 31 insertions(+) diff --git a/contracts/staking/stakeManager/StakeManager.sol b/contracts/staking/stakeManager/StakeManager.sol index a80008adc..6cd181274 100644 --- a/contracts/staking/stakeManager/StakeManager.sol +++ b/contracts/staking/stakeManager/StakeManager.sol @@ -392,6 +392,12 @@ contract StakeManager is IStakeManager, StakeManagerStorage, Initializable { _liquidateRewards(validatorId, msg.sender, reward); } + function migrateDelegation(uint256 fromValidatorId, uint256 toValidatorId, uint256 amount) public { + require(fromValidatorId < 8 && toValidatorId > 7, "Invalid migration"); + IValidatorShare(validators[fromValidatorId].contractAddress).migrateOut(msg.sender, amount); + IValidatorShare(validators[toValidatorId].contractAddress).migrateIn(msg.sender, amount); + } + function getValidatorId(address user) public view returns (uint256) { return NFTContract.tokenOfOwnerByIndex(user, 0); } diff --git a/contracts/staking/validatorShare/IValidatorShare.sol b/contracts/staking/validatorShare/IValidatorShare.sol index 8edde3d86..2978b4674 100644 --- a/contracts/staking/validatorShare/IValidatorShare.sol +++ b/contracts/staking/validatorShare/IValidatorShare.sol @@ -37,4 +37,8 @@ contract IValidatorShare { function slash(uint256 valPow, uint256 totalAmountToSlash) external returns (uint256); function updateDelegation(bool delegation) external; + + function migrateOut(address user, uint256 amount) external; + + function migrateIn(address user, uint256 amount) external; } diff --git a/contracts/staking/validatorShare/ValidatorShare.sol b/contracts/staking/validatorShare/ValidatorShare.sol index f76d378bf..42b676cb9 100644 --- a/contracts/staking/validatorShare/ValidatorShare.sol +++ b/contracts/staking/validatorShare/ValidatorShare.sol @@ -279,6 +279,27 @@ contract ValidatorShare is IValidatorShare, ERC20NonTransferable, OwnableLockabl require(rewards >= minAmount, "Too small rewards amount"); } + function migrateOut(address user, uint256 amount) external onlyOwner { + _withdrawAndTransferReward(user); + (uint256 totalStaked, uint256 rate) = _getTotalStake(user); + require(totalStaked >= amount, "Migrating too much"); + + uint256 precision = _getRatePrecision(); + uint256 shares = amount.mul(precision).div(rate); + _burn(user, shares); + + stakeManager.updateValidatorState(validatorId, -int256(amount)); + _reduceActiveStake(amount); + + stakingLogger.logShareBurned(validatorId, user, amount, shares); + stakingLogger.logStakeUpdate(validatorId); + stakingLogger.logDelegatorUnstaked(validatorId, user, amount); + } + + function migrateIn(address user, uint256 amount) external onlyOwner { + _buyShares(amount, 0, user); + } + function getLiquidRewards(address user) public view returns (uint256) { uint256 shares = balanceOf(user); if (shares == 0) { From 142bfb6542243140ada7beeb68bc5d62dbf4f7a6 Mon Sep 17 00:00:00 2001 From: Arth Patel Date: Fri, 23 Oct 2020 12:44:42 +0530 Subject: [PATCH 3/3] new: migrate delegation tests --- .../staking/stakeManager/StakeManager.test.js | 247 ++++++++++++++++++ 1 file changed, 247 insertions(+) diff --git a/test/units/staking/stakeManager/StakeManager.test.js b/test/units/staking/stakeManager/StakeManager.test.js index b7e0b223b..18aad5fcf 100644 --- a/test/units/staking/stakeManager/StakeManager.test.js +++ b/test/units/staking/stakeManager/StakeManager.test.js @@ -19,6 +19,8 @@ import { expectEvent, expectRevert, BN } from '@openzeppelin/test-helpers' import { wallets, freshDeploy, approveAndStake } from '../deployment' import { buyVoucher } from '../ValidatorShareHelper.js' +const ZeroAddr = '0x0000000000000000000000000000000000000000' + contract('StakeManager', async function(accounts) { let owner = accounts[0] @@ -1835,4 +1837,249 @@ contract('StakeManager', async function(accounts) { }) }) }) + + describe('Chad delegates to Alice then migrates partialy to Bob', async function() { + const aliceId = '2' // Matic + const bobId = '8' // Non-matic + const alice = wallets[2] + const bob = wallets[8] + const initialStakers = [wallets[1], alice, wallets[3], wallets[4], wallets[5], wallets[6], wallets[7], bob] + const stakeAmount = web3.utils.toWei('1250') + const stakeAmountBN = new BN(stakeAmount) + const delegationAmount = web3.utils.toWei('150') + const delegationAmountBN = new BN(delegationAmount) + const migrationAmount = web3.utils.toWei('100') + const migrationAmountBN = new BN(migrationAmount) + const delegator = wallets[9].getChecksumAddressString() + let aliceContract + let bobContract + + before('fresh deploy', async function() { + await freshDeploy.call(this) + await this.stakeManager.updateValidatorThreshold(10, { + from: owner + }) + for (const wallet of initialStakers) { + await approveAndStake.call(this, { wallet, stakeAmount, acceptDelegation: true }) + } + const aliceValidator = await this.stakeManager.validators(aliceId) + aliceContract = await ValidatorShare.at(aliceValidator.contractAddress) + const bobValidator = await this.stakeManager.validators(bobId) + bobContract = await ValidatorShare.at(bobValidator.contractAddress) + }) + + describe('Chad delegates to Alice', async function() { + before(async function() { + await this.stakeToken.mint(delegator, delegationAmount) + await this.stakeToken.approve(this.stakeManager.address, delegationAmount, { + from: delegator + }) + }) + + it('Should delegate', async function() { + this.receipt = await buyVoucher(aliceContract, delegationAmount, delegator) + }) + + it('ValidatorShare must mint correct amount of shares', async function() { + await expectEvent.inTransaction(this.receipt.tx, ValidatorShare, 'Transfer', { + from: ZeroAddr, + to: delegator, + value: delegationAmount + }) + }) + + it('must emit ShareMinted', async function() { + await expectEvent.inTransaction(this.receipt.tx, StakingInfo, 'ShareMinted', { + validatorId: aliceId, + user: delegator, + amount: delegationAmount, + tokens: delegationAmount + }) + }) + + it('must emit StakeUpdate', async function() { + await expectEvent.inTransaction(this.receipt.tx, StakingInfo, 'StakeUpdate', { + validatorId: aliceId, + newAmount: stakeAmountBN.add(delegationAmountBN).toString(10) + }) + }) + + it('Active amount must be updated', async function() { + const delegatedAliceAmount = await aliceContract.getActiveAmount() + assertBigNumberEquality(delegatedAliceAmount, delegationAmountBN) + }) + }) + + describe('Chad migrates delegation to Bob', async function() { + it('Should migrate', async function() { + this.receipt = await this.stakeManager.migrateDelegation(aliceId, bobId, migrationAmount, { from: delegator }) + }) + + it('Alice\'s contract must burn correct amount of shares', async function() { + await expectEvent.inTransaction(this.receipt.tx, ValidatorShare, 'Transfer', { + from: delegator, + to: ZeroAddr, + value: migrationAmount + }) + }) + + it('Alice\'s contract must emit ShareBurned', async function() { + await expectEvent.inTransaction(this.receipt.tx, StakingInfo, 'ShareBurned', { + validatorId: aliceId, + user: delegator, + amount: migrationAmount, + tokens: migrationAmount + }) + }) + + it('must emit StakeUpdate for Alice', async function() { + await expectEvent.inTransaction(this.receipt.tx, StakingInfo, 'StakeUpdate', { + validatorId: aliceId, + newAmount: stakeAmountBN.add(delegationAmountBN).sub(migrationAmountBN).toString(10) + }) + }) + + it('Bob\'s contract must mint correct amount of shares', async function() { + await expectEvent.inTransaction(this.receipt.tx, ValidatorShare, 'Transfer', { + from: ZeroAddr, + to: delegator, + value: migrationAmount + }) + }) + + it('Bob\'s contract must emit ShareMinted', async function() { + await expectEvent.inTransaction(this.receipt.tx, StakingInfo, 'ShareMinted', { + validatorId: bobId, + user: delegator, + amount: migrationAmount, + tokens: migrationAmount + }) + }) + + it('must emit StakeUpdate for Bob', async function() { + await expectEvent.inTransaction(this.receipt.tx, StakingInfo, 'StakeUpdate', { + validatorId: bobId, + newAmount: stakeAmountBN.add(migrationAmountBN).toString(10) + }) + }) + + it('Alice active amount must be updated', async function() { + const migratedAliceAmount = await aliceContract.getActiveAmount() + assertBigNumberEquality(migratedAliceAmount, delegationAmountBN.sub(migrationAmountBN)) + }) + + it('Bob active amount must be updated', async function() { + const migratedBobAmount = await bobContract.getActiveAmount() + assertBigNumberEquality(migratedBobAmount, migrationAmount) + }) + }) + }) + + describe('Chad tries to migrate from non matic validator', function() { + const aliceId = '9' // Non-matic + const bobId = '8' // Non-matic + const alice = wallets[9] + const bob = wallets[8] + const initialStakers = [wallets[1], wallets[2], wallets[3], wallets[4], wallets[5], wallets[6], wallets[7], bob, alice] + const stakeAmount = web3.utils.toWei('1250') + const delegationAmount = web3.utils.toWei('150') + const migrationAmount = web3.utils.toWei('100') + const delegator = wallets[9].getChecksumAddressString() + + before('fresh deploy and delegate to Alice', async function() { + await freshDeploy.call(this) + await this.stakeManager.updateValidatorThreshold(10, { + from: owner + }) + for (const wallet of initialStakers) { + await approveAndStake.call(this, { wallet, stakeAmount, acceptDelegation: true }) + } + + await this.stakeToken.mint(delegator, delegationAmount) + await this.stakeToken.approve(this.stakeManager.address, delegationAmount, { + from: delegator + }) + const aliceValidator = await this.stakeManager.validators(aliceId) + const aliceContract = await ValidatorShare.at(aliceValidator.contractAddress) + await buyVoucher(aliceContract, delegationAmount, delegator) + }) + + it('Migration should fail', async function() { + await expectRevert( + this.stakeManager.migrateDelegation(aliceId, bobId, migrationAmount, { from: delegator }), + 'Invalid migration') + }) + }) + + describe('Chad tries to migrate to matic validator', function() { + const aliceId = '8' // Non-matic + const bobId = '2' // Matic + const alice = wallets[8] + const bob = wallets[2] + const initialStakers = [wallets[1], bob, wallets[3], wallets[4], wallets[5], wallets[6], wallets[7], alice] + const stakeAmount = web3.utils.toWei('1250') + const delegationAmount = web3.utils.toWei('150') + const migrationAmount = web3.utils.toWei('100') + const delegator = wallets[9].getChecksumAddressString() + + before('fresh deploy and delegate to Alice', async function() { + await freshDeploy.call(this) + await this.stakeManager.updateValidatorThreshold(10, { + from: owner + }) + for (const wallet of initialStakers) { + await approveAndStake.call(this, { wallet, stakeAmount, acceptDelegation: true }) + } + + await this.stakeToken.mint(delegator, delegationAmount) + await this.stakeToken.approve(this.stakeManager.address, delegationAmount, { + from: delegator + }) + const aliceValidator = await this.stakeManager.validators(aliceId) + const aliceContract = await ValidatorShare.at(aliceValidator.contractAddress) + await buyVoucher(aliceContract, delegationAmount, delegator) + }) + + it('Migration should fail', async function() { + await expectRevert( + this.stakeManager.migrateDelegation(aliceId, bobId, migrationAmount, { from: delegator }), + 'Invalid migration') + }) + }) + + describe('Chad tries to migrate more than his delegation amount', async function() { + const aliceId = '2' + const bobId = '8' + const alice = wallets[2] + const bob = wallets[8] + const initialStakers = [wallets[1], alice, wallets[3], wallets[4], wallets[5], wallets[6], wallets[7], bob] + const stakeAmount = web3.utils.toWei('1250') + const delegationAmount = web3.utils.toWei('150') + const migrationAmount = web3.utils.toWei('200') // more than delegation amount + const delegator = wallets[9].getChecksumAddressString() + + before('fresh deploy and delegate to Alice', async function() { + await freshDeploy.call(this) + await this.stakeManager.updateValidatorThreshold(10, { + from: owner + }) + for (const wallet of initialStakers) { + await approveAndStake.call(this, { wallet, stakeAmount, acceptDelegation: true }) + } + + await this.stakeToken.mint(delegator, delegationAmount) + await this.stakeToken.approve(this.stakeManager.address, delegationAmount, { + from: delegator + }) + const aliceValidator = await this.stakeManager.validators(aliceId) + const aliceContract = await ValidatorShare.at(aliceValidator.contractAddress) + await buyVoucher(aliceContract, delegationAmount, delegator) + }) + + it('Migration should fail', async function() { + await expectRevert( + this.stakeManager.migrateDelegation(aliceId, bobId, migrationAmount, { from: delegator }), + 'Migrating too much') + }) + }) })