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

Commit fc65a84

Browse files
authored
Merge pull request #333 from maticnetwork/migrate-delegation
Migrate delegation
2 parents b1241e5 + 142bfb6 commit fc65a84

File tree

4 files changed

+291
-13
lines changed

4 files changed

+291
-13
lines changed

contracts/staking/stakeManager/StakeManager.sol

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,12 @@ contract StakeManager is IStakeManager, StakeManagerStorage, Initializable {
392392
_liquidateRewards(validatorId, msg.sender, reward);
393393
}
394394

395+
function migrateDelegation(uint256 fromValidatorId, uint256 toValidatorId, uint256 amount) public {
396+
require(fromValidatorId < 8 && toValidatorId > 7, "Invalid migration");
397+
IValidatorShare(validators[fromValidatorId].contractAddress).migrateOut(msg.sender, amount);
398+
IValidatorShare(validators[toValidatorId].contractAddress).migrateIn(msg.sender, amount);
399+
}
400+
395401
function getValidatorId(address user) public view returns (uint256) {
396402
return NFTContract.tokenOfOwnerByIndex(user, 0);
397403
}

contracts/staking/validatorShare/IValidatorShare.sol

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,8 @@ contract IValidatorShare {
3737
function slash(uint256 valPow, uint256 totalAmountToSlash) external returns (uint256);
3838

3939
function updateDelegation(bool delegation) external;
40+
41+
function migrateOut(address user, uint256 amount) external;
42+
43+
function migrateIn(address user, uint256 amount) external;
4044
}

contracts/staking/validatorShare/ValidatorShare.sol

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,8 @@ contract ValidatorShare is IValidatorShare, ERC20NonTransferable, OwnableLockabl
157157
}
158158

159159
function _buyVoucher(uint256 _amount, uint256 _minSharesToMint) internal returns(uint256) {
160-
_withdrawAndTransferReward();
161-
uint256 amountToDeposit = _buyShares(_amount, _minSharesToMint);
160+
_withdrawAndTransferReward(msg.sender);
161+
uint256 amountToDeposit = _buyShares(_amount, _minSharesToMint, msg.sender);
162162
require(stakeManager.delegationDeposit(validatorId, amountToDeposit, msg.sender), "deposit failed");
163163
return amountToDeposit;
164164
}
@@ -181,7 +181,7 @@ contract ValidatorShare is IValidatorShare, ERC20NonTransferable, OwnableLockabl
181181
uint256 liquidReward = _withdrawReward(msg.sender);
182182
require(liquidReward >= minAmount, "Too small rewards to restake");
183183

184-
uint256 amountRestaked = _buyShares(liquidReward, 0);
184+
uint256 amountRestaked = _buyShares(liquidReward, 0, msg.sender);
185185
if (liquidReward > amountRestaked) {
186186
// return change to the user
187187
require(stakeManager.transferFunds(validatorId, liquidReward - amountRestaked, msg.sender), "Insufficent rewards");
@@ -194,16 +194,16 @@ contract ValidatorShare is IValidatorShare, ERC20NonTransferable, OwnableLockabl
194194
return amountRestaked;
195195
}
196196

197-
function _buyShares(uint256 _amount, uint256 _minSharesToMint) private onlyWhenUnlocked returns(uint256) {
197+
function _buyShares(uint256 _amount, uint256 _minSharesToMint, address user) private onlyWhenUnlocked returns(uint256) {
198198
require(delegation, "Delegation is disabled");
199199

200200
uint256 rate = exchangeRate();
201201
uint256 precision = _getRatePrecision();
202202
uint256 shares = _amount.mul(precision).div(rate);
203203
require(shares >= _minSharesToMint, "Too much slippage");
204-
require(unbonds[msg.sender].shares == 0, "Ongoing exit");
204+
require(unbonds[user].shares == 0, "Ongoing exit");
205205

206-
_mint(msg.sender, shares);
206+
_mint(user, shares);
207207

208208
// clamp amount of tokens in case resulted shares requires less tokens than anticipated
209209
_amount = _amount.sub(_amount % rate.mul(shares).div(precision));
@@ -212,7 +212,7 @@ contract ValidatorShare is IValidatorShare, ERC20NonTransferable, OwnableLockabl
212212
stakeManager.updateValidatorState(validatorId, int256(_amount));
213213

214214
StakingInfo logger = stakingLogger;
215-
logger.logShareMinted(validatorId, msg.sender, _amount, shares);
215+
logger.logShareMinted(validatorId, user, _amount, shares);
216216
logger.logStakeUpdate(validatorId);
217217

218218
return _amount;
@@ -236,7 +236,7 @@ contract ValidatorShare is IValidatorShare, ERC20NonTransferable, OwnableLockabl
236236
uint256 shares = claimAmount.mul(precision).div(rate);
237237
require(shares <= maximumSharesToBurn, "too much slippage");
238238

239-
_withdrawAndTransferReward();
239+
_withdrawAndTransferReward(msg.sender);
240240

241241
_burn(msg.sender, shares);
242242
stakeManager.updateValidatorState(validatorId, -int256(claimAmount));
@@ -264,21 +264,42 @@ contract ValidatorShare is IValidatorShare, ERC20NonTransferable, OwnableLockabl
264264
return liquidRewards;
265265
}
266266

267-
function _withdrawAndTransferReward() private returns(uint256) {
268-
uint256 liquidRewards = _withdrawReward(msg.sender);
267+
function _withdrawAndTransferReward(address user) private returns(uint256) {
268+
uint256 liquidRewards = _withdrawReward(user);
269269
if (liquidRewards > 0) {
270-
require(stakeManager.transferFunds(validatorId, liquidRewards, msg.sender), "Insufficent rewards");
271-
stakingLogger.logDelegatorClaimRewards(validatorId, msg.sender, liquidRewards);
270+
require(stakeManager.transferFunds(validatorId, liquidRewards, user), "Insufficent rewards");
271+
stakingLogger.logDelegatorClaimRewards(validatorId, user, liquidRewards);
272272
}
273273

274274
return liquidRewards;
275275
}
276276

277277
function withdrawRewards() public {
278-
uint256 rewards = _withdrawAndTransferReward();
278+
uint256 rewards = _withdrawAndTransferReward(msg.sender);
279279
require(rewards >= minAmount, "Too small rewards amount");
280280
}
281281

282+
function migrateOut(address user, uint256 amount) external onlyOwner {
283+
_withdrawAndTransferReward(user);
284+
(uint256 totalStaked, uint256 rate) = _getTotalStake(user);
285+
require(totalStaked >= amount, "Migrating too much");
286+
287+
uint256 precision = _getRatePrecision();
288+
uint256 shares = amount.mul(precision).div(rate);
289+
_burn(user, shares);
290+
291+
stakeManager.updateValidatorState(validatorId, -int256(amount));
292+
_reduceActiveStake(amount);
293+
294+
stakingLogger.logShareBurned(validatorId, user, amount, shares);
295+
stakingLogger.logStakeUpdate(validatorId);
296+
stakingLogger.logDelegatorUnstaked(validatorId, user, amount);
297+
}
298+
299+
function migrateIn(address user, uint256 amount) external onlyOwner {
300+
_buyShares(amount, 0, user);
301+
}
302+
282303
function getLiquidRewards(address user) public view returns (uint256) {
283304
uint256 shares = balanceOf(user);
284305
if (shares == 0) {

test/units/staking/stakeManager/StakeManager.test.js

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import { expectEvent, expectRevert, BN } from '@openzeppelin/test-helpers'
1919
import { wallets, freshDeploy, approveAndStake } from '../deployment'
2020
import { buyVoucher } from '../ValidatorShareHelper.js'
2121

22+
const ZeroAddr = '0x0000000000000000000000000000000000000000'
23+
2224
contract('StakeManager', async function(accounts) {
2325
let owner = accounts[0]
2426

@@ -1835,4 +1837,249 @@ contract('StakeManager', async function(accounts) {
18351837
})
18361838
})
18371839
})
1840+
1841+
describe('Chad delegates to Alice then migrates partialy to Bob', async function() {
1842+
const aliceId = '2' // Matic
1843+
const bobId = '8' // Non-matic
1844+
const alice = wallets[2]
1845+
const bob = wallets[8]
1846+
const initialStakers = [wallets[1], alice, wallets[3], wallets[4], wallets[5], wallets[6], wallets[7], bob]
1847+
const stakeAmount = web3.utils.toWei('1250')
1848+
const stakeAmountBN = new BN(stakeAmount)
1849+
const delegationAmount = web3.utils.toWei('150')
1850+
const delegationAmountBN = new BN(delegationAmount)
1851+
const migrationAmount = web3.utils.toWei('100')
1852+
const migrationAmountBN = new BN(migrationAmount)
1853+
const delegator = wallets[9].getChecksumAddressString()
1854+
let aliceContract
1855+
let bobContract
1856+
1857+
before('fresh deploy', async function() {
1858+
await freshDeploy.call(this)
1859+
await this.stakeManager.updateValidatorThreshold(10, {
1860+
from: owner
1861+
})
1862+
for (const wallet of initialStakers) {
1863+
await approveAndStake.call(this, { wallet, stakeAmount, acceptDelegation: true })
1864+
}
1865+
const aliceValidator = await this.stakeManager.validators(aliceId)
1866+
aliceContract = await ValidatorShare.at(aliceValidator.contractAddress)
1867+
const bobValidator = await this.stakeManager.validators(bobId)
1868+
bobContract = await ValidatorShare.at(bobValidator.contractAddress)
1869+
})
1870+
1871+
describe('Chad delegates to Alice', async function() {
1872+
before(async function() {
1873+
await this.stakeToken.mint(delegator, delegationAmount)
1874+
await this.stakeToken.approve(this.stakeManager.address, delegationAmount, {
1875+
from: delegator
1876+
})
1877+
})
1878+
1879+
it('Should delegate', async function() {
1880+
this.receipt = await buyVoucher(aliceContract, delegationAmount, delegator)
1881+
})
1882+
1883+
it('ValidatorShare must mint correct amount of shares', async function() {
1884+
await expectEvent.inTransaction(this.receipt.tx, ValidatorShare, 'Transfer', {
1885+
from: ZeroAddr,
1886+
to: delegator,
1887+
value: delegationAmount
1888+
})
1889+
})
1890+
1891+
it('must emit ShareMinted', async function() {
1892+
await expectEvent.inTransaction(this.receipt.tx, StakingInfo, 'ShareMinted', {
1893+
validatorId: aliceId,
1894+
user: delegator,
1895+
amount: delegationAmount,
1896+
tokens: delegationAmount
1897+
})
1898+
})
1899+
1900+
it('must emit StakeUpdate', async function() {
1901+
await expectEvent.inTransaction(this.receipt.tx, StakingInfo, 'StakeUpdate', {
1902+
validatorId: aliceId,
1903+
newAmount: stakeAmountBN.add(delegationAmountBN).toString(10)
1904+
})
1905+
})
1906+
1907+
it('Active amount must be updated', async function() {
1908+
const delegatedAliceAmount = await aliceContract.getActiveAmount()
1909+
assertBigNumberEquality(delegatedAliceAmount, delegationAmountBN)
1910+
})
1911+
})
1912+
1913+
describe('Chad migrates delegation to Bob', async function() {
1914+
it('Should migrate', async function() {
1915+
this.receipt = await this.stakeManager.migrateDelegation(aliceId, bobId, migrationAmount, { from: delegator })
1916+
})
1917+
1918+
it('Alice\'s contract must burn correct amount of shares', async function() {
1919+
await expectEvent.inTransaction(this.receipt.tx, ValidatorShare, 'Transfer', {
1920+
from: delegator,
1921+
to: ZeroAddr,
1922+
value: migrationAmount
1923+
})
1924+
})
1925+
1926+
it('Alice\'s contract must emit ShareBurned', async function() {
1927+
await expectEvent.inTransaction(this.receipt.tx, StakingInfo, 'ShareBurned', {
1928+
validatorId: aliceId,
1929+
user: delegator,
1930+
amount: migrationAmount,
1931+
tokens: migrationAmount
1932+
})
1933+
})
1934+
1935+
it('must emit StakeUpdate for Alice', async function() {
1936+
await expectEvent.inTransaction(this.receipt.tx, StakingInfo, 'StakeUpdate', {
1937+
validatorId: aliceId,
1938+
newAmount: stakeAmountBN.add(delegationAmountBN).sub(migrationAmountBN).toString(10)
1939+
})
1940+
})
1941+
1942+
it('Bob\'s contract must mint correct amount of shares', async function() {
1943+
await expectEvent.inTransaction(this.receipt.tx, ValidatorShare, 'Transfer', {
1944+
from: ZeroAddr,
1945+
to: delegator,
1946+
value: migrationAmount
1947+
})
1948+
})
1949+
1950+
it('Bob\'s contract must emit ShareMinted', async function() {
1951+
await expectEvent.inTransaction(this.receipt.tx, StakingInfo, 'ShareMinted', {
1952+
validatorId: bobId,
1953+
user: delegator,
1954+
amount: migrationAmount,
1955+
tokens: migrationAmount
1956+
})
1957+
})
1958+
1959+
it('must emit StakeUpdate for Bob', async function() {
1960+
await expectEvent.inTransaction(this.receipt.tx, StakingInfo, 'StakeUpdate', {
1961+
validatorId: bobId,
1962+
newAmount: stakeAmountBN.add(migrationAmountBN).toString(10)
1963+
})
1964+
})
1965+
1966+
it('Alice active amount must be updated', async function() {
1967+
const migratedAliceAmount = await aliceContract.getActiveAmount()
1968+
assertBigNumberEquality(migratedAliceAmount, delegationAmountBN.sub(migrationAmountBN))
1969+
})
1970+
1971+
it('Bob active amount must be updated', async function() {
1972+
const migratedBobAmount = await bobContract.getActiveAmount()
1973+
assertBigNumberEquality(migratedBobAmount, migrationAmount)
1974+
})
1975+
})
1976+
})
1977+
1978+
describe('Chad tries to migrate from non matic validator', function() {
1979+
const aliceId = '9' // Non-matic
1980+
const bobId = '8' // Non-matic
1981+
const alice = wallets[9]
1982+
const bob = wallets[8]
1983+
const initialStakers = [wallets[1], wallets[2], wallets[3], wallets[4], wallets[5], wallets[6], wallets[7], bob, alice]
1984+
const stakeAmount = web3.utils.toWei('1250')
1985+
const delegationAmount = web3.utils.toWei('150')
1986+
const migrationAmount = web3.utils.toWei('100')
1987+
const delegator = wallets[9].getChecksumAddressString()
1988+
1989+
before('fresh deploy and delegate to Alice', async function() {
1990+
await freshDeploy.call(this)
1991+
await this.stakeManager.updateValidatorThreshold(10, {
1992+
from: owner
1993+
})
1994+
for (const wallet of initialStakers) {
1995+
await approveAndStake.call(this, { wallet, stakeAmount, acceptDelegation: true })
1996+
}
1997+
1998+
await this.stakeToken.mint(delegator, delegationAmount)
1999+
await this.stakeToken.approve(this.stakeManager.address, delegationAmount, {
2000+
from: delegator
2001+
})
2002+
const aliceValidator = await this.stakeManager.validators(aliceId)
2003+
const aliceContract = await ValidatorShare.at(aliceValidator.contractAddress)
2004+
await buyVoucher(aliceContract, delegationAmount, delegator)
2005+
})
2006+
2007+
it('Migration should fail', async function() {
2008+
await expectRevert(
2009+
this.stakeManager.migrateDelegation(aliceId, bobId, migrationAmount, { from: delegator }),
2010+
'Invalid migration')
2011+
})
2012+
})
2013+
2014+
describe('Chad tries to migrate to matic validator', function() {
2015+
const aliceId = '8' // Non-matic
2016+
const bobId = '2' // Matic
2017+
const alice = wallets[8]
2018+
const bob = wallets[2]
2019+
const initialStakers = [wallets[1], bob, wallets[3], wallets[4], wallets[5], wallets[6], wallets[7], alice]
2020+
const stakeAmount = web3.utils.toWei('1250')
2021+
const delegationAmount = web3.utils.toWei('150')
2022+
const migrationAmount = web3.utils.toWei('100')
2023+
const delegator = wallets[9].getChecksumAddressString()
2024+
2025+
before('fresh deploy and delegate to Alice', async function() {
2026+
await freshDeploy.call(this)
2027+
await this.stakeManager.updateValidatorThreshold(10, {
2028+
from: owner
2029+
})
2030+
for (const wallet of initialStakers) {
2031+
await approveAndStake.call(this, { wallet, stakeAmount, acceptDelegation: true })
2032+
}
2033+
2034+
await this.stakeToken.mint(delegator, delegationAmount)
2035+
await this.stakeToken.approve(this.stakeManager.address, delegationAmount, {
2036+
from: delegator
2037+
})
2038+
const aliceValidator = await this.stakeManager.validators(aliceId)
2039+
const aliceContract = await ValidatorShare.at(aliceValidator.contractAddress)
2040+
await buyVoucher(aliceContract, delegationAmount, delegator)
2041+
})
2042+
2043+
it('Migration should fail', async function() {
2044+
await expectRevert(
2045+
this.stakeManager.migrateDelegation(aliceId, bobId, migrationAmount, { from: delegator }),
2046+
'Invalid migration')
2047+
})
2048+
})
2049+
2050+
describe('Chad tries to migrate more than his delegation amount', async function() {
2051+
const aliceId = '2'
2052+
const bobId = '8'
2053+
const alice = wallets[2]
2054+
const bob = wallets[8]
2055+
const initialStakers = [wallets[1], alice, wallets[3], wallets[4], wallets[5], wallets[6], wallets[7], bob]
2056+
const stakeAmount = web3.utils.toWei('1250')
2057+
const delegationAmount = web3.utils.toWei('150')
2058+
const migrationAmount = web3.utils.toWei('200') // more than delegation amount
2059+
const delegator = wallets[9].getChecksumAddressString()
2060+
2061+
before('fresh deploy and delegate to Alice', async function() {
2062+
await freshDeploy.call(this)
2063+
await this.stakeManager.updateValidatorThreshold(10, {
2064+
from: owner
2065+
})
2066+
for (const wallet of initialStakers) {
2067+
await approveAndStake.call(this, { wallet, stakeAmount, acceptDelegation: true })
2068+
}
2069+
2070+
await this.stakeToken.mint(delegator, delegationAmount)
2071+
await this.stakeToken.approve(this.stakeManager.address, delegationAmount, {
2072+
from: delegator
2073+
})
2074+
const aliceValidator = await this.stakeManager.validators(aliceId)
2075+
const aliceContract = await ValidatorShare.at(aliceValidator.contractAddress)
2076+
await buyVoucher(aliceContract, delegationAmount, delegator)
2077+
})
2078+
2079+
it('Migration should fail', async function() {
2080+
await expectRevert(
2081+
this.stakeManager.migrateDelegation(aliceId, bobId, migrationAmount, { from: delegator }),
2082+
'Migrating too much')
2083+
})
2084+
})
18382085
})

0 commit comments

Comments
 (0)