59802 sc high double subtraction of validator effective stake will permanently lock other delegators staked vet

Submitted on Nov 16th 2025 at 00:10:54 UTC by @xKeywordx for Audit Comp | Vechain | Stargate Hayabusaarrow-up-right

  • Report ID: #59802

  • 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

The Stargate contract maintains a per-validator checkpointed value of delegators’ effective stake in this mapping:

mapping(address validator => Checkpoints.Trace224 amount) delegatorsEffectiveStake;

This is updated via _updatePeriodEffectiveStake whenever delegations are added, moved, exited, or unstaked.

There is a sequence of interaction between the Stargate::requestDelegationExit, Stargate::unstake and Stargate::getDelegationStatus functions, which can cause the delegatorsEffectiveStake of a validator to be subtracted twice, corrupting the delegatorsEffectiveStake accounting and leading to a permanent freezing of funds.

The following stepper explains how the bug can occur:

1

Step

We have two users, Alice and Bob. Both of them staked and have some valid Stargate NFTs. Assume Alice and Bob staked the same amounts for simplicity (50 tokens each).

2

Step

They both delegate their stakes to the same validator. The validator's delegatorsEffectiveStake = 100 right now.

3

Step

While the validator is active Bob wants to exit, so he calls requestDelegationExit, which updates the delegatorsEffectiveStake mapping for the validator. Bob's amount will be subtracted from the total because he's exiting, so the delegatorsEffectiveStake of the validator will be 50 now.

4

Step

The validator exits. This enables the bug.

5

Step

Bob calls unstake. Because the validator exited in the meantime, unstake will reduce the validator's delegatorsEffectiveStake again. This is wrong because Bob's amount was already subtracted when he called requestDelegationExit. Excerpt from the code:

function unstake(uint256 _tokenId) external whenNotPaused onlyTokenOwner(_tokenId) nonReentrant {
    // ..
    // if the delegation is pending or the validator is exited or unknown
    // decrease the effective stake of the previous validator
    if (currentValidatorStatus == VALIDATOR_STATUS_EXITED || delegation.status == DelegationStatus.PENDING) {
        // get the completed periods of the previous validator
        (, , , uint32 oldCompletedPeriods) = $.protocolStakerContract.getValidationPeriodDetails(delegation.validator);

        // decrease the effective stake of the previous validator
        _updatePeriodEffectiveStake(
            $,
            delegation.validator,
            _tokenId,
            oldCompletedPeriods + 2,
            false // decrease
        );
    }
    // ..
}
6

Step

Bob will successfully exit, because the validator had an extra 50 tokens from Alice, but Alice's funds will be permanently locked.

Root cause

Double subtraction of delegators’ effective stake for the same delegation, combined with a global (per-validator) aggregation.

1

Issue

requestDelegationExit already removes the token’s effective stake from the delegatorsEffectiveStake[validator] mapping via:

2

Issue

If the validator exits before the user gets to unstake, but after they have already requested an exit, unstake will again call:

This will remove the same amount twice, although it was already fully removed at the first call.

3

Issue

Because delegatorsEffectiveStake is a global variable stored per validator, the second subtraction is taken out of the aggregated sum, which may now consist only of other delegators’ stake.

Impact

Critical – Permanent freezing of funds

At some point, when other delegators later attempt to unstake, _updatePeriodEffectiveStake(..., false) runs with currentValue == 0 and effectiveStake > 0, causing a checked arithmetic underflow and revert.

In other words, a user with a staked NFT will reach a state where they have a valid delegation that should be exited, but any call to unstake(...) reverts with a panic 0x11, because delegatorsEffectiveStake[validator] has been driven to 0 by other delegators’ double subtraction.

Ensure that each delegation’s effective stake is added and removed exactly once.

Proof of Concept

Add this test in the following file Stake.test.ts and run it:

Test output:

As shown by the test, two users stake at the same level and delegate to the same validator. If the validator exits between requestDelegationExit and unstake called by one of the users, the validator's delegatorsEffectiveStake can be decreased twice, causing other delegators' funds to become permanently stuck.

Was this helpful?