59919 sc high loss of funds delegators can claim rewards for periods where they had no stake
Report ID: #59919
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
Submitted on: Nov 17th 2025 at 00:35:59 UTC by @xKeywordx
Competition: Audit Comp | Vechain | Stargate Hayabusa (https://immunefi.com/audit-competition/audit-comp-vechain-stargate-hayabusa)
#59919 [SC-High] Loss of funds - Delegators can claim rewards for periods where they had no stake
Summary
An NFT (tokenId) that has EXITED a delegation can still claim rewards for periods after its delegation ended. This is caused by an underconstrained check in _claimableDelegationPeriods combined with missing membership checks in _claimableRewardsForPeriod. The issue allows repeated overclaiming of rewards from other delegators for periods in which the attacker no longer had stake.
1. Delegate normally
When delegating, the contract sets:
(, , , uint32 completedPeriods) = $.protocolStakerContract.getValidationPeriodDetails(_validator);
// e.g. completedPeriods = 13
$.lastClaimedPeriod[_tokenId] = completedPeriods + 1; // = 14
_updatePeriodEffectiveStake($, _validator, _tokenId, completedPeriods + 2, true);
// => effective stake added starting from period 15startPeriod becomes completedPeriods + 2 (e.g. 15).
lastClaimedPeriod is completedPeriods + 1 (e.g. 14) so the first claimable period is 15.
2. Request exit while ACTIVE
When requesting exit during an ACTIVE delegation:
// inside requestDelegationExit(...)
$.protocolStakerContract.signalDelegationExit(delegationId);
(, , , uint32 completedPeriods) = $.protocolStakerContract.getValidationPeriodDetails(delegation.validator);
// e.g. completedPeriods = 14 (current validator period = 15)
_updatePeriodEffectiveStake($, delegation.validator, _tokenId, completedPeriods + 2, false);
// => remove effective stake starting from period 16ProtocolStaker sets endPeriod to the period in which exit was requested (e.g. 15).
Checkpoint logic ensures the token is counted in delegatorsEffectiveStake for period 15 and excluded for periods ≥16.
From protocol perspective: delegation is active only in [startPeriod = 15, endPeriod = 15].
3. Let several validator periods pass
Time passes; validator keeps operating:
getValidationPeriodDetails now returns a much larger completedPeriods (e.g. 30).
currentValidatorPeriod = completedPeriods + 1 (e.g. 31).
Delegation details remain startPeriod = 15, endPeriod = 15 (delegation is EXITED).
delegatorsEffectiveStake includes token stake at period 15 only and excludes it for periods 16+.
4. Claimable window erroneously extends past endPeriod
Inside _claimableDelegationPeriods the problematic check is:
if (
endPeriod != type(uint32).max &&
endPeriod < currentValidatorPeriod &&
endPeriod > nextClaimablePeriod // <-- strict '>' causes bypass when endPeriod == nextClaimablePeriod
) {
return (nextClaimablePeriod, endPeriod);
}With startPeriod = 15, endPeriod = 15, currentValidatorPeriod = 31, and nextClaimablePeriod = 15:
endPeriod != max -> true
endPeriod < currentValidatorPeriod -> true (15 < 31)
endPeriod > nextClaimablePeriod -> false (15 > 15 is false)
So this branch is not taken. The fallback returns:
if (nextClaimablePeriod < currentValidatorPeriod) {
return (nextClaimablePeriod, completedPeriods); // (15, 30)
}Therefore the claimable window becomes [15, 30] even though the delegation ended at 15. _claimableRewardsForPeriod then computes rewards for each of these periods.
Root cause
Two logic issues combine to enable the exploit:
Off-by-one / underconstrained check in
_claimableDelegationPeriods:
if (
endPeriod != type(uint32).max &&
endPeriod < currentValidatorPeriod &&
endPeriod > nextClaimablePeriod // strict '>' allows case endPeriod == nextClaimablePeriod to fallthrough
) {
return (nextClaimablePeriod, endPeriod);
}Because it uses strict > rather than >=, if endPeriod == nextClaimablePeriod the clause is bypassed, and the function can return a lastClaimablePeriod larger than endPeriod.
Missing delegation membership check in
_claimableRewardsForPeriod:
The function assumes if delegatorsEffectiveStake > 0 then the token is part of it.
It never checks whether _period lies in the delegation's [startPeriod, endPeriod].
After exit, the aggregator removed the token from delegatorsEffectiveStake for future periods, but effectiveStake for the token is still used in the numerator — enabling a share of rewards for periods it was not part of.
Combined, these issues let an exited delegator claim rewards for periods they were not staked.
Impact
Direct theft of funds (vtho) from rewards pools intended for currently-staked delegators.
Theft of unclaimed royalties / yield.
The attack is repeatable; an exited delegator can repeatedly overclaim as new periods pass.
Losses redistribute from honest delegators of the same validator to the attacker.
This is a high-severity, repeatable exploit that allows a delegator to claim rewards for periods after they have exited — effectively stealing rewards from other delegators.
Recommended mitigation
Revisit reward accounting around exited delegations.
Tighten
_claimableDelegationPeriodschecks so that when endPeriod == nextClaimablePeriod the returned last claimable period never exceeds endPeriod (e.g., use >= where appropriate or otherwise ensure correct bounds).In
_claimableRewardsForPeriod, ensure the token is actually a member of the delegators pool for the given period (i.e., verify the period is within [startPeriod, endPeriod] for that delegation) before using its effectiveStake in reward calculations.Consider clearing or invalidating delegation mapping or effective stake for tokenIds upon exit in a way that prevents reusing stale effectiveStake values for future periods.
Proof of Concept
Was this helpful?