BLK ———

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.

BitsFieldTypeUnit and meaning
0–103miduint104token1 base units per 1 token0 base unit, scaled by 1e18
104–119bidSpreaduint160.1 bp (1e-5) below mid
120–135askSpreaduint160.1 bp above mid
136–151maxFillMantissauint16max token0 filled per L2 block = mantissa × 10^exp, in base units
152–159maxFillExpuint8
160–175impactAtMaxuint160.1 bp of extra spread at a fill of maxFill, linear in size
176–183decayPerBlockuint80.1 bp of extra spread per L2 block of age
184–199ttlBlocksuint16fillable while block ≤ lastUpdateBlock + ttlBlocks
200–231lastUpdateBlockuint32stamped by the contract with the inclusion L2 block; your value is ignored
232–247sequint16your sequence number, echoed in Filled (wraps at 65,536)
248–255flagsuint8bit 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

EntryCalldataOne 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:

  1. the maker or the pool flag is paused
  2. mid = 0 or maxFill = 0
  3. currentL2Block > lastUpdateBlock + ttlBlocks (expired)
  4. strict mode and currentL2Block > lastUpdateBlock + 1 (stale)
  5. size0 plus what the maker already filled this L2 block exceeds maxFill (checked before pricing, which also bounds the impact term)
  6. a taker-sells spread of 100% or more
  7. the maker's output-side inventory is short
  8. 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.