All writing

From 142K to 28K gas per trade: the architecture behind an 80% reduction

· 11 min read

  • onchain matching
  • gas architecture
  • EVM

At 142,000 gas per trade, the onchain orderbook I architected at Portal to Bitcoin was burning nearly as much on matching engine execution as a full Uniswap V3 trade costs end to end, roughly 150,000 gas. We were approaching that ceiling for the orderbook logic alone, before token transfers, before settlement, before any downstream composability. Three months of storage level profiling, struct redesign, and targeted assembly work on Foundry's gas reporter brought the matching engine's hot path down to 28,000 gas per trade. An 80.3% reduction, and the first time the protocol was economically viable for retail sized trades. Three optimization layers did the work. One of them nearly broke the system.

The Gas Ceiling Nobody Talks About

The industry's default answer to onchain orderbook gas costs is simple: don't put the orderbook onchain. Move the matching engine offchain. Settle onchain. dYdX did it. Hyperliquid built an entire custom L1 around it. The standard approach solves the gas problem and reintroduces the trust problem.

We chose a different constraint. The protocol's crosschain settlement model required atomic execution: every order placement, every match, every fill had to be verifiable onchain in the same transaction context. Offchain matching would have fractured the atomicity guarantees the settlement layer depended on, HTLC based and trustless underneath. So we stayed onchain and inherited the gas ceiling.

The EVM's gas model was designed for token transfers and approval mappings. An orderbook matching engine demands sorted insertions, range queries across price levels, and deletions that leave no gaps. Every one of these operations hits SSTORE and SLOAD, the two most expensive opcodes in the EVM. A cold SSTORE (writing a new value to an empty slot) costs 22,100 gas. A cold SLOAD costs 2,100. A warm SLOAD drops to 100 gas, but an orderbook's access patterns are inherently cold heavy: each trade touches different price levels, different order slots, different balance entries.

At stress test peaks of 400K daily transactions on testnet, the gas profile looked terminal. Profiling on Foundry's gas reporter put the cost exactly where you'd expect: storage operations dominated the per trade total, with computation and calldata splitting the remainder. Every optimization that mattered was going to be a storage optimization.

Layer 1: The Order Struct Overhaul

The first implementation stored each order in five storage slots. Price as uint256. Amount as uint256. Timestamp as uint256. The trader address in a fourth slot. Order ID and status flags in a fifth. Five SSTOREs per order placement. At 22,100 gas per cold write, storage alone consumed 110,500 gas before any matching logic executed.

I redesigned the order struct to fit in two slots.

Price compressed from uint256 to uint96, yielding 12 bytes. For a DEX operating on 18 decimal tokens, uint96 covers a range from 1 wei to 79 billion tokens. Enough for any realistic price point. Amount compressed to uint96 for the same reason. Timestamp reduced to uint48, which covers Unix time until the year 8,921,556. The trader address occupies 20 bytes. Status flags and order type packed into a single uint8.

struct Order {
    // slot 0: 32 of 32 bytes
    address trader;    // 20 bytes
    uint96 price;      // 12 bytes
    // slot 1: 25 of 32 bytes, 7 bytes padding
    uint96 amount;     // 12 bytes
    uint48 timestamp;  //  6 bytes
    uint48 expiry;     //  6 bytes
    uint8 flags;       //  1 byte
}

Two slots instead of five. Two SSTOREs instead of five on order creation. Measured on Foundry's gas reporter with 10,000 randomized order inputs, per order storage cost dropped from 110,500 to 44,200 gas. A 60% reduction in the single most expensive operation.

But packing created its own problem. Every function that read or wrote order state now needed bitwise masking and shifting to extract individual fields from packed slots. The Solidity compiler handles this for you when you declare packed structs, but the generated code isn't optimal. Each field access in a packed struct adds 3-5 opcodes for masking. In the matching engine's inner loop, where the same order is read and updated multiple times per match, those extra opcodes accumulated.

The net gas reduction after struct packing, accounting for increased computation overhead: 42%. Less than the 60% raw storage savings suggested, but still the single largest optimization.

Layer 2: The Data Structure Decision

The data structure holding the book is the architectural decision that determines whether an onchain orderbook lives or dies.

The first matching engine kept each side of the book as a naive array of orders. Finding where a new order belonged meant a linear scan through the resting orders, O(n) time. On a book with 200 resting orders, a single insertion triggered 200 SLOAD operations in the worst case. At 2,100 gas per cold SLOAD, the scan alone consumed 420,000 gas before writing anything. The 142,000 baseline was the measured typical per trade cost at realistic book depth; the 420,000 scan is the unmitigated worst case this layer exists to eliminate.

I replaced the array with a red black tree for price levels, keeping a doubly linked list for orders within each price level. This hybrid structure, used in production by Secured Finance and conceptually similar to Econia's AVL queue on Aptos, gives O(log n) insertion across price levels and O(1) append for new orders at an existing price. For 200 resting orders across 50 price levels, insertion drops from 200 SLOADs to approximately 6 (log₂ 50 ≈ 6). At 2,100 gas per cold access, the worst case lookup dropped from 420,000 to 12,600 gas.

The tradeoff: tree rebalancing. Red black trees maintain balance through rotations after insertions and deletions. Each rotation touches 2-3 tree nodes, and each node lives in storage. In the worst case, a deletion triggers a rebalancing cascade that writes to 4 storage slots. I profiled the rebalancing cost across 100,000 random insert delete sequences: median cost was 1.3 rotations per operation, consuming 8,400 additional gas on average. The maximum observed rebalancing cost was 3 rotations at 19,200 gas.

Worth it. The red black tree reduced matching engine gas by 68% compared to the linear scan. Combined with struct packing, per trade gas dropped from 142,000 to approximately 52,000.

Still not enough.

Layer 3: Transient Storage and the Assembly Hot Path

The third optimization targeted the matching engine's inner loop, the function that executes when a taker order crosses one or more resting orders on the book. This is the hot path. Every gas unit saved here multiplies by the number of fills per trade.

EIP-1153 transient storage, deployed in Dencun on March 13, 2024, introduced TLOAD and TSTORE at a flat 100 gas each. No cold/warm distinction. No refund complexity. Data persists for the duration of the transaction and is discarded afterward. For the matching engine, it changed the math.

The matching loop previously wrote intermediate state (running totals, partial fill amounts, remaining taker quantity) to persistent storage because Solidity's optimizer couldn't guarantee that memory variables survived across the reentrancy guard's external calls to the settlement layer. Each iteration of the matching loop wrote 3-4 intermediate values to storage slots that were only read within the same transaction and never needed again.

I moved all intermediate matching state to transient storage. Four TSTORE/TLOAD operations at 100 gas each replaced four SSTORE/SLOAD operations at 5,000/2,100 gas each. Per fill savings: approximately 25,000 gas in storage writes alone.

The reentrancy guard itself migrated to transient storage. A traditional reentrancy lock costs about 5,000 gas to set (a cold nonzero to nonzero SSTORE) and 2,100 gas to check (a cold SLOAD). A transient reentrancy lock costs 100 gas to set and 100 gas to check. Savings: roughly 6,800 gas per trade.

Then came the assembly optimization that nearly broke everything.

The matching engine's price comparison logic, the innermost operation that determines whether a taker's price crosses a resting maker's price, was written in Solidity. The compiler generated 23 opcodes for a comparison that could be done in 7 using inline Yul. I rewrote the price comparison, the amount calculation, and the partial fill logic in Yul assembly. Measured savings: 2,100 gas per fill on the comparison logic alone.

The failure mode nobody warns you about: assembly optimized code interacts unpredictably with the Solidity compiler's stack scheduler. After the Yul rewrite, the matching function stopped compiling: a stack too deep error, which the compiler hits when too many local variables are live and it can't reorder them around an inline assembly block. Stack too deep is a compile time limit, not a runtime fault, so it blocks the build outright rather than reverting specific trades. The fix required restructuring the entire matching function to pass intermediate values through memory pointers instead of stack variables. This restructuring added 800 gas of memory operations but eliminated the stack depth constraint.

Net reduction from Layer 3: 46% of remaining gas. Per trade cost dropped from 52,000 to 28,000.

The ERC-6909 Decision

One optimization happened outside the matching engine. The protocol initially used ERC-20 transfers for every trade settlement. Each ERC-20 transfer costs approximately 60,000 gas (two SSTORE operations for balance updates plus call overhead). For a trade with two sides, that's 120,000 gas in token transfers alone, more than the matching engine itself after optimization.

I integrated ERC-6909, the multitoken standard Uniswap V4 adopted for gas efficiency. ERC-6909 drops the mandatory transfer callbacks that make ERC-1155 expensive and allows internal accounting without external contract calls. Instead of transferring tokens in and out per trade, the matching engine updates internal ERC-6909 balances. Users settle in batch when they withdraw from the protocol.

This moved token transfer costs from per trade to per session. For a market maker executing 50 trades before withdrawing, it reduced the amortized transfer cost from 120,000 gas per trade to 2,400 gas per trade.

What the Numbers Look Like

                                            gas/trade   cumulative
baseline (5 slot structs, array book)       142,000 , struct packing (5 slots → 2)                 82,400     42%
red black tree (O(n) → O(log n) matching)    52,000     63%
transient storage + assembly hot path        28,000     80.3%

These numbers reflect the matching engine's core gas consumption, measured on Foundry with 10,000 randomized order sequences across varied book depths. They exclude token transfer gas (handled by ERC-6909 batching) and base transaction costs (21,000 gas, unavoidable).

With Ethereum base fees sitting under a gwei, 28,000 gas is a fraction of a cent per trade. At 2023 era gas prices of 30 gwei and the ETH price of the time, the same trade cost roughly $1.68. The optimization delivered the roughly 5x cut in engine gas; sub gwei base fees compounded it. Together they moved the protocol's viable market from institutional only toward retail accessible.

The Tradeoffs

Every optimization traded something.

Struct packing sacrificed field level readability. Debugging packed structs requires bit level inspection of storage slots. We added custom Foundry test helpers that decode packed order state for assertion comparisons. The tests themselves consumed more gas to run because of the decoding overhead.

The red black tree introduced nondeterministic gas costs per trade. A simple limit order placement costs between 28,000 and 47,200 gas depending on whether the tree needs rebalancing. Market orders that cross multiple levels cost proportionally more. The AMM model's advantage is predictable gas: a swap costs roughly the same regardless of market conditions. Our architecture doesn't offer that guarantee. For our use case, where atomic settlement required onchain matching, the tradeoff was worth the variance.

Transient storage introduced a subtle dependency on transaction boundaries. State that previously persisted across blocks now vanishes at transaction end. Any composability pattern that expected to read matching engine state from a separate transaction broke. Callbacks from lending protocols checking fill status, for example. We documented the constraint and redesigned the settlement callbacks to complete within the matching transaction's context.

Where This Goes

The perpetual DEX market already told the story. In 2023, AMM based perps carried most of the volume. By late 2025, orderbook protocols had taken nearly all of it, with Hyperliquid leading. The spot market is following the same trajectory, fragmented but directional.

The technical barrier was never the orderbook concept. It was the gas ceiling. At 142,000 gas per trade on an EVM chain with 30 gwei gas prices, onchain orderbooks were an academic exercise. At 28,000 gas per trade with sub gwei base fees, they're a production architecture competing directly with AMMs on cost while offering something AMMs structurally can't: real price discovery without impermanent loss.

The question for protocol teams isn't whether onchain orderbooks are viable. The question is how long they'll keep paying the trust tax on offchain alternatives.