59723 sc high double decrease after exit validator exited leads to underflow and permanent freeze

Submitted on Nov 15th 2025 at 08:50:10 UTC by @yesofcourse for Audit Comp | Vechain | Stargate Hayabusaarrow-up-right

  • Report ID: #59723

  • 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

  • Impacts:

    • Permanent freezing of funds

Description

Brief / Intro

When a delegator requests exit while their validator is ACTIVE, the contract schedules a future decrease to the validator’s delegatorsEffectiveStake trace.

If, before the user calls unstake() or redelegates, the validator later transitions to EXITED, unstake()/_delegate() schedule a second decrease for the same token/validator line.

Because the first decrease already removed this token’s stake at that (or an earlier) checkpoint, the second subtracts from a zero (or too-small) value and triggers a Solidity 0.8 arithmetic underflow (panic 0x11), permanently reverting exit/redelegation.

On mainnet this strands the user’s principal VET.

Vulnerability details (core snippet)

The protocol mutates per-validator total delegators’ effective stake using Checkpoints.Trace224, writing an updated value for a future period:

Two separate call sites schedule decreases:

  • User-initiated exit (schedules a decrease one period in the future):

  • Validator EXITED (or PENDING) branch in exit/move flows (schedules another decrease):

Because the first decrease already “removes” the NFT’s effective stake for that future period, the second call often finds:

This is not an owner/admin/multisig scenario: it’s a natural race between (a) the user having requested exit and (b) the validator later becoming EXITED before the user finalizes with unstake()/redelegation.

In sole-delegator cases the underflow is guaranteed; in multi-delegator cases it may still underflow (if current < effectiveStake) or at minimum double-subtract accounting.

Reproduced behavior

1

Steps 1–3: Stake, become ACTIVE, request exit (schedules first decrease)

  • Delegate token, advance to ACTIVE.

  • Call requestDelegationExit() → schedules first decrease at completedPeriods + 2.

2

Step 4: Validator flips to EXITED before user finalizes

  • Validator transitions to EXITED (without the user calling unstake()).

3

Step 5: User calls unstake() / redelegates -> revert

  • unstake() (or redelegation) schedules a second decrease for oldCompletedPeriods + 2.

  • Both decreases land on the same checkpoint, so the second subtracts from zero and triggers a Solidity underflow (panic 0x11), reverting the transaction and freezing the position.

Impact details

triangle-exclamation
  • Blast radius:

    • Deterministic for sole-delegator validators (second decrease subtracts from zero).

    • Probabilistic but likely for multi-delegator validators (if aggregate at the second checkpoint < effectiveStake). Even when it doesn’t underflow, accounting is double-subtracted for an already-exited validator.

  • No user-land recovery: there is no user-accessible method to “unschedule” or compensate the duplicate decrease; only an admin upgrade/migration could unbrick affected NFTs.

  • Cost to protocol/users: frozen principal VET for each impacted token; potential support load and reputational risk; any downstream logic relying on the trace can be skewed.

Proof of Concept

Add the following file to contracts/test/unit/Stargate/DoubleDecreaseFreeze.test.ts and run with:

npx hardhat test test/unit/Stargate/DoubleDecreaseFreeze.test.ts

from the packages/contracts directory.

The PoC demonstrates that:

  • requesting exit while ACTIVE schedules a first future decrease at completedPeriods + 2;

  • if the validator later becomes EXITED, unstake() schedules a second decrease at oldCompletedPeriods + 2 for the same token/validator;

  • both decreases land on the same checkpoint, so currentValue at that period is already 0 when the second decrease runs;

  • _updatePeriodEffectiveStake then computes 0 - effectiveStake, triggering a Solidity ≥0.8 underflow panic (0x11);

  • unstake() (and redelegation) consistently reverts, leaving the position non-exitable.

chevron-rightRelevant code locationshashtag
  • Stargate.sol::_updatePeriodEffectiveStake (decrease branch assumes current ≥ effectiveStake) https://github.com/immunefi-team/audit-comp-vechain-stargate-hayabusa/blob/main/packages/contracts/contracts/Stargate.sol#L993-L1013

  • Stargate.sol::requestDelegationExit → schedules first decrease at completedPeriods + 2 https://github.com/immunefi-team/audit-comp-vechain-stargate-hayabusa/blob/main/packages/contracts/contracts/Stargate.sol#L568

  • Stargate.sol::unstake and Stargate.sol::_delegate → on validatorStatus == EXITED || status == PENDING schedule second decrease at oldCompletedPeriods + 2 https://github.com/immunefi-team/audit-comp-vechain-stargate-hayabusa/blob/main/packages/contracts/contracts/Stargate.sol#L280

Was this helpful?