Rari Capital / Fei Fuse Pools Reentrancy (2022-04-30)

  • Name: Rari Capital / Fei Fuse pools reentrancy
  • Date: 2022-04-30
  • Protocol: Fuse (Rari Capital / Fei Protocol)
  • Chain: Ethereum and Arbitrum
  • Category: cross-function reentrancy / CEther ETH-transfer reentrancy / bad-debt extraction
  • Loss: about 80M across seven Ethereum Fuse pools, plus a later smaller Arbitrum replay
  • Summary: The April 2022 Fuse exploit was not just “Compound fork reentrancy” in the abstract. Fuse’s CEther path handed ETH to the borrower with call.value(...) before borrow accounting was finalized, and Fuse’s reentrancy hardening still left a sibling collateral-management path that could act on that mid-update state. The attacker used the ETH-receive callback to call exitMarket() while the protocol still had not recorded the borrow, withdrew collateral that should have remained locked, and then used a three-account-per-pool structure to turn the fake solvency into real pool drains and concentrated bad debt. On Arbitrum, where ETH was not borrowable in the same way, the same family of bug still paid through ETH redemption reentrancy.
  • Trigger / entry point:
    • Fuse’s CEther implementation used a raw ETH send path (to.call.value(amount)("")) that gave an attacker-controlled borrower contract arbitrary execution during ETH transfer.
    • The affected borrow() / redeem() flow let control leave the protocol before the relevant debt / collateral accounting was fully sealed.
    • exitMarket() on the Comptroller side remained reachable during that callback window, so the attacker could remove an asset from collateral membership before the in-flight borrow had been booked.
  • Root cause:
    • Fuse relied on incomplete reentrancy hardening. On March 3, 2022, whitehats had already surfaced a cEther-family reentrancy issue in older Fuse deployments and triggered a global borrow pause plus emergency upgrades; the April 30 exploit showed the broader CEther callback hazard was still not retired from the live system.
    • The fundamental invariant failure was still classic checks-effects-interactions breakage: the system transferred ETH before finalizing borrow or redemption state.
    • Once exitMarket() could run during that window, the Comptroller accepted collateral withdrawal decisions against stale account liquidity.
    • This was also a remediation-completeness failure: the protocol had already learned raw ETH delivery before state finalization was dangerous, but the remaining callable surface still let a sibling solvency function act on mid-update state.
    • In short: ETH transfer surrendered control too early, and Fuse’s lock coverage was narrower than the state-consistency assumptions made by the market / Comptroller pair.
  • Exploit path:
    1. The attacker used contracts controlled from 0x6162759edad730152f0df8115c698a42e666157f and funded the attack with large flash loans.
    2. In the representative Ethereum exploit transaction 0xab486012f21be741c9e674ffda227e30518e8a1e37a5f1d58d0b0d41f6e76530, the attacker flash-loaned 150,000,000 USDC and 50,000 WETH.
    3. The attacker deposited the USDC into Fuse pool 127 via fUSDC-127 as collateral, then borrowed ETH through the vulnerable CEther path.
    4. During the ETH transfer, the attacker contract’s fallback executed before Fuse had updated accountBorrows / related borrow state.
    5. Inside that callback, the attacker called exitMarket() so the just-used collateral was treated as removable even though the borrow that depended on it had not yet been recorded.
    6. With collateral released and borrow state stale, the attacker recovered collateral that should have remained locked, repaid flash liquidity, and retained the pool’s ETH plus downstream borrow power.
    7. The attacker repeated this pattern across Fuse pools 8, 18, 27, 127, 144, 146, and 156, then drained additional stablecoin liquidity by leaning on the protocol’s now-fictitious account health.
    8. A later replay on Arbitrum used the same CEther-family reentrancy idea via ETH redemption in transaction 0x3212d091792f81f18a31aab753de6b3128d79dcb5e8392167249595f813203ef.
  • What the attacker needed to be true:
    • The borrower / redeemer had to be an attacker-controlled contract capable of executing logic in the ETH receive callback.
    • Fuse had to transfer ETH before finalizing debt or redemption accounting.
    • exitMarket() had to remain callable during the callback despite the protocol’s reentrancy protections.
    • The target pools needed enough ETH and stablecoin liquidity that turning stale solvency into real withdrawals was worth the flash-loan cost.
    • Pool / Comptroller logic had to trust account liquidity derived from pre-update state long enough for the attacker to complete the unwind.
  • Technical anatomy worth remembering:
    • This was a clean cross-function reentrancy, not only same-function recursion. The vulnerable window opened in CEther transfer logic, but the monetization step relied on exitMarket() in the Comptroller.
    • The exploit is a good reminder that “we added a reentrancy lock” is not the same as “the state machine is safe.” Partial lock coverage just moves the profitable callback target to a sibling function.
    • The March 2022 security-upgrade report matters because it shows this exploit family was already on the team’s radar: researchers had found CEther redemption reentrancy in older pools, borrowing had been globally paused, and pools were upgraded. The April loss is therefore a strong case study in why a known callback family must be retired as a class, not patched only where the first proof-of-concept landed.
    • The official postmortem notes the attacker used three accounts per affected pool to maximize extraction. Account 1 finished with the fake ETH supply plus most of the stablecoin bad debt, while accounts 2 and 3 carried ETH borrow balances roughly equal to the flash-loaned ETH plus the pool’s pre-attack ETH liquidity.
    • CertiK’s reconstruction of the representative Ethereum leg also preserves the scale of the stale-state window: 150,000,000 USDC supplied to fUSDC-127, 1,977 ETH borrowed through the vulnerable path, then collateral release via re-entrant exitMarket() before borrow records landed.
    • The Arbitrum replay matters because it shows the issue was not limited to one exact borrow ETH configuration; the broader problem was that CEther-style ETH transfer paths still exposed inconsistent intermediate state.
  • Representative transactions and addresses:
    • Main Ethereum exploit tx: 0xab486012f21be741c9e674ffda227e30518e8a1e37a5f1d58d0b0d41f6e76530
    • Arbitrum replay tx: 0x3212d091792f81f18a31aab753de6b3128d79dcb5e8392167249595f813203ef
    • Attacker EOA tracked in public writeups: 0x6162759edad730152f0df8115c698a42e666157f
    • Attack contracts used in public tracing: 0xE39f3C40966DF56c69AA508D8AD459E77B8a2bc1, 0x32075bad9050d4767018084f0cb87b3182d36c45
    • Example affected market called out in technical writeups: fUSDC-127
    • Affected Ethereum Fuse pools: 8, 18, 27, 127, 144, 146, 156
  • Response / mitigation:
    • Rari / Fei paused borrowing globally once the exploit was identified.
    • The postmortem says the CEther ETH-send path was changed away from unrestricted call.value(...), and borrow-state ordering was corrected so accounting finalized before transfer.
    • The team also flattened rates on affected pools to a temporary 3% to keep post-exploit utilization and interest dynamics from compounding the damage while bad debt remained outstanding.
    • To make cleanup tractable, administrators used a temporary CEther implementation to merge the attacker’s split ETH supply / borrow balances into a single bad-debt account before planning further remediation.
    • In the aftermath, Rari emphasized heavier review of every Fuse deviation from Compound, additional alerting around large liquidity movements, easier global / per-pool pause controls, and a design pivot toward a smaller Fuse v2 architecture.

Pattern tags

  • reentrancy
  • collateral-membership-desynchronization
  • Fuse preserved the clean callback reached sibling solvency authority before debt truth settled shape: the external handoff happened in CEther, but the value-authoritative mistake was that Comptroller collateral membership could still be rewritten through exitMarket() while the borrow that depended on that membership remained unrecorded.
  • It is also a strong remediation-completeness failure exemplar: March 2022 had already established the dangerous family, yet production still kept one CEther callback path plus one sibling collateral-management path that together recreated the same solvency break.

Why the exploit worked

  • Fuse let ETH delivery happen before borrow or redemption state became true. That meant the attacker did not need to defeat access control; they only needed the protocol to hand them one ordinary callback window.
  • During that window, exitMarket() still authenticated against stale account liquidity and stale collateral membership. The protocol had one economic story in the market contract (borrow is in flight) and another in the Comptroller (collateral is still removable).
  • The exploit paid because those two stories rejoined only after value had already moved. Once collateral was released, the attacker could keep the borrowed ETH, recycle collateral, and leave the protocol with concentrated bad debt rather than merely a transient accounting inconsistency.
  • The March 2022 upgrade work reduced some obvious CEther risk but did not retire the class. Fuse still behaved as if nonReentrant on the first path was enough even though a sibling solvency path remained callback-reachable.
  • The attacker’s three-account-per-pool structure then turned the stale-state window into maximum extractable bad debt: one account retained the fake supplied ETH while the others warehoused the borrow balances that the protocol would later socialize.

Three collateral-membership promotions this incident collapsed

  1. A callback-reachable collateral-release edge existed at all

    • Fuse’s CEther path did not just deliver ETH; it created an attacker-controlled execution edge before the borrow-side accounting had settled.
    • During that edge, exitMarket() was still reachable as a live collateral-release verb.
  2. That edited collateral set was treated as if it already described the same moment as the in-flight borrow

    • The attacker did not need Comptroller to forget all debt forever; they only needed it to accept this collateral is now removable against a borrow that had not yet been booked.
    • The dangerous jump was from callback can ask to leave the market to the post-exit collateral set is already the canonical solvency truth for this account.
  3. Other value-moving paths still spent against that false joined picture

    • Once the stale exitMarket() result had detached collateral from the debt that still depended on it, the attacker could recover collateral, keep borrowed ETH live, and then lean on the fake health to drain additional stablecoin liquidity.
    • The three-account-per-pool extraction pattern mattered because it turned that one temporary collateral-membership lie into durable pool bad debt and real withdrawn assets.

Detection and audit pivots

  • Treat native-asset sends inside borrow(), redeem(), refund, and liquidation paths as callback edges every bit as dangerous as ERC-777 hooks. If ETH leaves before debt, collateral flags, or reserves settle, the review is not done.
  • Diff lock coverage and invariant assumptions across the full market / Comptroller pair. The important question is not is CEther guarded? but what other authority surfaces still trust CEther's half-finished state?
  • Simulate the exact joined path, not isolated functions: borrow ETH -> callback -> exitMarket -> collateral release -> downstream borrow / repay / liquidate. Cross-function lending exploits often disappear in unit tests that stop at the first re-entry.
  • Review historical mitigations as adversarial evidence. If a protocol already patched a CEther-family callback bug once, search aggressively for sibling entrypoints that still act on the same temporary state instead of assuming the bug family is closed.
  • Ask whether post-exploit cleanup assumptions hide the same topology problem. Fuse had to merge split supply / borrow balances back into one bad-debt account precisely because the attack path had let one economic position fracture across multiple state views.

Failure-prevention checklist

  • Finalize borrow, redeem, and collateral-membership state before any native-asset transfer or externally controlled token callback can run. If value leaves first, every sibling solvency surface must be assumed attacker-reachable.
  • Apply one invariant envelope across market contracts and the Comptroller: asset counted as collateral, account liquidity, and borrow recorded must become true or false together, not in separate callback-observable phases.
  • Make exitMarket() and similar collateral-release verbs fail closed whenever any borrow, redeem, liquidation, repay, or migration path for that account is mid-flight.
  • Treat ETH-send paths as first-class integration risk, not a harmless implementation detail. Native-asset delivery deserves the same callback modeling, lock coverage, and adversarial testing as ERC-777 hooks or custom token callbacks.
  • When patching a discovered reentrancy family, enumerate and retire the whole family. Do not stop at the first verb or one specific pool configuration that made the proof-of-concept easiest.
  • Add invariant tests that exercise joined paths across contracts: borrow/redeem -> callback -> exitMarket -> withdraw/borrow elsewhere. Cross-function lending failures rarely show up in single-function harnesses.

Audit prompts

  • Can any borrower- or redeemer-controlled callback run before accountBorrows, collateral membership, reserve balances, or liquidity checks have been finalized?
  • Which sibling functions still trust temporary state created by borrow(), redeem(), liquidation, repay, or migration paths, especially across market / Comptroller boundaries?
  • If a protocol claims a CEther or native-asset reentrancy patch, what exact callback-reachable verbs remain live, and what invariants do they assume are already settled?
  • Could exitMarket() or a related collateral-release surface be invoked during a borrow or redemption callback and observe solvency that is only true because the debt or reserve effect has not landed yet?
  • Do post-incident admin cleanup tools, debt-merging utilities, or emergency implementations reveal that one economic position can fracture across multiple state views during exceptional flows?

Lessons that should already be standard

  • NonReentrant on the obvious function is not a security argument. The real unit of review is the full state machine, including sibling contracts that consume the same solvency facts.
  • Native-asset sends are callback surfaces. Protocols should stop treating ETH delivery as safer or simpler than token hooks just because there is no ERC interface involved.
  • Collateral membership is live balance-sheet authority, not a lightweight bookkeeping flag. If it can be removed while a debt-creating path is still in flight, the protocol is already lending against a fiction.
  • A known exploit family should be retired as a class. If the team already learned CEther callback before state finalization once, leaving one adjacent path alive is not partial hardening; it is unfinished remediation.
  • Multi-account extraction patterns are often a symptom, not the root cause: they usually mean the protocol let one economic position split across inconsistent state views and the attacker is just optimizing that inconsistency.

Why this incident matters

  • It is one of the clearest modern examples of cross-function reentrancy inside a lending protocol: the bug is not merely “borrow before update,” but “borrow before update plus sibling collateral-management path still trusts the old state.”
  • It also shows how incomplete mitigations can be worse than reassuring language suggests. Fuse had already identified CEther-related reentrancy risk in March 2022, yet the remaining callable path was still enough to drain roughly $80M a month later.
  • Alongside the C.R.E.A.M. AMP incident, this is a canonical case for why lending-market reentrancy review must cover token transfer semantics, ETH-send semantics, and Comptroller / market interactions together rather than as separate audit checklists.

Internal linkages

  • Core recurring pattern: reentrancy.
  • Sharper lending-state lens: collateral-membership-desynchronization — the profitable move was not just re-entry, but using the CEther callback to exitMarket() before Fuse had finalized the debt that still depended on that collateral membership.
  • Positive-design companion: helper-path-invariant-reuse-and-fail-closed-discipline — the deeper design failure was letting a sibling collateral-management path inherit weaker callback assumptions than the borrow path it could still economically rewrite.

Control surface