60080 sc high unstake exit requests can either lock funds or silently double deduct effective stake after validator exit

Submitted on Nov 18th 2025 at 10:02:01 UTC by @n0fr33w1f14u for Audit Comp | Vechain | Stargate Hayabusaarrow-up-right

  • Report ID: #60080

  • Report Type: Smart Contract

  • Report severity: High

  • Target: https://github.com/immunefi-team/audit-comp-vechain-stargate-hayabusa/tree/main/packages/contracts/contracts/Stargate.sol

  • Impact: Permanent freezing of funds

Description

Brief / Intro

The _updatePeriodEffectiveStake() function is called twice in the unstake flow:

  • First decrease in requestDelegationExit() when delegation status is ACTIVE

  • Later, if the validator status is EXITED or the delegation is still PENDING, unstake() decrements the same stake again

When both conditions happen in sequence, the second decrement occurs after the first already removed the position’s stake. If the validator doesn't have any effective stake recorded for that period (for example, the first call brought it to zero), the second subtraction tries to go below zero and triggers an underflow.

Vulnerability Details

When a user calls requestDelegationExit() while delegation is ACTIVE, the function always decreases the effective stake:

function requestDelegationExit(uint256 _tokenId) external ... {
    // ...
    if (delegation.status == DelegationStatus.ACTIVE) {
        $.protocolStakerContract.signalDelegationExit(delegationId);
    }
    
    // decrease the effective stake 
    _updatePeriodEffectiveStake($, delegation.validator, _tokenId, completedPeriods + 2, false);
}

Later, when the validator exits and the user calls unstake(), the function checks if the validator is EXITED and decreases the effective stake again:

Since the effective stake was already decreased in requestDelegationExit, the second decrease in unstake() may cause an underflow: 0 - effectiveStake.

The unstake() function checks if the validator is EXITED to determine whether to decrease effective stake, but it does not check if the exit was already requested via requestDelegationExit(). Both functions can decrease the same stake.

Impact Details

Any user who requests a delegation exit while the position is still active and later unstakes after the validator has exited will trigger an additional _updatePeriodEffectiveStake(..., false) inside unstake(). Two outcomes are possible:

  • Underflow revert: If the first decrease brings the validator’s effective stake for that period down to zero, the second decrease subtracts from zero and reverts, permanently preventing the user from unstaking. Their staked VET remains locked in the contract.

  • Silent loss of stake tracking: If other delegations still contribute positive stake to that period, the second decrease succeeds but reduces the validator’s recorded effective stake by the exiting token’s amount twice. This misaccounting silently deprives the validator’s delegators (including the exiting user) of future rewards and may cause downstream reward calculations to underpay or misallocate funds.

Either outcome is severe: users either can’t recover their staked funds or end up with incorrect reward accounting.

Proof of Concept

This is the high-level flow that demonstrates the issue:

1

Step: Stake and Delegate

  1. User stakes a token and delegates to a validator.

  2. Delegation becomes ACTIVE.

2

Step: Request Delegation Exit (first decrease)

  1. User calls requestDelegationExit() while delegation is ACTIVE.

    • This call decreases the effective stake for the upcoming period (first decrement).

3

Step: Validator Exits

  1. Validator exits (status becomes EXITED).

4

Step: Unstake (second decrease)

  1. User calls unstake().

    • unstake() checks validator status and, because it's EXITED, decreases the effective stake again for the same period (second decrement).

    • If the first decrease brought the recorded effective stake to zero, this second decrease will underflow and revert, locking the user's funds. Otherwise it will silently double-subtract and corrupt stake accounting.

Test environment

  • OS: Windows 11, PowerShell

  • Node.js: v22.20.0

  • npm: 10.9.3

  • Foundry: 1.3.5-stable

  • Hardhat: 2.26.3

Create the file DoubleDecreaseEffectiveStake.poc.test.ts in packages/contracts/test/unit/Stargate to run this PoC.

Test command:

Environment variable: VITE_APP_ENV=local

PoC test (keep as-is — demonstrates revert with Panic code 0x11 when double-decrement occurs):

Notes:

  • All links and repository paths are preserved as provided.

  • The PoC test demonstrates the underflow revert (Panic 0x11) when the double-decrement occurs.

Was this helpful?