Conic Finance ETH Omnipool Read-Only Reentrancy Exploit (2023-07-21)

  • Name: Conic Finance ETH Omnipool read-only reentrancy exploit
  • Date: 2023-07-21
  • Protocol: Conic Finance
  • Chain: Ethereum
  • Category: read-only reentrancy / LP-oracle manipulation / Curve integration failure / share-accounting overcredit
  • Loss: about 1,724 ETH, commonly reported as roughly 3.3M
  • Summary: Conic’s ETH Omnipool accepted deposits into a meta-vault that spread liquidity across Curve pools and priced some positions through CurveLPOracleV2. The attacker used a huge flash-loaned position in the Curve rETH/ETH pool, then entered Curve’s liquidity-removal path so LP-token supply was already burned while underlying balances were still mid-withdrawal. In that callback window, Curve’s get_virtual_price() no longer represented settled state: D was still being computed from not-yet-fully-updated balances while totalSupply() had already gone down, so the LP mark temporarily jumped. Conic let CurveLPOracleV2 consume that transient get_virtual_price() output and feed it into ETH Omnipool share accounting. The decisive bug was not a missing guard but a misrouted one: CurveHandlerV3 only ran its Curve reentrancy-context check when _isETH() concluded the pool held the 0xEeee... ETH sentinel, but the relevant Curve v2 pool exposed WETH instead. That let the attacker loop deposits and withdrawals against a temporarily overstated Omnipool NAV and drain about 1,724 ETH.
  • Trigger / entry point:
    • The victim pool was Conic’s ETH Omnipool at 0xbb787d6243a8d450659e09ea6fd82f1c859691e9.
    • The exploiter used contract 0x743599ba5cfa3ce8c59691af5ef279aaafa2e4eb, driven by EOA 0x8d67db0b205e32a5dd96145f022fa18aae7dc8aa.
    • Conic’s own postmortem identified the decisive mistake inside CurveHandlerV3: the handler only ran its Curve reentrancy-context check when _isETH() concluded that the target Curve pool contained ETH, but Curve v2 pools exposed WETH rather than the 0xEeee... ETH sentinel Conic expected.
    • That let the attacker reach the exact read-only reentrancy state ChainSecurity had warned integrators about: a Curve LP token whose totalSupply() had already gone down during remove_liquidity() while pool balances had not yet fully normalized.
  • Root cause:
    • Conic priced Curve LP exposure through an onchain oracle path that was unsafe during Curve callback windows.
    • The protocol tried to guard against Curve read-only reentrancy, but that protection was routed through a brittle pool-metadata heuristic: CurveHandlerV3 only checked Curve context when _isETH() believed the pool contained the ETH sentinel address.
    • Curve’s own meta-registry semantics for the relevant v2 pool did not match Conic’s assumption. The pool exposed WETH rather than 0xEeee..., so the safe-read check never ran even though the pool still had an ETH callback surface.
    • Once the guard was bypassed, CurveLPOracleV2 could observe Curve’s transiently inflated get_virtual_price() mid-remove_liquidity(): the stable-swap invariant D was still derived from balances that had not finished leaving the pool while LP-token totalSupply() had already been reduced.
    • Conic’s mint / withdraw accounting then treated the manipulated LP mark as if it were durable asset value, so the attacker could loop deposits and withdrawals at a positive exchange rate and extract more value than they had economically contributed.
  • Exploit path:
    1. The attacker assembled a very large temporary position, with public incident analysis describing flash-loaned inventory that included about 20,000 stETH from Aave plus roughly 20,550 rETH, 3,000 cbETH, and 28,504.2 WETH from Balancer.
    2. They added liquidity into the relevant Curve rETH/ETH pool and obtained the LP position that Conic’s oracle path would later value.
    3. They entered Curve’s remove_liquidity() flow. In the vulnerable state, Curve had already burned LP tokens, shrinking totalSupply(), but had not yet fully transferred underlying assets out of the pool, so get_virtual_price() temporarily violated its usual monotonic-growth intuition.
    4. The ETH transfer callback handed control back to the attacker while that supply-versus-balances mismatch was live.
    5. During the callback, the attacker interacted with Conic’s ETH Omnipool while CurveLPOracleV2 was still reading the inflated rETH/ETH LP valuation from get_virtual_price(). That let Conic over-credit the attacker’s position against a price that only existed inside the callback frame.
    6. After the Curve withdrawal finished and the LP valuation normalized, the attacker could still unwind against the excess value Conic had already recognized, effectively cycling deposits and withdrawals at a favorable exchange rate.
    7. The attacker repeated the maneuver, then forwarded the stolen ETH to 0x3d32c5a2e592c7b17e16bddc87eab75f33ae3010.
  • What the attacker needed to be true:
    • Conic had to trust a live onchain Curve LP valuation during mint, withdraw, or other accounting-sensitive paths.
    • The relevant Curve pool had to expose a callback-capable path where LP supply and balances were temporarily inconsistent.
    • Conic’s protection logic had to miss that it was inside a vulnerable Curve context; the ETH-versus-WETH meta-registry assumption created exactly that blind spot.
    • The attacker needed enough flash liquidity that a temporary LP overvaluation could be translated into material over-crediting inside the Omnipool.
  • Technical anatomy worth remembering:
    • This was not a classic write twice into the same storage reentrancy bug. The money was made by forcing Conic to read Curve’s half-finished state and convert that transient read into Omnipool share value.
    • The exact upstream number that became counterfeit was Curve’s get_virtual_price(). ChainSecurity’s writeup is the useful mental model here: during remove_liquidity(), LP supply was already lower while pool balances had not fully settled, so the numerator/denominator pair feeding virtual price stopped describing a quiescent pool.
    • The brittle point was not the existence of a guard, but the condition for enabling it. Conic knew Curve read-only reentrancy mattered and still lost because its safe-read gate was hidden behind pool-type / ETH-address detection.
    • The exploit is a good reminder that wrapper-address assumptions are security assumptions. In cross-protocol integrations, ETH versus WETH is not cosmetic metadata if it decides whether a safety check runs.
    • The healthier boundary was broader than check for reentrancy somewhere: publish one canonical safe-read surface that survives pool-version and asset-alias drift, rather than letting metadata branches silently decide whether callback-window state deserves price authority.
    • Conic’s same-day crvUSD Omnipool loss was a different mechanism entirely: a later imbalance / sandwich-style drain after the first incident had already destabilized operations. The preserved lesson in this note is the first ETH Omnipool exploit.
  • Representative transactions and addresses:
    • ETH Omnipool victim: 0xbb787d6243a8d450659e09ea6fd82f1c859691e9
    • Exploiter EOA: 0x8d67db0b205e32a5dd96145f022fa18aae7dc8aa
    • Exploiter contract: 0x743599ba5cfa3ce8c59691af5ef279aaafa2e4eb
    • Main exploit tx: 0x8b74995d1d61d3d7547575649136b8765acb22882960f0636941c44ec7bbe146
    • Earlier failed attempt: 0x97a8315e942dd180fb90a17b92f7dabd6e8a2e5b9fd5e4a95ee4049ff33d2f16
    • First attack proceeds forwarded to: 0x3d32c5a2e592c7b17e16bddc87eab75f33ae3010

Three safe-read-boundary promotions this incident collapsed

  1. A Curve callback window and an inflated LP mark existed

    • During remove_liquidity(), Curve had already burned LP supply while underlying balances had not yet finished leaving the pool, so get_virtual_price() temporarily stopped describing a quiescent pool.
    • That only proved an upstream callback-frame inconsistency existed. It did not yet mean Conic’s ETH Omnipool should treat that in-flight Curve ratio as canonical Omnipool asset truth.
  2. Conic promoted that callback-window read into canonical Omnipool share truth

    • Because CurveHandlerV3 only ran the Curve-context check when _isETH() saw the 0xEeee... sentinel, the relevant v2 pool’s WETH-returning metadata path silently bypassed the intended safe-read guard.
    • That was the decisive false promotion: a transient upstream LP mark stopped being merely Curve is mid-transition and became this is the real rETH/ETH LP value Omnipool accounting should mint and withdraw against right now.
  3. That published share truth still inherited immediate reserve-spending authority

    • Once the Omnipool had over-credited the attacker with extra cncETH exposure under the inflated mark, later deposits and withdrawals could still settle against the healthier pool balance sheet after the callback window closed.
    • This is where the read bug became a loss event. Conic did not just observe a bad view; it let the accepted callback-window read survive long enough to authorize real ETH extraction from the Omnipool.
  • Response / mitigation:
    • Conic disabled deposits while investigating and later published a postmortem covering both the ETH Omnipool exploit and the separate crvUSD Omnipool incident later that day.
    • Conic attributed the ETH exploit to the Curve v2 ETH/WETH address assumption and the resulting bypass of its reentrancy-context check.
    • The incident reinforced a wider integration lesson already documented by ChainSecurity and Balancer: onchain LP valuation paths need explicit protection against upstream read-only reentrancy, not heuristic assumptions about pool type.
    • Conic later negotiated partial recovery from a frontrunning MEV bot involved in the second same-day incident, but the main ETH exploit proceeds remained the central loss event.

Pattern tags

  • guard existed but metadata routing decided whether it ran
  • ETH sentinel versus WETH aliasing became part of the safe-read boundary
  • Curve callback-window LP overvaluation was converted into Omnipool share overcredit
  • adapter-level pool taxonomy silently overrode oracle safety assumptions

Why the exploit worked

  • Conic already knew Curve read-only reentrancy was a real integration hazard, but it did not expose one unconditional safe-read path. Instead, the protocol let _isETH() decide whether the Curve-context check would run at all.
  • That meant a metadata assumption — the belief that an ETH-sensitive Curve pool would surface the 0xEeee... sentinel rather than WETH — became a hidden security boundary. Once the relevant pool failed that heuristic, Conic consumed Curve’s mid-withdrawal LP valuation exactly when the guard was supposed to stop it.
  • The attacker did not need to falsify Curve’s value durably. They only needed Conic to recognize the callback-window mark as real Omnipool NAV long enough to over-credit shares and unwind against that overstatement.
  • The exploit therefore preserves a sharper lesson than add a reentrancy check: if the check is conditional on pool taxonomy, wrapper identity, or asset-label guesses, the taxonomy itself becomes part of the oracle-truth object and can fail open.

Detection and audit pivots

  • Search adapter code for safety checks that only run on selected pool types, token aliases, or registry classifications. If the protection is conditional, audit the condition as a first-class trust boundary.
  • Compare meta-registry or registry-returned asset identities against the adapter’s expected aliases (ETH, WETH, wrapped natives, pool-version labels) and ask whether a mismatch disables read-safety logic without disabling the integration itself.
  • Test LP-oracle consumers under live callback-window conditions rather than only on quiescent pricing paths. The important question is whether mint, withdraw, collateral, or liquidation logic can still reach the oracle while the upstream pool is mid-transition.
  • Prefer one canonical safe-read surface per venue over duplicated if pool looks like X, run guard Y logic. Read-only-reentrancy defenses that depend on venue classification tend to age badly as wrappers and pool versions drift.
  • Review every path that turns LP valuation into spendable Omnipool or vault accounting and ask whether a temporary upstream markup can survive long enough to become durable share overcredit.

Failure-prevention checklist

  • Do not let venue classification, wrapped-native aliases, or registry-returned coin labels decide whether read-only-reentrancy protection runs. If a Curve LP mark is unsafe during callback windows, the integration should route every economically equivalent pool shape through the same safe-read boundary.
  • Treat upstream LP valuation as untrusted while remove_liquidity(), remove_liquidity_imbalance(), or any callback-capable withdrawal path is still live. If the venue cannot publish a machine-checkable settled read surface, mint and withdrawal accounting should fail closed or use delayed settled snapshots instead.
  • Bind deposit and withdrawal accounting to the same settled price object. A protocol should never mint Omnipool shares from one LP valuation surface and let users exit against another while the upstream pool is still finishing the same transaction.
  • Re-audit every adapter whenever upstream registries, wrapper conventions, or pool versions change. ETH versus WETH is not cosmetic metadata if it decides whether an oracle safety check executes.
  • Prefer one venue-wide safe-read abstraction over per-pool heuristics. The integration should ask is this venue publishing settled truth right now? rather than does this pool look like the ETH shape we originally expected?.
  • Add same-transaction throttles, loss guards, or monotonicity assertions around LP-backed share accounting so a transient upstream mark cannot silently create a positive deposit/withdraw loop before operators can pause.
  • Monitor for repeated same-tx or same-block cycles where LP-backed shares are minted and then mostly redeemed while upstream pool state is oscillating around large liquidity removals. That pattern is a production signature for callback-window valuation abuse.
  • Keep upstream safe-read assumptions explicit in tests. Reproduce the exact callback window with flash liquidity and assert that every accounting path either reverts or prices from a delayed settled object.

Audit prompts

  • What exact upstream read gives this protocol authority to mint or burn LP-backed shares today: raw get_virtual_price(), a venue adapter, a registry-routed helper, or a delayed snapshot? Can that read still be reached while the upstream pool is mid-withdrawal?
  • Which pool-type checks, coin-alias branches, wrapper assumptions, or registry lookups decide whether reentrancy-context protection runs? Would ETH/WETH or similar alias drift disable the guard without disabling the integration?
  • Can an attacker enter an upstream callback frame where LP supply has already changed but balances have not yet settled, then force downstream accounting to treat that transient ratio as durable NAV?
  • Do deposit and withdrawal paths rely on the same safe-read object, or can one path mint from a callback-window mark while another later redeems from normalized state?
  • If the venue cannot expose a trustworthy live read surface, what concrete mechanism forces this protocol onto delayed snapshots, same-block quarantine, or full fail-closed behavior rather than optimistic raw reads?
  • Would a simple invariant like LP-backed share price cannot jump by X within one callback-bearing transaction or withdrawal cannot realize more principal than just deposited under the same unsettled mark have stopped the exploit before loss socialized?
  • Which assumptions in the adapter came from venue folklore rather than code-enforced guarantees? Conic’s postmortem is the warning that the registry told us WETH, so the ETH guard never ran is itself a security defect.
  • Are emergency pauses, deposit disables, or withdrawal throttles keyed to impossible same-block LP valuation moves, or do operators only learn after downstream share accounting has already recognized the false mark?

Lessons that should already be standard

  • A guard that runs only on the right metadata shape is not really guarding the economic surface. If wrapper identity or registry taxonomy can silence the protection, that taxonomy is part of the trust boundary and must be treated like security-critical code.
  • Upstream LP or wrapper prices are not settled truth just because the function is view. If the venue is still burning supply, transferring assets, or executing callbacks, the ratio is an intermediate observation, not solvency authority.
  • Integrations need one canonical safe-read surface per venue, not a patchwork of per-pool heuristics. Otherwise the oldest adapter assumption becomes the live oracle policy.
  • Wrapped-native aliasing is a recurring exploit seam. ETH, WETH, stETH, bridged wrappers, and sentinel addresses should be assumed adversarially whenever they decide which safety path executes.
  • If no trustworthy live read exists, delayed settled pricing is healthier than pretending one more adapter check makes an in-flight number money-good.
  • Read-only-reentrancy incidents are often share-accounting incidents in disguise. The attacker does not need to keep the upstream price false forever; they only need the protocol to recognize the transient mark long enough to mint or redeem claims against it.

Why this incident matters

  • It is one of the clearest post-Sentiment examples of read-only reentrancy escaping a protocol that already knew about the bug class and had tried to defend against it.
  • It broadens the corpus beyond Balancer-style stale-reserve reads into Curve-LP oracle reads, where the transient mismatch is virtual price / LP valuation rather than a BPT collateral helper.
  • It preserves a sharper normative lesson too: many integration failures live in whether a protocol publishes one canonical safe-read surface, not merely in whether some guard exists somewhere in adapter code.
  • It is also a clean reminder that pool taxonomy and asset aliasing can become hidden security boundaries. Here, ETH versus WETH decided whether callback-window state received immediate solvency authority.

Internal linkages

Control surface