60575 sc high double subtraction of delegator effective stake on exit can freeze vet and break reward distribution

Submitted on Nov 24th 2025 at 07:12:15 UTC by @unineko for Audit Comp | Vechain | Stargate Hayabusaarrow-up-right

  • Report ID: #60575

  • 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

    • Contract fails to deliver promised returns, but doesn't lose value

    • Permanent freezing of unclaimed yield

Description

Brief/Intro

When a delegator requests to exit and later unstakes after the validator has exited, Stargate decreases that delegator's effective stake twice for the same period range. The first decrease happens in requestDelegationExit, and a second decrease happens in unstake (and similarly in some re-delegation flows), without checking whether an exit was already requested. In the single-delegator case this can cause an underflow and revert, permanently blocking unstake and freezing the staked VET. In the multi-delegator case it silently double-subtracts the exiting delegator's stake from the per-period totals, causing remaining delegators to receive no rewards for those periods. If no upgrade or manual rescue mechanism is available, this effectively results in a permanent freezing of the user's staked VET for affected validators.


Vulnerability Details

1. One-Time Decrease on requestDelegationExit

When a user requests to exit an active delegation, Stargate decreases that token's effective stake starting from a future period so that it stops participating in rewards:

Conceptually:

  • Before the call, delegatorsEffectiveStake[validator][period] includes this delegator's effective stake

  • requestDelegationExit calls _updatePeriodEffectiveStake with _isIncrease = false, subtracting that stake from the delegator totals used to compute rewards

  • At this point, the accounting correctly reflects that the delegator will not earn rewards from that future period onward


2. Second Decrease on Unstake After Validator Exit

Later, once the validator has exited and the user calls unstake, Stargate's unstake implementation performs another decrease of the same delegator's effective stake based only on validator status and delegation status, without checking whether requestDelegationExit already ran:

Key issues:

  • The condition uses only currentValidatorStatus and delegation.status

  • It does not check whether an exit was already requested (for example via endPeriod != type(uint32).max or a dedicated hasRequestedExit flag)

  • In the common flow where the user first calls requestDelegationExit and later unstake after the validator has exited, the same token's effective stake is decreased twice for overlapping or identical period ranges


3. Decrease Implementation Can Underflow

The decrease logic is centralized in _updatePeriodEffectiveStake:

Solidity 0.8.x reverts on underflow. Therefore:

  • If currentValue already excludes this delegator's stake (because it was previously subtracted in requestDelegationExit), and

  • currentValue < effectiveStake,

  • then currentValue - effectiveStake underflows and the entire transaction reverts


4. Concrete Scenarios

Case A: Single Delegator on a Validator (Underflow and Freeze)

  1. Validator V has a single delegator A with effective stake 100

  2. For the future period P = completedPeriods + 2, delegatorsEffectiveStake[V][P] = 100

  3. A calls requestDelegationExit(tokenId):

    • _updatePeriodEffectiveStake is invoked with _isIncrease = false

    • currentValue = 100, effectiveStake = 100

    • New value = 100 - 100 = 0

    • Stored value: delegatorsEffectiveStake[V][P] = 0

  4. The validator eventually exits and its status becomes VALIDATOR_STATUS_EXITED

  5. A calls unstake(tokenId):

    • The if condition in unstake is satisfied (currentValidatorStatus == EXITED)

    • _updatePeriodEffectiveStake is called again for the same _validator and period range

    • currentValue = delegatorsEffectiveStake[V][P] = 0, effectiveStake = 100

    • New value attempts to compute 0 - 100, which underflows and reverts

Result:

  • unstake reverts before the VET refund

  • There is no alternative unstake path that skips this second decrease

  • A's staked VET is effectively frozen until a contract upgrade or manual intervention

  • No special attacker privileges are required; this is triggered by the normal sequence "request exit → validator exit → unstake"

Case B: Multiple Delegators (Silent Reward Mis-Accounting)

Now assume three delegators on V:

  • A with effective stake 100 (eventually exits)

  • B with effective stake 50

  • C with effective stake 50

  • Total effective stake prior to A's exit is 200

  1. A calls requestDelegationExit:

    • delegatorsEffectiveStake[V][P] goes from 200 to 100 (only B + C)

    • This is correct so far

  2. Later, when V is EXITED, A calls unstake:

    • currentValue at _updatePeriodEffectiveStake time is 100 (the stake of B + C)

    • effectiveStake for A is still 100

    • New value becomes 100 - 100 = 0

    • delegatorsEffectiveStake[V][P] is now 0

From this point onward, reward calculation for period P that relies on delegatorsEffectiveStake will treat the total effective stake as 0, even though B and C are still active and should have a combined stake of 100. Depending on how division-by-zero or zero-total cases are handled, this can:

  • Completely remove B and C from future reward allocations for that period range, or

  • Force a special case that results in no rewards distributed to delegators

Effectively, the protocol underpays or never pays promised rewards to remaining delegators for those periods, even though no funds are directly stolen.


Impact Details

Chosen Impacts

1. Permanent Freezing of Funds

In the single-delegator scenario (Case A):

  • The delegator follows the intended flow: delegate, request exit, wait for validator exit, then call unstake

  • Because the effective stake has already been subtracted once, the second subtraction in unstake underflows and reverts

  • The transaction fails before any VET is refunded and there is no alternative unstake path that avoids this logic

  • If no upgrade or administrative workaround is available, this amounts to effectively permanent freezing of the delegator's staked VET. Every subsequent attempt to unstake under the same conditions will revert for the same reason

2. Contract Fails to Deliver Promised Returns / Permanent Freezing of Unclaimed Yield

In the multi-delegator scenario (Case B):

  • The exiting delegator's effective stake is subtracted twice from the validator's per-period delegator totals

  • The second subtraction removes not only the exiting delegator's contribution but also the remaining delegators' contribution

  • As a result, the protocol's accounting may consider the total effective stake for that period to be zero, even though other delegators are still active

Depending on the exact reward calculation, this can:

  • Cause remaining delegators to receive no yield for those periods

  • Permanently lose their claim to rewards that conceptually should be theirs (unclaimed yield is effectively frozen or never generated at the accounting level)

This is not a direct theft of principal, but it is a failure to deliver promised rewards to honest delegators and a permanent loss of expected yield.

Attacker Model and Likelihood

  • No special role or privileged access is required

  • The issue is triggered by normal usage patterns:

    1. Delegator requests exit

    2. Validator later exits

    3. Delegator calls unstake

  • Single-delegator validators or small-validator sets make the underflow scenario especially likely

  • For larger validator sets, mis-accounting of rewards for remaining delegators is possible without causing a revert, degrading reward correctness over time


References

Contract References

contracts/Stargate.sol

  • requestDelegationExit(uint256 _tokenId) (Line 586) - first decrease of effective stake via _updatePeriodEffectiveStake(..., false) using completedPeriods + 2

  • unstake(uint256 _tokenId) (Lines 266-288) - second decrease of effective stake when currentValidatorStatus == EXITED or delegation.status == PENDING, without checking whether exit was already requested

  • _updatePeriodEffectiveStake(...) (Lines 1026-1046) - performs currentValue - effectiveStake on delegatorsEffectiveStake and relies on Solidity 0.8 underflow checks, which revert on negative results

These functions together implement a state machine where a delegator's effective stake can be decreased twice for the same period range, leading to either a reverting unstake (funds freeze) or incorrect reward totals (permanent loss of yield) depending on validator composition.

Proof of Concept

npx hardhat test --network hardhat test/unit/PoC/001_C1_PoC.test.ts --show-stack-traces

Was this helpful?