Conic ETH Oracle Reentrancy
Exploit Transactions
0x8b74995d1d61d3d7547575649136b8765acb22882960f0636941c44ec7bbe146Victim Addresses
0xbb787d6243a8d450659e09ea6fd82f1c859691e9EthereumLoss Breakdown
Similar Incidents
Conic crvUSD Oracle Exploit
53%Sturdy LP Oracle Manipulation
39%Curve Vyper Lock Reentrancy
36%OMPxContract bonding-curve loop exploit drains ETH reserves
33%Aave AMM LP Oracle Manipulation Through Delegated Recursive LP Looping
33%dForce Oracle Reentrancy Liquidation
32%Root Cause Analysis
Conic ETH Oracle Reentrancy
1. Incident Overview TL;DR
On Ethereum mainnet block 17740955, transaction 0x8b74995d1d61d3d7547575649136b8765acb22882960f0636941c44ec7bbe146 let an unprivileged adversary turn Conic Finance's ETH omnipool into an over-withdrawal target. The sender EOA 0x8d67db0b205e32a5dd96145f022fa18aae7dc8aa drove the attack through contract 0x743599ba5cfa3ce8c59691af5ef279aaafa2e4eb, used public Balancer and Aave flash loans, deposited 8498000000000000000000 wei of WETH into the Conic ETH pool, then withdrew 10298071317678691497281 wei and finished with 1723726595625401118794 wei of net native profit after repayments and gas.
The root cause is Conic's reliance on CurveLPOracleV2::getUSDPrice inside ConicEthPool.withdraw(uint256,uint256) and ConicEthPool.handleDepeggedCurvePool(address). CurveLPOracleV2 prices Curve LP tokens from live balances() and totalSupply() during in-progress Curve liquidity operations, and its balance check only tests get_dy(0, i, 1 unit of coin0). During read-only-reentrant Curve callbacks, those transient reads overstated LP value and let the attacker both zero pool weights through false depeg handling and redeem more WETH than the burned Curve LP justified.
2. Key Background
Conic ETH pool 0xbb787d6243a8d450659e09ea6fd82f1c859691e9 is an omnipool that allocates WETH across three Curve pools: stETH/WETH 0xDC24316b9AE028F1497c275EB9192a3Ea0f67022, cbETH/WETH 0x5FAE7E604FC3e24fd43A72867ceBaC94c65b404A, and rETH/WETH 0x0f3159811670c117c372428D4E69AC32325e4D0F.
ConicEthPool.getTotalAndPerPoolUnderlying()values each held Curve LP position through_curveLpToUnderlying, which callscontroller.priceOracle().getUSDPrice(curveLpToken_).GenericOracleV2at0x286ef89cd2da6728fd2cb3e1d1c5766bcea344b0routes unsupported LP pricing toCurveLPOracleV2at0x7b528b4fd3e9f6b1701817c83f2ccb16496ba03e.CurveLPOracleV2sums current Curve pool balances and divides by LPtotalSupply(), then callsCurvePoolUtils.ensurePoolBalanced, which checks onlyget_dy(0, i, 1 unit of coin0).ConicEthPool.handleDepeggedCurvePool(address)is permissionless. If the current LP price differs from the cached LP price by more thandepegThreshold, it sets that pool's weight to zero.
Snippet origin: Conic ETH pool withdrawal and depeg handling code.
function withdraw(uint256 conicLpAmount, uint256 minUnderlyingReceived)
public
override
returns (uint256)
{
(
uint256 totalUnderlying_,
uint256 allocatedUnderlying_,
uint256[] memory allocatedPerPool
) = getTotalAndPerPoolUnderlying();
uint256 underlyingToReceive_ = conicLpAmount.mulDown(_exchangeRate(totalUnderlying_));
if (underlyingBalanceBefore_ < underlyingToReceive_) {
uint256 underlyingToWithdraw_ = underlyingToReceive_ - underlyingBalanceBefore_;
_withdrawFromCurve(allocatedUnderlying_, allocatedPerPool, underlyingToWithdraw_);
}
...
}
function handleDepeggedCurvePool(address curvePool_) external override {
require(isRegisteredCurvePool(curvePool_), "pool is not registered");
require(weights.get(curvePool_) != 0, "pool weight already 0");
address lpToken_ = controller.curveRegistryCache().lpToken(curvePool_);
require(_isDepegged(lpToken_), "pool is not depegged");
_setWeightToZero(curvePool_);
}
Snippet origin: Conic oracle routing and Curve LP pricing code.
function getUSDPrice(address token) external view virtual returns (uint256) {
if (chainlinkOracle.isTokenSupported(token)) {
return chainlinkOracle.getUSDPrice(token);
}
if (address(customOracles[token]) != address(0)) {
return customOracles[token].getUSDPrice(token);
}
return curveLpOracle.getUSDPrice(token);
}
function getUSDPrice(address token) external view returns (uint256) {
...
uint256 balance = _getBalance(pool, i);
value += balance.convertScale(uint8(decimals[i]), 18).mulDown(price);
...
CurvePoolUtils.ensurePoolBalanced(...);
return value.divDown(IERC20(token).totalSupply());
}
3. Vulnerability Analysis & Root Cause Summary
This was a protocol bug, not a pure MEV unwind. Conic converted external oracle reads directly into both solvency-sensitive withdrawal math and permissionless emergency state transitions. The vulnerable oracle, CurveLPOracleV2, reads raw Curve balances and LP supply during live Curve execution, so the returned LP price is not guaranteed to reflect a settled, realizable state. The downstream Conic code assumes the price is safe for both _underlyingToCurveLp and _curveLpToUnderlying, which means a transiently inflated LP price reduces the LP amount Conic decides to burn for a target WETH withdrawal. The same oracle path also feeds _isDepegged, so the attacker can force handleDepeggedCurvePool(address) while the Curve pool is inside a callback window. The explicit invariant is that LP prices used by Conic must remain stable and realizable during external liquidity operations; the concrete breakpoint is CurveLPOracleV2::getUSDPrice reading transient balances() / totalSupply() and Conic immediately consuming that result in handleDepeggedCurvePool and _underlyingToCurveLp.
4. Detailed Root Cause Analysis
The exploitable call chain is:
ConicEthPool.withdraw
-> getTotalAndPerPoolUnderlying / _underlyingToCurveLp
-> controller.priceOracle().getUSDPrice(curveLpToken_)
-> GenericOracleV2.getUSDPrice
-> CurveLPOracleV2.getUSDPrice
CurveLPOracleV2 violates the required pricing invariant in two ways. First, it prices LP from live pool balances and LP supply instead of a settled withdrawal quote. Second, CurvePoolUtils.ensurePoolBalanced only checks get_dy(0, i, 1 unit of coin0), which does not bound the actual remove_liquidity_one_coin path Conic later uses to redeem WETH.
Snippet origin: seed trace during the stETH callback window.
└─ ← [Return] 2163275888017846844152 [2.163e21]
├─ 0xBb787d6243a8D450659E09ea6fD82F1C859691e9::handleDepeggedCurvePool(
│ 0xDC24316b9AE028F1497c275EB9192a3Ea0f67022
│ )
├─ emit HandledDepeggedCurvePool(
│ 0xDC24316b9AE028F1497c275EB9192a3Ea0f67022
│ )
The validated root cause already records the cached LP price as 2033184303435690244360 and the manipulated callback read as 2163275888017846844152, which is above Conic's 3% depeg threshold. The trace confirms that this inflated read is immediately followed by a successful permissionless handleDepeggedCurvePool call on the stETH pool. The same pattern is later repeated for the cbETH pool, and both weights are driven to zero.
Snippet origin: seed trace showing the accounting mismatch on the rETH/WETH withdrawal path.
emit Withdraw(provider: 0x989AEb4d175e16225E39E87d0D97A3360524AD80, value: 466543208091409354292)
Vyper_contract::remove_liquidity_one_coin(
466543208091409354292,
0,
0,
false,
0xBb787d6243a8D450659E09ea6fD82F1C859691e9
)
emit Deposit(provider: 0x0f3159811670c117c372428D4E69AC32325e4D0F, value: 982904064388661242457)
This is the decisive accounting breakpoint. Conic decided that burning 466543208091409354292 Curve LP was enough to withdraw the desired WETH amount, yet the subsequent remove_liquidity_one_coin returned 982904064388661242457 wei, or about 2.106780352477196 WETH per LP. That mismatch proves the oracle-derived LP valuation no longer bounded the realizable one-coin withdrawal path. Because Conic trusted the manipulated price in _underlyingToCurveLp, it burned too little LP for too much WETH.
The exploit required only public conditions:
- Access to public Balancer and Aave flash liquidity.
- A Conic-held Curve LP token priced through
CurveLPOracleV2. - The ability to call Conic functions from inside Curve
remove_liquiditycallback windows. - A transient LP price move large enough to exceed
depegThresholdand unlockhandleDepeggedCurvePool.
The violated security principles are also explicit:
- Oracle reads used for accounting must not depend on transient external state.
- Permissionless emergency state transitions must not be keyed off manipulable spot prices.
- The valuation model used for LP pricing must match the protocol's actual withdrawal path.
5. Adversary Flow Analysis
The adversary cluster consists of EOA 0x8d67db0b205e32a5dd96145f022fa18aae7dc8aa and exploit contract 0x743599ba5cfa3ce8c59691af5ef279aaafa2e4eb. The victim-side components are Conic ETH pool 0xbb787d6243a8d450659e09ea6fd82f1c859691e9, Controller 0x013a3da6591d3427f164862793ab4e388f9b587e, GenericOracleV2 0x286ef89cd2da6728fd2cb3e1d1c5766bcea344b0, and CurveLPOracleV2 0x7b528b4fd3e9f6b1701817c83f2ccb16496ba03e.
-
Permissionless Flash FundingThe exploit contract takes public Balancer flash loans of rETH, cbETH, and WETH, plus Aave V2/V3 flash loans of stETH and cbETH. No privileged signer, allowlist, or private state is required. -
Mint Conic ETH LPUsing the flash-funded WETH, the attacker deposits into Conic ETH seven times and mints8478045845540324381061Conic LP tokens. The validated analysis ties this stage to the seed trace and the Conic deposit path. -
Trigger Callback Oracle MisreadsThe attacker enters Curve liquidity-removal flows, then calls back into Conic while Curve accounting is transient. In the stETH callback window the oracle price spikes enough to zero the stETH pool weight; the same pattern later zeroes the cbETH pool weight. -
Redeem Under Manipulated AccountingAfter the false depeg transitions, the attacker callsConicEthPool.withdrawtwice. The first major withdrawal burns6623698366188315193522LP from the relevant Curve position and returns7131846119079586205266wei into Conic, which Conic then forwards as part of a9315167253290030254824wei WETH withdrawal to the attacker contract. The later rETH-path withdrawal repeats the same accounting flaw on a smaller LP amount and returns982904064388661242457wei. -
Repay Loans And Realize ProfitOnce the Conic LP balance is fully redeemed, the attacker repays Balancer and Aave inside the same transaction. The seed balance diff shows the sender EOA's native balance increasing by1723726595625401118794wei after all premiums and gas, confirming realized profit rather than an unrealized mark-to-market gain.
6. Impact & Losses
The directly measured protocol loss is 1800071317678691497281 wei of WETH from the Conic ETH omnipool. That amount is encoded in the validated loss summary as raw on-chain units with decimal = 18.
The exploit had three concrete impacts:
- Conic ETH paid out more WETH than the attacker deposited.
- The stETH and cbETH Curve pool weights were permissionlessly set to zero under false depeg conditions.
- The attacker sender EOA realized
1723726595625401118794wei of net native profit after flash-loan repayment and gas.
7. References
- Seed transaction:
0x8b74995d1d61d3d7547575649136b8765acb22882960f0636941c44ec7bbe146at block17740955. - Seed transaction metadata:
/workspace/session/artifacts/collector/seed/1/0x8b74995d1d61d3d7547575649136b8765acb22882960f0636941c44ec7bbe146/metadata.json - Seed execution trace:
/workspace/session/artifacts/collector/seed/1/0x8b74995d1d61d3d7547575649136b8765acb22882960f0636941c44ec7bbe146/trace.cast.log - Seed balance diff:
/workspace/session/artifacts/collector/seed/1/0x8b74995d1d61d3d7547575649136b8765acb22882960f0636941c44ec7bbe146/balance_diff.json - Conic ETH pool source:
/workspace/session/.tmp/conicethpool/ConicEthPool.sol - Curve LP oracle source:
/workspace/session/.tmp/conic-extra/CurveLPOracleV2-0x7b528b4fd3e9f6b1701817c83f2ccb16496ba03e.sol - Generic oracle source:
/workspace/session/.tmp/conic-extra/GenericOracleV2-0x286ef89cd2da6728fd2cb3e1d1c5766bcea344b0.sol - Controller source:
/workspace/session/.tmp/conic-extra/Controller-0x013a3da6591d3427f164862793ab4e388f9b587e.sol