Change: The Order Type That Unlocks Parallel Market Making
A native order modification primitive designed for parallel execution on Monad.
Every perpetual DEX faces the same problem: when price moves, market makers need to update their quotes. The faster the chain, the more this matters, and the more it can go wrong.
The Problem Nobody Talks About
On every on-chain order book today, requoting works the same way: cancel your old order, post a new one. Two operations. This seems fine until you look at what actually happens inside the EVM.
Cancel deallocates an order ID. Post allocates a new one. Both write to the same global data structure — an index that tracks which IDs are in use. When ten market makers requote in the same block, all ten of them write to this same storage slot. On a parallel-execution chain like Monad, this creates a bottleneck: the execution engine detects the conflict, throws out its parallel work, and runs every requote one after another. Serial. Slow.
Worse, each Cancel removes that market maker's liquidity before the Post restores it at the new price. During the cascade, the book thins out. A taker arriving mid-block might see 40% of normal depth, wide spreads, and bad fills — exactly when volatility is highest and liquidity matters most.
The block-by-block sequence makes this concrete. Three market makers receive the same oracle update and race to requote. Because all three touch the global ID index, Monad serializes them — each re-executed after the previous one commits:
Index16Bit.root and numOrders conflicts.
What Perpl Does Differently
Perpl's exchange contract has a native Change operation. Instead of cancel + post, a market maker submits a single transaction that modifies their order in place: new price, new size, same order ID.
This sounds like a small optimization. It isn't.
Because Change reuses the existing order ID, it never touches the global ID index. It never increments or decrements the order counter. The only storage it writes to is the market maker's own order slot and the price level it's moving to. Each market maker's Change is completely independent of every other market maker's Change.
On Monad, this means they all execute in parallel — and because no MM ever leaves the book, a taker sees an atomic transition from old prices to new prices with zero intermediate state:
Index16Bit or numOrders contention — all MMs execute in true parallel.
The reason lies in which storage slots each operation touches. Post/Cancel conflicts on three global structures shared across all market makers. Change touches only the market maker's own private slot:
Post/Cancel : Shared State Conflicts
Index16Bit.rootIndex16Bit.leaf[i]OrderBook.slot1 (numOrders)idOrderMap[orderId]orderLockMap[lockId]priceLevelOrderIds[price]Change : No Shared State
Index16Bit.rootIndex16Bit.leaf[i]OrderBook.slot1 (numOrders)idOrderMap[orderId]account.lockedBalCNSpriceLevelOrderIds[price]What This Looks Like
Cancel / Post
- 10 steps to requote 5 MMs
- Executed serially (state conflicts)
- Depth drops to ~60%
- Each MM absent for 2 steps
- ~120–160k gas per requote
Change
- 1 step to requote 5 MMs
- Executed in parallel (no conflicts)
- Depth stays at 100%
- No MM ever absent
- ~50–70k gas per requote
With ten market makers, cancel/post drops depth below 40%. Change still holds at 100%.
The gap compounds with scale. At 10 MMs and 20 requotes per block, post/cancel forces each transaction to wait behind all the ones that conflicted before it — 20 sequential re-executions at worst. Change batches them all into a handful of parallel passes:
Post/Cancel : Serial Pipeline
Each tx conflicts with previous via Index16Bit.root. Re-execution on every step.
Change : Parallel Pipeline
No shared slots between MMs at different prices. All execute in first pass.
For a taker watching from the outside, the serial drain of post/cancel looks like temporary illiquidity during every volatility event. Change eliminates the intermediate state entirely:
Post/Cancel : Progressive Thinning
Change : Atomic Transition
Why This Matters
Monad's architecture (400ms blocks, parallel execution, 200M gas per block) is built for high-throughput applications. But parallel execution only helps when transactions don't fight over shared state. Cancel/post turns every requote into a fight over the same global storage slots, collapsing Monad's parallelism back to serial execution.
Change is designed for this architecture. It keeps each market maker's state isolated, letting Monad do what it's built to do: run everything at once.
The result is an order book that stays deep through volatility, gives takers better fills, and lets market makers quote tighter spreads with lower gas costs. Not because of a faster chain, but because the contract was designed to use the chain correctly.
| Metric | Post/Cancel | Change |
|---|---|---|
| Execution | Serial (1 MM at a time) | Parallel (all at once) |
| Depth during requote | 40–80% | 100% |
| Steps for 5 MMs | 10 | 1 |
| Gas per requote (Monad) | ~120–160k | ~50–70k |
| State conflicts | N (fully connected) | 0 (disconnected) |
| Liquidity gap | Yes | Never |
The operational impact extends beyond on-chain execution. When a market maker builds a post/cancel transaction, they face a dependency problem: the Post can't be assembled until the Cancel's outcome is known — which balance will be freed, which order ID will be allocated. Change has no such dependency. The order ID is unchanged and the locked balance stays put. The market maker builds and fires in fewer steps with a tighter gas estimate:
Post/Cancel : Dependency Chain
Change : No Dependencies
docs.monad.xyz) •
Perpl docs (docs.perpl.xyz)