For the complete documentation index, see llms.txt. This page is also available as Markdown.

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.

  • 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

Test PoC (Rewards.test.ts)

Test output (truncated):

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

Was this helpful?