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$10Mat 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 payableBank.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. Whenwork()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 ratioibETH.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
Bankfallback accepted direct ETH transfers, soBank.totalETH()could be inflated mid-transaction even when no canonical deposit shares were minted. - Alpha’s dormant
SushiswapGoblinandStrategyAddTwoSidesOptimalsurfaces still remained callable and let the attacker reach a malicious tokenapprove()callback insidework().
- Rari’s Ethereum Pool valued its Alpha strategy through
-
Root cause:
- Rari treated Alpha’s ibETH
totalETH()read as ifviewimplied economically settled truth. - But inside
Bank.work(), Alpha temporarily admitted extra ETH intoaddress(this).balance, which immediately changedtotalETH()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 wasshare 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.
- Rari treated Alpha’s ibETH
-
Exploit path:
- The attacker EOA
0xcb36b1ee0af68dce5578a487ff2da81282512233prepared a fake SushiSwap pair involving a malicious token and repeatedly entered Alpha Homora V1’sBank.work()through Alpha’s dormantSushiswapGoblin0x9eed7274ea4b614acc217e46727d377f7e6f9b24andStrategyAddTwoSidesOptimal0x81796c4602b82054a727527cd16119807b8c7608stack. - Inside the strategy flow, Alpha called the malicious token’s
approve()function. That callback became the attacker’s execution window while Alpha’swork()frame was still live. - 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(). - Still inside the same callback window, the attacker sent a large amount of ETH directly to Alpha’s
Bankpayable fallback. That raisedaddress(ibETH).balanceand thereforeBank.totalETH()without minting corresponding ibETH shares. - The attacker then called Rari’s withdrawal path before
Bank.work()had finished. Because Rari now read a higheribETH.totalETH() / totalSupply()ratio, it burned fewer REPT for the same ETH withdrawal than it had minted in step 3. - When Alpha’s
work()call unwound, Alpha refunded the extra ETH sitting inBankback to the caller. The temporary ibETH inflation disappeared, but the attacker still held the surplus REPT left over from the mint-versus-burn mismatch. - The attacker redeemed or
donate()-processed those leftover REPT into additional ETH, then repeated the sequence across roughly 200 transactions until Rari had lost about2,600 ETH.
- The attacker EOA
-
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
Bankneeded to affecttotalETH()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 inflatetotalETH()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
settledibETH valuation surface. Alpha exposed a rawtotalETH()number that changed as soon as ETH hit the Bank balance, but neither Alpha nor Rari had a way to distinguishtemporary ETH parked inside an open work framefromETH 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
viewhelper 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
-
Raw ibETH state was still visible inside Alpha’s open
work()frame- Direct ETH sent to Alpha’s payable
Bankfallback changedBank.totalETH()immediately, soibETH.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.
- Direct ETH sent to Alpha’s payable
-
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 intothis is the wrapper valuation object the upstream system stands behind for mint/burn accounting.
-
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 andwithdraw()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.
- Once Rari had accepted the live ibETH ratio as pool value,
-
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 incontracts/5/Bank.sol - Rari code path:
RariFundManager.deposit(),RariFundManager.withdraw(),AlphaPoolController.getBalance(), andAlphaPoolController.withdraw()
- Attacker EOA:
-
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 fromwork(). - 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.
- Rari paused the Ethereum Pool after the exploit and publicly acknowledged that its Alpha integration had assumed
Pattern tags
wrapper exchange-rate truth was read inside an upstream callback windowmint path and burn path consumed different upstream state objects in one economic cycletemporary raw balance admission changed published collateral value before durable share backing existedunused 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 intoBank.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 wascallback rail + unsafe upstream read surface + mint/burn asymmetry, not merelyibETH 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.
Unusedis not the same thing ascannot 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()ortotalAssets()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
unusedexecution 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
unusedorlegacyonly 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 valueor 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
Viewwrapper 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 betteris 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 strategyis 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
unusedornever activated in practicestrategy surfaces can still be live enough to become the callback rail that makes an otherwise theoretical transient-state bug exploitable.
Internal linkages
- Closest recurring pattern: read-only-reentrancy.
- Upstream safe-read design companion: safe-read oracle interfaces.
- Delayed-valuation companion when no trustworthy live read exists: Delayed snapshots and same-block quarantine.
- Broader parent family: reentrancy.
- Adjacent monetization lens: lp-token-oracle-mispricing.
Control surface
-
The decisive leverage sat at the seam between Alpha’s callback-bearing
work()execution frame and Rari’s willingness to trust ibETH’s livetotalETH()/totalSupply()ratio as if it were settled value. -
More specifically,
Bank.totalETH()and the payable fallback let raw ETH balance become externally visible valuation state beforework()had finished deciding where that ETH would end up economically. -
Rari did not need Alpha to publish a permanently false price. It only needed Alpha to make one transiently inflated state externally readable before the same frame returned that capital to the attacker.
-
The exploit therefore belonged to the
integrator consumed a callback-window ratio as final truthfamily, not to durable oracle manipulation. -
Sources:
- https://medium.com/rari-capital/5-8-2021-rari-ethereum-pool-post-mortem-60aab6a6f8f9
- https://hackmd.io/AxM6EeBOS928w28b_kUSfg
- https://github.com/AlphaFinanceLab/alphahomora/blob/5017311b56acf64020cd834e2e82a9ad556fea68/contracts/5/Bank.sol#L99-L101
- https://github.com/AlphaFinanceLab/alphahomora/blob/5017311b56acf64020cd834e2e82a9ad556fea68/contracts/5/Bank.sol#L111-L115
- https://github.com/AlphaFinanceLab/alphahomora/blob/5017311b56acf64020cd834e2e82a9ad556fea68/contracts/5/Bank.sol#L123-L163
- https://github.com/AlphaFinanceLab/alphahomora/blob/5017311b56acf64020cd834e2e82a9ad556fea68/contracts/5/Bank.sol#L243-L243
- https://github.com/Rari-Capital/rari-ethereum-pool-contracts/blob/75fb2562b8258a214256a5e938738d98cbd6ad05/contracts/lib/pools/AlphaPoolController.sol#L39-L41
- https://github.com/Rari-Capital/rari-ethereum-pool-contracts/blob/75fb2562b8258a214256a5e938738d98cbd6ad05/contracts/lib/pools/AlphaPoolController.sol#L56-L63
- https://github.com/Rari-Capital/rari-ethereum-pool-contracts/blob/75fb2562b8258a214256a5e938738d98cbd6ad05/contracts/RariFundManager.sol#L464-L464
- https://github.com/AlphaFinanceLab/alphahomora/blob/master/audit_report/PeckShield-Security-Review-Alpha-Homora-v1.0.pdf
- https://etherscan.io/address/0xcb36b1ee0af68dce5578a487ff2da81282512233
- https://etherscan.io/tx/0x171072422efb5cd461546bfe986017d9b5aa427ff1c07ebe8acc064b13a7b7be
-
Last reviewed: 2026-08-01 UTC