Technical Documentation

Limit Order Protocol

Technical deep dive into M0's onchain settlement layer for trustless, intent-based token exchanges across chains with partial fills, gasless orders, and secure cancellation.

The Limit Order Protocol is the onchain settlement layer for M0's Liquidity Delivery Network. It provides a trustless, intent-based protocol for exchanging tokens across chains with support for partial fills, gasless orders, and secure cancellation.

Overview

The Limit Order Protocol allows users to submit same-chain or cross-chain limit orders to exchange one token for another. While the primary use case is stablecoin orchestration - swapping between USDC/USDT and M0 extensions - the protocol is asset-agnostic.

Key Design Principles

  • Trustless execution - No trust assumptions on users or solvers; only bridge contracts and messaging protocols are trusted parties
  • Partial fills - Orders can be filled incrementally, allowing solvers to cycle inventory for large orders
  • Deterministic pricing - Users specify exact amountIn and amountOut
  • Cross-chain native - Orders are created on the source chain but can be filled on any configured destination
  • Secure order cancellations - Refunds respect finality buffers to prevent race conditions between cancellations and fills

Architecture

The system consists of:

  • OrderBook contracts deployed on each supported chain
  • Portal/Messenger for cross-chain communication via Portal V2
  • Solvers that monitor and fill orders

Chain Interactions

For same-chain orders:

  1. User opens order on Chain A
  2. Solver fills order on Chain A
  3. Tokens are exchanged atomically

For cross-chain orders:

  1. User opens order on Chain A (origin), locking input tokens
  2. Solver fills order on Chain B (destination), delivering output tokens to recipient
  3. Fill report is sent back to Chain A via the messaging layer
  4. Solver receives input tokens on Chain A

Order Lifecycle

1. Order Creation

Users create orders by calling openOrder() on the OrderBook contract on the origin chain. The order specifies:

ParameterDescription
destChainIdDestination chain where tokens will be delivered
tokenInInput token address on origin chain
tokenOutOutput token address on destination chain
amountInAmount of input token to exchange
amountOutExpected amount of output token
recipientAddress to receive tokens on destination chain
fillDeadlineTimestamp by which order must be filled
solver(Optional) Exclusive solver address, or zero for any solver
struct OrderParams {
    uint32 destChainId;
    uint32 fillDeadline;
    address tokenIn;
    bytes32 tokenOut;      // bytes32 supports non-EVM destinations
    uint128 amountIn;
    uint128 amountOut;
    bytes32 recipient;
    bytes32 solver;
}

When an order is created:

  • Input tokens are transferred from the user to the OrderBook
  • A unique orderId is generated from the order parameters
  • The order is stored with status Created

2. Order Filling

Solvers fill orders by calling fillOrder() on the destination chain's OrderBook:

function fillOrder(
    bytes32 orderId_,
    OrderData calldata orderData_,
    FillParams calldata fillerParams_
) external;

Partial fills are supported - solvers can fill any portion of an order:

  • The solver specifies amountOutToFill in their fill parameters
  • A proportional amount of tokenIn is released to the solver
  • Multiple solvers can fill the same order until it's complete

Same-chain fills release input tokens immediately to the solver.

Cross-chain fills trigger a fill report sent back to the origin chain:

  • The messenger delivers the fill report
  • Input tokens are released to the solver's specified originRecipient

3. Order Completion

An order reaches Completed status when:

  • The full amountOut has been filled, OR
  • The user claims a refund after cancellation or deadline expiry

4. Cancellation and Refunds

Users can cancel orders before the fill deadline by calling requestCancelOrder():

function requestCancelOrder(bytes32 orderId_) external;

For gasless cancellation, users can sign an EIP-712 message and have a relayer submit it:

function requestCancelOrderFor(bytes32 orderId_, bytes calldata signature_) external;
Cross-chain refund delaysFor cross-chain orders, refunds cannot be claimed immediately. A finality buffer must pass to ensure no in-flight fills arrive after cancellation.
ScenarioRefund Available
Same-chain order cancelledImmediately
Cross-chain order cancelledAfter cancelRequestedAt + finalityBuffer
Order deadline expiredAfter fillDeadline + finalityBuffer

The finality buffer is configured per destination chain and accounts for messaging latency and potential reorgs.

Orders in CancelRequested status can still receive fills until the refund is claimed. This protects solvers who submitted fills before seeing the cancellation request.

Edge Cases

Late fill reports: If a cross-chain fill is not reported within the finality buffer, the user can claim a refund after fillDeadline + finalityBuffer. Late reportFill() calls will fail if the refund was already claimed - the solver bears the loss in this scenario. This can occur when messaging networks experience delays, fill transactions are stuck in the destination chain mempool, or bridge relays are slower than expected.

Concurrent cancel and fill: If a user requests cancellation while a fill is in-flight, the fill can still succeed until the refund is claimed. CancelRequested status does not block fills. Fill reports received before the refund claim are processed normally.

Gasless Orders

Users can create orders without paying gas using EIP-712 signatures:

function openOrderFor(
    GaslessOrderParams calldata orderParams_,
    bytes calldata orderSignature_
) external returns (bytes32);

A relayer (often the solver) submits the order on behalf of the user. The signature includes all order parameters, the user's nonce (to prevent replay attacks), and the origin chain ID (to prevent cross-chain replay).

Permit signatures can also be combined with gasless orders to allow token approval in the same transaction:

// With split signature (v, r, s)
function openOrderForWithPermit(
    GaslessOrderParams calldata orderParams_,
    bytes calldata orderSignature_,
    uint256 deadline_,
    uint8 v_,
    bytes32 r_,
    bytes32 s_
) external returns (bytes32);

// With packed signature
function openOrderForWithPermit(
    GaslessOrderParams calldata orderParams_,
    bytes calldata orderSignature_,
    uint256 deadline_,
    bytes memory permitSignature_
) external returns (bytes32);

Order Identification

Each order has a unique ID derived from hashing its parameters:

orderId = keccak256(abi.encodePacked(
    version,
    sender,
    nonce,
    originChainId,
    destChainId,
    fillDeadline,
    tokenIn,
    tokenOut,
    amountIn,
    amountOut,
    recipient,
    solver
))

This ensures orders are globally unique across all chains, solvers can verify order authenticity without trusting the origin chain, and the same order parameters always produce the same ID.

Solver Integration

Solvers monitor OrderBook events to discover fillable orders:

event OrderOpened(
    bytes32 indexed orderId,
    address sender,
    address tokenIn,
    uint128 amountIn,
    uint32 indexed destChainId,
    bytes32 tokenOut,
    uint128 amountOut,
    bytes32 indexed solver
);

Exclusive Solvers

If an order specifies a solver address, only that address can fill the order. Setting solver to zero allows any solver to fill (permissionless racing).

Fill Strategy

Solvers determine their own fill strategy - fill entire orders when inventory allows, partially fill large orders to manage risk, or prioritize orders by profitability or deadline urgency.

Receiving Tokens

For cross-chain fills, solvers specify an originRecipient in their fill parameters. This address receives the input tokens on the origin chain after the fill report is processed.

Security Considerations

Finality Buffers

Each destination chain has a configured finality buffer that determines how long to wait before processing refunds and protection against reorgs causing double-spends.

Finality buffer changes are time-delayed to protect existing orders:

  • Increases take effect immediately (more conservative)
  • Decreases take effect after the old buffer duration passes

Token Compatibility

Non-standard token risksThe OrderBook has known edge cases with non-standard tokens. Review this section carefully before whitelisting tokens.
Token TypeBehaviorSeverityRecommendation
Pausable tokens (USDC/USDT)If paused during cross-chain fill, user may receive destination tokens AND claim origin refund (double-spend)CRITICALAvoid for cross-chain orders or implement pause monitoring
Rebase tokensIf token rebases downward after order creation, reportFill() and claimRefund() may revert permanently, locking fundsHIGHDo not use
Yield-bearing tokensYield accrues to OrderBook contract, not recoverable by user or solverMEDIUMUse short order lifetimes; configure fee recovery via MEarnerManager
Fee-on-transfer tokensfillOrder() reverts via safeTransferExact; claimRefund() may lose fee amountMEDIUMDo not use

Solver Safety

Solver fund loss risksThe following behaviors can cause fund loss for solvers.

Invalid recipient address: Passing address(0) as originRecipient in FillParams will silently burn tokens. The contract does not validate this address. Always verify originRecipient is a valid, non-zero address before submitting fills.

Dust fills with decimal mismatch: When tokenIn has fewer decimals than tokenOut, very small fills can round to zero amountIn. The pro-rata formula is:

amountInToRelease = (amountIn * amountOutToFill) / amountOut

Solvers should validate minimum fill amounts to avoid zero-value releases.

Rounding Behavior

Pro-rata calculations for partial fills always round down (floor):

  • Users receive at least their proportional share
  • Solvers may receive slightly less than the theoretical maximum
  • Very small fills relative to decimal differences may round to zero

Messenger Trust

The only trusted component is the messenger contract that relays fill reports between chains. M0 uses its Portal V2 infrastructure for secure cross-chain messaging.

Contract Reference

The Limit Order Protocol is implemented in the OrderBook.sol contract.

Key Functions

FunctionDescription
openOrder()Create a new limit order
openOrderWithPermit()Create order with EIP-2612 permit (2 overloads)
openOrderFor()Create gasless order on behalf of user
openOrderForWithPermit()Create gasless order with permit (2 overloads)
fillOrder()Fill an order (full or partial, 2 overloads)
requestCancelOrder()Request order cancellation
requestCancelOrderFor()Request gasless order cancellation
claimRefund()Claim refund for cancelled/expired order
reportFill()Report a cross-chain fill (messenger only)
setDestinationConfig()Configure destination chain (requires DEFAULT_ADMIN_ROLE)

View Functions

FunctionDescription
getOrder()Get order details by ID
getOrderId()Compute order ID from OrderData
getFilledAmounts()Get filled amounts for an order
getSenderNonce()Get next nonce for gasless orders
isDestinationSupported()Check if chain is supported
getDestinationFinalityBuffer()Get finality buffer for chain
getDestinationConfig()Get full destination configuration
getGaslessOrderDigest()Get EIP-712 digest for gasless order
getCancelRequestDigest()Get EIP-712 digest for gasless cancellation

Data Structures

OrderStatus

enum OrderStatus {
    DoesNotExist,    // Order has never been created
    Created,         // Order is active and fillable
    CancelRequested, // User requested cancellation (fills still accepted)
    Completed        // Order fully filled or refund claimed
}

Order

Complete data about an order originated on this chain (stored on origin chain only):

struct Order {
    OrderStatus status;
    uint16 version;
    address sender;
    uint64 nonce;
    uint32 destChainId;
    uint32 fillDeadline;
    uint32 cancelRequestedAt;
    address tokenIn;
    bytes32 tokenOut;
    uint128 amountIn;
    uint128 amountOut;
    bytes32 recipient;
    bytes32 solver;
}

OrderData

Used to identify and fill orders on the destination chain:

struct OrderData {
    uint16 version;
    bytes32 sender;
    uint64 nonce;
    uint32 originChainId;
    uint32 destChainId;
    uint64 fillDeadline;
    bytes32 tokenIn;
    bytes32 tokenOut;
    uint128 amountIn;
    uint128 amountOut;
    bytes32 recipient;
    bytes32 solver;
}

FillParams

struct FillParams {
    uint128 amountOutToFill;
    bytes32 originRecipient;
}

FillReport

struct FillReport {
    bytes32 orderId;
    uint128 amountInToRelease;
    uint128 amountOutFilled;
    bytes32 originRecipient;
    bytes32 tokenIn;
}

FilledAmounts

struct FilledAmounts {
    uint128 amountInReleased;
    uint128 amountOutFilled;
}

Events

// Order lifecycle
event OrderOpened(
    bytes32 indexed orderId,
    address sender,
    address tokenIn,
    uint128 amountIn,
    uint32 indexed destChainId,
    bytes32 tokenOut,
    uint128 amountOut,
    bytes32 indexed solver
);

event OrderFilled(
    bytes32 indexed orderId,
    address indexed solver,
    uint128 amountInToRelease,
    uint128 amountOutFilled
);

event OrderCompleted(bytes32 orderId);

// Cancellation
event CancelRequested(bytes32 indexed orderId, uint32 cancelRequestedAt);

event RefundClaimed(
    bytes32 indexed orderId,
    address indexed sender,
    uint128 amountInRefunded
);

// Configuration
event DestinationConfigUpdated(
    uint32 indexed destChainId,
    bool newIsSupported,
    uint32 newFinalityBuffer,
    uint64 newFinalityBufferEffectiveTimestamp
);

Error Codes

ErrorDescription
AmountInZeroOrder input amount cannot be zero
AmountOutZeroOrder output amount cannot be zero
FillAmountZeroFill amount must be greater than zero
FinalityPendingFinality buffer has not elapsed; cannot claim refund yet
InvalidDeadlineFill deadline is in the past or invalid
InvalidDestinationChainDestination chain is not supported
InvalidFinalityBufferFinality buffer value is invalid
InvalidNonceNonce doesn't match sender's expected nonce
InvalidOrderStatusOrder is not in a valid status for this operation
InvalidOrderVersionOrder version doesn't match contract version
InvalidOriginChainOrigin chain ID doesn't match this chain
InvalidRecipientRecipient address is invalid
InvalidReportFill report data is invalid
NotAuthorizedCaller is not authorized for this operation
OrderExpiredOrder fill deadline has passed
OrderAlreadyFilledOrder has already been completely filled
OrderIdMismatchComputed order ID doesn't match provided ID
Copyright © M0 Foundation 2026