Rari Capital Ethereum Pool ibETH totalETH() / work() Read-Only Reentrancy Drain (2021-05-08)

  • Name: Rari Capital Ethereum Pool ibETH totalETH() / work() read-only reentrancy drain

  • Date: 2021-05-08

  • Protocol: Rari Capital Ethereum Pool

  • Chain: Ethereum

  • Category: read-only reentrancy / transient-state oracle desynchronization / integration incompatibility / pool-share inflation drain

  • Loss: about 2,600 ETH (about $10M at the time)

  • Summary: Rari’s Ethereum Pool treated Alpha Homora V1’s ibETH accounting ratio ibETH.totalETH() / ibETH.totalSupply() as if it were safe same-transaction valuation truth. The attacker entered Alpha’s payable Bank.work() path, used an attacker-controlled callback inside Alpha’s dormant Sushi strategy stack, deposited ETH into Rari when ibETH’s ratio still looked normal, then temporarily pushed extra ETH into ibETH through Alpha’s payable fallback before redeeming. Because Rari minted REPT shares against the pre-inflation ratio and burned them against the callback-window inflated ratio, the attacker got back the same principal ETH while keeping surplus REPT, which they later redeemed for real ETH. When work() finished, Alpha returned the temporarily inserted ETH to the attacker, resetting ibETH’s ratio and leaving Rari short.

  • Trigger / entry point:

    • Rari’s Ethereum Pool valued its Alpha strategy through AlphaPoolController.getBalance() using the live ibETH ratio ibETH.balanceOf(this) * ibETH.totalETH() / ibETH.totalSupply().
    • Alpha Homora V1’s Bank.work() was payable and could route execution through attacker-shaped downstream calls before it returned any excess ETH to the caller.
    • Alpha’s Bank fallback accepted direct ETH transfers, so Bank.totalETH() could be inflated mid-transaction even when no canonical deposit shares were minted.
    • Alpha’s dormant SushiswapGoblin and StrategyAddTwoSidesOptimal surfaces still remained callable and let the attacker reach a malicious token approve() callback inside work().
  • Root cause:

    • Rari treated Alpha’s ibETH totalETH() read as if view implied economically settled truth.
    • But inside Bank.work(), Alpha temporarily admitted extra ETH into address(this).balance, which immediately changed totalETH() while the same call frame still had authority to send that ETH back out to the caller before final settlement.
    • That meant Rari’s deposit and withdrawal math consumed two different ibETH exchange-rate realities inside one still-open Alpha execution frame.
    • The decisive invariant failure was not generic reentrancy exists; it was share mint/burn and underlying valuation must read one settled upstream state object. Rari minted REPT against one ibETH ratio and burned it against another, even though the attacker’s real economic position had not changed proportionally.
  • Exploit path:

    1. The attacker EOA 0xcb36b1ee0af68dce5578a487ff2da81282512233 prepared a fake SushiSwap pair involving a malicious token and repeatedly entered Alpha Homora V1’s Bank.work() through Alpha’s dormant SushiswapGoblin 0x9eed7274ea4b614acc217e46727d377f7e6f9b24 and StrategyAddTwoSidesOptimal 0x81796c4602b82054a727527cd16119807b8c7608 stack.
    2. Inside the strategy flow, Alpha called the malicious token’s approve() function. That callback became the attacker’s execution window while Alpha’s work() frame was still live.
    3. From that callback, the attacker flash-loaned ETH from dYdX and deposited ETH into the Rari Ethereum Pool. At this moment, Rari minted REPT against the ordinary ibETH ratio exposed through AlphaPoolController.getBalance().
    4. Still inside the same callback window, the attacker sent a large amount of ETH directly to Alpha’s Bank payable fallback. That raised address(ibETH).balance and therefore Bank.totalETH() without minting corresponding ibETH shares.
    5. The attacker then called Rari’s withdrawal path before Bank.work() had finished. Because Rari now read a higher ibETH.totalETH() / totalSupply() ratio, it burned fewer REPT for the same ETH withdrawal than it had minted in step 3.
    6. When Alpha’s work() call unwound, Alpha refunded the extra ETH sitting in Bank back to the caller. The temporary ibETH inflation disappeared, but the attacker still held the surplus REPT left over from the mint-versus-burn mismatch.
    7. The attacker redeemed or donate()-processed those leftover REPT into additional ETH, then repeated the sequence across roughly 200 transactions until Rari had lost about 2,600 ETH.
  • What the attacker needed to be true:

    • Rari needed to price its Alpha strategy from Alpha’s live ibETH ratio rather than from a delayed or callback-safe valuation surface.
    • Alpha’s Bank.work() needed to allow arbitrary enough downstream execution that an attacker-controlled callback could reach Rari mid-frame.
    • Direct ETH sent to Alpha’s Bank needed to affect totalETH() immediately even though no matching ibETH share issuance occurred.
    • Alpha needed to return the excess ETH at the end of work(), so the attacker could temporarily inflate totalETH() without permanently donating that capital to ibETH LPs.
    • Rari’s deposit and withdrawal flows needed to be callable in the same callback window, with no same-block or same-frame quarantine.
  • Technical anatomy worth remembering:

    • This is an early clean read-only-reentrancy / transient-state valuation case. The upstream protocol whose state got misread was Alpha; the protocol that actually lost funds was the integrator, Rari.
    • The key accounting fiction was not an arbitrary oracle price from an external market. It was Alpha’s own pool ratio being read during a state where extra ETH was present but not economically committed to ibETH share holders.
    • Put differently, the missing object was a canonical settled ibETH valuation surface. Alpha exposed a raw totalETH() number that changed as soon as ETH hit the Bank balance, but neither Alpha nor Rari had a way to distinguish temporary ETH parked inside an open work frame from ETH that had actually become durable backing for ibETH shares.
    • Alpha’s dormant strategy modules mattered because they gave the attacker a callback-bearing route into work() even though those contracts had reportedly never been used before the exploit.
    • The attack also preserves an important design lesson for integration reviews: a view helper that is safe in quiescent state may still be unsafe if the integrated protocol exposes any mid-transition callback window around the values that helper reads.

Three read-authority promotions this incident collapsed

  1. Raw ibETH state was still visible inside Alpha’s open work() frame

    • Direct ETH sent to Alpha’s payable Bank fallback changed Bank.totalETH() immediately, so ibETH.totalETH() / totalSupply() could still be observed while Alpha had not yet decided whether that ETH would become durable ibETH backing or would be refunded before frame-end.
    • That only answered a ratio is readable right now. It did not yet answer whether Alpha was publishing one settled wrapper-valuation object downstream protocols were supposed to trust for solvency math.
  2. Rari promoted that raw read into canonical pool valuation truth

    • AlphaPoolController.getBalance() treated the live ibETH ratio as if it already represented the canonical settled value of Rari’s Alpha position.
    • This is where the sharper mistake lived. Rari did not merely observe Alpha’s transient state; it treated a callback-window totalETH() observation as if it had already crossed the stronger boundary into this is the wrapper valuation object the upstream system stands behind for mint/burn accounting.
  3. Rari’s mint and burn paths still spent against that promoted truth immediately

    • Once Rari had accepted the live ibETH ratio as pool value, deposit() minted REPT and withdraw() burned REPT against contradictory points on the same still-open Alpha frame.
    • That final promotion turned a transient read into real ETH loss: Rari let the accepted callback-window valuation inherit immediate share-issuance and redemption authority instead of forcing one delayed, settled valuation object for the whole economic cycle.
  • Representative transactions and addresses:

    • Attacker EOA: 0xcb36b1ee0af68dce5578a487ff2da81282512233
    • Example exploit transaction: 0x171072422efb5cd461546bfe986017d9b5aa427ff1c07ebe8acc064b13a7b7be
    • Alpha SushiswapGoblin: 0x9eed7274ea4b614acc217e46727d377f7e6f9b24
    • Alpha StrategyAddTwoSidesOptimal: 0x81796c4602b82054a727527cd16119807b8c7608
    • Alpha Homora V1 Bank / ibETH code path: Bank.totalETH(), Bank.withdraw(), Bank.work(), and the payable fallback in contracts/5/Bank.sol
    • Rari code path: RariFundManager.deposit(), RariFundManager.withdraw(), AlphaPoolController.getBalance(), and AlphaPoolController.withdraw()
  • Response / mitigation:

    • Rari paused the Ethereum Pool after the exploit and publicly acknowledged that its Alpha integration had assumed ibETH.totalETH() could not be manipulated during external calls from work().
    • Rari proposed stronger invariant checks, integrator reviews with upstream protocols, and same-block deposit/withdraw restrictions for lossless pools.
    • The post-incident lesson for the wider ecosystem was broader than Rari alone: protocols integrating ibETH or similar callback-sensitive wrapper ratios needed to treat upstream read surfaces as security boundaries, not just arithmetic helpers.

Pattern tags

  • wrapper exchange-rate truth was read inside an upstream callback window
  • mint path and burn path consumed different upstream state objects in one economic cycle
  • temporary raw balance admission changed published collateral value before durable share backing existed
  • unused strategy surface still supplied the callback rail that made the integration exploitable

Why the exploit worked

  • Rari’s lossless-pool accounting assumed Alpha’s ibETH.totalETH() / totalSupply() ratio was one canonical value during the attacker’s round trip, but Alpha only published a raw same-frame balance observation. That let Rari mint REPT against one upstream state and burn it against another.
  • Alpha’s payable Bank.work() frame gave the attacker exactly the timing boundary they needed: they could insert ETH into Bank.totalETH() through the fallback, make Rari read the inflated ratio, and then let Alpha return that ETH before transaction-end settlement. The extra ETH temporarily changed valuation truth without becoming durable ibETH backing.
  • Because Rari allowed deposit and withdrawal logic to consume the live ibETH ratio inside the same callback window, the attacker did not need a durable oracle manipulation. They only needed one mid-transition overvaluation long enough to keep surplus REPT after withdrawing the original ETH.
  • The dormant Sushi strategy path mattered because it reopened external control flow inside Alpha’s work() machinery. In practice, the exploit was callback rail + unsafe upstream read surface + mint/burn asymmetry, not merely ibETH ratio moved.

Detection and audit pivots

  • Audit any integration that values wrapper collateral from totalAssets(), totalETH(), exchangeRate(), or similar ratios and ask whether the upstream protocol can change the numerator or denominator transiently inside a callback-capable frame.
  • Look for payable fallbacks, direct-transfer accounting, or temporary balance parking that can alter a published exchange rate before the protocol decides whether that balance really became share backing.
  • Trace deposit and withdrawal math together. If the same economic position can be minted against one upstream snapshot and redeemed against a later snapshot inside one transaction, the invariant failure may live in cross-path valuation disagreement rather than in either path alone.
  • Treat dormant or deprecated adapter and strategy surfaces as live if they can still provide attacker-chosen control flow into the upstream protocol. Unused is not the same thing as cannot become the callback boundary.
  • Prefer upstream safe-read surfaces or delayed settled snapshots over raw live ratios when another protocol’s callback sequencing can decide whether a reported balance is durable collateral or just in-flight capital.

Failure-prevention checklist

  • Do not price upstream wrapper or vault positions from raw same-transaction exchange-rate helpers unless the upstream protocol publishes a machine-checkable safe-read surface. If totalETH() or totalAssets() can be observed during a still-open callback frame, the integration should fail closed or use a delayed settled snapshot.
  • Bind mint and burn paths to the same valuation object. If deposits mint claims from one upstream state and withdrawals burn them against another still-reachable upstream state in the same transaction, the wrapper ratio has already become exploitable accounting truth.
  • Treat direct-transfer balance changes as untrusted until the upstream protocol proves they became durable backing for shares. A payable fallback, stray ETH transfer, or other ambient-balance admission should never immediately count as redeemable collateral value for downstream integrators.
  • Review upstream helper surfaces and dormant strategy modules together. Callback-capable but unused execution paths are still part of the integration boundary if they can reopen external control flow around the upstream accounting object your protocol prices.
  • Quarantine same-block or same-transaction deposit/withdraw round trips when the upstream venue cannot offer a trustworthy live valuation surface. Flash-loan friction is not a complete fix, but it is still a useful blast-radius reduction when the sharper design answer is do not trust live reads.
  • Add pool-level monotonicity or loss-threshold assertions for nominally lossless wrappers so a transient upstream ratio spike cannot silently let users exit with more principal than entered.
  • Monitor for repeated same-tx sequences where wrapper-backed share supply is minted and then mostly redeemed while the underlying integrator balance trends downward. That pattern is a strong production signature for transient valuation abuse.
  • Re-audit integrations whenever the upstream protocol adds new adapters, strategy routes, payable entrypoints, or refund semantics. The exploitability here depended as much on Alpha’s callback surface as on Rari’s valuation formula.

Audit prompts

  • What exact upstream read authorizes minting and burning the integrated claim today: raw totalETH() / totalSupply(), a guarded helper, or a delayed snapshot? Can that read still be observed while the upstream protocol is halfway through deciding whether incoming ETH is durable backing or just temporary call-frame inventory?
  • Can the attacker cause the upstream numerator to rise or fall transiently by direct transfer, callback-time refund mechanics, strategy adapter behavior, or temporary custody parking without changing share supply in the same way?
  • Does any same-transaction path let the attacker mint protocol shares against one wrapper ratio and redeem against a different wrapper ratio before the upstream frame settles?
  • Which upstream strategy, adapter, or callback-capable route can re-enter the integrator mid-frame, and are any of those routes considered unused or legacy only by convention rather than by explicit deauthorization?
  • If the upstream protocol cannot expose a safe live read surface, what concrete guard forces this integration onto delayed settled pricing rather than raw balance-sheet reads?
  • Would a monotonic value per share cannot fall below prior settled value or similar invariant have stopped the exploit before loss socialized across pool LPs?
  • Are any downstream emergency pauses, borrow caps, or withdrawal throttles keyed to impossible same-block changes in wrapper exchange rate or integrator solvency?
  • Which assumptions in the integration came from upstream folklore rather than code-enforced interface guarantees? Rari’s own postmortem is the warning that we did not know this helper was manipulable inside work() is itself a security smell.

Lessons that should already be standard

  • View wrapper exchange rates are not settlement objects. If the upstream protocol can still refund, rebalance, or unwind the same capital before the frame ends, the quoted ratio is only an intermediate observation.
  • Integrations own the burden of proving upstream quiescence before pricing against another protocol’s accounting. Alpha knows its code better is true, but downstream solvency still should not rest on undocumented assumptions about callback behavior.
  • Ambient balance is not the same thing as durable backing. If sending ETH directly to a contract can move published value before mint, accounting, or refund logic settles, downstream protocols must assume the number is adversarial until proven otherwise.
  • Dormant modules are live attack surface when they still carry authority to reopen control flow. Nobody uses this strategy is not a defense if the contract is still callable.
  • Lossless-pool stories need explicit invariant checks. If a pool markets principal safety, then same-tx accounting paths should already assert that users cannot mint and burn against contradictory upstream valuations without someone tripping a circuit breaker.
  • Some integrations should admit that no safe live oracle exists. When the upstream truth object is callback-sensitive, delayed settled pricing is often healthier than pretending one more raw ratio read is close enough.

Why this incident matters

  • It preserves a pre-Curve, pre-Balancer example of the same core read-only-reentrancy shape later seen in LP-oracle exploits: an upstream ratio was only false inside a callback window, but that was enough for an integrating protocol to mint and redeem claims at inconsistent prices.
  • It is also a strong reminder that view-only pricing mistakes can sit in integrations rather than in the attacked protocol’s own user-facing paths.
  • The case is especially useful because the dangerous read was a wrapper exchange-rate surface, not a classic AMM LP price helper. That keeps the recurring lesson broader than Curve-style virtual price bugs: any upstream share ratio can become false solvency truth if it is published before settlement is final.
  • And it shows how unused or never activated in practice strategy surfaces can still be live enough to become the callback rail that makes an otherwise theoretical transient-state bug exploitable.

Internal linkages

Control surface