Onchain matching engine: the AMM to orderbook call
Owner of the contract layer, Portal to Bitcoin · 2024 — 2025
- gas per trade
- 142K→28K
- gas reduction across the system
- 70-80%
- coverage with fuzz + invariant testing
- 95%+
The defining call I owned at Portal to Bitcoin was a reversal, not a feature: replace the protocol's AMM with an onchain orderbook, then make that orderbook cheap enough to live onchain. The first half was an architecture argument. The second half took gas per trade from 142K to 28K.
Context
Portal to Bitcoin is noncustodial crosschain settlement, trustless BTC to EVM with no bridge operator and no wrapped assets. When I came in, the matching side was early AMM code and the default path was to keep building on it. AMMs are the settled answer for single chain token markets. For settlement against Bitcoin, the model breaks, and not at the edges.
The call, and the rejected alternative
The rejected alternative was the obvious one: keep the AMM. I argued against it on four grounds.
Settlement against Bitcoin needs both parties committed to an exact price before funds lock on either chain. An orderbook produces exactly that, a specific agreed price between two identified counterparties. An AMM prices at execution from pool state. With Bitcoin's ten minute confirmation window, the gap between quote and execution stops being slippage and becomes a free option: complete the trade if the price moved your way, walk if it didn't. That pattern has been exploited in production against HTLC based settlement, the lock mechanism this protocol runs on.
Stale pricing compounds it. An AMM's price only moves when someone trades against the pool, and arbitrage extraction grows with the time between updates. A protocol where one leg confirms in ten minutes maximizes that window by design.
Impermanent loss isn't an implementation bug, it's the constant product invariant. BTC moves 2x in months, so the liquidity providers the protocol depends on most pay a structural tax.
And Bitcoin can't host an AMM. Script gives you hashlocks and timelocks, not persistent mutable state. Keeping the AMM meant wrapped BTC or a sidechain, a trust assumption that defeats the protocol's reason to exist.
The honest cost of the call: onchain orderbooks are gas expensive, which is why most teams don't ship them. I led the decision, so that cost became my problem.
What I built
I designed and built the matching engine: the data structure that holds the book, the matching logic that crosses orders against it, and the gas architecture that made both cheap enough to run onchain.
The core is the book itself. The first implementation was the naive one, a flat array scanned linearly, 142K gas per trade and a cost that climbed with every resting order. I replaced it with the structure a serious matching engine actually needs: a red black tree over the price levels, a doubly linked list of orders inside each level, and a hash index that reaches any order in the book directly.
That combination is what makes the book scale, and the scaling matters more than the gas figure it produces:
- Finding or inserting a price level is logarithmic in the number of distinct price levels, not linear in the number of resting orders. A new order at a level that already exists appends at the tail in constant time.
- Cancelling an order reaches it through the hash index and unlinks it with pointer surgery, no scan of the book and no gap left behind. Market makers cancel and replace constantly, so a book that pays a full scan per cancel collapses under exactly the flow you want most. This one does not feel it.
- Best bid and best ask, the two reads every match begins from, are constant time.
- Because the tree stays balanced, the worst case is bounded at logarithmic instead of degrading to linear. Onchain that bound is not a nicety. Gas is paid on the worst case, and an unbounded worst case is a griefing vector and a block gas limit blowout waiting to happen.
The result is a book whose cost per operation stops tracking how full it is. A deep, liquid book, the thing you actually want, stops punishing the people trading on it. That is the scalability the 142K to 28K number is only a proxy for.
On top of the structure, three gas layers took the hot path from 142K to 28K:
- Struct packing. Order state went from 5 storage slots to 2, cutting the cold reads and writes on the hottest path in the system.
- Transient storage. Intermediate matching state, running totals and partial fills that die at transaction end, moved off SSTORE/SLOAD and onto EIP-1153 TSTORE/TLOAD and stopped paying persistent storage prices.
- ERC-6909 accounting moved token settlement from per trade to per session, so transfers stopped dominating the cost of a trade.
A matching engine is an attack surface, not just a data structure. I bounded match depth per transaction against gas griefing, added a cancellation cooldown against order spoofing, and held the book under fuzzed random order sequences and invariant suites at 95%+ coverage. I made external adversarial review the release gate: the third party audit came back with 6 high/medium findings, all resolved within 24 hours.
Outcomes
- Gas per trade down from 142K to 28K, an 80% cut.
- 70-80% gas reduction across the system.
- 95%+ coverage with fuzz and invariant testing.
- Stress tested past 14 million settlements on testnet before launch.
- Zero exploits since the November 2025 mainnet launch. Happy to walk through the matching internals decision by decision.
Links
The optimization work has a full writeup: Orderbook gas optimization, 142K to 28K per trade.