Conic crvUSD Oracle Exploit
Exploit Transactions
0x37acd17a80a5f95728459bfea85cb2e1f64b4c75cf4a4c8dcb61964e26860882Victim Addresses
0x369cBC5C6f139B1132D3B91B87241B37Fc5B971fEthereumLoss Breakdown
Similar Incidents
Conic ETH Oracle Reentrancy
53%Sturdy LP Oracle Manipulation
39%Curve crvUSD sDOLA Market In-Tx Oracle Refresh Liquidation Attack
39%Aave AMM LP Oracle Manipulation Through Delegated Recursive LP Looping
35%Flashstake LP Share Inflation
33%Ploutos Market Oracle Feed Misconfiguration Enabled Undercollateralized WETH Borrow
32%Root Cause Analysis
Conic crvUSD Oracle Exploit
1. Incident Overview TL;DR
On Ethereum mainnet block 17743470, transaction 0x37acd17a80a5f95728459bfea85cb2e1f64b4c75cf4a4c8dcb61964e26860882 exploited the Conic crvUSD omnipool at 0x369cBC5C6f139B1132D3B91B87241B37Fc5B971f. The attacker used a Balancer flash loan of 12,000,000 USDC, 80,000 WETH, and 9,000,000 USDT, opened a temporary 93,000,000 crvUSD position in Curve's WETH controller, and then ran four Conic mint/burn loops while distorting the Curve LP prices that Conic used for accounting. That sequence extracted 22021279201078190713626 raw crvUSD units, or 22,021.279201078190713626 crvUSD, from the Conic pool. The seed balance diff shows the sender EOA moved from 1.798689406880727662 ETH before the transaction to 12.569151262061076955 ETH after it, for a net increase of 10.770461855180349293 ETH after paying 0.792135884958363214 ETH in gas.
The root cause is a same-transaction oracle-accounting failure in ConicPoolV2. Conic valued Curve LP inventory with a spot-priced oracle during both deposit and withdrawal accounting, even though that oracle path was built from live Curve pool state that could be moved with flash-loan swaps inside the same transaction.
2. Key Background
Conic's crvUSD omnipool aggregates liquidity across several Curve crvUSD pools and Convex staking wrappers, then issues the cncCRVUSD LP token at 0xB569bD86ba2429fd2D8D288b40f17EBe1d0f478f. Depositors and withdrawers do not interact with each Curve pool directly; instead, Conic computes a pool-wide totalUnderlying value and uses that aggregate accounting to mint or burn Conic LP shares.
The critical pricing dependency is Conic's controller price oracle. During accounting, Conic converts Curve LP balances into underlying with controller.priceOracle().getUSDPrice(curveLpToken_) and converts requested underlying withdrawals back into Curve LP with the inverse formula. The Etherscan V2 verified ConicPoolV2 proxy implementation shows exactly that dependency:
uint256 underlyingPrice_ = controller.priceOracle().getUSDPrice(address(underlying));
return _getTotalAndPerPoolUnderlying(underlyingPrice_);
uint256 poolUnderlying_ = _curveLpToUnderlying(
controller.curveRegistryCache().lpToken(curvePool_),
totalCurveLpBalance(curvePool_),
underlyingPrice_
);
function _curveLpToUnderlying(...) internal view returns (uint256) {
return curveLpAmount_
.mulDown(controller.priceOracle().getUSDPrice(curveLpToken_))
.divDown(underlyingPrice_)
.convertScale(18, underlying.decimals());
}
function _underlyingToCurveLp(...) internal view returns (uint256) {
return underlyingAmount_
.mulDown(controller.priceOracle().getUSDPrice(address(underlying)))
.divDown(controller.priceOracle().getUSDPrice(curveLpToken_))
.convertScale(underlying.decimals(), 18);
}
The seed trace shows that the oracle path is live and manipulation-sensitive. GenericOracleV2::getUSDPrice enters CurveLPOracleV2::getUSDPrice, which then reads each relevant Curve pool's balances, get_dy, price_oracle, and totalSupply during the exploit transaction. Those are public on-chain values that move immediately when a large swap executes.
3. Vulnerability Analysis & Root Cause Summary
This was an accounting exploit, not a privileged access issue. Conic assumed the Curve LP USD price it read during mint and burn accounting was stable enough to represent redeemable value, but the oracle was only a same-block spot read of live Curve pool state. Because deposits and withdrawals both trusted that price, the attacker could first push the Curve crvUSD-USDC and crvUSD-USDT pools away from equilibrium, let Conic mint or redeem against the distorted valuation, and then restore the pools afterward. That made Conic over-credit value on entry and under-charge value on exit over repeated loops. The violated invariant is straightforward: a Conic LP share must always represent a proportional claim on the omnipool's real redeemable underlying, independent of transient external swap pressure in connected Curve pools. The concrete breakpoint is ConicPoolV2's LP-to-underlying and underlying-to-LP conversions, both of which call controller.priceOracle().getUSDPrice(...) on the manipulated Curve LP tokens during user state transitions.
4. Detailed Root Cause Analysis
The public pre-state immediately before the exploit included Balancer flash liquidity, the Conic crvUSD omnipool, the Curve crvUSD-USDC and crvUSD-USDT pools, and the Curve WETH/crvUSD controller. No privileged role, whitelist, or attacker-specific artifact was required; an unprivileged actor only needed enough temporary liquidity to move the oracle inputs inside one transaction.
The controller leg was also publicly accessible. The verified crvUSD controller at 0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635 exposes create_loan(...) and repay(...) to any caller that satisfies its collateral rules, which is why the attacker could temporarily mint 93,000,000 crvUSD against flash-loaned WETH inside the same transaction.
The source-side accounting flaw is that Conic's aggregate balance calculation and withdrawal conversion both trusted live LP prices. In the verified ConicPoolV2 proxy, deposit() snapshots totalUnderlying before and after depositing, and both snapshots depend on _getTotalAndPerPoolUnderlying(). That function values each Curve LP position through _curveLpToUnderlying(), which in turn multiplies by controller.priceOracle().getUSDPrice(curveLpToken_). During withdrawals, _underlyingToCurveLp() uses the same oracle path in reverse to decide how much Curve LP must be removed.
The on-chain trace proves that the price source was manipulable in exactly the way the analysis claims. Around the first Conic deposit, the trace shows:
0x369cBC5C6f139B1132D3B91B87241B37Fc5B971f::deposit(47000000000000000000000000, 0, false)
GenericOracleV2::getUSDPrice(Vyper_contract: [0x390f3595...997BF4])
CurveLPOracleV2::getUSDPrice(Vyper_contract: [0x390f3595...997BF4])
Vyper_contract::balances(0)
Vyper_contract::price_oracle()
Vyper_contract::get_dy(0, 1, 1000000)
Vyper_contract::totalSupply()
Those reads are not a time-weighted or delayed oracle. They are the current pool balances and pricing variables of the same Curve pools that the attacker was actively moving with large swaps in the same transaction. The trace also shows the attacker repeatedly calling exchange on the two relevant Curve pools before and after each Conic accounting event, which let the attacker skew the LP price when Conic needed it and then partially restore the pool after the mint or burn completed.
Once that dependency is recognized, the exploit mechanics become deterministic. Conic mints LP shares based on a manipulated view of totalUnderlying, then later redeems those shares against a differently manipulated view of the same inventory. Because the mint-side and burn-side valuations are inconsistent, the attacker can round-trip capital through Conic and accumulate more crvUSD than they started with. The four-cycle carried balances recorded in the analysis show the extraction compounding over time: 47,000,000 crvUSD, then 46,993,926.028521139059418832, then 46,999,926.758661999645518914, then 47,005,922.602688286604808663, and finally a post-unwind surplus of 22,021.279201078190713626 crvUSD.
5. Adversary Flow Analysis
The adversary cluster in the seed transaction is:
- EOA
0xb6369f59fc24117b16742c9dfe064894d03b3b80, the transaction sender and final profit recipient. - Contract
0x486cb3f61771ed5483691dd65f4186da9e37c68e, the attacker-controlled helper that received the flash loan and executed the strategy.
The trace shows the full end-to-end sequence in a single transaction:
0xBA12222222228d8Ba445958a75a0704d566BF2C8::flashLoan(
0x486cb3f61771Ed5483691dd65f4186DA9e37c68e,
[USDC, WETH, USDT],
[12000000000000, 80000000000000000000000, 9000000000000],
...
)
0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635::create_loan(80000000000000000000000, 93000000000000000000000000, 10)
0x369cBC5C6f139B1132D3B91B87241B37Fc5B971f::deposit(47000000000000000000000000, 0, false)
0x369cBC5C6f139B1132D3B91B87241B37Fc5B971f::withdraw(46899358149942271423430570, 0)
0x369cBC5C6f139B1132D3B91B87241B37Fc5B971f::deposit(46993926028521139059418832, 0, false)
0x369cBC5C6f139B1132D3B91B87241B37Fc5B971f::withdraw(46910394885176859071833011, 0)
0x369cBC5C6f139B1132D3B91B87241B37Fc5B971f::deposit(46999926758661999645518914, 0, false)
0x369cBC5C6f139B1132D3B91B87241B37Fc5B971f::withdraw(46935171522866520294297614, 0)
0x369cBC5C6f139B1132D3B91B87241B37Fc5B971f::deposit(47005922602688286604808663, 0, false)
0x369cBC5C6f139B1132D3B91B87241B37Fc5B971f::withdraw(46959959448429816084300283, 0)
0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635::repay(93000000000000000000000000, 0x486cb3..., type(int256).max, false)
Between those deposit and withdrawal calls, the attacker repeatedly skewed the two Curve pools with large swaps. The trace shows repeated exchange(1, 0, 28_000_000e18, 0) and exchange(1, 0, 39_000_000e18, 0) style unwinds during the first three cycles, followed by exchange(1, 0, 9_000_000e18, 0) and exchange(1, 0, 12_000_000e18, 0) in the final unwind, matching the flash-loaned stablecoin legs that had to be restored before repayment.
After the fourth cycle, the attacker sold only enough crvUSD to recover the flash-loaned USDC and USDT, repaid the controller debt to release the 80,000 WETH collateral, repaid Balancer, and kept the extracted surplus. The balance diff confirms that the sender EOA, not the helper contract, ended the transaction with the realized ETH profit.
6. Impact & Losses
The Conic crvUSD omnipool lost one directly measurable token balance:
crvUSD:22021279201078190713626raw units, withdecimal = 18
The profit and balance evidence from the seed artifacts is:
{
"native_balance_deltas": [
{
"address": "0xb6369f59fc24117b16742c9dfe064894d03b3b80",
"before_wei": "1798689406880727662",
"after_wei": "12569151262061076955",
"delta_wei": "10770461855180349293"
}
]
}
That corresponds to a net increase of 10.770461855180349293 ETH after gas at the sender EOA. The transaction paid 21658306 gas at 36574230919 wei, or 0.792135884958363214 ETH in fees. The extracted stablecoin value and the final ETH realization are therefore both deterministic from the collected artifacts.
7. References
- Exploit transaction:
0x37acd17a80a5f95728459bfea85cb2e1f64b4c75cf4a4c8dcb61964e26860882 - Victim proxy:
0x369cBC5C6f139B1132D3B91B87241B37Fc5B971f - Victim implementation from Etherscan V2:
0x635228edaead8a76b6ae1779bd7682043321943d - crvUSD controller:
0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635 - Curve crvUSD-USDC pool:
0x4DEcE678ceceb27446b35C672dC7d61F30bAD69E - Curve crvUSD-USDT pool:
0x390f3595bCa2Df7d23783dFd126427CCeb997BF4 - Seed transaction metadata:
/workspace/session/artifacts/collector/seed/1/0x37acd17a80a5f95728459bfea85cb2e1f64b4c75cf4a4c8dcb61964e26860882/metadata.json - Seed trace:
/workspace/session/artifacts/collector/seed/1/0x37acd17a80a5f95728459bfea85cb2e1f64b4c75cf4a4c8dcb61964e26860882/trace.cast.log - Seed balance diff:
/workspace/session/artifacts/collector/seed/1/0x37acd17a80a5f95728459bfea85cb2e1f64b4c75cf4a4c8dcb61964e26860882/balance_diff.json - ConicPoolV2 verified source:
https://etherscan.io/address/0x369cbc5c6f139b1132d3b91b87241b37fc5b971f#code - crvUSD controller verified source:
https://etherscan.io/address/0xa920de414ea4ab66b97da1bfe9e6eca7d4219635#code