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 advanceslastClaimedPeriod._exceedsMaxClaimablePeriods()eventually becomes permanently true for that NFT.As a result,
unstake()anddelegate()revert withMaxClaimablePeriodsExceeded, 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,lastClaimedPeriodbecomesendPeriod.nextClaimablePeriod = lastClaimedPeriod + 1 = endPeriod + 1._claimableDelegationPeriods()uses the conditionendPeriod > nextClaimablePeriodto detect ended delegations. After the user has claimed up toendPeriod, that condition becomes false, so the function falls through to the "active" branch and returns a fake claimable window fromendPeriod + 1up tocompletedPeriods, even though no rewards exist for those periods._claimRewards()computesclaimableAmountfor that window. Because there are no rewards afterendPeriod,claimableAmount == 0and the function returns early without updatinglastClaimedPeriod.As
completedPeriodsgrows (validator keeps producing blocks), the phantom window[firstClaimablePeriod, lastClaimablePeriod] = [endPeriod + 1, completedPeriods]grows untillastClaimablePeriod - firstClaimablePeriod >= maxClaimablePeriods._exceedsMaxClaimablePeriods()then returns true permanently for that NFT.Both
unstake()anddelegate()check_exceedsMaxClaimablePeriods()and revert withMaxClaimablePeriodsExceededbefore calling_claimRewards(). BecauseclaimRewards()cannot advancelastClaimedPeriod(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 updatinglastClaimedPeriod): 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
endPeriodgrows beyondmaxClaimablePeriods,_exceedsMaxClaimablePeriods()becomes permanentlytrue.From that point, every call to
unstake()ordelegate()for that NFT reverts withMaxClaimablePeriodsExceeded, 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 advancelastClaimedPeriod, so the fake window grows ascompletedPeriodsincreases and the revert condition never clears.Recovery requires an out-of-band admin/upgrade action via
UPGRADER_ROLE.
Proof of Concept
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
lastClaimedPeriodeven when claimableAmount==0, or change the ended-delegation condition to treatendPeriod >= nextClaimablePeriodas 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?