Skip to content

Commit f34e90b

Browse files
authored
Allow unstake passing a larger amount than available (#487)
* staking: allow unstake with larger amount than available * staking: remove unused check and add comment
1 parent 834854a commit f34e90b

File tree

2 files changed

+46
-12
lines changed

2 files changed

+46
-12
lines changed

contracts/staking/Staking.sol

+12-5
Original file line numberDiff line numberDiff line change
@@ -707,27 +707,34 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking {
707707

708708
/**
709709
* @dev Unstake tokens from the indexer stake, lock them until thawing period expires.
710+
* NOTE: The function accepts an amount greater than the currently staked tokens.
711+
* If that happens, it will try to unstake the max amount of tokens it can.
712+
* The reason for this behaviour is to avoid time conditions while the transaction
713+
* is in flight.
710714
* @param _tokens Amount of tokens to unstake
711715
*/
712716
function unstake(uint256 _tokens) external override notPartialPaused {
713717
address indexer = msg.sender;
714718
Stakes.Indexer storage indexerStake = stakes[indexer];
715719

716-
require(_tokens > 0, "!tokens");
717720
require(indexerStake.tokensStaked > 0, "!stake");
718-
require(indexerStake.tokensAvailable() >= _tokens, "!stake-avail");
721+
722+
// Tokens to lock is capped to the available tokens
723+
uint256 tokensToLock = MathUtils.min(indexerStake.tokensAvailable(), _tokens);
724+
require(tokensToLock > 0, "!stake-avail");
719725

720726
// Ensure minimum stake
721-
uint256 newStake = indexerStake.tokensSecureStake().sub(_tokens);
727+
uint256 newStake = indexerStake.tokensSecureStake().sub(tokensToLock);
722728
require(newStake == 0 || newStake >= minimumIndexerStake, "!minimumIndexerStake");
723729

724-
// Before locking more tokens, withdraw any unlocked ones
730+
// Before locking more tokens, withdraw any unlocked ones if possible
725731
uint256 tokensToWithdraw = indexerStake.tokensWithdrawable();
726732
if (tokensToWithdraw > 0) {
727733
_withdraw(indexer);
728734
}
729735

730-
indexerStake.lockTokens(_tokens, thawingPeriod);
736+
// Update the indexer stake locking tokens
737+
indexerStake.lockTokens(tokensToLock, thawingPeriod);
731738

732739
emit StakeLocked(indexer, indexerStake.tokensLocked, indexerStake.tokensLockedUntil);
733740
}

test/staking/staking.test.ts

+34-7
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@ import {
1414
latestBlock,
1515
toBN,
1616
toGRT,
17+
provider,
1718
Account,
1819
} from '../lib/testHelpers'
1920

20-
const { AddressZero } = constants
21+
const { AddressZero, MaxUint256 } = constants
2122

2223
function weightedAverage(
2324
valueA: BigNumber,
@@ -269,14 +270,18 @@ describe('Staking:Stakes', () => {
269270
expect(afterIndexerBalance).eq(beforeIndexerBalance.add(tokensToUnstake))
270271
})
271272

272-
it('reject unstake zero tokens', async function () {
273-
const tx = staking.connect(indexer.signer).unstake(toGRT('0'))
274-
await expect(tx).revertedWith('!tokens')
273+
it('should unstake available tokens even if passed a higher amount', async function () {
274+
// Try to unstake a bit more than currently staked
275+
const tokensOverCapacity = tokensToStake.add(toGRT('1'))
276+
await staking.connect(indexer.signer).unstake(tokensOverCapacity)
277+
278+
// Check state
279+
const tokensLocked = (await staking.stakes(indexer.address)).tokensLocked
280+
expect(tokensLocked).eq(tokensToStake)
275281
})
276282

277-
it('reject unstake more than available tokens', async function () {
278-
const tokensOverCapacity = tokensToStake.add(toGRT('1'))
279-
const tx = staking.connect(indexer.signer).unstake(tokensOverCapacity)
283+
it('reject unstake zero tokens', async function () {
284+
const tx = staking.connect(indexer.signer).unstake(toGRT('0'))
280285
await expect(tx).revertedWith('!stake-avail')
281286
})
282287

@@ -305,6 +310,28 @@ describe('Staking:Stakes', () => {
305310
await staking.connect(indexer.signer).unstake(tokensToStake)
306311
expect(await staking.getIndexerCapacity(indexer.address)).eq(0)
307312
})
313+
314+
it('should allow unstake of full amount with no upper limits', async function () {
315+
// Use manual mining
316+
await provider().send('evm_setAutomine', [false])
317+
318+
// Setup
319+
const newTokens = toGRT('2')
320+
const stakedTokens = await staking.getIndexerStakedTokens(indexer.address)
321+
const tokensToUnstake = stakedTokens.add(newTokens)
322+
323+
// StakeTo & Unstake
324+
await staking.connect(indexer.signer).stakeTo(indexer.address, newTokens)
325+
await staking.connect(indexer.signer).unstake(MaxUint256)
326+
await provider().send('evm_mine', [])
327+
328+
// Check state
329+
const tokensLocked = (await staking.stakes(indexer.address)).tokensLocked
330+
expect(tokensLocked).eq(tokensToUnstake)
331+
332+
// Restore automine
333+
await provider().send('evm_setAutomine', [true])
334+
})
308335
})
309336

310337
describe('withdraw', function () {

0 commit comments

Comments
 (0)