59904 sc high it s possible to decrease twice delegator stake in certain conditions

Submitted on Nov 16th 2025 at 20:50:22 UTC by @Paludo0x for Audit Comp | Vechain | Stargate Hayabusaarrow-up-right

  • Report ID: #59904

  • 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

    • Theft of unclaimed yield

Description

Brief / Intro

A delegator can trigger two scheduled decreases of the validator’s cumulative delegators effective stake by first calling requestDelegationExit while the validator is ACTIVE and later calling unstake after the validator becomes EXITED.

Vulnerability Details

Updates of delegatorsEffectiveStake are handled through _updatePeriodEffectiveStake, which reads the current cumulative value for _period using upperLookup and then adds or subtracts the token’s current effective stake:

function _updatePeriodEffectiveStake(
    StargateStorage storage $,
    address _validator,
    uint256 _tokenId,
    uint32 _period,
    bool _isIncrease
) private {
    // calculate the effective stake
    uint256 effectiveStake = _calculateEffectiveStake($, _tokenId);

    // get the current effective stake
    uint256 currentValue = $.delegatorsEffectiveStake[_validator].upperLookup(_period);

    // calculate the updated effective stake
    uint256 updatedValue = _isIncrease
        ? currentValue + effectiveStake
        : currentValue - effectiveStake;

    // push the updated effective stake
    $.delegatorsEffectiveStake[_validator].push(_period, SafeCast.toUint224(updatedValue));
}

There are two different paths which schedule a decrease of delegatorsEffectiveStake. These are represented below as steps.

1

Exit requested while validator is ACTIVE (schedules a decrease at completedPeriods + 2)

This path schedules a decrease when the delegator calls requestDelegationExit while the validator is ACTIVE.

2

Unstake when validator is EXITED (or delegation is PENDING) (schedules a decrease at oldCompletedPeriods + 2)

This path schedules a decrease when unstake is called and the validator is EXITED or the delegation is PENDING.

Because there is no specific guard preventing both scheduling actions for the same token, both scheduled decreases may subtract the same token’s effective stake at different future periods.

This is the snippet of the faulty code in unstake():

Impact Details

The share formula used by _claimableRewardsForPeriod:

Consequences:

  • When the second decrease lands on a period where the cumulative already reflects the first decrease (zero in a single-delegator validator), the subtraction attempts cause an underflow and therefore revert inside _updatePeriodEffectiveStake.

  • When there are multiple delegators so the cumulative stays > 0, the double subtraction understates the denominator from that later period onward, inflating all other positions' shares.

Critical impact: Permanent freezing of funds — in pools where there is a sole delegator, the second scheduled decrease causes an underflow reverting _updatePeriodEffectiveStake. All flows that must apply this scheduled decrease will keep reverting, effectively freezing funds permanently.

Proof of Concept

chevron-rightPoC logs and test cases (expand to view)hashtag

PoC 1 — demonstrates the vulnerability (revert / underflow when sole delegator):

PoC 2 — demonstrates expected behaviour when validator stays ACTIVE (no second scheduled decrease):

PoC tests to copy into Delegation.test.ts:

Summary

  • Root cause: lack of guard to prevent scheduling the same token's effective stake to be decreased twice via two different code paths (requestDelegationExit while ACTIVE and unstake when EXITED/PENDING).

  • Consequences: underflow reverts when sole delegator -> permanent freeze; incorrect reward distribution when multiple delegators -> inflated shares for others.

  • Files referenced: Stargate.sol (target repository link above).

Was this helpful?