Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield
Title
Permanent Denial of Service: Users Cannot Unstake When Both User and Validator Exit Due to Double Effective Stake Decrease
Description
A critical vulnerability exists in the Stargate.sol contract's unstake() function that can permanently lock user funds when a specific sequence of events occurs. The contract attempts to decrease a validator's effective stake twice when both the user requests an exit AND the validator exits, which can cause an arithmetic underflow and revert.
Vulnerability Flow
1
User delegates
User delegates their NFT to a validator β effective stake is increased.
2
User requests exit
User calls requestDelegationExit() β effective stake is decreased (FIRST DECREASE).
3
Validator exits
Validator exits (external event) β validator status becomes VALIDATOR_STATUS_EXITED.
4
User attempts unstake
When the user calls unstake(), the contract tries to decrease effective stake AGAIN (SECOND DECREASE).
5
SafeCast overflow revert
The second decrease causes an arithmetic underflow (0 - stake amount), triggering a SafeCast revert.
6
Funds permanently locked
The revert prevents unstake β user funds, withdrawals, and redelegations are blocked permanently until a contract upgrade.
Root Cause
The unstake() function contains a conditional that decreases effective stake when the validator is EXITED but does not verify whether the user already requested an exit (which already decreased the effective stake):
requestDelegationExit() already decreased the effective stake earlier (line 568). The second decrease attempt causes:
Impact
Direct Financial Impact:
Users' staked VET can become permanently locked with no recovery mechanism.
Users cannot withdraw their principal funds.
Users lose access to unclaimed rewards.
Affected users cannot redelegate to other validators.
Scale of Impact:
Affects any user who requests exit before their validator exits.
Validators may exit for maintenance, rotation, or penalties β potentially affecting many users.
No workaround exists β funds are locked until a contract upgrade.
Affected functions:
unstake() - can permanently revert.
delegate() - cannot redelegate if stuck in EXITED accounting.
Users have no way to recover funds except a contract upgrade.
Attack Scenarios
1
Natural occurrence (high probability)
User delegates to validator V1.
User requests exit to switch validators.
Validator exits for scheduled maintenance.
User's funds become permanently locked.
2
Griefing attack (medium probability)
Attacker delegates and immediately requests exit.
When validator exits, attacker's funds are locked.
Demonstrates protocol vulnerability publicly.
3
Mass DoS (low probability, extreme impact)
Social engineering to get users to request exits.
Coordinated validator exit.
Hundreds of users may be locked simultaneously.
Severe reputational damage.
Code Execution Trace
Step 1: User requests exit
Step 2: Validator exits (external event)
Step 3: User attempts unstake
Step 4: Underflow causes revert
Recommended Mitigation
Solution: Check if User Already Requested Exit
Add a check to verify if the user previously requested an exit before attempting to decrease effective stake again.
Fix implementation:
Why this fix works:
endPeriod == type(uint32).max indicates the user never called requestDelegationExit().
When requestDelegationExit() is called, it sets endPeriod to a specific period number.
By checking this condition, the code decreases effective stake only if it has not been decreased already.
This prevents the double decrease that causes the underflow.
Alternative Solution (Defense in Depth)
Add underflow protection directly in _updatePeriodEffectiveStake():
Recommendation: Implement both fixes for defense in depth.
Proof of Concept
Click to expand the full PoC test (TypeScript / Hardhat)
Save File In:packages/contracts/test/integration/CriticalBug_DoubleDecreaseDoS.test.ts
To run the test:
If you want, I can:
Produce a minimal patch/diff for the suggested fixes (both the unstake() fix and the _updatePeriodEffectiveStake() defensive check) in unified-diff format for easier PR creation.
Run through the critical code locations to list exact line numbers and function contexts for the changes (based on the target file path you provided).
// File: Stargate.sol, Lines 268-280
function unstake(uint256 _tokenId) external {
// ... existing code ...
if (
currentValidatorStatus == VALIDATOR_STATUS_EXITED ||
delegation.status == DelegationStatus.PENDING
) {
// β FIX: Only decrease if user didn't already request exit
// delegation.endPeriod == type(uint32).max means user never requested exit
// If user requested exit, endPeriod would be set to a specific period number
if (delegation.endPeriod == type(uint32).max) {
(, , , uint32 oldCompletedPeriods) = $
.protocolStakerContract
.getValidationPeriodDetails(delegation.validator);
_updatePeriodEffectiveStake(
$,
delegation.validator,
_tokenId,
oldCompletedPeriods + 2,
false
);
}
}
// ... rest of function ...
}