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’sget_virtual_price()no longer represented settled state:Dwas still being computed from not-yet-fully-updated balances whiletotalSupply()had already gone down, so the LP mark temporarily jumped. Conic letCurveLPOracleV2consume that transientget_virtual_price()output and feed it into ETH Omnipool share accounting. The decisive bug was not a missing guard but a misrouted one:CurveHandlerV3only ran its Curve reentrancy-context check when_isETH()concluded the pool held the0xEeee...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 EOA0x8d67db0b205e32a5dd96145f022fa18aae7dc8aa. - 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 the0xEeee...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 duringremove_liquidity()while pool balances had not yet fully normalized.
- The victim pool was Conic’s ETH Omnipool at
- 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:
CurveHandlerV3only 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,
CurveLPOracleV2could observe Curve’s transiently inflatedget_virtual_price()mid-remove_liquidity(): the stable-swap invariantDwas still derived from balances that had not finished leaving the pool while LP-tokentotalSupply()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:
- The attacker assembled a very large temporary position, with public incident analysis describing flash-loaned inventory that included about
20,000 stETHfrom Aave plus roughly20,550 rETH,3,000 cbETH, and28,504.2 WETHfrom Balancer. - They added liquidity into the relevant Curve rETH/ETH pool and obtained the LP position that Conic’s oracle path would later value.
- They entered Curve’s
remove_liquidity()flow. In the vulnerable state, Curve had already burned LP tokens, shrinkingtotalSupply(), but had not yet fully transferred underlying assets out of the pool, soget_virtual_price()temporarily violated its usual monotonic-growth intuition. - The ETH transfer callback handed control back to the attacker while that supply-versus-balances mismatch was live.
- During the callback, the attacker interacted with Conic’s ETH Omnipool while
CurveLPOracleV2was still reading the inflated rETH/ETH LP valuation fromget_virtual_price(). That let Conic over-credit the attacker’s position against a price that only existed inside the callback frame. - 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.
- The attacker repeated the maneuver, then forwarded the stolen ETH to
0x3d32c5a2e592c7b17e16bddc87eab75f33ae3010.
- The attacker assembled a very large temporary position, with public incident analysis describing flash-loaned inventory that included about
- 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 storagereentrancy 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: duringremove_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,
ETHversusWETHis 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.
- This was not a classic
- 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
- ETH Omnipool victim:
Three safe-read-boundary promotions this incident collapsed
-
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, soget_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.
- During
-
Conic promoted that callback-window read into canonical Omnipool share truth
- Because
CurveHandlerV3only ran the Curve-context check when_isETH()saw the0xEeee...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-transitionand becamethis is the real rETH/ETH LP value Omnipool accounting should mint and withdraw against right now.
- Because
-
That published share truth still inherited immediate reserve-spending authority
- Once the Omnipool had over-credited the attacker with extra
cncETHexposure 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.
- Once the Omnipool had over-credited the attacker with extra
- 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 ranETH sentinel versus WETH aliasing became part of the safe-read boundaryCurve callback-window LP overvaluation was converted into Omnipool share overcreditadapter-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 Ylogic. 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.
ETHversusWETHis 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 thandoes 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/WETHor 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 transactionorwithdrawal cannot realize more principal than just deposited under the same unsettled markhave 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 ranis 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
rightmetadata 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,
ETHversusWETHdecided whether callback-window state received immediate solvency authority.
Internal linkages
- Core recurring pattern: read-only-reentrancy.
- Positive-design companion: safe-read oracle interfaces.
- Delayed-valuation companion when no trustworthy live read exists: Delayed snapshots and same-block quarantine.
- Adjacent oracle surface: lp-token-oracle-mispricing.
- Broader parent pattern: reentrancy.
- Useful comparison: sentiment-balancer-bpt-read-only-reentrancy-2023-04-04.
Control surface
-
The decisive leverage sat in Conic’s Curve adapter logic, the conditional reentrancy-context check inside
CurveHandlerV3, and the ETH Omnipool’s willingness to trust a callback-window LP valuation during share accounting. -
Conic did not just have a bad oracle formula. It had an integration-boundary failure where pool taxonomy and asset aliasing decided whether the protocol believed Curve was publishing settled truth.
-
Said differently: the real broken object was the safe-read boundary itself. A metadata branch silently chose whether mid-callback state deserved immediate price authority.
-
Sources:
- https://medium.com/@ConicFinance/post-mortem-eth-and-crvusd-omnipool-exploits-c9c7fa213a3d
- https://www.chainsecurity.com/blog/curve-lp-oracle-manipulation-post-mortem
- https://rekt.news/conic-finance-rekt
- https://www.certik.com/blog/conic-finance-incident-analysis
- https://etherscan.io/tx/0x8b74995d1d61d3d7547575649136b8765acb22882960f0636941c44ec7bbe146
- https://etherscan.io/tx/0x97a8315e942dd180fb90a17b92f7dabd6e8a2e5b9fd5e4a95ee4049ff33d2f16
-
Last reviewed: 2026-08-03 UTC