60028 sc high a delegator who has requested an exit continues to accumulate rewards

  • Report ID: #60028

  • 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:

    • Theft of unclaimed yield

    • Temporary freezing of funds for at least 24 hour

#60028 [SC-High] A delegator who has requested an exit continues to accumulate rewards

Submitted on Nov 17th 2025 at 17:44:19 UTC by @shaflow1 for Audit Comp | Vechain | Stargate Hayabusa (https://immunefi.com/audit-competition/audit-comp-vechain-stargate-hayabusa)

Description

Brief/Intro

There is a logic flaw in the _claimableDelegationPeriods function, which allows a delegator to continue accumulating and claiming rewards even after calling requestDelegationExit, as long as they do not unstake. This can lead to delegation rewards being stolen and also cause inconsistencies in reward token distribution. When the reward tokens in the Stargate contract are insufficient to cover payouts for other delegators, their fund withdrawals can be DOS-ed.

Vulnerability Details

The _claimableDelegationPeriods function is used to return the start and end periods of claimable rewards, but it contains a logical flaw.

Stargate.sol (excerpt)
    function _claimableDelegationPeriods(
        StargateStorage storage $,
        uint256 _tokenId
    ) private view returns (uint32, uint32) {
        // get the delegation
        uint256 delegationId = $.delegationIdByTokenId[_tokenId];
        // if the token does not have a delegation, return 0
        if (delegationId == 0) {
            return (0, 0);
        }
        (address validator, , , ) = $.protocolStakerContract.getDelegation(delegationId);
        if (validator == address(0)) {
            return (0, 0);
        }

        (uint32 startPeriod, uint32 endPeriod) = $
            .protocolStakerContract
            .getDelegationPeriodDetails(delegationId);
        (, , , uint32 completedPeriods) = $.protocolStakerContract.getValidationPeriodDetails(
            validator
        );

        // current validator period is the next period because
        // the current period is the one that is not completed yet
        uint32 currentValidatorPeriod = completedPeriods + 1;

        // next claimable period is the last claimed period + 1
        uint32 nextClaimablePeriod = $.lastClaimedPeriod[_tokenId] + 1;
        // if the next claimable period is before the start period, set it to the start period
        if (nextClaimablePeriod < startPeriod) {
            nextClaimablePeriod = startPeriod;
        }

        // check first for delegations that ended
        // endPeriod is not max if the delegation is exited or requested to exit
        // if the endPeriod is before the current validator period, it means the delegation ended
        // because if its equal it means they requested to exit but the current period is not over yet
        if (
            endPeriod != type(uint32).max &&
            endPeriod < currentValidatorPeriod &&
 @>           endPeriod > nextClaimablePeriod
        ) {
            return (nextClaimablePeriod, endPeriod);
        }

        // check that the start period is before the current validator period
        // and if it is, return the start period and the current validator period.
        // we use "less than" because if we use "less than or equal", even
        // if the delegation started, the current period rewards are not claimable
        if (nextClaimablePeriod < currentValidatorPeriod) {
            return (nextClaimablePeriod, completedPeriods);
        }

        // the rest are either pending, non existing or are active but have no claimable periods
        return (0, 0);
    }

When an NFT calls requestDelegationExit to mark a delegation for exit, the endPeriod is set to the current period. In the next period, the delegator's funds will no longer participate in delegation and can be withdrawn. Therefore, in this case, the _claimableDelegationPeriods function should never return an end period for rewards that exceeds the ExitPeriod (endPeriod).

However, the check endPeriod > nextClaimablePeriod can cause the function to return the validator's completedPeriods as the end period even if the delegator has already exited in the following scenario:

1

Scenario step

There are two delegators. delegator1 calls requestDelegationExit at currentPeriod = 5, marking the exit, so endPeriod = 5.

2

Scenario step

After 1 period, currentPeriod = 6. delegator1 can now exit, and delegatorsEffectiveStake is reduced.

3

Scenario step

delegator1 calls claimRewards to claim rewards. At this point, _claimableDelegationPeriods returns the reward end period as endPeriod = 5, and after the call, lastClaimedPeriod[_tokenId] = 5.

4

Scenario step

After another period, currentPeriod = 7, delegator1 claims rewards again. Now, nextClaimablePeriod = lastClaimedPeriod[_tokenId] + 1 = 6 > endPeriod = 5. Therefore, _claimableDelegationPeriods does not enter the if branch returning (nextClaimablePeriod, endPeriod) and instead returns (nextClaimablePeriod (6), completedPeriods (6)). As a result, delegator1 can continue to claim rewards for period 6.

5

Scenario step

Subsequently, delegator1 continues accumulating rewards each period. Because the denominator delegatorsEffectiveStake is reduced, the yield may even exceed normal delegation. Meanwhile, the rewards allocated by the node to the contract are insufficient, causing reward tokens to run out, and delegator2 cannot claim rewards or unstake.

Impact Details

The above issue allows malicious actors to claim excessive rewards, while the staking rewards allocated by the validator to Stargate remain unchanged. If there are many such attackers in the system, reward tokens can be stolen by them, and legitimate delegators may be unable to claim rewards or advance reward periods due to insufficient reward tokens, preventing them from unstaking and effectively locking their funds.

References

https://github.com/immunefi-team/audit-comp-vechain-stargate-hayabusa/blob/e9c0bc9b0f24dc0c44de273181d9a99aaf2c31b0/packages/contracts/contracts/Stargate.sol#L919

Proof of Concept

The following test demonstrates that after requestDelegationExit, rewards can still be claimed even after the endPeriod has passed. Add this test to the end of packages/contracts/test/unit/Stargate/Rewards.test.ts to reproduce.

Was this helpful?