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

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 Hayabusa

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