59919 sc high loss of funds delegators can claim rewards for periods where they had no stake

  • Report ID: #59919

  • 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

  • Submitted on: Nov 17th 2025 at 00:35:59 UTC by @xKeywordx

  • Competition: Audit Comp | Vechain | Stargate Hayabusa (https://immunefi.com/audit-competition/audit-comp-vechain-stargate-hayabusa)

#59919 [SC-High] Loss of funds - Delegators can claim rewards for periods where they had no stake

Summary

An NFT (tokenId) that has EXITED a delegation can still claim rewards for periods after its delegation ended. This is caused by an underconstrained check in _claimableDelegationPeriods combined with missing membership checks in _claimableRewardsForPeriod. The issue allows repeated overclaiming of rewards from other delegators for periods in which the attacker no longer had stake.

1

1. Delegate normally

When delegating, the contract sets:

(, , , uint32 completedPeriods) = $.protocolStakerContract.getValidationPeriodDetails(_validator);
// e.g. completedPeriods = 13

$.lastClaimedPeriod[_tokenId] = completedPeriods + 1; // = 14
_updatePeriodEffectiveStake($, _validator, _tokenId, completedPeriods + 2, true);
// => effective stake added starting from period 15
  • startPeriod becomes completedPeriods + 2 (e.g. 15).

  • lastClaimedPeriod is completedPeriods + 1 (e.g. 14) so the first claimable period is 15.

2

2. Request exit while ACTIVE

When requesting exit during an ACTIVE delegation:

// inside requestDelegationExit(...)
$.protocolStakerContract.signalDelegationExit(delegationId);

(, , , uint32 completedPeriods) = $.protocolStakerContract.getValidationPeriodDetails(delegation.validator);
// e.g. completedPeriods = 14 (current validator period = 15)

_updatePeriodEffectiveStake($, delegation.validator, _tokenId, completedPeriods + 2, false);
// => remove effective stake starting from period 16
  • ProtocolStaker sets endPeriod to the period in which exit was requested (e.g. 15).

  • Checkpoint logic ensures the token is counted in delegatorsEffectiveStake for period 15 and excluded for periods ≥16.

  • From protocol perspective: delegation is active only in [startPeriod = 15, endPeriod = 15].

3

3. Let several validator periods pass

Time passes; validator keeps operating:

  • getValidationPeriodDetails now returns a much larger completedPeriods (e.g. 30).

  • currentValidatorPeriod = completedPeriods + 1 (e.g. 31).

  • Delegation details remain startPeriod = 15, endPeriod = 15 (delegation is EXITED).

  • delegatorsEffectiveStake includes token stake at period 15 only and excludes it for periods 16+.

4

4. Claimable window erroneously extends past endPeriod

Inside _claimableDelegationPeriods the problematic check is:

if (
    endPeriod != type(uint32).max &&
    endPeriod < currentValidatorPeriod &&
    endPeriod > nextClaimablePeriod  // <-- strict '>' causes bypass when endPeriod == nextClaimablePeriod
) {
    return (nextClaimablePeriod, endPeriod);
}

With startPeriod = 15, endPeriod = 15, currentValidatorPeriod = 31, and nextClaimablePeriod = 15:

  • endPeriod != max -> true

  • endPeriod < currentValidatorPeriod -> true (15 < 31)

  • endPeriod > nextClaimablePeriod -> false (15 > 15 is false)

So this branch is not taken. The fallback returns:

if (nextClaimablePeriod < currentValidatorPeriod) {
    return (nextClaimablePeriod, completedPeriods); // (15, 30)
}

Therefore the claimable window becomes [15, 30] even though the delegation ended at 15. _claimableRewardsForPeriod then computes rewards for each of these periods.

5

5. Repeatable theft

  • The delegation mapping is not cleared upon requesting exit.

  • After overclaiming, the attacker can wait for new periods and call claimRewards again.

  • The same flawed logic computes a claimable window that includes post-exit periods, allowing repeated claims.

Root cause

Two logic issues combine to enable the exploit:

  1. Off-by-one / underconstrained check in _claimableDelegationPeriods:

if (
    endPeriod != type(uint32).max &&
    endPeriod < currentValidatorPeriod &&
    endPeriod > nextClaimablePeriod  // strict '>' allows case endPeriod == nextClaimablePeriod to fallthrough
) {
    return (nextClaimablePeriod, endPeriod);
}

Because it uses strict > rather than >=, if endPeriod == nextClaimablePeriod the clause is bypassed, and the function can return a lastClaimablePeriod larger than endPeriod.

  1. Missing delegation membership check in _claimableRewardsForPeriod:

  • The function assumes if delegatorsEffectiveStake > 0 then the token is part of it.

  • It never checks whether _period lies in the delegation's [startPeriod, endPeriod].

  • After exit, the aggregator removed the token from delegatorsEffectiveStake for future periods, but effectiveStake for the token is still used in the numerator — enabling a share of rewards for periods it was not part of.

Combined, these issues let an exited delegator claim rewards for periods they were not staked.

Impact

  • Direct theft of funds (vtho) from rewards pools intended for currently-staked delegators.

  • Theft of unclaimed royalties / yield.

  • The attack is repeatable; an exited delegator can repeatedly overclaim as new periods pass.

  • Losses redistribute from honest delegators of the same validator to the attacker.

circle-exclamation
  • Revisit reward accounting around exited delegations.

  • Tighten _claimableDelegationPeriods checks so that when endPeriod == nextClaimablePeriod the returned last claimable period never exceeds endPeriod (e.g., use >= where appropriate or otherwise ensure correct bounds).

  • In _claimableRewardsForPeriod, ensure the token is actually a member of the delegators pool for the given period (i.e., verify the period is within [startPeriod, endPeriod] for that delegation) before using its effectiveStake in reward calculations.

  • Consider clearing or invalidating delegation mapping or effective stake for tokenIds upon exit in a way that prevents reusing stale effectiveStake values for future periods.

Proof of Concept

chevron-rightTest PoC (Rewards.test.ts)hashtag

Test output (truncated):

As shown, the user repeatedly claims rewards for periods after exit.

Was this helpful?