Rodeo unshETH Oracle Exploit
Exploit Transactions
0x5f16637460021994d40430dadc020fffdb96937cfaf2b8cb6cbc03c91980ac7c0x4164e14add41403d8cc02b68a5a272ba976748791ae35425d0b810189e55588c0x32c5257ae03c2bbf2736b9f73f58bb15c769144a063d4f9fe27878483cbae6c50x9a462209e573962f2654cac9bfe1277abe443cf5d1322ffd645925281fe65a2e0x4b13c9347b32acce386309003df81120ac6f9716b1715441011c756e046fbc6a0x219d4c9c4ea4aa352647bb6a8d815b3bcbdcee3b360a6237aeede1454edb6c930xefaf90ac027b432f59386daac67bb7284e76ac7a3f6ffcf24e68b808d59cd9d30x39f8e47e363aeb654ee9bb31ec820f1851551c39e33dcecb08de9030fb2ca3b80xaef2104804cf0393a6eb78dcc188722356c4a769c822acaecd292429eebd62520x6171e54917c74935ed071c74f30cabc4a28fac00aa749c9e2067729e2a8215020x1d06f766c3e9bd4b892347dee91697ebb2fd286d2af7b49f378f36f2f8a47bfc0xb1be5dee3852c818af742f5dd44def285b497ffc5c2eda0d893af542a09fb25aVictim Addresses
0x8accf43dd31dfcd4919cc7d65912a475bfa60369Arbitrum0x0032f5e1520a66c6e572e96a11fbf54aea26f9beArbitrum0x9e058177a854336d962d5ae576e9a46cfd8374d7Arbitrum0x72f7101371201cefd43af026eef1403652f115eeArbitrumLoss Breakdown
Similar Incidents
Rodeo Oracle Shortfall
61%Themis Oracle Manipulation
34%Sentiment Balancer Oracle Overborrow
32%NEU Convert LP Mispricing Drain
32%dForce Oracle Reentrancy Liquidation
31%DEI burnFrom Allowance Inversion
29%Root Cause Analysis
Rodeo unshETH Oracle Exploit
1. Incident Overview TL;DR
On Arbitrum, an unprivileged attacker conditioned Rodeo Finance's unshETH collateral oracle by trading around two public OracleTWAP.update() keeper calls, then opened a leveraged borrow that Rodeo accepted at the manipulated mark. The terminal exploit transaction 0xb1be5dee3852c818af742f5dd44def285b497ffc5c2eda0d893af542a09fb25a removed 400000000000 USDC units from Rodeo pool 0x0032f5e1520a66c6e572e96a11fbf54aea26f9be while the protocol still reported the position as healthy.
The root cause is that Rodeo valued unshETH collateral through a reserve-based spot oracle wrapped by a four-sample snapshot average, not a manipulation-resistant TWAP. The concrete breakpoint is the path OracleUniswapV2Eth.latestAnswer() -> OracleTWAP.latestAnswer() -> StrategyHelper.value() -> StrategyLong._rate() -> InvestorActor._life(), which let attacker-shaped Camelot reserves become accepted collateral value.
2. Key Background
Rodeo's borrow acceptance depends on strategy share value, and that strategy share value depends directly on oracle output for the underlying collateral token. The relevant victim-side code is:
function price(address ast) public view returns (uint256) {
IOracle oracle = IOracle(oracles[ast]);
if (address(oracle) == address(0)) revert UnknownOracle();
return uint256(oracle.latestAnswer()) * 1e18 / (10 ** oracle.decimals());
}
function value(address ast, uint256 amt) public view returns (uint256) {
return amt * price(ast) / (10 ** IERC20(ast).decimals());
}
That snippet shows StrategyHelper converting oracle output into collateral value. StrategyLong then uses that value for the whole inventory:
function _rate(uint256 sha) internal view override returns (uint256) {
uint256 val = strategyHelper.value(address(token), token.balanceOf(address(this)));
return sha * val / totalShares;
}
InvestorActor turns that manipulated strategy rate into the health check gating borrow acceptance:
function _life(uint256 id, uint256 sha, uint256 bor) internal view returns (uint256) {
(, address pol, uint256 str,,,,) = investor.positions(id);
IPool pool = IPool(pol);
if (bor == 0) return 1e18;
uint256 value = (IStrategy(investor.strategies(str)).rate(sha) * pool.liquidationFactor()) / 1e18;
uint256 borrow = _borrowValue(pool, bor);
return value * 1e18 / borrow;
}
The collected supporting facts show that unshETH was configured to use OracleTWAP at 0xf3721d8a2c051643e06bf2646762522fa66100da, whose underlying source was OracleUniswapV2Eth at 0x2d9985e57591def5ef939542e28169c8ea5d05a2 for Camelot pair 0x29fc01f04032c76ca40f353c7df685f4444c15ed.
3. Vulnerability Analysis & Root Cause Summary
The vulnerability is an attack-class oracle manipulation issue, not a privileged-only flow. OracleUniswapV2Eth derives price from live Camelot reserves, and OracleTWAP merely stores four discrete snapshots and averages them. That design means a public attacker can buy before a keeper update, let the manipulated reserve ratio be recorded, and unwind immediately after the update while the inflated mark remains inside the four-sample average. Rodeo then consumes that average as if it were realizable collateral value. The protocol does not enforce a manipulation-resistant oracle source, an unwind-realizability check, or any other control that would reject a borrow backed by attacker-shaped marks. Because the keeper updates were public and the trading venue was permissionless, the sequence was anyone-can-take.
4. Detailed Root Cause Analysis
The attack started from Arbitrum block 110025246, before the first keeper snapshot used by the exploit. The attacker cluster consisted of EOA 0x2f3788f2396127061c46fc07bd0fcb91faace328 and helper contract 0xe9544ee39821f72c4fc87a5588522230e340aa54, which the EOA had deployed earlier in transaction 0x3eace33baf649103e091a49a9aa3c2f06747728be3665785d06efe90ef531d86.
The first manipulation cycle was:
0x5f16637460021994d40430dadc020fffdb96937cfaf2b8cb6cbc03c91980ac7c: the helper bought46.382820245732301534unshETH for49.961938783663539467aeWETH.0x4164e14add41403d8cc02b68a5a272ba976748791ae35425d0b810189e55588c: public keeper0x3b1f14068fa2af4b08b578e80834bc031a52363dcalledOracleTWAP.update().0x32c5257ae03c2bbf2736b9f73f58bb15c769144a063d4f9fe27878483cbae6c5: the helper sold the same unshETH back for49.923921758587463120aeWETH.
The second cycle repeated the same pattern:
0x9a462209e573962f2654cac9bfe1277abe443cf5d1322ffd645925281fe65a2e: the helper bought46.348904090322488851unshETH for49.923921758587463120aeWETH.0x4b13c9347b32acce386309003df81120ac6f9716b1715441011c756e046fbc6a: the same public keeper calledOracleTWAP.update()again.0x219d4c9c4ea4aa352647bb6a8d815b3bcbdcee3b360a6237aeede1454edb6c93: the helper sold the second inventory back for48.234158715323887220aeWETH.
The collected oracle state at exploit block 110043453 shows why the later borrow passed:
{
"last_timestamp": "1689058862",
"prices": [
"2639236641922531347490",
"4973350465837606916071",
"4994765044571862817590",
"4269731359394042641877"
],
"latest_answer": "4219270877931510930757"
}
That is an averaged mark above $4,000 per unshETH, derived from attacker-shaped reserve snapshots. The attacker then staged additional unshETH inventory into the helper through mint transactions 0x39f8e47e363aeb654ee9bb31ec820f1851551c39e33dcecb08de9030fb2ca3b8 and 0xaef2104804cf0393a6eb78dcc188722356c4a769c822acaecd292429eebd6252, followed by transfer transactions 0x6171e54917c74935ed071c74f30cabc4a28fac00aa749c9e2067729e2a821502 and 0x1d06f766c3e9bd4b892347dee91697ebb2fd286d2af7b49f378f36f2f8a47bfc. The helper entered the terminal transaction with exactly 47294222088336002957 unshETH.
The exploit transaction 0xb1be5dee3852c818af742f5dd44def285b497ffc5c2eda0d893af542a09fb25a then executed the actual Rodeo borrow. The collected execution trace shows:
Investor::earn(0xE9544Ee39821F72c4fc87A5588522230e340aa54, 0x0032F5E1520a66C6E572e96A11fBF54aea26f9bE, 41, 0, 400000000000, ...)
emit Borrow(param0: 0x6ff76f4794e4A6122734A11d680dBb351533Ae95, param1: 400000000000, param2: 382349705467)
OracleTWAP::latestAnswer() [staticcall]
The corresponding balance diff confirms the result:
{
"pool_usdc_delta": "-400000000000",
"strategy_unsheth_minted": "105710624227042722164",
"helper_ae_weth_delta": "144219004344154985727",
"helper_unsheth_delta": "-47294222088336002957"
}
The safety invariant that should have held is: a new borrow must only be accepted when the collateral value used by InvestorActor._life() reflects a manipulation-resistant and realizable value. Rodeo violated that invariant because it accepted a borrow using an averaged reserve mark that the attacker could cheaply shape around public update calls.
5. Adversary Flow Analysis
The adversary flow is deterministic and end-to-end complete:
- Deploy helper contract under sole attacker control.
- Use permissionless Camelot trades to buy unshETH before a public keeper update.
- Let the public keeper record the inflated reserve ratio into
OracleTWAP. - Immediately sell back after the keeper update to minimize inventory risk while keeping the manipulated sample in the oracle ring buffer.
- Repeat the bracketing sequence a second time so the four-sample average remains materially inflated.
- Mint and transfer extra unshETH inventory into the helper.
- Call the helper's exploit path, which invokes Rodeo
Investor.earn(...)to create the leveraged position and borrow400000000000USDC from the pool. - End the transaction with
144.219004344154985727aeWETH in the helper while the pool loses400000000000USDC and the strategy receives105.710624227042722164unshETH under the manipulated valuation.
This sequence is ACT because each action is public and permissionless: Camelot trading, observing or bracketing keeper calls, and opening a Rodeo position through Investor.earn(...).
6. Impact & Losses
The measured protocol loss is 400000000000 raw USDC units, or 400,000 USDC at 6 decimals, removed from Rodeo pool 0x0032f5e1520a66c6e572e96a11fbf54aea26f9be. The exploit transaction also left the helper with 144.219004344154985727 aeWETH. The immediate victimized components were the Rodeo USDC pool, the Rodeo investor flow that accepted the borrow, and the strategy valuation path that marked manipulated unshETH inventory as healthy collateral.
7. References
- Incident seed trace:
0xb1be5dee3852c818af742f5dd44def285b497ffc5c2eda0d893af542a09fb25a - Manipulation trades and keeper updates:
0x5f16637460021994d40430dadc020fffdb96937cfaf2b8cb6cbc03c91980ac7c,0x4164e14add41403d8cc02b68a5a272ba976748791ae35425d0b810189e55588c,0x32c5257ae03c2bbf2736b9f73f58bb15c769144a063d4f9fe27878483cbae6c5,0x9a462209e573962f2654cac9bfe1277abe443cf5d1322ffd645925281fe65a2e,0x4b13c9347b32acce386309003df81120ac6f9716b1715441011c756e046fbc6a,0x219d4c9c4ea4aa352647bb6a8d815b3bcbdcee3b360a6237aeede1454edb6c93 - Supporting facts artifact:
/workspace/session/artifacts/auditor/iter_3/supporting_facts.json - Balance diff artifact:
/workspace/session/artifacts/collector/seed/42161/0xb1be5dee3852c818af742f5dd44def285b497ffc5c2eda0d893af542a09fb25a/balance_diff.json - Rodeo victim code:
StrategyHelper.sol,StrategyLong.sol,InvestorActor.solin the collected contract sources