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

60173 sc high the phantom claimable periods can permanently lock the staked vet for ended delegations

Submitted on Nov 19th 2025 at 15:01:52 UTC by @XDZIBECX for Audit Comp | Vechain | Stargate Hayabusa

  • Report ID: #60173

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

    • Permanent freezing of funds

Description

Brief / Intro

In this contract there is a mismatch between how Stargate decides which periods are claimable and how it tracks the last period that actually paid out. After a delegation has cleanly ended and the user has already claimed all legitimate rewards up to the end period, the contract can still report a growing “claimable” window of zero-reward periods. This happens because:

  • _claimableDelegationPeriods() keeps returning this phantom window.

  • _claimRewards() is a no-op when the window contains zero rewards and never advances lastClaimedPeriod.

  • _exceedsMaxClaimablePeriods() eventually becomes permanently true for that NFT.

  • As a result, unstake() and delegate() revert with MaxClaimablePeriodsExceeded, permanently freezing the staked VET of that NFT, even though the delegation ended and all real rewards were claimed.

A test demonstrating this issue is included below.


Vulnerability Details

Root cause summary:

  • Once a delegation has ended and the user has claimed all rewards up to endPeriod, lastClaimedPeriod becomes endPeriod.

  • nextClaimablePeriod = lastClaimedPeriod + 1 = endPeriod + 1.

  • _claimableDelegationPeriods() uses the condition endPeriod > nextClaimablePeriod to detect ended delegations. After the user has claimed up to endPeriod, that condition becomes false, so the function falls through to the "active" branch and returns a fake claimable window from endPeriod + 1 up to completedPeriods, even though no rewards exist for those periods.

  • _claimRewards() computes claimableAmount for that window. Because there are no rewards after endPeriod, claimableAmount == 0 and the function returns early without updating lastClaimedPeriod.

  • As completedPeriods grows (validator keeps producing blocks), the phantom window [firstClaimablePeriod, lastClaimablePeriod] = [endPeriod + 1, completedPeriods] grows until lastClaimablePeriod - firstClaimablePeriod >= maxClaimablePeriods.

  • _exceedsMaxClaimablePeriods() then returns true permanently for that NFT.

  • Both unstake() and delegate() check _exceedsMaxClaimablePeriods() and revert with MaxClaimablePeriodsExceeded before calling _claimRewards(). Because claimRewards() cannot advance lastClaimedPeriod (no rewards in the phantom window), the state can never be corrected on-chain by the user.

Relevant code excerpts (links preserved):

  • The ended-delegation branch condition in _claimableDelegationPeriods() (bug origin): https://github.com/immunefi-team/audit-comp-vechain-stargate-hayabusa/blob/e9c0bc9b0f24dc0c44de273181d9a99aaf2c31b0/packages/contracts/contracts/Stargate.sol#L911C1-L922C10

  • The active branch that can return a fake window: https://github.com/immunefi-team/audit-comp-vechain-stargate-hayabusa/blob/e9c0bc9b0f24dc0c44de273181d9a99aaf2c31b0/packages/contracts/contracts/Stargate.sol#L924C8-L930C10

  • _exceedsMaxClaimablePeriods() which becomes permanently true: https://github.com/immunefi-team/audit-comp-vechain-stargate-hayabusa/blob/e9c0bc9b0f24dc0c44de273181d9a99aaf2c31b0/packages/contracts/contracts/Stargate.sol#L956C3-L973C6

  • unstake() gating on _exceedsMaxClaimablePeriods(): https://github.com/immunefi-team/audit-comp-vechain-stargate-hayabusa/blob/e9c0bc9b0f24dc0c44de273181d9a99aaf2c31b0/packages/contracts/contracts/Stargate.sol#L296C4-L304C1

  • delegate() also gating similarly: https://github.com/immunefi-team/audit-comp-vechain-stargate-hayabusa/blob/e9c0bc9b0f24dc0c44de273181d9a99aaf2c31b0/packages/contracts/contracts/Stargate.sol#L429C6-L439C10

  • _claimRewards() returning early when no rewards are available (and thus not updating lastClaimedPeriod): https://github.com/immunefi-team/audit-comp-vechain-stargate-hayabusa/blob/e9c0bc9b0f24dc0c44de273181d9a99aaf2c31b0/packages/contracts/contracts/Stargate.sol#L736C3-L776C6

Because lastClaimedPeriod never advances beyond endPeriod, the phantom window never closes. _exceedsMaxClaimablePeriods() remains true and the NFT can never successfully call unstake() or delegate() — freezing the staked VET.


Impact Details

  • Severity: Critical/High due to the potential for permanent freeze of staked VET behind an NFT.

  • Once the phantom claimable window after endPeriod grows beyond maxClaimablePeriods, _exceedsMaxClaimablePeriods() becomes permanently true.

  • From that point, every call to unstake() or delegate() for that NFT reverts with MaxClaimablePeriodsExceeded, even though the delegation ended and no rewards accrue for it.

  • The user cannot fix this on-chain: claimRewards() sees a zero-reward window and does not advance lastClaimedPeriod, so the fake window grows as completedPeriods increases and the revert condition never clears.

  • Recovery requires an out-of-band admin/upgrade action via UPGRADER_ROLE.


Proof of Concept

Test demonstrating the permanent freeze (click to expand)

The test output:


References

All relevant code links are included inline in the Vulnerability Details section (kept as originally referenced).


If you want, I can:

  • Suggest specific code-level fixes (patches) to correctly handle ended delegations and advance lastClaimedPeriod even when claimableAmount==0, or change the ended-delegation condition to treat endPeriod >= nextClaimablePeriod as ended, etc.

  • Provide a minimal unit test that asserts the fixed behavior.

  • Draft a changelog entry / mitigation guidance for operators (e.g., recommend an immediate upgrade or admin intervention).

Was this helpful?