60298 sc high duplicate effectivestake decrement path bricks unstake re delegate

Submitted on Nov 21st 2025 at 05:14:34 UTC by @Rhaydden for Audit Comp | Vechain | Stargate Hayabusaarrow-up-right

  • Report ID: #60298

  • 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

Finding description and impact

Stargate tracks each validator’s total delegators’ effective stake per period using OZ checkpoints. When a user:

  • delegates, it schedules an increase at validator’s next period (completedPeriods + 2),

  • requests exit, it schedules a decrease at (completedPeriods + 2),

  • and later unstakes while the validator is EXITED or pending, it schedules another decrease at (completedPeriods + 2).

So now the issue here is that requestDelegationExit()arrow-up-right unconditionally schedules a decrease for the upcoming period.

unstake() schedules another decrease if the validator is EXITED (or if the delegation is PENDING), without checking whether a decrease was already scheduled by a prior exit request. The checkpointing uses upperLookup, so once the first decrease writes a 0 from period N+2 onward, a second decrease at period M+2 (M ≥ N) tries to do 0 - effectiveStake which reverts due to arithmetic underflow.

Here is the code to reference what were talking about above:

In requestDelegationExit() (decrease scheduled):

Then in unstake() (second decrease scheduled when validator EXITED or delegation PENDING):

In _updatePeriodEffectiveStake() (unsafe subtract on duplicate decrement):

Impact

Users who requested exit and whose validator later becomes EXITED cannot call unstake() (reverts), effectively freezing their staked VET until an upgrade. Also the same duplicate-decrement condition exists in the redelegation path (when switching validators), risking reverts or incorrect accounting if not underflowing.

Impact- Critical: Permanent freezing of funds.

Consider a simple scenario:

  1. User delegates NFT T to validator V.

  2. User calls requestDelegationExit(T) while ACTIVE. Contract schedules a decrease at period R+2.

  3. Validator V later becomes EXITED.

  4. User calls unstake(T). Contract schedules another decrease at period F+2 (F ≥ R).

  5. Checkpoint at F+2 already reflects the first decrease (0), so second subtraction underflows and reverts. User can’t unstake or redelegate.

In unstake() (and in the mirrored block inside _delegate()), do not schedule a second decrease if the user has already requested exit. We could:

  • Compute bool userAlreadySignaledExit = delegation.endPeriod != type(uint32).max;

  • Replace:

    • if (currentValidatorStatus == EXITED || delegation.status == PENDING) { ... decrease }

  • With:

    • if (delegation.status == PENDING || (currentValidatorStatus == EXITED && !userAlreadySignaledExit)) { ... decrease }

Apply the same guard in the redelegation path (_delegate), where a previous delegation on an EXITED validator also triggers a decrease.

It'd be good to also add a defensive check in _updatePeriodEffectiveStake before decreasing:

  • If !_isIncrease, ensure currentValue >= effectiveStake, otherwise revert with a descriptive custom error (prevents silent state corruption and turns it into a clear failure).

Proof of Concept

Proof of concept

Created a test file called DoubleDecrementUnderflow.test.ts:

The poc:

  • Stakes and delegates a token to a validator.

  • Advances periods so delegation is ACTIVE.

  • Calls requestDelegationExit() (first scheduled decrease).

  • Sets validator status to EXITED.

  • Calls unstake() and expects revert due to the second scheduled decrease hitting 0 total.

Run the poc with VITE_APP_ENV=local npx hardhat test --network hardhat test/unit/Stargate/DoubleDecrementUnderflow.test.ts

Logs

Was this helpful?