Skip to content

Commit edadbb0

Browse files
committed
staking: withdraw any unlocked tokens before unstaking
This fixes: tokens ready for withdrawal can get locked again by future unstaking calls. - Withdraw tokens that already passed the thawing period when unstake is called - Add require to avoid unstaking zero tokens
1 parent e59aef2 commit edadbb0

File tree

3 files changed

+83
-20
lines changed

3 files changed

+83
-20
lines changed

contracts/staking/Staking.sol

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,7 @@ contract Staking is StakingV1Storage, GraphUpgradeable, IStaking {
551551
address indexer = msg.sender;
552552
Stakes.Indexer storage indexerStake = stakes[indexer];
553553

554+
require(_tokens > 0, "Cannot unstake zero tokens");
554555
require(indexerStake.hasTokens(), "Indexer has no stakes");
555556
require(
556557
indexerStake.tokensAvailable() >= _tokens,
@@ -564,6 +565,12 @@ contract Staking is StakingV1Storage, GraphUpgradeable, IStaking {
564565
"Stake must be above minimum required"
565566
);
566567

568+
// Before locking more tokens, withdraw any unlocked ones
569+
uint256 tokensToWithdraw = indexerStake.tokensWithdrawable();
570+
if (tokensToWithdraw > 0) {
571+
_withdraw(indexer);
572+
}
573+
567574
indexerStake.lockTokens(_tokens, thawingPeriod);
568575

569576
emit StakeLocked(indexer, indexerStake.tokensLocked, indexerStake.tokensLockedUntil);
@@ -573,16 +580,7 @@ contract Staking is StakingV1Storage, GraphUpgradeable, IStaking {
573580
* @dev Withdraw indexer tokens once the thawing period has passed.
574581
*/
575582
function withdraw() external override notPaused {
576-
address indexer = msg.sender;
577-
578-
// Get tokens available for withdraw and update balance
579-
uint256 tokensToWithdraw = stakes[indexer].withdrawTokens();
580-
require(tokensToWithdraw > 0, "No tokens available to withdraw");
581-
582-
// Return tokens to the indexer
583-
require(graphToken().transfer(indexer, tokensToWithdraw), "!transfer");
584-
585-
emit StakeWithdrawn(indexer, tokensToWithdraw);
583+
_withdraw(msg.sender);
586584
}
587585

588586
/**
@@ -881,6 +879,23 @@ contract Staking is StakingV1Storage, GraphUpgradeable, IStaking {
881879
emit StakeDeposited(_indexer, _tokens);
882880
}
883881

882+
/**
883+
* @dev Withdraw indexer tokens once the thawing period has passed.
884+
* @param _indexer Address of indexer to withdraw funds from
885+
*/
886+
function _withdraw(address _indexer) internal {
887+
Stakes.Indexer storage indexerStake = stakes[_indexer];
888+
889+
// Get tokens available for withdraw and update balance
890+
uint256 tokensToWithdraw = indexerStake.withdrawTokens();
891+
require(tokensToWithdraw > 0, "No tokens available to withdraw");
892+
893+
// Return tokens to the indexer
894+
require(graphToken().transfer(_indexer, tokensToWithdraw), "!transfer");
895+
896+
emit StakeWithdrawn(_indexer, tokensToWithdraw);
897+
}
898+
884899
/**
885900
* @dev Allocate available tokens to a subgraph deployment.
886901
* @param _indexer Indexer address to allocate funds from.

contracts/staking/libs/Stakes.sol

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,20 +56,20 @@ library Stakes {
5656
}
5757

5858
/**
59-
* @dev Lock tokens until a thawing period expires.
59+
* @dev Lock tokens until a thawing period pass.
6060
* @param stake Stake data
6161
* @param _tokens Amount of tokens to unstake
62-
* @param _thawingPeriod Period in blocks that need to pass before withdrawal
62+
* @param _period Period in blocks that need to pass before withdrawal
6363
*/
6464
function lockTokens(
6565
Stakes.Indexer storage stake,
6666
uint256 _tokens,
67-
uint256 _thawingPeriod
67+
uint256 _period
6868
) internal {
6969
// Take into account period averaging for multiple unstake requests
70-
uint256 lockingPeriod = _thawingPeriod;
70+
uint256 lockingPeriod = _period;
7171
if (stake.tokensLocked > 0) {
72-
lockingPeriod = stake.getLockingPeriod(_tokens, _thawingPeriod);
72+
lockingPeriod = stake.getLockingPeriod(_tokens, _period);
7373
}
7474

7575
// Update balances

test/staking/staking.test.ts

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -197,10 +197,10 @@ describe('Staking:Stakes', () => {
197197
const tx1 = await staking.connect(indexer.signer).unstake(tokensToUnstake)
198198
const receipt1 = await tx1.wait()
199199
const event1: Event = receipt1.events.pop()
200-
const tokensLockedUntil1 = event1.args[2]
200+
const tokensLockedUntil1 = event1.args['until']
201201

202-
// Move forward
203-
await advanceBlockTo(tokensLockedUntil1)
202+
// Move forward before the tokens are unlocked for withdrawal
203+
await advanceBlockTo(tokensLockedUntil1.sub(5))
204204

205205
// Calculate locking time for tokens taking into account the previous unstake request
206206
const currentBlock = await latestBlock()
@@ -215,9 +215,57 @@ describe('Staking:Stakes', () => {
215215
// Unstake (2)
216216
const tx2 = await staking.connect(indexer.signer).unstake(tokensToUnstake)
217217
const receipt2 = await tx2.wait()
218+
219+
// Verify events
218220
const event2: Event = receipt2.events.pop()
219-
const tokensLockedUntil2 = event2.args[2]
220-
expect(expectedLockedUntil).eq(tokensLockedUntil2)
221+
expect(event2.args['until']).eq(expectedLockedUntil)
222+
223+
// Verify state
224+
const afterIndexerStake = await staking.stakes(indexer.address)
225+
expect(afterIndexerStake.tokensLocked).eq(tokensToUnstake.mul(2)) // we unstaked two times
226+
expect(afterIndexerStake.tokensLockedUntil).eq(expectedLockedUntil)
227+
})
228+
229+
it('should unstake and withdraw if some tokens are unthawed', async function () {
230+
const tokensToUnstake = toGRT('10')
231+
const thawingPeriod = toBN(await staking.thawingPeriod())
232+
233+
const beforeIndexerBalance = await grt.balanceOf(indexer.address)
234+
235+
// Unstake (1)
236+
const tx1 = await staking.connect(indexer.signer).unstake(tokensToUnstake)
237+
const receipt1 = await tx1.wait()
238+
const event1: Event = receipt1.events.pop()
239+
const tokensLockedUntil1 = event1.args['until']
240+
241+
// Move forward after the tokens are unlocked for withdrawal
242+
await advanceBlockTo(tokensLockedUntil1)
243+
244+
// Calculate locking time for tokens taking into account some tokens are withdraweable
245+
const currentBlock = await latestBlock()
246+
const expectedLockedUntil = currentBlock.add(thawingPeriod).add(toBN('1'))
247+
248+
// Unstake (2)
249+
const tx2 = await staking.connect(indexer.signer).unstake(tokensToUnstake)
250+
const receipt2 = await tx2.wait()
251+
252+
// Verify events
253+
const unstakeEvent: Event = receipt2.events.pop()
254+
const withdrawEvent: Event = receipt2.events.pop()
255+
expect(unstakeEvent.args['until']).eq(expectedLockedUntil)
256+
expect(withdrawEvent.args['tokens']).eq(tokensToUnstake)
257+
258+
// Verify state
259+
const afterIndexerStake = await staking.stakes(indexer.address)
260+
const afterIndexerBalance = await grt.balanceOf(indexer.address)
261+
expect(afterIndexerStake.tokensLocked).eq(tokensToUnstake)
262+
expect(afterIndexerStake.tokensLockedUntil).eq(expectedLockedUntil)
263+
expect(afterIndexerBalance).eq(beforeIndexerBalance.add(tokensToUnstake))
264+
})
265+
266+
it('reject unstake zero tokens', async function () {
267+
const tx = staking.connect(indexer.signer).unstake(toGRT('0'))
268+
await expect(tx).revertedWith('Cannot unstake zero tokens')
221269
})
222270

223271
it('reject unstake more than available tokens', async function () {

0 commit comments

Comments
 (0)