59756 sc high exiting delegators stakes can be bricked permanently by the validator signaling an exit after them in the same period

Submitted on Nov 15th 2025 at 14:51:49 UTC by @flacko for Audit Comp | Vechain | Stargate Hayabusaarrow-up-right

  • Report ID: #59756

  • 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

Summary

Unstaking and re-delegation incorrectly decrease the effective stake of the old validator when the validator has an Exited status and as a result open up a vulnerability where a validator can brick exiting delegators funds by also signaling an exit in the same staking period as them. This can happen both intentionally by a malicious validator or naturally without the knowledge of the validator on the possible outcome.

Vulnerability Details

The problematic code is in Stargate.sol:

    /// @inheritdoc IStargate
    function requestDelegationExit(
        uint256 _tokenId
    ) external whenNotPaused onlyTokenOwner(_tokenId) nonReentrant {        
    
        // decrease the effective stake
        // Get the latest completed period of the validator
        (, , , uint32 completedPeriods) = $.protocolStakerContract.getValidationPeriodDetails(
            delegation.validator
        );
        (, uint32 exitBlock) = $.protocolStakerContract.getDelegationPeriodDetails(delegationId);
        
        // decrease the effective stake
_updatePeriodEffectiveStake($, delegation.validator, _tokenId, completedPeriods + 2, false);

Signaling an exit for a delegation exit decreases the effective stake for that validator for period completedPeriods + 2 which is equal to currentIteration + 1.

When a delegation signals an exit it must wait for the currentIteration staking period to pass and can withdraw the stake via unstake() in the next period (currentIteration + 1). The same stands true for validators: when they signal an exit their CompletedIterations is set to currentIteration.

If both parties have signaled an exit and the next period starts (currentIteration + 1) and the delegator calls unstake(), _updatePeriodEffective() will be called with oldCompletedPeriods + 2 which will be currentIteration + 2. As there's no checkpoint for that period, the contract will do an upper lookup and pick up the most recent checkpoint which is the one that got updated when requestDelegationExit() was called whose value already reflects the deducted effective stake of the delegator.

Further, the same effective stake deduction is applied inside delegate() when re-delegating away from an exited or pending validator:

Impact Details

  • If the exiting delegator's stake is ≥ 50% of all stake delegated to that validator, they would be bricked because total effective stake - 2 * delegator effective stake would underflow.

  • If multiple delegators have signaled an exit it depends on the size of their stake and the order in which they call unstake() but at least some delegators will not be able to withdraw their stake since each delegator's effective stake is subtracted from the total stake twice. Example: 4 delegators each with 25% of total effective stake — if 2 delegators signal an exit, the two that didn't signal an exit may not be able to withdraw their stake because the earlier operations reduced the validator's effective stake twice, leading to underflow and revert.

Note: re-delegation (via delegate()) suffers the same double-deduction behavior, so it's not a guaranteed rescue path.

Proof of Concept

Test setup addition

Add this snippet to the end of the beforeEach function block in packages/contracts/test/unit/Stargate/Stake.test.ts. This adds a second validator to show impact on delegate() as well.

Test case (PoC)

Add this test case to the same test unit file.

Was this helpful?