Order book and mark
The Market contract holds the order book and does the matching. The book has two kinds of liquidity: real resting orders and maker ladders. Takers see one merged book.
Source: contracts/src/Market.sol, especially _place, _match, setLadder, _quote, and _updateMark.
Orders
An order has a side, a price in annualized variance, a size in contracts, a type, and an expiry.
| Field | Rule |
|---|---|
| Price | Greater than zero, at most 100 |
| Size | Greater than zero, at most 1,000 contracts |
| Type | Limit, or IOC (immediate or cancel) |
| Expiry | From the current block time to 3,600 seconds later |
Match limit (maxMatches) | From 1 to 32 match attempts for the order |
The terminal sends each session order with an expiry of 120 seconds and a match limit of 32.
A "market order" in the terminal is an IOC order with a price limit.
Results of an order
The contract emits OrderResult with three quantities:
| Quantity | Meaning |
|---|---|
filled | The size that traded |
resting | The size that stays in the book |
canceled | The size that the contract removed |
A confirmed order can have zero fills. The remainder of an IOC order is always canceled. The remainder of a limit order rests only if it does not cross the opposite side and the account has enough margin. If not, the contract cancels it.
The book holds at most 128 real resting orders. A new resting order fails with BOOK_FULL above that limit.
Matching
The contract matches a new order against the best opposite price of the merged book. Real orders with the same price keep time priority.
Each match attempt does these checks:
- Self trade. If the best opposite order belongs to the taker, the contract cancels that resting order or that ladder level. It does not fill it.
- Maker risk. If the maker cannot pass the margin check after the fill, the contract removes the maker order. For a ladder, it takes the rest of that ladder side out of the book until the next quote.
- Taker risk. If the taker cannot pass the margin check after the fill, matching stops.
An attempt that removes an order counts against the match limit.
Observation gate
After an observation boundary passes, no order fills until the contract accepts the observation for that boundary. An order that does not cross can rest. Cancellation stays available. Thus a trader cannot see a price move, enter, and receive the payment for that interval.
Maker ladders
A ladder is a set of virtual quotes of one maker. The contract does not store each level. It calculates the price and the size of a level from the ladder shape and from the center of the last quote.
| Step | Call | Caller |
|---|---|---|
| Register a shape | setLadder(index, first, step, levels, base, sizeStep) | The maker |
| Move all ladders | quote(center, tick, validUntil, updateData) | The quoter of the market |
Level i of a ladder rests at this distance from the center, on each side:
price offset = (first + i * step) * tick
size = (base + i * sizeStep) * 0.01 contractsA market has 4 ladder slots. A ladder has at most 32 levels. The full size of a ladder side must be 1,000 contracts or less, and the maker must have the margin for it. The maker accepts that the quoter sets the center of all ladders.
A quote is valid until validUntil, which is at most 3,600 seconds after the call. A fill uses up ladder size from the inside level. The next quote restores the full ladder.
One quote call writes one storage slot. It can also carry the signed oracle observation in updateData. This is the one call of the operator loop. See Oracle and observations.
The ladders of the demo scripts
The local scripts and the testnet scripts use the same two shapes (ladderShapes in scripts/common.ts). The tick is 0.001.
| Maker | First level | Step | Levels | Size of level 0 | Size increase for each level |
|---|---|---|---|---|---|
| Maker 0 | 1 tick | 2 ticks | 10 | 1.00 contract | 15 contracts |
| Maker 1 | 2 ticks | 2 ticks | 10 | 1.50 contracts | 15 contracts |
Maker 0 rests on the odd ticks and maker 1 rests on the even ticks. The book thus shows 20 levels on each side. The inside level of maker 0 is one contract, so the 2 contracts of mark depth always come from two owners.
The mark
The mark is the variance price that the contract uses for the fixed charge, for margin, and for mark profit and loss.
The contract calculates it in this sequence:
- Take the best 2 contracts on the bid side. Calculate their average price.
- Take the best 2 contracts on the ask side. Calculate their average price.
- The mark is the midpoint of the two average prices.
The result is valid only if all of these conditions are true:
- Each side has the full 2 contracts of depth.
- The measured depth of each side has two or more different owners.
- The bid average is not above the ask average.
- The spread is 20% of the midpoint or less.
If a condition fails, the mark freezes. The stored mark stays in use. A mark also counts as frozen when it is older than two observation intervals, or when the oracle is halted.
Two owners are not two independent makers
Two addresses do not prove independent ownership or a fair price. The owner rule only makes a single account unable to set the mark alone.
When the mark updates
| Call | Mark update |
|---|---|
quote | Yes |
submitObservation | Yes, before the contract saves the next charge |
withdraw | Yes, and the withdrawal needs a valid mark |
liquidate | Yes, and the liquidation needs a valid mark |
updateMark | Yes. Any address can call it. |
The initial mark of a new market is a deployment input. It starts frozen until the book gives a valid mark.
During a mark freeze
- Matching continues. Trades can remove crossed quotes, and new quotes can restore the depth.
- A fill that adds exposure must pass the margin check at the last valid mark and at its own execution price.
- Withdrawals and liquidations wait for a valid mark.
- A frozen mark stays the charge of the next funding interval. There is no fallback to a historical price.
The order queue of QueuedMarket
QueuedMarket extends Market. It adds an on-chain FIFO queue of IOC requests and two combined calls.
| Call | Effect |
|---|---|
enqueue(requests) | Adds up to 16 requests. Reserves margin for them. The queue holds at most 128 pending requests. |
cancelQueued(id) | The owner removes a request and releases its reservation. |
clearQueue(maxOrders) | Any address processes up to 16 requests in queue sequence. |
quoteAndClear(...) | The quoter moves the ladders, then clears the queue, in one transaction. |
quoteAndTake(...) | The quoter moves the ladders, then places one IOC order for its own account, in one transaction. |
Each queued request executes as an IOC order against the same book, with a new margin check. An expired or rejected request cannot block later requests. A request that fails because the call has too little gas reverts the batch, so the order stays in the queue.
The web application does not show the queue. The services report queue: true in GET /markets for a QueuedMarket.
Operator fills on testnet
On testnet, the operator pushes the fills:
- The standard loop (
pnpm testnet serve) sends a random taker order for each market at a fixed interval. - The fast loop (
pnpm testnet serve-fast) usesquoteAndTake. One transaction moves the ladders, accepts a due observation, and fills one IOC order of the operator.
The operator fill model is for testnet only
These fills use the collateral of the operator. They keep the book, the mark, and the trade feed active while no independent traders are there. They are not the fill model for a market with real funds. In that market, independent makers and takers supply the orders, and quoteAndTake is not necessary. An order of a user goes through the same matching and the same margin checks in the two cases.