Calculated from recorded token losses using historical USD prices at the incident time.
0x8b74995d1d61d3d7547575649136b8765acb22882960f0636941c44ec7bbe1460xbb787d6243a8d450659e09ea6fd82f1c859691e9EthereumOn 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.
Conic ETH pool is an omnipool that allocates WETH across three Curve pools: stETH/WETH , cbETH/WETH , and rETH/WETH .
0xbb787d6243a8d450659e09ea6fd82f1c859691e90xDC24316b9AE028F1497c275EB9192a3Ea0f670220x5FAE7E604FC3e24fd43A72867ceBaC94c65b404A0x0f3159811670c117c372428D4E69AC32325e4D0FConicEthPool.getTotalAndPerPoolUnderlying() values each held Curve LP position through _curveLpToUnderlying, which calls controller.priceOracle().getUSDPrice(curveLpToken_).GenericOracleV2 at 0x286ef89cd2da6728fd2cb3e1d1c5766bcea344b0 routes unsupported LP pricing to CurveLPOracleV2 at 0x7b528b4fd3e9f6b1701817c83f2ccb16496ba03e.CurveLPOracleV2 sums current Curve pool balances and divides by LP totalSupply(), then calls CurvePoolUtils.ensurePoolBalanced, which checks only get_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 than depegThreshold, 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());
}
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.
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:
CurveLPOracleV2.remove_liquidity callback windows.depegThreshold and unlock handleDepeggedCurvePool.The violated security principles are also explicit:
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 Funding
The 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 LP
Using the flash-funded WETH, the attacker deposits into Conic ETH seven times and mints 8478045845540324381061 Conic LP tokens. The validated analysis ties this stage to the seed trace and the Conic deposit path.
Trigger Callback Oracle Misreads
The 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 Accounting
After the false depeg transitions, the attacker calls ConicEthPool.withdraw twice. The first major withdrawal burns 6623698366188315193522 LP from the relevant Curve position and returns 7131846119079586205266 wei into Conic, which Conic then forwards as part of a 9315167253290030254824 wei WETH withdrawal to the attacker contract. The later rETH-path withdrawal repeats the same accounting flaw on a smaller LP amount and returns 982904064388661242457 wei.
Repay Loans And Realize Profit
Once 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 by 1723726595625401118794 wei after all premiums and gas, confirming realized profit rather than an unrealized mark-to-market gain.
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:
1723726595625401118794 wei of net native profit after flash-loan repayment and gas.0x8b74995d1d61d3d7547575649136b8765acb22882960f0636941c44ec7bbe146 at block 17740955./workspace/session/artifacts/collector/seed/1/0x8b74995d1d61d3d7547575649136b8765acb22882960f0636941c44ec7bbe146/metadata.json/workspace/session/artifacts/collector/seed/1/0x8b74995d1d61d3d7547575649136b8765acb22882960f0636941c44ec7bbe146/trace.cast.log/workspace/session/artifacts/collector/seed/1/0x8b74995d1d61d3d7547575649136b8765acb22882960f0636941c44ec7bbe146/balance_diff.json/workspace/session/.tmp/conicethpool/ConicEthPool.sol/workspace/session/.tmp/conic-extra/CurveLPOracleV2-0x7b528b4fd3e9f6b1701817c83f2ccb16496ba03e.sol/workspace/session/.tmp/conic-extra/GenericOracleV2-0x286ef89cd2da6728fd2cb3e1d1c5766bcea344b0.sol/workspace/session/.tmp/conic-extra/Controller-0x013a3da6591d3427f164862793ab4e388f9b587e.sol