Skip to main content
Pre-Release Documentation

This describes a pre-release version of LBAMM. Interfaces and behavior may change.

Verify the exact repository commit before building production integrations.

CLOB Transfer Handler

The CLOBTransferHandler is a transfer handler that implements an onchain central-limit order book (CLOB) and settles AMM swaps by filling CLOB orders.

Conceptually:

  • Order makers deposit tokens into the handler and open limit orders at a price.
  • When an AMM swap uses the CLOBTransferHandler, the AMM sends the swap’s output token to the handler.
  • The handler uses that output to fill maker orders (crediting makers internally), and then transfers the required input token back to the AMM to settle the swap.

This page documents:

  • How the CLOB integrates with LBAMM swap settlement
  • Order book identity (orderBookKey / groupKey)
  • Maker lifecycle (deposit → open → close → withdraw)
  • Fill path via ammHandleTransfer and refund semantics
  • Hook and token-hook validation surfaces
  • Events and how indexers can maintain derived state

Deep details (order matching math, rounding, and onchain data structures) are described at the level needed for integrators to safely use and index the system.


Key design constraints

Recipient must be the handler

Swaps settled through the CLOBTransferHandler must set:

  • swapOrder.recipient == address(CLOBTransferHandler)

The handler requires custody of the AMM’s output tokens in order to:

  • credit filled makers’ balances in the output token, and
  • retain those funds in the handler so makers can withdraw later or open new orders in the opposite direction.

Output-based swaps are not supported

CLOB settlement only supports input-based swaps (swapOrder.amountSpecified >= 0).

If an output-based swap is attempted, ammHandleTransfer reverts.


Order books: orderBookKey and groupKey

Each order book is uniquely identified by an orderBookKey:

orderBookKey = keccak256(tokenIn, tokenOut, groupKey)

Where groupKey encodes:

  • a CLOB hook address
  • a minimum order size (base + scale)

groupKey layout

bytes32 groupKey = bytes32(uint256(uint160(hook)) << 96)
| bytes32(uint256(minimumOrderBase) << 8)
| bytes32(uint256(minimumOrderScale));

Helpers exist to decode:

  • hook
  • minimumOrder = minimumOrderBase * 10^minimumOrderScale

minimumOrderScale is bounded (max 72) to avoid overflow.

Initializing orderBookKey metadata

Order books are lazily initialized on first use, but an explicit helper exists:

  • initializeOrderBookKey(tokenIn, tokenOut, hook, minimumOrderBase, minimumOrderScale)

Once initialized, a public mapping exposes the decoded parameters:

  • orderBookKeys[orderBookKey] -> OrderBookKey

Pricing model

Orders are placed at a price represented as sqrtPriceX96 (Q96 fixed point):

  • Price is encoded as sqrt(price) * 2^96

The handler computes tokenOut required for a given tokenIn at a price via:

function calculateFixedInput(uint256 amountIn, uint160 sqrtPriceX96)
internal pure returns (uint256 amountOut)
{
amountOut = FullMath.mulDivRoundingUp(amountIn, sqrtPriceX96, Q96);
amountOut = FullMath.mulDivRoundingUp(amountOut, sqrtPriceX96, Q96);
}

Notes:

  • This effectively applies the squared price (in Q96) with rounding up.
  • orderAmount is always denominated in tokenIn.

“Best price for taker”

When filling, the handler fills starting from the best price for the taker (the AMM swap executor):

  • the price that yields the most tokenIn for the least tokenOut

At a given price level, orders fill FIFO.


Maker lifecycle

Deposit

Makers can pre-fund balances:

  • depositToken(token, amount)

The handler tracks balances in:

  • makerTokenBalance[token][maker]

A deposit:

  • transfers tokens into the handler
  • increments the maker’s internal balance
  • emits TokenDeposited

Withdraw

Makers can withdraw available balances:

  • withdrawToken(token, amount)

A withdraw:

  • decrements the maker’s internal balance
  • transfers tokens out
  • emits TokenWithdrawn

Non-standard ERC-20 behavior (e.g., rebasing or external balance adjustment) can break the “1:1 backing” assumption and cause drift between internal balances and the handler’s actual token balances.

Open a limit order

Makers open orders via:

  • openOrder(tokenIn, tokenOut, sqrtPriceX96, orderAmount, groupKey, hintSqrtPriceX96, hookData)

Key behaviors:

  • orderAmount is funded from existing internal balance; if insufficient, the handler attempts to transferFrom the shortfall from the maker.
  • orderAmount must be ≥ minimumOrder(groupKey).
  • If the group hook is non-zero, the handler calls ICLOBHook.validateMaker(...).
  • If token hooks have handler-order validation enabled, the handler calls tokenHook.validateHandlerOrder(...) for tokenIn and/or tokenOut.
  • The order is inserted into the per-price FIFO queue.

Close an order

Makers close an order via:

  • closeOrder(tokenIn, tokenOut, sqrtPriceX96, orderNonce, groupKey)

Closing:

  • marks the order closed
  • returns the unfilled tokenIn amount back to the maker’s internal balance
  • emits OrderClosed

Swap settlement via ammHandleTransfer

When a swap uses the CLOBTransferHandler, the AMM calls:

  • ammHandleTransfer(executor, swapOrder, amountIn, amountOut, exchangeFee, feeOnTop, transferExtraData)

Preconditions enforced by the handler

The handler reverts unless:

  • msg.sender == AMM
  • transferExtraData.length > 0
  • swapOrder.recipient == address(this)
  • swapOrder.amountSpecified >= 0 (input-based)

transferExtraData format

transferExtraData must decode as:

struct FillParams {
bytes32 groupKey;
uint256 maxOutputSlippage;
bytes hookData;
}

The handler derives the order book:

  • orderBookKey = hash(tokenIn, tokenOut, groupKey)

Taker (executor) validation via group hook

If groupKey encodes a non-zero hook, the handler calls:

  • ICLOBHook(hook).validateExecutor(orderBookKey, executor, swapOrder, amountIn, amountOut, exchangeFee, feeOnTop, hookData)

This is the CLOB’s primary surface for validating takers/executors.

Fill algorithm (high-level)

The handler calls into the matching engine:

  • CLOBHelper.fillOrder(orderBook, makerTokenBalance[tokenOut], amountIn, amountOut)

Filling behavior:

  • Orders fill from the current best price upward until the required amountIn is fully sourced.
  • At each price level, orders fill FIFO.
  • As each maker order consumes tokenOut, the maker’s internal balance in tokenOut is incremented.

The helper returns:

  • fillOutputRemaining: amount of AMM output tokenOut not needed to fill the required input
  • endingOrderNonce, endingOrderInputRemaining: head-of-book details after filling

The handler emits:

  • OrderBookFill(orderBookKey, endingOrderNonce, endingOrderInputRemaining)

Why “output remaining” exists

An AMM swap can produce more tokenOut than the CLOB needs to source the required amountIn. This typically happens when:

  • the CLOB’s available prices have moved materially since the swap was constructed, so fewer orders (or better prices) are available than implied by the caller’s assumptions.

Slippage bound for unused output

If fillOutputRemaining > 0, the handler compares it to maxOutputSlippage:

  • if fillOutputRemaining > maxOutputSlippage: revert
  • otherwise: the handler schedules a refund of fillOutputRemaining to the executor

This parameter bounds how much “unused output” the executor is willing to tolerate before re-submitting with updated assumptions.

Refund callback after swap finalization

If a refund is required, the handler returns callback data:

  • afterSwapRefund(executor, tokenOut, fillOutputRemaining)

After swap finalization, the AMM executes this callback on the handler.

Refund behavior:

  • For wrapped native, the handler attempts to unwrap and send native value to the executor; if unwrap fails it transfers wrapped native.
  • For ERC-20, it transfers the token directly.

Settling the AMM

After filling, the handler transfers the required input token to the AMM:

  • safeTransfer(tokenIn, AMM, amountIn)

If the transfer fails, the handler reverts.


Validation surfaces

CLOB group hook (ICLOBHook)

A group hook can enforce:

  • maker eligibility and constraints at order open
  • executor/taker eligibility and constraints at fill time

Interface:

interface ICLOBHook is ITransferHandlerExecutorValidation {
function validateMaker(
bytes32 orderBookKey,
address depositor,
uint160 sqrtPriceX96,
uint256 orderAmount,
bytes calldata hookData
) external;
}

Token hook validateHandlerOrder (order open only)

When opening an order, the handler checks token settings in the AMM:

  • if TOKEN_SETTINGS_HANDLER_ORDER_VALIDATE_FLAG is enabled for tokenIn and/or tokenOut, it calls the token hook:
validateHandlerOrder(
maker,
hookForTokenIn,
tokenIn,
tokenOut,
amountIn,
amountOut,
handlerOrderParams,
hookData
)

For CLOB orders, handlerOrderParams is:

abi.encode(orderBookKey, sqrtPriceX96)

This hook is invoked only on order open (not during fills or closes).


Internal order identifiers

Within a price bucket, FIFO ordering uses an internal orderId derived from the order’s storage slot.

function _orderToOrderId(Order storage ptrOrder) internal pure returns (bytes32 orderId) {
assembly ("memory-safe") { orderId := ptrOrder.slot }
}

function _orderIdToOrder(bytes32 orderId) internal pure returns (Order storage ptrOrder) {
assembly ("memory-safe") { ptrOrder.slot := orderId }
}

This is an internal implementation detail for maintaining linked lists; integrators should treat orderNonce as the public identifier for maker actions.


Events and indexing

CLOBTransferHandler is designed to be indexable using events. An indexer can maintain derived state for:

  • maker balances (by token)
  • open orders per order book and price
  • order book head pointer (best price and next order)

Event reference

event TokenDeposited(address indexed token, address indexed depositor, uint256 amount);
event TokenWithdrawn(address indexed token, address indexed depositor, uint256 amount);

event OrderBookInitialized(
bytes32 indexed orderBookKey,
address tokenIn,
address tokenOut,
address hook,
uint16 minimumOrderBase,
uint8 minimumOrderScale
);

event OrderOpened(
address indexed maker,
bytes32 indexed orderBookKey,
uint256 orderAmount,
uint160 sqrtPriceX96,
uint256 orderNonce
);

event OrderClosed(
address indexed maker,
bytes32 indexed orderBookKey,
uint256 unfilledInputAmount,
uint256 orderNonce
);

event OrderBookFill(
bytes32 indexed orderBookKey,
uint256 endingOrderNonce,
uint256 endingOrderInputRemaining
);

Suggested indexing strategy

Discover order books

Listen for OrderBookInitialized(orderBookKey, ...).

  • This yields the canonical tokenIn, tokenOut, hook, and minimum-size parameters for the order book.
  • Order books may also be created lazily; if you observe OrderOpened/OrderBookFill for an unknown key, you can query orderBookKeys[orderBookKey] to resolve metadata (if initialized).

Track maker balances (derived)

For a best-effort derived ledger:

  • On TokenDeposited(token, maker, amount): balance[token][maker] += amount
  • On TokenWithdrawn(token, maker, amount): balance[token][maker] -= amount

Order events imply additional balance movements:

  • On OrderOpened(maker, orderBookKey, orderAmount, ...):

    • tokenIn = orderBookKeys[orderBookKey].tokenIn
    • balance[tokenIn][maker] -= orderAmount
    • (Note: the handler may have pulled additional tokens from the maker if their internal balance was insufficient. That extra pull emits TokenDeposited(tokenIn, maker, depositRequired) inside openOrder.)
  • On OrderClosed(maker, orderBookKey, unfilledInputAmount, orderNonce):

    • tokenIn = orderBookKeys[orderBookKey].tokenIn
    • balance[tokenIn][maker] += unfilledInputAmount
  • On OrderBookFill(orderBookKey, ...):

    • makers’ tokenOut balances are credited internally as fills occur.
    • The handler does not emit per-maker fill events in this version. An indexer cannot attribute fills to individual makers using events alone.

Because fills are aggregated, a purely event-driven indexer can still infer which orders were fully filled by combining:

  • OrderOpened (which provides sqrtPriceX96 and orderNonce), and
  • OrderBookFill(orderBookKey, endingOrderNonce, endingOrderInputRemaining) (which provides the new head order)

Since the ending head price can be resolved from the maintained order book state (see below), after each OrderBookFill an indexer can conclude:

  • all price levels strictly better than the ending head price have been fully consumed, and
  • within the ending head price bucket, all orders with nonces lower than endingOrderNonce have been fully consumed, and
  • the head order at endingOrderNonce has endingOrderInputRemaining remaining.

Empty book sentinel: when both endingOrderNonce and endingOrderInputRemaining are zero, the order book has been completely cleared by the fill.

This allows an event-driven indexer to deterministically attribute fills to makers: all orders strictly before the ending head (by price, then FIFO nonce within price) are fully filled, and the ending head order—if non-zero—represents the only partially filled order in that bucket.

Track open orders

From events alone:

  • You can track the set of opened orders by (orderBookKey, maker, orderNonce, sqrtPriceX96, orderAmount) using OrderOpened.
  • You can mark an order closed using OrderClosed(maker, orderBookKey, ..., orderNonce).

However, you cannot infer partial fills from events alone. To present “remaining size” for an open order, an indexer must query onchain storage or rely on future events.

Track head-of-book (best price)

OrderBookFill(orderBookKey, endingOrderNonce, endingOrderInputRemaining) provides the new head order after a fill.

To interpret it fully, the indexer needs the ending head price. There are two practical approaches:

  • Event + offchain reconstruction (recommended): maintain per-order-book price buckets from OrderOpened and advance the “best price” pointer deterministically as fills occur.

    • Since the book fills FIFO at the current best price and then moves monotonically to the next worse price until the required input is sourced, the head price after a fill is the lowest price that still has remaining orders.
  • Onchain query: query orderBookKeys[orderBookKey] for metadata and read the handler state if you maintain a stateful indexer that can access contract storage.


Integration notes

  • Swaps settled via CLOB must set the recipient to the handler.
  • Only input-based swaps are supported.
  • maxOutputSlippage bounds how much unused AMM output can be refunded rather than consumed.
  • Token hook validateHandlerOrder is invoked only when opening orders, and only if enabled in token settings.

Limit Break

TwitterLimitBreak.comMedium

© 2026 Limit Break International, Inc. All rights reserved.

Privacy PolicyTerms of ServiceCookie PolicyDo Not Sell My Info