Reproduced Exploit
KittenSwap: `getRewardForPeriod` lets a voter claim a still-open future period
Chain
Other
Category
untagged
Date
Jan 1970
Source
AuditVault
EVM Playground
Source-level debugger — step opcodes and Solidity in sync
The attack is replayed in an in-browser EVM preloaded with the exact dumped fork state. The execution tree shows every call; step by Solidity line or by opcode across all depths — source, Stack, Memory, Storage, Balances (native / ERC-20 / NFT), Transient storage and Return value stay in sync. Click a tree node, opcode, or source line to jump. No backend, no live RPC.
Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: KittenSwap-security-review_2025-06-12. The historical source/toolchain is unavailable; this entry is documentation only and claims no executable Forge PoC.
Vulnerability classes: vuln/theft · vuln/reward-accounting
Reproduction: a faithful minimal reproduction of the vulnerable finding — the three reward functions
incentivize/_deposit/getRewardForPeriodare reproduced verbatim (the vulnerable claim marked@>) with faithful minimal doubles for the reward accounting,Voter/veKittengates and ERC20; local deploy, no fork.
Root cause#
In KittenSwap's VotingReward, getRewardForPeriod forwards any caller-supplied _period into _getReward without ever checking it is not a still-open future period. Both incentivize and _deposit write into getCurrentPeriod() + 1, so a voter can claim that next, still-accumulating period while they are momentarily its only voter and take 100% of its incentives. The vulnerable functions, reproduced verbatim from the finding:
function incentivize(
address _token,
uint256 _amount
) external virtual nonReentrant {
// Here we have one white list for token. So we cannot manipulate the token.
if (voter.isWhitelisted(_token) == false)
revert NotWhitelistedRewardToken();
uint256 currentPeriod = getCurrentPeriod() + 1;
uint256 amount = _addReward(currentPeriod, _token, _amount);
}
function _deposit(uint256 _amount, uint256 _tokenId) external onlyVoter {
uint256 nextPeriod = getCurrentPeriod() + 1;
tokenIdVotesInPeriod[nextPeriod][_tokenId] += _amount;
totalVotesInPeriod[nextPeriod] += _amount;
}
function getRewardForPeriod(
uint256 _period,
uint256 _tokenId,
address _token
) external nonReentrant {
// Only owner or approved can get rewards.
if (!veKitten.isApprovedOrOwner(msg.sender, _tokenId))
revert NotApprovedOrOwner();
@> _getReward(_period, _tokenId, _token, msg.sender);
}
getRewardForPeriod gates only on veNFT ownership; the claimed _period is never compared against getCurrentPeriod(), so rewards booked for the next, still-open period are immediately claimable.
Why it's exploitable here#
Following the finding's scenario with a 500 USDC incentive and equal voting power:
- A protocol seeds 500 USDC to reward voters of the next period
X = getCurrentPeriod() + 1viaincentivize— the token lands intokenRewardsPerPeriod[usdc][X]. - The attacker votes 100 voting power into period
X(still open) through theVoter, funding zero incentives. - The attacker calls
getRewardForPeriod(X, ...). There is no future-period guard, so the claim succeeds whileXis still open. As the sole voter so far,earned = 500e6 * 100 / 100 = 500e6— the entire incentive. - The attacker walks away with 500 USDC it never funded. An honest co-voter who then votes 100 into
Xis owed500e6 * 100 / 200 = 250 USDC, but the reward pool now holds 0 and cannot pay her.
Attack path#
Marked-line walkthrough (Playground)#
The EVM Playground pins each step to the exact executed source line in 0xbd4fd5a3…:
- L55 — Whitelisted USDC incentive token: Setup: the whitelisted reward token is a 6-decimal USDC double that a protocol seeds as voter incentives for an upcoming period.
- L134 — Per-period claim ledger: Setup: rewards are booked per (period, tokenId, token) and this claim flag only blocks a second claim of the same period.
- L159 — Pro-rata reward split: earned pays each voter rewards * myVotes / totalVotes, so whoever is the only voter in a period is owed 100% of its incentives.
- L170 — Reward paid to caller: The computed reward is transferred straight out to the claimant, so real USDC leaves the contract the moment a claim is honored.
- L198 — Only owner check enforced: getRewardForPeriod only checks the caller owns the veNFT; it never validates that the requested
_periodhas actually closed. - L200 — Missing future-period guard: Root cause:
_getRewardruns with no_period <= getCurrentPeriod()guard, so a lone voter drains 100% of a still-open future period.
PoC#
Registry (Foundry, local deploy — verbatim vulnerable source + harm-asserting test):
cd 58207-c-03-incentive-rewards-may-be-stolen-pashov-audit-group-none_exp
forge test -vvv
The browser Playground replays the same synthetic opcode-for-opcode and measures the harm: seed 500 USDC, vote into the still-open future period, claim 100% while sole voter, leaving an honest co-voter owed 250 USDC the drained pool cannot pay. Both gates are green (registry forge test PASS + Playground _verify-poc VERDICT: PASS).
Sources & further analysis#
Reproductions & code
- No executable Forge reproduction is claimed; the historical source/toolchain was unavailable for this finding.
- AuditVault finding: KittenSwap-security-review_2025-06-12.
- Upstream DeFiHackLabs PoC directory: src/test.
Alerts & third-party analyses
- DeFiHackLabs incident explorer: search "KittenSwap:
getRewardForPeriodlets a voter claim a still-open future period". - Web3Sec X hacked database: search.
- Rekt leaderboard: search.
- Solodit incident search: search.
These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.