All audits
FailedEthereumIndependent

Fei / Rari Fuse Reentrancy (2022)

Smart contract security audit by TierZero · 2023-02-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 report analyzes a publicly documented incident using information available in public post-mortems, block explorer data, and community writeups. It does not disclose any new or undisclosed vulnerability in live software.

What the protocol did

Rari Capital's Fuse was a permissionless money-market factory: anyone could spin up an isolated lending pool, choose the collateral and borrowable assets, and set risk parameters, all built on a fork of Compound v2's cToken/Comptroller architecture. After the Fei Protocol and Rari Capital merger (forming Tribe DAO), Fuse pools — including a large Fei/Rari pool — held a mix of FEI, ETH, and long-tail collateral. On April 30, 2022, an attacker drained roughly $80 million in assets from multiple Fuse pools in a single sequence of transactions. The loss was severe enough that it became a direct contributor to Tribe DAO's vote to wind down Fei Protocol later that year.

The vulnerability

The root cause was a textbook checks-effects-interactions (CEI) violation inherited from the Compound v2 fork, compounded by Fuse's multi-pool, multi-market design. The CEther contract's borrow() path sent ETH to the caller via a low-level call before the borrower's debt snapshot and the market's totalBorrows were updated:

// simplified — pattern present in the vulnerable CEther fork
function borrowFresh(address payable borrower, uint borrowAmount) internal {
    // 1. INTERACTION happens first
    (bool success, ) = borrower.call{value: borrowAmount}("");
    require(success, "transfer failed");

    // 2. EFFECTS happen after — too late
    accountBorrows[borrower].principal += borrowAmount;
    totalBorrows += borrowAmount;
}

Each individual cToken carried a nonReentrant modifier, but that guard was scoped per contract instance, not per account or per pool. Fuse's design meant a single borrower interacted with several distinct cToken contracts inside the same Comptroller during one borrow flow (collateral checks call out to price feeds and sibling markets). A reentrant call routed into a different cToken — or into the same market via a different entry point not covered by that instance's lock — sailed past the guard entirely. This is the cross-contract reentrancy class: the state that needed protecting spanned multiple contracts, but the lock only ever protected one of them at a time. This is a different failure mode than the CPI-ordering risks we cover for Solana programs in our piece on cross-program invocation risk in Solana DeFi — same root idea (state mutated after an external call/handoff), different execution model.

How the exploit worked

  1. The attacker deployed a malicious contract implementing a receive()/fallback that would re-enter the protocol on receipt of ETH.
  2. They supplied a modest amount of collateral to a Fuse pool, enough to pass an initial getAccountLiquidity() check against the Comptroller.
  3. They called borrow() against that collateral. The vulnerable cToken sent ETH to the attacker's contract before writing the new borrow balance to storage.
  4. Inside the fallback triggered by that ETH transfer, the attacker's contract called borrow() again — either on the same market or a sibling market in the pool. Because the borrower's debt had not yet been recorded, the Comptroller's liquidity check still saw the account as under-collateralized headroom that didn't actually exist, and approved the second borrow.
  5. This was repeated recursively down the call stack, each nested call borrowing against collateral that had already been "spent" by an outer call but not yet reflected in state. The result was the attacker draining far more value than their posted collateral should have permitted, across several pools that shared this fork lineage.
  6. The attacker then moved the drained ETH and tokens through mixers and bridges; a portion was later returned after negotiation, but the majority of the loss was never recovered.

How an audit catches this

This bug class is caught by process, not by luck, and it's exactly what we run on every borrow/withdraw/liquidate path during a smart contract audit:

  • CEI enforcement as a hard rule, not a style preference. Any function that moves value externally (ETH transfer, ERC-20 transfer/safeTransfer, hook-bearing tokens) must have all state writes — balances, debt, totalBorrows, indices — committed before the external call. We flag any external call preceding a storage write as a finding regardless of whether a reentrancy modifier is present.
  • Reentrancy-guard scope review. We map every nonReentrant modifier to the actual shared state it's meant to protect. A guard local to one contract instance is worthless if the invariant it protects spans multiple contracts (multiple cTokens under one Comptroller, multiple pools sharing an oracle, a vault plus its strategy). We require a shared, protocol-level lock — or an architecture that doesn't need one — whenever state spans contracts.
  • Fuzzing and invariant tests for cross-contract call sequences. We write invariant tests that recursively re-enter every state-changing entry point mid-execution (borrow-within-borrow, withdraw-within-supply, liquidate-within-borrow) and assert that account liquidity never goes negative and totalBorrows always reconciles with the sum of accountBorrows.
  • Callback-surface inventory. Any point where control passes to an address the protocol doesn't control — call{value:}, ERC-777 hooks, ERC-721/1155 onReceived, flash-loan callbacks — gets enumerated explicitly and tested as an attacker-controlled reentry point, not treated as inert "just sending funds." Our code review and audit engagements specifically build this callback inventory before line-by-line review starts, because it's where CEI violations hide.
  • Fork-lineage diffing. When a codebase forks Compound, Aave, or another established money market, we diff against the upstream contracts to confirm every known historical fix (including this exact reentrancy pattern, fixed upstream years earlier) actually made it into the fork, rather than assuming inherited code is safe by association.

Remediation

The durable fix is unglamorous: move all state writes ahead of external calls in every function that touches value transfer, and use SafeERC20/pull-payment patterns wherever the receiving contract's behavior isn't guaranteed. Where a protocol's architecture genuinely spans multiple contracts sharing risk state — as Fuse's pools did — the reentrancy lock has to be implemented at the level of the shared state (a single Comptroller-level mutex, or a global lock inherited by every market it deploys), not per-contract. Teams building multi-pool or multi-market lending systems from scratch should treat this as a first-class design constraint from day one, which is the kind of decision we push clients on when we're brought in during smart contract development rather than after deployment. It's also worth pairing CEI discipline with the kind of approval-hygiene review we describe in our piece on Permit2 gasless approvals and EVM sniping bot exposure — reentrancy and approval-surface bugs both stem from the same underlying habit of trusting external control flow more than the code actually warrants.

If your protocol forks a lending market, runs isolated pools, or has any function that sends value before it finishes writing state, that's worth a second set of eyes — our smart contract audit process is built to catch exactly this pattern before it reaches mainnet.

Need your contracts audited?

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

Get an audit