59997 sc medium claimrewards fails to update state for zero value periods causing permanent fund freeze in unstake
Submitted on Nov 17th 2025 at 13:07:33 UTC by @hunraj for Audit Comp | Vechain | Stargate Hayabusa
Report ID: #59997
Report Type: Smart Contract
Report severity: Medium
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
A logic flaw exists in the _claimRewards function. When a user has a backlog of claimable periods with a total reward value of zero, the function returns early without updating the user's lastClaimedPeriod state. This creates a "trap" state: the user cannot call unstake because unstake checks _exceedsMaxClaimablePeriods and relies on claimRewards to advance the checkpoint. Since claimRewards returns early, the backlog never shrinks and the user's principal becomes permanently frozen in the contract.
Vulnerability Details
The protocol uses two safety mechanisms that interact incorrectly:
unstakeGas Safety:unstakereverts if the user has more claimable periods thanmaxClaimablePeriods(e.g., 832) to avoid out-of-gas errors during automatic reward claims. This forces manual clearing of the backlog.claimRewardsBatching:claimRewardsis intended to clear the backlog in batches, advancinglastClaimedPeriodper batch.
Root cause: _claimRewards returns early if the batch's claimableAmount is zero, and therefore never updates lastClaimedPeriod.
Code excerpt showing the root cause:
The Catch-22 Execution Path
Impact
This is a Critical outcome: permanent freezing of user funds. A user can be permanently blocked from unstaking if they accumulate many zero-value claimable periods.
Recommended Remediation
Always update lastClaimedPeriod when processing a batch, even when the batch's reward is zero. The function's responsibility is both to pay rewards and to advance the checkpoint for processed periods. Move the state update before returning for zero-value batches.
Proposed fix (move the state update before the zero-amount return):
This ensures progress is always made on clearing claimable periods and prevents permanent lockouts.
Proof of Concept
A runnable PoC test was provided. The ProtocolStakerMock was slightly modified to allow setting delegator rewards to zero via a helper function so the test can simulate the "unlucky delegator" scenario.
Modification to ProtocolStakerMock.sol (added state variable + helper setter):
Runnable Hardhat Test (excerpt):
Test output from the PoC run:
If you want, I can:
Produce a patch/PR diff applying the proposed fix in the repository structure referenced.
Run through alternative mitigations (e.g., special-case
unstaketo allow manual exception flows).
Was this helpful?