Reproduced Exploit
Epoch Island: withdrawForfeit recomputes the forfeit on an un-scaled reward base, so partial forfeits over-repay
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: 59895-calling-withdrawforfeit-multiple-times-for-a-single-deposit. The historical source/toolchain is unavailable; this entry is documentation only and claims no executable Forge PoC.
Vulnerability classes: vuln/reward-accounting · vuln/missing-state-scaling
Reproduction: a faithful minimal reproduction of the vulnerable finding — the vulnerable function is reproduced verbatim (marked
@>) with faithful minimal doubles; local deploy, no fork.
Root cause#
forfeitReward is computed as (earned + rewardTokensClaimed) * percentage / 1e18, but rewardTokensClaimed is never reduced as the deposit is partially forfeited. Forfeiting in multiple partial steps recomputes the forfeit against the un-scaled base each time, so the user repays more rewardToken (150 vs the fair 100) than they ever received.
// VERBATIM vulnerable path from Vepoch.sol::withdrawForfeit().
function withdrawForfeit(uint256 _depositId, uint256 percentage) external {
uint256 forfeitReward = ((earned[_depositId] + rewardTokensClaimed[_depositId]) * percentage) / 1e18; // @>
rewardToken.transferFrom(msg.sender, address(this), forfeitReward);
Why it's exploitable here#
- Each partial forfeit recomputes against the full, un-reduced reward base.
rewardTokensClaimedis never decreased as the deposit is partially forfeited.- Summed over multiple partial steps, the user repays strictly more than a single full forfeit would cost.
Attack path#
Marked-line walkthrough (Playground)#
The EVM Playground pins each step to the exact executed source line in Vepoch:
- Line 90 — VULN. the forfeit uses (earned + rewardTokensClaimed) * percentage, but rewardTokensClaimed is not scaled down per partial forfeit.
- Line 91 — the computed forfeitReward is pulled from the user via transferFrom.
- Line 92 — totalForfeitPaid accumulates; across multiple partial steps the user over-repays by the un-scaled excess.
PoC#
Registry (Foundry, local deploy — exploit path + a fixed-variant control):
cd 59895-calling-withdrawforfeit-multiple-times-for-a-single-deposi_exp
forge test -vv
Expected: both tests PASS — the exploit test forfeits in partial steps and asserts 50 tokens of over-repayment; the fixed version scales the base so total repayment equals the fair 100. The browser EVM Playground is served at /hacks/59895-calling-withdrawforfeit-multiple-times-for-a-single-deposi/.
Remediation#
Scale rewardTokensClaimed (and the reward base) down by the forfeited percentage so partial forfeits sum to a single full forfeit.
References#
- AuditVault finding: https://github.com/Auditware/AuditVault/blob/main/findings/59895-calling-withdrawforfeit-multiple-times-for-a-single-deposit.md
Sources & further analysis#
Reproductions & code
- No executable Forge reproduction is claimed; the historical source/toolchain was unavailable for this finding.
- AuditVault finding: 59895-calling-withdrawforfeit-multiple-times-for-a-single-deposit.
- Upstream DeFiHackLabs PoC directory: src/test.
Alerts & third-party analyses
- DeFiHackLabs incident explorer: search "Epoch Island: withdrawForfeit recomputes the forfeit on an un-scaled reward base, so partial forfeits over-repay".
- 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.