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

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 Hayabusa

  • 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() 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?