Quote encoding spec
Each maker has one 32-byte storage slot per pool. The SDK's encodeQuote / decodeQuote
(packages/sdk/src/quote.ts) and the contract's QuoteLib (packages/contracts/src/libraries/QuoteLib.sol)
implement this layout bit-for-bit; property-based tests cross-check them.
Slot layout
Bit 0 is the least significant bit.
| Bits | Field | Type | Unit and meaning |
|---|---|---|---|
| 0–103 | mid | uint104 | token1 base units per 1 token0 base unit, scaled by 1e18 |
| 104–119 | bidSpread | uint16 | 0.1 bp (1e-5) below mid |
| 120–135 | askSpread | uint16 | 0.1 bp above mid |
| 136–151 | maxFillMantissa | uint16 | max token0 filled per L2 block = mantissa × 10^exp, in base units |
| 152–159 | maxFillExp | uint8 | |
| 160–175 | impactAtMax | uint16 | 0.1 bp of extra spread at a fill of maxFill, linear in size |
| 176–183 | decayPerBlock | uint8 | 0.1 bp of extra spread per L2 block of age |
| 184–199 | ttlBlocks | uint16 | fillable while block ≤ lastUpdateBlock + ttlBlocks |
| 200–231 | lastUpdateBlock | uint32 | stamped by the contract with the inclusion L2 block; your value is ignored |
| 232–247 | seq | uint16 | your sequence number, echoed in Filled (wraps at 65,536) |
| 248–255 | flags | uint8 | bit 0 = strict mode, bit 1 = paused (this pool only) |
Range and precision
mid is fixed-point with a 1e-18 unit in raw token units. With 104 bits it represents raw prices from
1e-12 to about 2e13 at better than 0.01 bp relative precision at the bottom of that range. Raw prices
fold in decimals: 1 HYPE (18 decimals) at 77.21 USDC (6 decimals) is a raw price of 7.721e-11, stored as
mid = 77_210_000.
maxFill uses a decimal mantissa and exponent so a 16-bit field covers dust to billions of tokens at
better than 0.016% precision (the mantissa is at least 6,554 after normalization). The SDK's
encodeMaxFill rounds down, which only ever tightens your cap.
Why ttlBlocks instead of an absolute expiry
The contract stamps the inclusion block, so validUntilBlock = lastUpdateBlock + ttlBlocks is exact
no matter which block your transaction lands in. An absolute validUntilL2Block would force you to guess
inclusion. Decoders expose the derived validUntilBlock.
Batch format for updateQuotes(bytes packed)
packed is a concatenation of 34-byte entries, one per pool:
| uint16 poolIdx (big-endian) | uint256 slot (big-endian) | x N
poolIdx is the compact index the hook assigned when the pool was created (poolInfo(poolId).idx).
Entries apply in order; a later entry for the same pool overwrites an earlier one.
Each batch emits one QuoteUpdated(address indexed quoter) log whose data is the raw packed bytes
(not ABI-encoded, which saves gas). The log's block number is the stamped lastUpdateBlock.
Two entry points, one behavior
| Entry | Calldata | One pool, measured on Elysium testnet |
|---|---|---|
updateQuotes(bytes packed) | standard ABI (selector, offset, length, padded bytes) | ~30,100 gas |
updateQuotesPacked344079() | selector 0x0000763e followed directly by packed | ~29,300 gas |
The packed entry skips the 68-byte ABI head and its name is mined so its selector sorts first in the
contract's dispatcher, which tests selectors one after another. A fallback() would avoid the selector
entirely but is reached only after every comparison fails, and measured ~600 gas more than
updateQuotes. The SDK's QuoteBatcher uses the packed entry by default.
Pricing a fill
All amounts are in base units. SPREAD_UNIT = 100,000, WAD = 1e18, BPS = 10,000. Every division
rounds against the taker.
age = currentL2Block − lastUpdateBlock
size0 = token0 amount of the trade, valued at mid
spread = baseSpread + age × decayPerBlock + ceil(impactAtMax × size0 / maxFill)
bid = floor(mid × (SPREAD_UNIT − spread) / SPREAD_UNIT) taker sells token0
ask = ceil(mid × (SPREAD_UNIT + spread) / SPREAD_UNIT) taker buys token0
Exact input (amountIn gross of fee):
fee = ceil(amountIn × feeBps / BPS); net = amountIn − fee
zeroForOne: size0 = net; out = floor(net × bid / WAD)
oneForZero: size0 = ceil(net × WAD / mid); out = floor(net × WAD / ask)
Exact output:
zeroForOne: size0 = ceil(out × WAD / mid); net = ceil(out × WAD / bid)
oneForZero: size0 = out; net = ceil(out × ask / WAD)
amountIn = ceil(net × BPS / (BPS − feeBps))
Size impact is valued from the side the taker specifies: exact-in sizes the trade from its input, exact-out from its output, both at mid. Exact-in is therefore very slightly more conservative. Quoting exact-out and then paying that input exact-in can return a hair less than requested when size impact is non-zero (at most the impact on the size difference, typically under 0.1 bp). The other direction always holds: an exact-out quote for an exact-in result never costs more than the original input.
Eligibility
A maker is skipped when any of these hold, checked in this order:
- the maker or the pool flag is paused
mid = 0ormaxFill = 0currentL2Block > lastUpdateBlock + ttlBlocks(expired)- strict mode and
currentL2Block > lastUpdateBlock + 1(stale) size0plus what the maker already filled this L2 block exceedsmaxFill(checked before pricing, which also bounds the impact term)- a taker-sells spread of 100% or more
- the maker's output-side inventory is short
- the pool has a reference-price adapter and the execution price deviates from it by more than
maxDeviationBps
The best remaining maker (highest output for exact-in, lowest input for exact-out; ties go to the
earlier-registered maker) fills the whole trade. If none qualifies the swap reverts: with the shared
reason (Expired, CapExceeded, …) when every maker failed the same way, else NoEligibleQuote.