59361 sc high off by one in claimabledelegationperiods allows claimrewards to pay for periods after delegation end over claim theft of unclaimed yield
Submitted on Nov 11th 2025 at 14:25:06 UTC by @daxun for Audit Comp | Vechain | Stargate Hayabusa
Report ID: #59361
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
Stargate.sol computes the range of claimable periods for a delegated token and then pays VTHO rewards for every claimable period. Due to an off-by-one conditional in _claimableDelegationPeriods, when the delegation endPeriod equals the token's nextClaimablePeriod, the function fails to cap the last claimable period at endPeriod. If the validator’s completedPeriods later advances past endPeriod, claimRewards() will allow claims up to completedPeriods — including periods after the delegation ended — causing the contract to pay rewards the token wasn’t entitled to.
Vulnerability Details
Location: packages/contracts/contracts/Stargate.sol — function _claimableDelegationPeriods(...).
https://github.com/immunefi-team/audit-comp-vechain-stargate-hayabusa/blob/e9c0bc9b0f24dc0c44de273181d9a99aaf2c31b0/packages/contracts/contracts/Stargate.sol#L879-L934
Problematic code fragment:
if (
endPeriod != type(uint32).max &&
endPeriod < currentValidatorPeriod &&
endPeriod > nextClaimablePeriod
) {
return (nextClaimablePeriod, endPeriod);
}endPeriodis the period when the delegation ends (set totype(uint32).maxwhen no exit requested).nextClaimablePeriodislastClaimedPeriod + 1, adjusted to delegationstartPeriodif needed.currentValidatorPeriod = completedPeriods + 1(the ongoing period).
When endPeriod == nextClaimablePeriod (the equal case), the endPeriod > nextClaimablePeriod check fails. Execution falls through and later the function may return (nextClaimablePeriod, completedPeriods) (the validator's completedPeriods) if nextClaimablePeriod < currentValidatorPeriod. If completedPeriods > endPeriod, the claimable window now includes periods after endPeriod. The reward calculation _claimableRewardsForPeriod() uses stored effectiveStake values and does not verify per-period participation; therefore, the token owner receives VTHO for those extra periods.
This is a pure logic bug (off-by-one) — no race conditions, no privileged access, and reproducible with protocol state manipulation.
Impact Details
Category: Theft of unclaimed yield (in-scope, High severity).
Concrete impact: Token owners can receive VTHO for periods during which their token was not delegated. The attacker can:
Create a delegation with
startPeriod = X,endPeriod = X(equal tonextClaimablePeriod), or otherwise arrange state soendPeriod == nextClaimablePeriod.Let
completedPeriodsadvance beyondendPeriod(natural protocol progression).Call
claimRewards()and receive rewards throughcompletedPeriods, including periods afterendPeriod.
Financial impact: Any amount of VTHO paid per period × number of over-claimable periods. Depending on production reward sizes and multiple tokens this can be materially high. This is direct protocol loss (funds transfer out of protocol-controlled VTHO).
Exploit prerequisites: None privileged. Requires only an attacker-controlled token and normal protocol progression of
completedPeriods. Fully reproducible via local test harness or mainnet fork.
References
Contract:
packages/contracts/contracts/Stargate.sol(function_claimableDelegationPeriods)Relevant functions:
_claimRewards,_claimableRewards,_claimableRewardsForPeriodin same file.Program scope:
Stargate.solis explicitly in-scope per the audit competition.
Proof of Concept
Setup
Deploy contracts (or use a mock):
Stargate(initialize withprotocolStakerContractpointing to a mockProtocolStakerandstargateNFTContractpointing to a mockStargateNFT).MockProtocolStakershould allow controllingcompletedPeriodsand provide delegation period responses.MockStargateNFTshould allow minting a token whosevetAmountStakedandlevelare known.
Create delegation
Create a delegation for token T:
Ensure at delegation time
completedPeriods = N.On delegation, Stargate sets
lastClaimedPeriod = completedPeriods + 1(which equalsN+1), sonextClaimablePeriod = N+2normally — but craft mocks so that the delegation start/end results innextClaimablePeriod == endPeriod. Achieve this by configuring the mockProtocolStakerto set delegationstartPeriodandendPeriodappropriately during PoC.
Minimal deterministic test skeleton (conceptual)
Use a
MockProtocolStakerthat:Returns controlled
completedPeriodsviagetValidationPeriodDetails.When
addDelegationis called, returns a delegation id withstartPeriod = completedPeriods + 1andend = type(uint32).max.When
signalDelegationExit(delegationId)is called, setsend = start(soend == nextClaimablePeriod).getDelegatorsRewards(validator, period)returns fixed reward per period (e.g.,1e18) so the PoC can assert transfer amounts.
Use a
MockStargateNFTthat:mint()returns a token withvetAmountStakedandlevelthat produce a stableeffectiveStake.ownerOfreturns the attacker address.
Run the sequence:
stakeAndDelegate()->signalDelegationExit()-> advancecompletedPeriods->claimRewards()-> assert VTHO transfer includes periods afterendPeriod.
Notes
The root cause is an off-by-one comparison: the
>check onendPeriodexcludes the equal case, which should be capped atendPeriodto avoid paying out periods after delegation end.Fix should ensure
endPeriod >= nextClaimablePeriod(or equivalently adjust comparisons) so the returned last claimable period is at mostendPeriodwhenendPeriodis nottype(uint32).max.
Was this helpful?