60150 sc high off by one in claim window lets exited delegations harvest post exit rewards

Submitted on Nov 19th 2025 at 09:23:14 UTC by @yesofcourse for Audit Comp | Vechain | Stargate Hayabusaarrow-up-right

  • Report ID: #60150

  • 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

Description

Brief/Intro

A logic error in Stargate._claimableDelegationPeriods selects a rewards window that can extend past the delegation’s end when nextClaimablePeriod == endPeriod.

The function falls through to an “active” branch and sets the last claimable period to the validator’s completedPeriods, enabling an exited position to claim rewards for post-exit periods.

Because _claimableRewardsForPeriod uses the token’s current effective stake (numerator) but the per-period validator total (denominator) already excludes the exited token, the caller siphons yield that belongs to remaining delegators.

This is a High impact theft of unclaimed yield issue.

Vulnerability Details

The claim window is derived in _claimableDelegationPeriods:

If a user has already claimed up to endPeriod - 1, then nextClaimablePeriod == endPeriod. Even though the delegation ended (endPeriod < currentValidatorPeriod), the strict comparison endPeriod > nextClaimablePeriod is false at equality, so the function returns (endPeriod, completedPeriods) instead of clamping to endPeriod.

Downstream, _claimableRewardsForPeriod pays per period without validating membership for that period:

  • The numerator uses the token’s current effective stake (level factor × VET) regardless of whether the token was delegated in _period.

  • The denominator is the validator’s snapshotted total for _period, which, after exit, excludes this token due to the scheduled decrease.

Result: if the window wrongfully includes period > endPeriod, the exited token obtains a share of delegationPeriodRewards for periods it did not participate in.

Minimal, reproducible scenario

1

Alice delegates; later requests exit, yielding endPeriod = E.

2

Before exit finalizes, she has claimed up to E−1nextClaimable = E.

3

Validator advances to completedPeriods = C > E.

4

Alice calls claimRewards(tokenId).

  • _claimableDelegationPeriods returns (E, C) (off-by-one).

  • Loop pays for all period ∈ [E, C], including E+1…C, where Alice was no longer delegated.

  • _lastClaimedPeriod[tokenId] = C finalizes the over-claim.

  • The off-by-one is explicit (> instead of >=).

  • The fallback returns completedPeriods, which grows with time, increasing the illegitimate range.

  • No per-period membership check exists to zero out post-exit periods for the token.

Impact Details

  • Per-period theft: For each post-exit period p, payout ≈ delegationRewards[p] * effectiveStake(token) / delegatorsEffectiveStake[p], where the denominator excludes the token. Example: prior to exit, the token was 10 of a 100 total (10%). After exit, pool is 90. The bug lets the token claim ~11.11% (10/90) of each later period.

  • Cumulative loss: By delaying the claim, an attacker can harvest multiple post-exit periods in one call ([endPeriod, completedPeriods]).

  • Systemic risk: Broad exploitation by high-stake NFTs can materially drain rewards from honest delegators. Depending on treasury/refill mechanics, this can cascade into underpayments for later claimants and accounting drift.

References

  • Stargate.sol::_claimableDelegationPeriods — end-window selection with strict > on endPeriod > nextClaimablePeriod. https://github.com/immunefi-team/audit-comp-vechain-stargate-hayabusa/blob/main/packages/contracts/contracts/Stargate.sol#L919

  • Stargate.sol::_claimableRewards / _claimRewards — iterate and pay over returned window; set lastClaimedPeriod to returned last. https://github.com/immunefi-team/audit-comp-vechain-stargate-hayabusa/blob/main/packages/contracts/contracts/Stargate.sol#L792-L823

  • Stargate.sol::_claimableRewardsForPeriod — numerator uses current effective stake; denominator is per-period snapshot; no membership guard. https://github.com/immunefi-team/audit-comp-vechain-stargate-hayabusa/blob/main/packages/contracts/contracts/Stargate.sol#L829-L855

  • Stargate.sol::_updatePeriodEffectiveStake — explains why post-exit denominators exclude the token (scheduled future decrease). https://github.com/immunefi-team/audit-comp-vechain-stargate-hayabusa/blob/main/packages/contracts/contracts/Stargate.sol#L993

Proof of Concept

Save the following file as test/unit/Stargate/OffByOnePostExitOverclaim.test.ts and run with npx hardhat test test/unit/Stargate/OffByOnePostExitOverclaim.test.ts:

The PoC demonstrates that:

  • The off-by-one in _claimableDelegationPeriods (when nextClaimablePeriod == endPeriod) expands the window to completedPeriods instead of clamping to endPeriod.

  • claimableDelegationPeriods(tokenA) returns (2, 4) in the setup, confirming the faulty window.

  • _claimableRewardsForPeriod pays post-exit periods because the numerator uses the token’s current effective stake while the denominator excludes it.

  • The attacker NFT (A) receives 0.25 VTHO (0.05 for period 2 + 0.1 + 0.1 for periods 3–4), i.e., rewards for periods after exit.

  • The control test (window [2..2]) pays exactly 0.05, proving the over-claim only occurs due to the off-by-one.

Was this helpful?