Permissionless Order Creation Let an Attacker Abort Buyers and Steal 20.247 ETH | PandaSecurity
Security Research

Permissionless Order Creation Let an Attacker Abort Buyers and Steal 20.247 ETH

PandaSecurity Team •
#Access Control#Order Factory#Ethereum Exploit
Order factory access-control exploit showing an attacker inserted as seller between buyers and escrowed ETH

In September 2026, an attacker exploited a missing authorization check in an Ethereum order factory and extracted 20.247 ETH from five buyers in a single transaction. The broader incident loss was reported at approximately 24.7 ETH.

The vulnerability was straightforward but devastating: the factory allowed any address to create an order on behalf of a buyer with an active request. The caller did not need to be the buyer, present a buyer signature, or supply a buyer-approved seller.

By selecting themselves as the seller, the attacker could create unauthorized orders tied to existing buyers and then trigger the order’s abort path. The buyers’ escrowed ETH was subsequently transferred to the attacker-controlled seller address.

The factory is unverified, so the exact source-level function name is not publicly confirmed. In the available transaction analysis, the vulnerable entry point uses selector 0xbde886fc and behaves like a createOrderForBuyer function. This article uses that descriptive name for clarity.

Incident overview

MetricValue
NetworkEthereum
VulnerabilityMissing authorization in order creation
CategoryAccess control
Reported incident lossApproximately 24.7 ETH
ETH extracted in the analyzed transaction20.247 ETH
Victims in the analyzed transaction5 buyers
Vulnerable selector0xbde886fc
Factory sourceUnverified

The five buyer balances taken in the analyzed transaction were:

Together, these amounts total 20.247 ETH.

The vulnerable order-creation flow

The factory supported an order-creation path that accepted a buyer address and an attacker-controlled seller address. Based on the observed behavior, its authorization logic was effectively equivalent to the following simplified pseudocode:

function createOrderForBuyer(address buyer, address seller, /* order data */)
    external
    returns (address order)
{
    require(accountSystem.getState(buyer) == 1, "buyer is not active");

    order = orderDeployer.createOrder(buyer, seller, /* order data */);
}

The factory verified that the buyer had an active request by checking getState(buyer) == 1. That check established that the buyer was eligible to participate in an order, but it did not establish that the buyer had authorized this particular order.

Three critical controls were absent:

  1. No msg.sender == buyer requirement.
  2. No buyer signature or signed order intent.
  3. No validation that the supplied seller had been selected or approved by the buyer.

As a result, knowing the address of an active buyer was enough to create an order for that buyer.

Why checking buyer state was not authorization

This bug came from confusing an account-state check with a permission check.

The condition getState(buyer) == 1 may prove that:

It does not prove that:

Authorization must bind the user to the exact action being performed. A valid account state is only contextual information; it is not consent.

Attack flow

The exploit can be reduced to four steps.

1. Find buyers with active requests

The attacker identified buyer accounts for which the account system returned state 1. These users already had ETH committed to, or reachable through, the order workflow.

2. Create an unauthorized order for each buyer

For every target, the attacker called the factory’s 0xbde886fc entry point and supplied:

Because the factory only checked the buyer’s state, each unauthorized order was accepted and deployed.

3. Enter the order’s abort path

The attacker then invoked the relevant abort or cancellation flow on the newly created order. The order treated the attacker-controlled address as the legitimate seller because that identity had already been recorded during creation.

4. Receive the buyer’s ETH

The abort settlement sent the escrowed ETH to the seller address. Since the attacker had chosen themselves as the seller, the refund or settlement logic became the mechanism used to extract the victims’ funds.

The attacker repeated this sequence across five buyers in the same transaction, receiving 10.05 ETH, 5.05 ETH, 3.05 ETH, 1.547 ETH, and 0.55 ETH.

Root cause

The root cause was not the abort function in isolation. The deeper failure occurred when the factory created a trusted order relationship from untrusted caller-supplied identities.

Once the order stored the attacker as the seller, downstream components operated on the assumption that the factory had already authenticated the buyer-seller relationship. The abort logic then followed its designed settlement rules using poisoned initialization data.

This is a common factory-pattern risk. A deployed child contract can enforce its own role checks correctly and still be exploitable when the factory assigns those roles without proper authorization.

Why the impact was severe

The function was permissionless and could target existing active buyers. That combination turned a single missing check into a scalable theft primitive.

The attacker did not need to:

They only needed to identify active buyers, submit attacker-selected order parameters, and use the normal abort flow.

The vulnerable function also appears to have allowed multiple victims to be processed within one transaction. This reduced execution risk for the attacker and allowed the theft to be completed atomically.

How the vulnerability should be fixed

There are two safe authorization models for an order factory of this kind.

Require the buyer to create the order directly

If delegated order creation is not a product requirement, the simplest fix is to require the caller to be the buyer:

function createOrderForBuyer(address buyer, address seller, /* order data */)
    external
    returns (address order)
{
    require(msg.sender == buyer, "caller is not buyer");
    require(accountSystem.getState(buyer) == 1, "buyer is not active");

    order = orderDeployer.createOrder(buyer, seller, /* order data */);
}

This binds the on-chain action to the buyer’s account, but it is only suitable when the buyer is expected to submit the transaction.

Verify a buyer-signed order intent

If relayers or third parties must be able to create orders for buyers, the factory should verify an EIP-712 signature covering the complete order intent:

The nonce must be consumed after successful verification to prevent replay. The signature must also bind the seller, otherwise a relayer could replace the intended counterparty while keeping the rest of the signed data unchanged.

Additional defensive controls

The direct authorization fix is essential, but several supporting controls would make the order system safer.

Validate the seller

The seller should be explicitly approved by the buyer or selected through a trusted matching process. Merely accepting an arbitrary seller address from the transaction caller is unsafe when that role can receive funds.

Bind cancellation payouts to economic ownership

Abort and cancellation paths should not automatically assume that the recorded seller is entitled to the buyer’s escrow. Settlement logic should clearly define who funded the order, why a refund is due, and which authenticated party may receive it.

Prevent duplicate use of buyer requests

An active request should have a unique identifier or nonce that is consumed when an order is created. This prevents the same buyer request from being reused across multiple orders.

Test factory initialization as part of the threat model

Security tests often focus on the deployed order contract while treating the factory as simple plumbing. Factories that assign buyers, sellers, owners, beneficiaries, or administrators are part of the core authorization boundary and require the same depth of review.

Audit lessons

This incident highlights several questions auditors should ask whenever a protocol creates contracts or records relationships on behalf of another user:

It is not enough to review whether individual functions have modifiers. Auditors must trace how identities enter the system, where they become trusted, and which later functions rely on them.

Key takeaways

The exploit was caused by a small omission at a critical trust boundary. One missing authorization check allowed the attacker to turn legitimate buyer requests into attacker-controlled orders and use the protocol’s own abort mechanism to drain escrowed ETH.

For order factories, marketplaces, and escrow protocols, every function that acts “for” a user should trigger an immediate security question: where is that user’s consent verified?

Related articles

← Back to Blog