When a validator exits after the delegator has already requested delegation exit, the protocol incorrectly decrements the validatorβs effective stake a second time when the user later calls unstake or redelegates the NFT. This double decrement causes an arithmetic underflow in _updatePeriodEffectiveStake, making both unstake and delegate revert permanently for that token. As a result, the delegator cannot recover their staked VET, leading to permanent freezing of funds (Critical).
Vulnerability Details
The core issue is how Stargate updates a validatorβs effective stake when:
A delegation has already EXITED,
The validator itself has also EXITED, and
The user later calls unstake or delegate again.
Once a delegator has exited, the protocol has already decreased the validatorβs effective stake for that token via _updatePeriodEffectiveStake as part of the normal exit flow when the user calls requestDelegationExit. After this point, delegatorsEffectiveStake[validator].upperLookup(period) correctly reflects the reduced value (often going to zero if this was the only delegator).
Later, when the user calls unstake after both:
the delegation is EXITED, and
the validator is in status EXITED,
we hit the branch:
Concretely, that branch does roughly this:
_updatePeriodEffectiveStake is called again with false (subtract), over periods where the first decrement has already been applied. This leads to:
An underflow (panic 0x11) , or
Double subtraction of their amount from the validatorβs effective stake if other delegators remain.
The same double-decrement pattern appears when re-delegating, since delegate also calls _updatePeriodEffectiveStake under currentValidatorStatus == VALIDATOR_STATUS_EXITED. In both cases, once the validator and delegation have exited, subsequent unstake and re-delegate calls revert due to underflow in _updatePeriodEffectiveStake, preventing users from recovering or moving their staked funds.
Impact Details
Critical β Permanent freezing of funds
In the single delegator case, once:
The delegation is EXITED, and
The validator is EXITED
both:
unstake(tokenId) and
delegate(tokenId, newValidator)
permanently revert with panic 0x11 in _updatePeriodEffectiveStake. There is no alternative user accessible function that allows withdrawal of the staked VET. Their VET is permanently frozen.
In the multi-delegator case:
The double decrement leads to incorrect delegatorsEffectiveStake[validator], reducing it to zero while some delegations still exist.
This can:
Underpay remaining delegators (they receive rewards as if there were no stake), and/or
Break any logic relying on effective stake (reward share, monitoring, etc.).
Even in the multi-delegator case, if the remaining delegatorsβ combined effective stake is smaller than the exiting delegatorβs stake (because of different token levels), the second decrease in _updatePeriodEffectiveStake will underflow and cause their subsequent unstake or delegate calls to revert (freezing of funds).
Proof of Concept
Proof of Concept
We show the bug with two tests:
Test "should revert when unstaking after validator exit due to effective stake underflow"
A user stakes and delegates the stake to a validator: Effective stake of the validator increases.
The user then calls requestDelegationExit: Effective stake of the validator becomes zero.
The validator exits.
The user calls unstake: The call reverts because of underflow in _updatePeriodEffectiveStake
Same with delegate: The user calls delegate: The call reverts because of underflow in _updatePeriodEffectiveStake
Test "should reduce effective stake twice incorrectly"
Two users stake and delegate the stake to a validator: Effective stake of the validator increases by the sum of the two stakes.
One user then calls requestDelegationExit: Effective stake of the validator decreases by half.
The validator exits.
The user who exited calls delegate: The effective stake of the validator becomes zero (the user's stake was incorrectly subtracted again).
Here are the tests:
We used TEST_LOGS=1 VITE_APP_ENV=local npx hardhat test --network hardhat test/unit/Stargate/ReportUnstake.test.ts
// if the delegation is pending or the validator is exited or unknown
// decrease the effective stake of the previous validator
if (
currentValidatorStatus == VALIDATOR_STATUS_EXITED ||
delegation.status == DelegationStatus.PENDING
)