Skip to content

Commit

Permalink
staking: allow unstake with larger amount than available
Browse files Browse the repository at this point in the history
  • Loading branch information
abarmat committed Jul 19, 2021
1 parent c4ee6d7 commit b2a4c17
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 32 deletions.
52 changes: 27 additions & 25 deletions contracts/staking/Staking.sol
Original file line number Diff line number Diff line change
Expand Up @@ -712,19 +712,23 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking {

require(_tokens > 0, "!tokens");
require(indexerStake.tokensStaked > 0, "!stake");
require(indexerStake.tokensAvailable() >= _tokens, "!stake-avail");

// Tokens to lock is capped to the available tokens
uint256 tokensToLock = MathUtils.min(indexerStake.tokensAvailable(), _tokens);
require(tokensToLock > 0, "!stake-avail");

// Ensure minimum stake
uint256 newStake = indexerStake.tokensSecureStake().sub(_tokens);
uint256 newStake = indexerStake.tokensSecureStake().sub(tokensToLock);
require(newStake == 0 || newStake >= minimumIndexerStake, "!minimumIndexerStake");

// Before locking more tokens, withdraw any unlocked ones
// Before locking more tokens, withdraw any unlocked ones if possible
uint256 tokensToWithdraw = indexerStake.tokensWithdrawable();
if (tokensToWithdraw > 0) {
_withdraw(indexer);
}

indexerStake.lockTokens(_tokens, thawingPeriod);
// Update the indexer stake locking tokens
indexerStake.lockTokens(tokensToLock, thawingPeriod);

emit StakeLocked(indexer, indexerStake.tokensLocked, indexerStake.tokensLockedUntil);
}
Expand Down Expand Up @@ -976,10 +980,10 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking {

// -- Collect protocol tax --
// If the Allocation is not active or closed we are going to charge a 100% protocol tax
uint256 usedProtocolPercentage =
(allocState == AllocationState.Active || allocState == AllocationState.Closed)
? protocolPercentage
: MAX_PPM;
uint256 usedProtocolPercentage = (allocState == AllocationState.Active ||
allocState == AllocationState.Closed)
? protocolPercentage
: MAX_PPM;
uint256 protocolTax = _collectTax(graphToken, queryFees, usedProtocolPercentage);
queryFees = queryFees.sub(protocolTax);

Expand Down Expand Up @@ -1114,17 +1118,16 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking {
// Creates an allocation
// Allocation identifiers are not reused
// The assetHolder address can send collected funds to the allocation
Allocation memory alloc =
Allocation(
_indexer,
_subgraphDeploymentID,
_tokens, // Tokens allocated
epochManager().currentEpoch(), // createdAtEpoch
0, // closedAtEpoch
0, // Initialize collected fees
0, // Initialize effective allocation
_updateRewards(_subgraphDeploymentID) // Initialize accumulated rewards per stake allocated
);
Allocation memory alloc = Allocation(
_indexer,
_subgraphDeploymentID,
_tokens, // Tokens allocated
epochManager().currentEpoch(), // createdAtEpoch
0, // closedAtEpoch
0, // Initialize collected fees
0, // Initialize effective allocation
_updateRewards(_subgraphDeploymentID) // Initialize accumulated rewards per stake allocated
);
allocations[_allocationID] = alloc;

// Mark allocated tokens as used
Expand All @@ -1135,7 +1138,7 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking {
subgraphAllocations[alloc.subgraphDeploymentID] = subgraphAllocations[
alloc.subgraphDeploymentID
]
.add(alloc.tokens);
.add(alloc.tokens);

emit AllocationCreated(
_indexer,
Expand Down Expand Up @@ -1208,7 +1211,7 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking {
subgraphAllocations[alloc.subgraphDeploymentID] = subgraphAllocations[
alloc.subgraphDeploymentID
]
.sub(alloc.tokens);
.sub(alloc.tokens);

emit AllocationClosed(
alloc.indexer,
Expand Down Expand Up @@ -1310,10 +1313,9 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking {
uint256 delegatedTokens = _tokens.sub(delegationTax);

// Calculate shares to issue
uint256 shares =
(pool.tokens == 0)
? delegatedTokens
: delegatedTokens.mul(pool.shares).div(pool.tokens);
uint256 shares = (pool.tokens == 0)
? delegatedTokens
: delegatedTokens.mul(pool.shares).div(pool.tokens);

// Update the delegation pool
pool.tokens = pool.tokens.add(delegatedTokens);
Expand Down
41 changes: 34 additions & 7 deletions test/staking/staking.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ import {
latestBlock,
toBN,
toGRT,
provider,
Account,
} from '../lib/testHelpers'

const { AddressZero } = constants
const { AddressZero, MaxUint256 } = constants

function weightedAverage(
valueA: BigNumber,
Expand Down Expand Up @@ -269,17 +270,21 @@ describe('Staking:Stakes', () => {
expect(afterIndexerBalance).eq(beforeIndexerBalance.add(tokensToUnstake))
})

it('should unstake available tokens even if passed a higher amount', async function () {
// Try to unstake a bit more than currently staked
const tokensOverCapacity = tokensToStake.add(toGRT('1'))
await staking.connect(indexer.signer).unstake(tokensOverCapacity)

// Check state
const tokensLocked = (await staking.stakes(indexer.address)).tokensLocked
expect(tokensLocked).eq(tokensToStake)
})

it('reject unstake zero tokens', async function () {
const tx = staking.connect(indexer.signer).unstake(toGRT('0'))
await expect(tx).revertedWith('!tokens')
})

it('reject unstake more than available tokens', async function () {
const tokensOverCapacity = tokensToStake.add(toGRT('1'))
const tx = staking.connect(indexer.signer).unstake(tokensOverCapacity)
await expect(tx).revertedWith('!stake-avail')
})

it('reject unstake under the minimum indexer stake', async function () {
const minimumIndexerStake = await staking.minimumIndexerStake()
const tokensStaked = (await staking.stakes(indexer.address)).tokensStaked
Expand All @@ -305,6 +310,28 @@ describe('Staking:Stakes', () => {
await staking.connect(indexer.signer).unstake(tokensToStake)
expect(await staking.getIndexerCapacity(indexer.address)).eq(0)
})

it('should allow unstake of full amount with no upper limits', async function () {
// Use manual mining
await provider().send('evm_setAutomine', [false])

// Setup
const newTokens = toGRT('2')
const stakedTokens = await staking.getIndexerStakedTokens(indexer.address)
const tokensToUnstake = stakedTokens.add(newTokens)

// StakeTo & Unstake
await staking.connect(indexer.signer).stakeTo(indexer.address, newTokens)
await staking.connect(indexer.signer).unstake(MaxUint256)
await provider().send('evm_mine', [])

// Check state
const tokensLocked = (await staking.stakes(indexer.address)).tokensLocked
expect(tokensLocked).eq(tokensToUnstake)

// Restore automine
await provider().send('evm_setAutomine', [true])
})
})

describe('withdraw', function () {
Expand Down

0 comments on commit b2a4c17

Please sign in to comment.