For the complete documentation index, see llms.txt. This page is also available as Markdown.

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 Hayabusa

  • 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

  • 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.

Relevant code locations
  • 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?