Reproduced Exploit
KittenSwap: `votingReward` not set on `Gauge` traps all swap fees
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/dos · vuln/logic
Reproduction: a faithful minimal reproduction of the vulnerable finding — the fee-claim block of
Gauge.notifyRewardAmount()is reproduced verbatim (marked@>) with faithful minimal doubles; local deploy, no fork.
Root cause#
The Gauge contract never sets votingReward during initialization and exposes no setter, so it stays address(0). When notifyRewardAmount() claims the pair's swap fees and reaches the forwarding call, it invokes a function on a zero-code address, which reverts and permanently blocks fee distribution. The vulnerable block, reproduced verbatim:
(claimed0, claimed1) = IPair(address(lpToken)).claimFees();
(address _token0, address _token1) = IPair(address(lpToken)).tokens();
if (claimed0 > 0) {
IERC20(_token0).approve(address(votingReward), claimed0);
@> votingReward.notifyRewardAmount(_token0, claimed0);
votingReward is declared with the IVotingReward type but is assigned nowhere — the constructor sets only lpToken, and no setVotingReward-style function exists. Because Solidity guards this high-level external call with an extcodesize check on the target, every call to the codeless address(0) reverts, taking the whole notifyRewardAmount() transaction (including the claimFees() that already moved fees to the gauge) down with it.
Why it's exploitable here#
The failure needs no attacker — it triggers on the protocol's own reward-distribution flow:
- Swap fees accrue in the pair; in the reproduction
100e18oftoken0fees are available to be claimed. - Anyone calls
Gauge.notifyRewardAmount(). It runsclaimFees(), pulling the100e18into the gauge, and enters theclaimed0 > 0branch. - The verbatim line
votingReward.notifyRewardAmount(_token0, claimed0)callsaddress(0)— theextcodesizecheck fails and the call reverts. - The entire transaction rolls back: the
100e18of claimed fees is never forwarded and stays trapped in the pair. Distribution stays broken for every future call until an entirely newGaugeimplementation is deployed, at which point the accrued split among depositors may already have shifted — an unfair distribution.
Attack path#
Marked-line walkthrough (Playground)#
The EVM Playground pins each step to the exact executed source line in 0xbd4fd5a3…:
- L140 — Enter fee distribution function: The gauge's
notifyRewardAmount()entrypoint runs, claiming the pair's accrued swap fees so they can be forwarded on to the voting-reward contract. - L150 — votingReward never initialized: Root cause:
votingRewardwas never set on init and has no setter, so it staysaddress(0); the gauge approves this zero-code address before calling it. - L151 — Call to address(0) reverts: The gauge invokes
notifyRewardAmount()on theaddress(0)votingReward, a high-level call to a codeless address that reverts and aborts the distribution. - L153 — Distribution never completes: Because the forwarding call reverts, execution never reaches this closing brace; the whole transaction rolls back and no swap fees are ever forwarded.
- L156 — Fees stay locked in pair: Every
notifyRewardAmount()attempt reverts identically, so the claimed swap fees remain stuck in the pair until an entirely newGaugeis deployed. - L157 — Harm: 100e18 rewards trapped: The driver accrues
100e18of swap fees, then shows every distribution call reverts, permanently trapping depositors' rewards inside the pair.
PoC#
Registry (Foundry, local deploy — verbatim vulnerable source + harm-asserting test):
cd 58208-h-01-votingreward-not-set-on-gauge-pashov-audit-group-none-k_exp && forge test -vvv
The browser Playground replays the same synthetic opcode-for-opcode and measures the harm: accrue 100e18 of swap fees, then show every notifyRewardAmount() call reverts, leaving the fees permanently stuck in the pair. 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:
votingRewardnot set onGaugetraps all swap fees". - 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.