Cohort Order Book: an O(1) fully on-chain limit order book via generational fungible liquidity
Draft for Ethereum Magicians. Looking for critique, especially on the one-sided-add justification in §4 and the FIFO concerns in §6.
TL;DR
A fully on-chain limit order book where orders at each price point are fungibilized into time-ordered “cohorts.” Fills touch ≤2 cohorts per price point → O(1) (no per-order loop), partial fills are exact, and cancellation is just “withdraw your own position.” The design’s real justification isn’t gas — it’s that it’s the clean way to accept one-sided liquidity adds to a partially-filled price level, which pro-rata AMMs cannot do without either two-sided adds or subtle rebasing math. The cost is a coarse, tunable batch-FIFO. I want to pressure-test whether that price is acceptable — and whether this is worth building at all.
1. Motivation
On-chain order books are gas-hostile: filling loops over resting orders, O(n) in the number of orders. That’s why on-chain trading converged on AMMs, which fungibilize liquidity per price level (O(1) fills) — but in doing so give up clean limit-order semantics.
The specific semantic that breaks is one-sided adds to a partially-filled level (§4). This post proposes a structure that keeps O(1) fills and clean one-sided adds, and is honest that the price is a coarse FIFO.
2. Core idea — cohorts
At each price point (tick), resting liquidity lives in cohorts — fungible batches (one fungible token per cohort). At most two are live at any time:
- Staging — where new orders land; not yet being filled.
- Open — the batch currently being filled; frozen (no new orders join it).
3. Mechanism
- Place → mint a share of the current staging cohort.
- Promote → on the first fill at this price, staging is frozen → open, and a fresh staging opens.
- Fill → consume open, tracked by a single fill fraction (
filled / frozen total). - Settle → when open is fully consumed it settles (redeemable), and the next staging promotes.
Because only one open + one staging are ever live, a fill touches ≤2 cohorts per price point → O(1) (settled cohorts just await redemption, O(1) per LP). A trade sweeping multiple ticks is O(ticks crossed), same as any tick-based AMM.
Freeze-on-fill is what gives exact partial-fill accounting: each cohort has a single clean fill fraction, and new orders never inherit someone else’s partial fill.
Worked example (one tick): Alice 100 + Bob 50 → Staging C1=150. Fill 90 → C1 freezes (60% filled), new Staging C2. Carol 30 → C2=30. Fill 60 → C1 fully settles. Fill 20 → C2 promotes, consumed 67%.
4. Why generations at all? — the one-sided-add problem
This is the actual justification, and it’s easy to miss.
A limit order is one-sided (you post a single token at a price). In a pro-rata AMM (Uniswap V3 range order), once a tick is partially crossed (price inside the range), you can only add liquidity in the current two-token ratio — you cannot cleanly top up one-sided. In practice, V3 range-order users open a new position per add during a partial fill — i.e. they manually create generations.
You can pool one-sided adds with a per-LP accumulator, but consuming fills rebase the remaining one-sided balance while accruing the counter-asset — a rebasing-token-plus-rewards accounting that is doable but subtle and error-prone.
Cohorts automate the “new position per add” workaround: freeze the partially-filled batch, route fresh one-sided liquidity to a clean 0%-filled batch. That’s the real value.
The same structure that enables this — draining the open batch before promoting the next — is also what imposes a batch-FIFO ordering across cohorts. That’s the main tradeoff, discussed in §6.
5. Prior art (this is not novel in isolation)
- Ambient (CrocSwap) “knockout liquidity” is essentially this: on-chain limit orders as concentrated liquidity that fully converts when crossed, using pivots/epochs — which are cohorts by another name — precisely to handle one-sided adds + partial fills. This post is largely a generalization/formalization of that pattern as a standalone order book.
- Trader Joe Liquidity Book — fungible per-bin liquidity (the recurring, two-sided endpoint).
- Uniswap V3/V4 range orders +
feeGrowth— the pro-rata accumulator alternative in production.
If there’s prior art that handles one-sided adds more cleanly, I’d love pointers.
6. Known concerns / where I expect pushback
Being upfront, because these are real:
- FIFO on-chain is arguably not real FIFO. Intra-block ordering is set by builders (MEV), so “time priority” is partly auctioned, not earned. The promotion boundary (“first fill freezes the cohort”) is itself a potential griefing/MEV surface (dust-fill to freeze a rival cohort; snipe into staging before a known large fill). Mitigation: coarsen cohorts (promote on a volume/time threshold, not every first fill) so it’s pro-rata within large batches and FIFO only at rare boundaries — but does that fully address it?
- Front-of-queue adverse selection. The oldest cohort fills first, i.e. gets picked off first by toxic flow. “Priority” may be a poisoned chalice.
- Fresh-liquidity discouragement. Back cohorts fill last, so why add depth right before a move? Pro-rata welcomes liquidity anytime.
- Capital + adverse selection. A one-shot resting order earns nothing while waiting and is a free option to takers (the classic reason on-chain order books lost to AMMs). This is independent of the cohort structure.
- Is one-sided limit UX even worth building given off-chain intents (CoW/1inch/UniswapX: gasless, no lockup, free cancel) and AMM range orders already serve most limit-order demand?
7. Discussion questions
- Does keeping O(1) fills + clean one-sided adds necessarily force batch-FIFO, or is there a pro-rata construction that avoids it?
- Does coarsening cohorts (threshold-based promotion) reduce the FIFO/MEV concerns to de minimis, or just move them?
- Is there a cleaner accounting for one-sided adds under a pro-rata accumulator that I’m underrating (rebase index done right)?
- Given Ambient already ships the pattern — is there value in a standalone, generalized order-book form, or is “limit orders as a layer inside an AMM” the only version worth having?
- For a term-structure / maturity-bucketed market, a far-dated limit is a composite of per-period orders that must fill atomically — does the cohort model extend, or does that push you back to intent/solver execution?
Thanks in advance for tearing into it.