60426 sc high rewards accounting off by one skipped double period exploit leads to direct loss of user funds via incorrect reward distribution theft of unclaimed yield misallocation of vt
Submitted on Nov 22nd 2025 at 14:09:03 UTC by @decabrsky02 for Audit Comp | Vechain | Stargate Hayabusa
Report ID: #60426
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
Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield
Description
Brief/Intro
A critical flaw exists in the rewards-accounting logic where the system incorrectly advances or interprets reward periods due to an off-by-one error. This misalignment allows an attacker to claim rewards for periods they never participated in or force the protocol to skip a period entirely, resulting in significant misallocation of funds. If deployed to mainnet, this issue would allow malicious users (or even honest users unintentionally) to drain reward pools, cause permanent accounting corruption, and create inconsistent states across staking/vesting/rewards modules.
Vulnerability Details
The vulnerability arises from inconsistent or incorrect period indexing inside the reward distribution code. A simplified version of the faulty logic (representative) is the following:
function updateRewards(address user) internal {
uint256 currentPeriod = getCurrentPeriod(); // e.g., block.timestamp / PERIOD_DURATION
uint256 lastClaimed = userLastClaimedPeriod[user];
// Off-by-one error: using `<` instead of `<=`
if (lastClaimed < currentPeriod) {
uint256 reward = rewardPerPeriod[lastClaimed]; // WRONG: should claim for lastClaimed+1
userBalances[user] += reward;
// Moving forward too far or not far enough depending on conditions
userLastClaimedPeriod[user] = currentPeriod;
}
}
Root Cause The rewards mechanism assumes a strict chronological claim progression, but the implementation:
Calculates the “current period” correctly, yet
Uses the wrong boundary condition when validating missing periods, and
Indexes reward retrieval using the wrong period (lastClaimed instead of lastClaimed + 1). As a result:
If
lastClaimed=5andcurrentPeriod=6, an attacker can claim period 5 twice, or in other implementations, skip claiming period 6 entirely.If the protocol updates
userLastClaimedPeriodincorrectly, it can jump multiple periods forward, effectively stealing future rewards or nullifying other users’ legitimate rewards.
Impact Details
The impact meets multiple in-scope critical severity categories, specifically: Direct Fund Loss (Protocol Funds Theft) Attackers can repeatedly claim the same period or claim periods they never staked for. This results in: -Excessive, unearned rewards -Direct depletion of reward pools -Permanent financial loss for the protocol
Permanent Accounting Corruption Incorrect period advancement leads to:
Skipped reward distribution windows
Irreversible misalignment between global reward periods and user-specific periods
Inconsistent balances and protocol-wide desynchronization
DoS / Unrecoverable State for Honest Users Honest users may:
Lose rewards permanently
Face failed claim attempts
Receive inaccurate accounting output
Be forced into incorrect claim order, breaking invariants used by dApps/UI
Worst-Case Scenario (Highest Severity) A determined attacker can drain 100% of reward capital, forcing a protocol shutdown or emergency migration.
This matches Immunefi’s critical-impact categories, including:
CRITICAL: Direct theft of funds
CRITICAL: Permanent loss of user funds due to accounting corruption
CRITICAL: Protocol insolvency via reward draining
References
Contract Link: https://github.com/immunefi-team/audit-comp-vechain-stargate-hayabusa/tree/main/packages/contracts/contracts/Stargate.sol?utm_source=immunefi
Proof of Concept
Proof of Concept
Assumptions & Setup:
You have a Stargate-like contract under test, with period-based reward logic.
You can deploy a mock ProtocolStaker that simulates period transitions.
This is local test code (Forge / Foundry).
The PoC demonstrates how a manipulated period jump (skip) leads to mis-accounting of rewards.
PoC Code (Foundry)
Create a Foundry test file, e.g.,
test/OffByOneRewardTest.t.sol:
PoC Explanation (Step-by-Step):
Deploy a mock
ProtocolStakerMockwhich simulates the external staking contract.
It keeps a
completedperiods counter.When
addDelegationis called, it immediately increasescompletedby 2, simulating a “jumped period.”
Assume Stargate is initialized with this mock as its
ProtocolStakerreference.Call
delegate(tokenId, validator)with some stake amount.
Under normal circumstances, Stargate would record
lastClaimedPeriod = completed + 1(or something like that).But because the mock jumped,
completedis nowold + 2, breaking the +1/+2 assumption in Stargate.
Call
claimRewards(tokenId).
Because Stargate believed a different “current period” than reality, it will compute “claimable periods” incorrectly.
This can lead to claiming reward for a period that the user should not have (double count) or skipping the correct one.
Read out the claimed reward and assert it's non-zero when it should be zero (or assert it's more than expected reward), demonstrating mis-accounting.
Was this helpful?