---
id: TIP-1088
title: Unify quote/swap fill logic
description: Route the StablecoinDEX quote paths through the same per-order fill engine as the swap paths to eliminate deterministic quote/swap drift.
authors: Dan Robinson
status: Draft
related: TIP-1002, TIP-1005
protocolVersion: T12
---

# TIP-1088: Unify quote/swap fill logic

## Abstract

`quote()` and `swap()` currently use different internal logic to traverse liquidity and apply rounding. `quote()` operates per-tick using the tick-level aggregate, while `swap()` operates per-order. Over multi-order paths, this causes deterministic divergence between quoted and executed prices due to accumulated rounding differences at order boundaries within a tick.

This TIP routes `quote()` through the same per-order fill logic as `swap()`, executed in a read-only simulation. Quote becomes "swap without state writes," so a quote returns exactly what the matching swap executes. The change is gated at T12; below T12 the existing per-tick quote is unchanged.

Since T12 quotes no longer use the stored per-tick `totalLiquidity` aggregate, T12 also stops maintaining that aggregate and instead derives `getTickLevel.totalLiquidity` on demand from active orders.

## Motivation

Today, `quote()` and `swap()` duplicate matching logic with different traversal granularity and rounding accumulation:

- `quote()` traverses liquidity **per tick**, using the tick-level aggregate to determine how much fills at each level, and rounds once per tick.
- `swap()` traverses liquidity **per order**, iterating individual orders within each tick via the linked list, and rounds each order's fill.

The four affected entrypoints are:
- `quoteSwapExactAmountIn(tokenIn, tokenOut, amountIn)` → returns `amountOut`
- `quoteSwapExactAmountOut(tokenIn, tokenOut, amountOut)` → returns `amountIn`
- `swapExactAmountIn(tokenIn, tokenOut, amountIn, minAmountOut)` → executes swap
- `swapExactAmountOut(tokenIn, tokenOut, amountOut, maxAmountIn)` → executes swap

When multiple orders exist at the same tick, `quote()` rounds once against the aggregate while `swap()` rounds per order, causing deterministic price divergence with no intervening state change. Because `swap()` rounds each order's output down, the executed output can be strictly less than the quote — as little as one unit — so a caller that sets `minAmountOut` to the quote can see the swap revert with `InsufficientOutput`. This is surprising for integrators and makes swap routing harder, since quoted prices cannot be trusted as exact execution prices.

Making `quote()` accurate requires an implementation that iterates over individual orders, exactly as `swap()` does.

Once quotes no longer use `totalLiquidity`, maintaining that aggregate imposes redundant storage writes during order placement, fills, and cancellation.

---

# Specification

1. Introduce a shared fill engine with two modes:
   - `Simulate`: identical per-order matching and rounding logic, no state mutation.
   - `Execute`: existing swap behavior with state mutation.

2. Route both quote and swap entrypoints through this shared engine:
   - `quoteSwapExactAmountIn` and `quoteSwapExactAmountOut` call shared fill logic in `Simulate` mode.
   - `swapExactAmountIn` and `swapExactAmountOut` call shared fill logic in `Execute` mode.

3. In `Simulate` mode, the engine MUST:
   - Traverse the same reachable ticks and the same individual orders within those ticks, in the same priority order as `Execute` mode. Traversal occurs at **order granularity**, not tick-aggregate granularity.
   - Apply the same rounding direction at every conversion step.
   - Produce the same fill amounts and price progression as `Execute` mode for an identical state snapshot.
   - Avoid all writes and side effects:
     - no order remaining updates
     - no order/tick removals
     - no best-bid/best-ask updates
     - no balance transfers
     - no events
     - no flip-order placement
     - no transient storage writes
     - no gas refunds, discounts, or flip-specific gas accounting (e.g. TIP-1044)

4. In `Execute` mode, the engine behavior remains functionally equivalent to current swap semantics, except that at T12 write paths MUST NOT update the stored per-tick `totalLiquidity` field. This applies to order placement, partial and full fills, cancellation, and flip-order placement.

5. Tick liquidity compatibility at T12:
   - The `getTickLevel` function signature and return shape remain unchanged.
   - `getTickLevel.totalLiquidity` MUST equal the checked sum of `remaining` across all active orders in the tick's linked list.
   - Write paths MUST NOT traverse a tick's orders to compute or validate aggregate liquidity.
   - Existing `TickLevel.totalLiquidity` storage slots remain in place but become stale, deprecated state. No migration or clearing is required.

6. API compatibility:
   - External quote and swap method signatures remain unchanged.
   - Quote precision now matches swap precision exactly for the same state snapshot. Parity is scoped to the fill math (tick/order traversal, rounding, and fill amounts); caller-level and token-specific constraints (e.g., TIP-20 transfer fees) and execute-only side effects are outside this guarantee. In particular, `swap()` places flip orders and `quote()` does not: a flip-order placement that raises a system error reverts `swap()` (post-T1A) but not `quote()`. Business-logic flip failures are swallowed and do not change the taker fill, so fill-math parity still holds; only execute-only revert outcomes differ.
   - Gas costs change: `quote()` and `getTickLevel()` become more expensive because they traverse individual orders, while swap and order-management paths avoid the storage operations previously used to maintain `totalLiquidity`. Callers SHOULD NOT depend on specific gas costs for these functions.
   - When available liquidity cannot satisfy the trade, `quote()` fails with `InsufficientLiquidity`, matching `swap()`.

# Invariants

1. Execution price preservation:
   - This change MUST NOT alter the execution price of the `swap()` function, or any of the orders filled by it, for identical inputs and state.

2. Quote/swap deterministic parity (fill math only):
   - For any fixed state snapshot and identical inputs, `quote()` MUST produce the same fill amounts and price progression as the orderbook fill engine of `swap()` before state writes are applied. This means identical tick/order traversal sequence, identical rounding direction at every conversion step, and identical terminal fill amounts. Caller-level and token-specific constraints (e.g., TIP-20 transfer fees) and execute-only side effects (e.g. flip-order placement, which can revert `swap()` via a system error) are outside this guarantee.

3. No-write simulation:
   - Running `quote()` MUST leave orderbook state, balances, and tick metadata unchanged.

4. Liquidity-exhaustion parity:
   - `quote()` MUST fail with `InsufficientLiquidity` for exactly the inputs where the matching `swap()` runs out of liquidity.

5. Tick-level liquidity correctness:
   - At T12, `getTickLevel.totalLiquidity` MUST equal the checked sum of `remaining` across all active orders in the requested tick.
