All articles
Shadow ArenaMay 22, 20264 min read

Compound V2 donation attacks: compute the shares in raw units

Trace a historical Flux Finance low-supply market from its initial exchange rate to a zero-share deposit, with an exact integer boundary and realistic mitigation limits.

By Carlos (Bloqarl)

TL;DR

  • A direct underlying transfer can increase cash without increasing cToken supply.
  • With very small supply, that can inflate the rate enough that a successful deposit receives zero shares.
  • Token decimals and the initial rate determine the first useful mint. A one-wei deposit is not automatically one share.
  • Test initialization, eligibility and output protection. The formula alone does not prove that every fork or live market is exploitable.

The Compound V2 donation boundary uses two different units

A depositor transfers underlying tokens and expects a useful share claim in return. To review that expectation, separate raw underlying units, raw share units and the fixed-point rate scale. Calling all three numbers “tokens” hides the rounding that matters.

In the historical Flux snapshot, exchangeRateStoredInternal returns the configured initial rate while supply is zero. Otherwise it derives the rate from cash, borrows, reserves and supply. The underlying balance reader includes direct transfers.

Write the relationship as fixed-point arithmetic, with every amount in its raw units:

rate = floor((cash + borrows - reserves) × 10^18 / share_supply)
new_shares = floor(actual_underlying_received × 10^18 / rate)

These are explanatory equations, not a standalone Solidity implementation. In particular, the mint path reads the rate before transferring the new deposit in. It then truncates the share output and updates supply. Read the full mintFresh sequence, including policy and freshness checks.

Work a zero-share case, then its boundary

Use an initial rate of 2e26, an underlying asset with 18 decimals, no borrows/reserves and eligible participants. This is an illustrative configuration, not a universal cToken rate.

A first deposit of 2e8 raw underlying units mints one raw share unit:

floor(2e8 × 1e18 / 2e26) = 1

A single raw underlying unit would instead mint zero under that equation. Do not claim that mint(1) creates the one-share starting state.

Now transfer 1000e18 underlying units directly to the market. Cash becomes 1000e18 + 2e8, while supply stays one. For a subsequent 1000e18 deposit:

new_shares = floor(1000e18 / (1000e18 + 2e8)) = 0

The denominator is slightly greater than the numerator. Ignoring the small initial cash amount makes them look equal and changes the answer.

For a negative control, deposit exactly the pre-deposit cash amount instead. That yields one share. These neighboring cases identify the integer boundary rather than merely showing that a large donation makes the exchange rate large.

If the zero-share transfer succeeds, the existing sole share holder can benefit from the increased cash on redemption. Actual feasibility still requires the access checks, initialization, liquidity, asset behavior and redemption path to permit the sequence. The published Ondo contest report identifies the historical finding as M-02; it is not a current-deployment assessment.

Why reading only the fork diff misses it

The vendored Compound reference has the same cash/supply accounting relationship and a truncating conversion. It is an older Solidity 0.5 revision, not the direct parent of the Solidity 0.8 Flux files, so the full diff includes changes Ondo did not make. The matching CTokenCash mint path additionally requires KYC; the report cites CTokenModified. A review focused only on newly inserted KYC checks can miss an inherited assumption about market initialization.

That does not make every market with this equation vulnerable. A deliberately locked seed, existing supply, output constraints or participant restrictions can change the conditions. Conversely, a fork relationship or earlier audit does not establish that those controls exist.

Review protections against a stated threat model

An initialization seed needs a reasoned size and a durable claim that the attacker cannot recover the supposedly locked supply. A minimum first deposit that can immediately be withdrawn does not establish that property.

A depositor-facing minimum-share/output condition addresses accepting a worse conversion than intended. Rejecting only zero output still leaves near-zero output and economic loss to evaluate. Combining controls can be useful, but no hard-coded seed amount guarantees safety across all asset decimals, deposit sizes and attacker resources.

Before recommending a fix, predict ordinary minting, direct donations, zero and near-zero outputs, partial redemption and a return toward low supply. Run those cases against the actual contracts when claiming an executable reproduction. The calculations here are a worksheet, not an EVM test or evidence of a live exploit.

The Compound V2 Build supplies the accounting background. In Flux Finance, apply it to the pinned exercise, then compare the reviewed debrief with your own detection path. For a different inherited assumption, follow the KYC approval removal/replay lifecycle.

Updated 29 October 2026. An earlier version said three of six Shadow Arena targets share this bug and tied it to the Hundred Finance loss. This version works only the Flux case, in raw units, and drops those claims.

Tagged

Shadow ArenaCompound V2Donation AttackSmart Contract Security