All incidents

Rodeo unshETH Oracle Exploit

Share
Jul 11, 2023 06:29 UTCAttackLoss: 400,000 USDCPending manual check12 exploit txWindow: 1h 15m
Estimated Impact
400,000 USDC
Label
Attack
Exploit Tx
12
Addresses
4
Attack Window
1h 15m
Jul 11, 2023 06:29 UTC → Jul 11, 2023 07:45 UTC

Exploit Transactions

TX 1Arbitrum
0x5f16637460021994d40430dadc020fffdb96937cfaf2b8cb6cbc03c91980ac7c
Jul 11, 2023 06:29 UTCExplorer
TX 2Arbitrum
0x4164e14add41403d8cc02b68a5a272ba976748791ae35425d0b810189e55588c
Jul 11, 2023 06:30 UTCExplorer
TX 3Arbitrum
0x32c5257ae03c2bbf2736b9f73f58bb15c769144a063d4f9fe27878483cbae6c5
Jul 11, 2023 06:30 UTCExplorer
TX 4Arbitrum
0x9a462209e573962f2654cac9bfe1277abe443cf5d1322ffd645925281fe65a2e
Jul 11, 2023 07:00 UTCExplorer
TX 5Arbitrum
0x4b13c9347b32acce386309003df81120ac6f9716b1715441011c756e046fbc6a
Jul 11, 2023 07:01 UTCExplorer
TX 6Arbitrum
0x219d4c9c4ea4aa352647bb6a8d815b3bcbdcee3b360a6237aeede1454edb6c93
Jul 11, 2023 07:01 UTCExplorer
TX 7Arbitrum
0xefaf90ac027b432f59386daac67bb7284e76ac7a3f6ffcf24e68b808d59cd9d3
Jul 11, 2023 07:33 UTCExplorer
TX 8Arbitrum
0x39f8e47e363aeb654ee9bb31ec820f1851551c39e33dcecb08de9030fb2ca3b8
Jul 11, 2023 07:35 UTCExplorer
TX 9Arbitrum
0xaef2104804cf0393a6eb78dcc188722356c4a769c822acaecd292429eebd6252
Jul 11, 2023 07:40 UTCExplorer
TX 10Arbitrum
0x6171e54917c74935ed071c74f30cabc4a28fac00aa749c9e2067729e2a821502
Jul 11, 2023 07:41 UTCExplorer
TX 11Arbitrum
0x1d06f766c3e9bd4b892347dee91697ebb2fd286d2af7b49f378f36f2f8a47bfc
Jul 11, 2023 07:45 UTCExplorer
TX 12Arbitrum
0xb1be5dee3852c818af742f5dd44def285b497ffc5c2eda0d893af542a09fb25a
Jul 11, 2023 07:45 UTCExplorer

Victim Addresses

0x8accf43dd31dfcd4919cc7d65912a475bfa60369Arbitrum
0x0032f5e1520a66c6e572e96a11fbf54aea26f9beArbitrum
0x9e058177a854336d962d5ae576e9a46cfd8374d7Arbitrum
0x72f7101371201cefd43af026eef1403652f115eeArbitrum

Loss Breakdown

400,000USDC

Similar Incidents

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:

  1. 0x5f16637460021994d40430dadc020fffdb96937cfaf2b8cb6cbc03c91980ac7c: the helper bought 46.382820245732301534 unshETH for 49.961938783663539467 aeWETH.
  2. 0x4164e14add41403d8cc02b68a5a272ba976748791ae35425d0b810189e55588c: public keeper 0x3b1f14068fa2af4b08b578e80834bc031a52363d called OracleTWAP.update().
  3. 0x32c5257ae03c2bbf2736b9f73f58bb15c769144a063d4f9fe27878483cbae6c5: the helper sold the same unshETH back for 49.923921758587463120 aeWETH.

The second cycle repeated the same pattern:

  1. 0x9a462209e573962f2654cac9bfe1277abe443cf5d1322ffd645925281fe65a2e: the helper bought 46.348904090322488851 unshETH for 49.923921758587463120 aeWETH.
  2. 0x4b13c9347b32acce386309003df81120ac6f9716b1715441011c756e046fbc6a: the same public keeper called OracleTWAP.update() again.
  3. 0x219d4c9c4ea4aa352647bb6a8d815b3bcbdcee3b360a6237aeede1454edb6c93: the helper sold the second inventory back for 48.234158715323887220 aeWETH.

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:

  1. Deploy helper contract under sole attacker control.
  2. Use permissionless Camelot trades to buy unshETH before a public keeper update.
  3. Let the public keeper record the inflated reserve ratio into OracleTWAP.
  4. Immediately sell back after the keeper update to minimize inventory risk while keeping the manipulated sample in the oracle ring buffer.
  5. Repeat the bracketing sequence a second time so the four-sample average remains materially inflated.
  6. Mint and transfer extra unshETH inventory into the helper.
  7. Call the helper's exploit path, which invokes Rodeo Investor.earn(...) to create the leveraged position and borrow 400000000000 USDC from the pool.
  8. End the transaction with 144.219004344154985727 aeWETH in the helper while the pool loses 400000000000 USDC and the strategy receives 105.710624227042722164 unshETH 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.sol in the collected contract sources