The Compound V2 Fork Donation Attack: 3 of 6 Shadow Arena Targets Have It
Three of six audit targets in the Zealynx Academy Shadow Arena are Compound V2 forks, and all three share the same cToken exchange-rate manipulation. Walking through the attack, the math, and why $7M of Hundred Finance proved the design is fragile under fork.
TL;DR
- The Zealynx Academy Shadow Arena has six audit targets, drawn from real past contests, totaling 13,712 lines of Solidity and 63 documented findings (14 High, 49 Medium).
- Three of those six are Compound V2 forks: Flux Finance (Ondo's KYC-gated CASH market), Canto v2 (with the cNote stablecoin layer), and Venus (BSC's largest lending protocol).
- All three share the same exchange-rate manipulation surface: an attacker mints a tiny cToken position, transfers underlying directly to the cToken contract (bypassing
mint()), inflates the exchange rate, and the next legitimate depositor's tokens round to zero shares. - Hundred Finance lost approximately $7M to this exact attack in 2023. The attack is unchanged from then; it just keeps showing up in new forks because forking a complex codebase rarely preserves all the protections.
- Defenders have two real options: enforce a non-zero
MINIMUM_LIQUIDITYon first mint (analogous to Uniswap V2's defense), or seed every market with a meaningful initial deposit at deployment so the math never hits the rounding boundary.
Why this matters
Compound V2 is a tempting fork target. The codebase is mature, well-audited, and supports almost any underlying ERC-20. Teams reach for it as a base layer for stablecoin minting, KYC-gated lending, cross-chain markets, and dozens of other niches.
The forking process feels safe. The tests still pass, the audits exist, the deployment guides are written. What teams miss is that some of Compound V2's defenses are not in the code at all. They are in the operational practice of how Compound deploys markets: with a well-funded initial deposit, supplied by Compound itself or by a counterparty with the protocol's blessing.
When a fork ships a market without that initial deposit, the cToken's exchange rate starts at the boundary where the donation attack works. Three of six Shadow Arena targets have shipped exactly that way, and each made a different team rediscover the attack the hard way.
The attack, step by step
The setup: a cToken market for some underlying, deployed but not yet seeded. totalSupply is 0. cash (the cToken's underlying balance) is 0. The exchange rate is whatever the market's initialExchangeRateMantissa says, typically 2e16 (i.e. 1 cToken = 0.02 underlying, on the canonical Compound assumption that underlying has 18 decimals and cToken has 8 decimals).
Step 1. Attacker calls mint(1) on the cToken with a tiny underlying amount, say 1 wei. The cToken's mint() function does:
mintTokens = mintAmount / exchangeRate;
// mintTokens = 1 / 2e16 = 0 (truncated)
But wait, this would mint zero cTokens. The exact math depends on the fork's mantissa handling. In some forks, the rounding direction or the order of operations means the attacker actually receives 1 cToken in this scenario. Or they call mint() with a slightly larger amount (50_000_000 wei, say) so the math gives them 2_500_000_000 cTokens (0.000025 cTokens, but expressed in the cToken's 8-decimal units, that is 25 raw units). The exact attacker-controlled minimum is fork-specific.
The key property: the attacker establishes a cToken position with a vanishingly small backing of underlying.
Step 2. The attacker transfers a large amount of underlying directly to the cToken contract using IERC20(underlying).transfer(cToken, donateAmount). This bypasses the cToken's mint() function entirely. No cTokenBalance change. No accountBorrows change. Just cash goes up.
The exchange rate, derived as:
exchangeRate = (cash + totalBorrows - totalReserves) / totalSupply
now changes dramatically. With totalSupply still tiny (only the attacker's position) and cash large (attacker's donation), the exchange rate inflates.
Step 3. A legitimate user sees the cToken market and tries to deposit. Suppose they deposit 100 underlying tokens. The cToken math:
mintTokens = mintAmount * 1e18 / exchangeRate;
// if exchangeRate has been inflated to a value > 100 * 1e18,
// mintTokens = 0 (truncated)
The user sends 100 underlying. The cToken contract receives 100 underlying. The cToken contract attempts to mint 0 cTokens. Depending on the fork's checks, the transaction may revert with a divide-by-zero or insufficient-mint error, or it may proceed silently, leaving the user with 0 cTokens and the protocol with 100 extra underlying.
Step 4. The attacker burns their cToken position. The redemption math gives them their original tiny stake back, but at the inflated exchange rate, that stake is now worth most of the underlying balance, including the legitimate user's deposit.
The attacker walks away with the user's 100 underlying minus the attacker's original tiny seed. Net theft.
Why it persists across forks
Compound V2's defense against this attack is not a code defense at all. It is a deployment practice: at the moment a new market is created, the deployer (or the protocol itself) supplies an initial meaningful amount of underlying. That initial deposit makes totalSupply non-trivial, raises the exchange rate above the rounding boundary, and forces any future donation attack to spend more on the donation than they can extract.
When forks deploy markets without this practice, the protection is gone.
The attack also persists because most code review focuses on what the cToken does, not on what happens between the cToken and the underlying. The transfer-direct-to-the-contract path is invisible to the cToken's own code; from the cToken's perspective, cash just increased, with no direct cause. The protocol does not know it was a donation versus a normal mint.
Three Shadow Arena targets, three flavors
The Shadow Arena documents this attack pattern across three different forks, each with slightly different framing.
Flux Finance. Flux is Ondo's KYC-gated lending protocol with a cToken-style market for the CASH stablecoin. Findings flag the donation attack against the underlying integrations and recommend the operational fix: seed every market with a meaningful underlying deposit at deployment, and run the test suite that proves a 1-wei mint followed by a large donation cannot be exploited.
Canto v2. Canto's lending layer extended the Compound V2 base with the cNote stablecoin, a custom rate model, and per-market parameter overrides. The donation attack surface compounds with H-04 (per-year vs per-block confusion in the rate calculation): when you can both manipulate the exchange rate and trigger interest-accrual mismatches, the surface widens. The fix is the same: seeded markets at deployment, plus a sanity check on mint() that fails closed when the result would be zero or near-zero cTokens.
Venus. Venus is BSC's largest lending protocol. Beyond the donation attack itself, Venus has a parallel exploit class via the H-01 blocksPerYear = 2,102,400 bug. Compound on Ethereum assumes 15-second blocks. BSC has 3-second blocks. The constant inherited unchanged means accrual happens 5x faster than the team intended, which warps the exchange rate growth and gives the donation attack a moving target. The fix here is two-pronged: chain-aware constants AND seeded markets.
The fix
If you are forking Compound V2, you should do two things at deployment, every time:
-
Seed each market with a meaningful initial deposit. "Meaningful" means: enough that the smallest plausible legitimate mint cannot be rounded to zero. For most assets, 100 underlying tokens is sufficient. The protocol team supplies this from its own funds at deployment and accepts that the resulting cTokens are permanently locked. The cost is small (a few hundred dollars per market). The defense is total.
-
Add a
MINIMUM_LIQUIDITYcheck to the cToken'smint(). Mirror what Uniswap V2 does for LP tokens: refuse to allow the first mint of a market to leave the protocol in a state where the next mint would round to zero. The minimum can be modest (1e3cTokens equivalent) but it must exist. A fork that ships without this and without seeded markets has shipped a vulnerability.
Some forks add a third defense: explicit accounting of cash increases that did not come through mint(). This requires comparing IERC20.balanceOf(cToken) with the cached cash value and treating differences as suspect. It is more invasive but closes the surface entirely.
Related questions
Why does this only affect cToken markets, not Compound itself? Canonical Compound's deployed markets were seeded with substantial liquidity by the team and integrations at the moment of deployment. The exchange rate has been well above the rounding boundary for years. Forks deploying fresh markets without this practice ship the attack surface in the open.
Could this be fixed at the cToken contract level alone? Yes. The MINIMUM_LIQUIDITY defense is the same pattern Uniswap V2 uses for LP tokens, and adapting it to cTokens is straightforward. The reason it is not in canonical Compound is that the deployment practice was assumed; adding a code-level defense is recommended for any fork that cannot guarantee that practice.
Did Hundred Finance recover the funds? A portion was returned by the white-hat negotiation. Most was not. The post-mortem is public and is one of the canonical reads for anyone working on a Compound V2 fork.
Are there any safe Compound V2 forks? Plenty, but the safety came from deployment discipline, not code. The forks that survived this class of attack are the ones that seeded markets, ran fork-specific test suites that probed the rounding boundary, and had operational playbooks for new market launches. None of those are visible in a code-only review.
Where to study this further
Open the Shadow Arena targets at /shadow-arena/flux-finance, /shadow-arena/canto-v2, and /shadow-arena/venus. Each ships with the original audit findings, the affected functions, and reproducible attack scenarios. The rooms are time-boxed; you submit findings, get scored against the actual contest results, and either find this class of bug yourself or get points subtracted for reporting it incorrectly.
If you have not built a Compound V2 fork from scratch, the Zealynx Academy Compound V2 module is the prerequisite. The donation attack is one of the things the test suite specifically checks for. By the time you have implemented mint(), redeem(), accrueInterest(), and the comptroller integration, you understand why the donation attack works and why the deployment practice matters as much as the code.
Tagged