Skip to content
This repository has been archived by the owner on Mar 1, 2024. It is now read-only.

Commit

Permalink
Merge pull request #333 from maticnetwork/migrate-delegation
Browse files Browse the repository at this point in the history
Migrate delegation
  • Loading branch information
arthcp authored Oct 23, 2020
2 parents b1241e5 + 142bfb6 commit fc65a84
Show file tree
Hide file tree
Showing 4 changed files with 291 additions and 13 deletions.
6 changes: 6 additions & 0 deletions contracts/staking/stakeManager/StakeManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
4 changes: 4 additions & 0 deletions contracts/staking/validatorShare/IValidatorShare.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
47 changes: 34 additions & 13 deletions contracts/staking/validatorShare/ValidatorShare.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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");
Expand All @@ -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));
Expand All @@ -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;
Expand All @@ -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));
Expand Down Expand Up @@ -264,21 +264,42 @@ 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");
}

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) {
Expand Down
247 changes: 247 additions & 0 deletions test/units/staking/stakeManager/StakeManager.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down Expand Up @@ -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')
})
})
})

0 comments on commit fc65a84

Please sign in to comment.