All audits
FailedEthereumIndependent

Onyx Compound-Fork Rounding (2023)

Smart contract security audit by TierZero · 2023-12-15

Independent research. This is an unsolicited security analysis published by TierZero — not a commissioned audit, and no certificate is issued. It reflects our own review of public code and on-chain events.

Findings summary

1
Critical
2
High
2
Medium
1
Low
1
Informational

Independent research — not a commissioned audit. This is TierZero's own post-incident analysis of a publicly disclosed exploit, prepared for educational purposes.

What the protocol did

Onyx Protocol was a Compound V2 fork offering pooled lending markets: users supply an asset, receive a receipt token (an "oToken," mirroring Compound's cToken design) representing a claim on the pool plus accrued interest, and can borrow against posted collateral up to a liquidation threshold. Like most Compound forks, Onyx inherited the exchange-rate accounting model wholesale, including the code path that governs how a brand-new, unfunded market behaves before it has meaningful liquidity.

On November 1, 2023, an attacker drained roughly $2.1 million from Onyx by targeting a market that had just been listed and had not yet accumulated real deposits — a PEPE lending pool. The mechanism is a known Compound V2 fork weakness that had already surfaced in Hundred Finance, Midas Capital, and other forks before Onyx; Onyx shipped a new market without the mitigations those prior incidents had made public.

The vulnerability

The bug class is commonly called the "empty market" or "donation" attack, and it lives in how the exchange rate between the underlying asset and the receipt token is computed:

// simplified Compound V2 exchangeRate logic
exchangeRate = (getCash() + totalBorrows - totalReserves) / totalSupply;

On a freshly deployed market, totalSupply is zero or near-zero. The first depositor mints receipt tokens at a 1:1 (or protocol-default) rate. If that first mint is a small, attacker-controlled amount, totalSupply stays tiny — and the attacker can then transfer (donate) underlying tokens directly to the pool contract, bypassing the mint function entirely. Because getCash() reads the pool's raw token balance rather than tracking supply-weighted deposits, the donation inflates the numerator without touching totalSupply. The exchange rate spikes, and every unit of the attacker's receipt token is suddenly worth many times more collateral than was actually paid in.

This is a rounding and initialization-order defect, not a reentrancy or access-control bug: each individual line of arithmetic is "correct," but the sequence — mint-with-near-zero-supply, then donate — produces a state the invariant exchangeRate ≈ deposited value / receipt tokens outstanding was never designed to tolerate.

How the exploit worked

  1. Attacker identifies a recently listed Onyx market (oPEPE) with negligible existing deposits.
  2. Attacker mints a minimal amount of oPEPE, becoming effectively the sole holder of the receipt token.
  3. Attacker sends a large direct token transfer of the underlying asset straight to the oToken contract — no mint call, no receipt tokens issued for this transfer.
  4. The next exchangeRateStored() read reflects the donated balance, so the attacker's small oPEPE holding now prices out at a hugely inflated value.
  5. Attacker posts that oPEPE as collateral and borrows other, liquid assets from the protocol (BNB, stablecoins, and other blue-chip markets on Onyx) up to the inflated collateral value.
  6. Attacker walks away with the borrowed liquid assets; the collateral left behind is the artificially priced, illiquid oPEPE — worthless to the protocol once the market corrects.
  7. Funds were bridged and laundered through mixers shortly after extraction, consistent with prior Compound-fork incidents.

The entire attack executes atomically or near-atomically within one or two transactions, which is why pre-deployment prevention — not runtime monitoring — is the only real defense.

How an audit catches this

This is a textbook finding for anyone who has reviewed a Compound-family codebase before, and it is exactly the kind of pattern-matching a specialized review is built to catch — see our smart contract audit service for how we scope this against fork-specific known-issue checklists rather than treating every fork as novel code. Concretely, we check for:

  • Minimum-liquidity seeding: does market initialization force an unrescindable minimum deposit (à la Uniswap V2's dead-share mint) before any user-facing mint/borrow path is reachable?
  • Donation resistance: does getCash() (or equivalent) derive from internally tracked accounting state rather than balanceOf(address(this)), so a bare token transfer cannot move the exchange rate?
  • Exchange-rate invariant tests: fuzz/property tests that assert exchangeRate cannot move by more than a bounded percentage per block absent a proportional totalSupply change — this single invariant, run against every new market listing, would have flagged Onyx's PEPE pool before launch.
  • Listing runbook: a repeatable, signed-off checklist for adding new markets, since the failure here was operational (a known class of bug reintroduced via governance action) as much as it was code-level. We apply the same discipline we describe in our audit checklist for trading protocols, adapted for EVM lending markets rather than Solana programs.
  • Regression check against public incident databases: any Compound V2/V3 fork gets diffed against the Hundred Finance, Midas, and Onyx disclosures specifically, since forks tend to reintroduce fixed-elsewhere bugs.

Remediation

The fix that Compound-fork operators converged on after this incident class is straightforward and now standard practice: require a protocol-owned minimum liquidity deposit at market creation that permanently inflates totalSupply above the range where a small donation can meaningfully move the exchange rate, and/or switch getCash()-style accounting to internal ledger balances that ignore unsolicited transfers. Some forks additionally cap the first N blocks of a new market to deposit-only, no-borrow, so an attacker cannot immediately monetize a manipulated rate. None of this is exotic engineering — it is closing a gap that was already public knowledge before Onyx shipped its PEPE market, which is the actual lesson: forking mature DeFi code without re-auditing the fork's specific deltas and listing procedures is how known bugs get paid for twice. Teams shipping permit-based approval flows alongside these lending markets should also review the adjacent gasless-approval attack surface we cover in our analysis of Permit2 and EVM sniping bots, since the same "trust the pattern, verify the instance" discipline applies there.

If you're listing new markets on a Compound-derived lending protocol, get the listing path reviewed before it goes live — talk to us about a smart contract audit.

Need your contracts audited?

Manual review + tooling across TON, Solana and EVM — certificate and full report included.

Get an audit