PROTOCOL DETAILS

Limit Order Protocol

Technical deep dive into M0's onchain settlement layer for trustless, intent-based token exchanges across chains with partial fills, destination-chain cancellation, and permissionless refunds.

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, destination-chain cancellation, and permissionless refunds after expiry.

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 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 - Cancels are initiated on the destination chain so that in-flight fills and cancellations cannot race

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
senderAddress that owns the order, holding cancel and refund rights
struct OrderParams {
    uint32 destChainId;
    uint32 fillDeadline;
    address tokenIn;
    bytes32 tokenOut;      // bytes32 supports non-EVM destinations
    uint128 amountIn;
    uint128 amountOut;
    bytes32 recipient;
    bytes32 solver;
    address sender;
}

When an order is created:

  • Input tokens are transferred from the funder (msg.sender) to the OrderBook
  • A unique orderId is generated from the order parameters
  • The order is stored with status Created

OrderParams.sender lets the address that pays for an order differ from the address that owns it. Tokens are pulled from msg.sender (the funder), while sender holds cancellation rights and receives any refund. This allows a wrapper or router contract to fund an order on behalf of an end user without taking custody of the order itself. For the common case where the caller owns its own order, set sender to msg.sender.

Two openOrderWithPermit() overloads accept an EIP-2612 permit so a user can approve and open in a single transaction - one taking split (v, r, s) values and one taking a packed signature.

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 payable returns (bytes32 messageId_);

Two further overloads accept bridgeAdapterArgs_, or both bridgeAdapter_ and bridgeAdapterArgs_, to route the resulting FillReport through a specific Portal V2 bridge adapter.

fillOrder() validates that the supplied orderData_ hashes to orderId_, that this chain is the destination, that the deadline has not passed, that orderData_.version matches the contract VERSION, that createdAt is not in the future, that amountOutToFill != 0, that originRecipient != 0, and — if the order names an exclusive solver — that msg.sender is that solver.

Partial fills are supported. The amount actually filled is the lesser of fillerParams_.amountOutToFill and the remaining unfilled amount, so a solver can safely over-specify:

  • A proportional amount of tokenIn is released to the solver
  • Multiple solvers can fill the same order until it is complete

Same-chain fills release input tokens immediately to the solver's originRecipient. msg.value must be 0, otherwise InvalidMsgValue.

Cross-chain fills send a FillReport back to the origin chain:

  • msg.value is forwarded to the Portal as the bridge fee
  • fillerParams_.refundAddress receives any bridge overpayment; if zero, it defaults to msg.sender
  • When the report is delivered, reportFill() releases input tokens to the solver's originRecipient

3. Order Completion

An order reaches Completed status when the full amountOut has been filled.

4. Cancellation and Refunds

Cancellation is a single action, taken on the destination chain — the same chain where fills happen, so that a cancel and an in-flight fill resolve against the same state.

Cancel an order by calling cancelOrder():

function cancelOrder(bytes32 orderId_, OrderData calldata orderData_)
    external payable returns (bytes32 messageId_);

As with fillOrder(), overloads accept bridgeAdapterArgs_, or both bridgeAdapter_ and bridgeAdapterArgs_, to direct the CancelReport through a specific bridge.

For gasless cancellation, the recipient signs an EIP-712 message and anyone can submit it:

function cancelOrderFor(
    bytes32 orderId_,
    OrderData calldata orderData_,
    bytes calldata signature_
) external payable returns (bytes32 messageId_);

The signature is verified against orderData_.recipient and covers the orderId, bridgeAdapter, and bridgeAdapterArgs. Both 65-byte and EIP-2098 compact 64-byte signatures are accepted.

Who can cancel depends on the order type and whether the deadline has passed:

ScenarioWho can cancel
Same-chain order, before deadlineThe order's sender or recipient
Cross-chain order, before deadlineThe order's recipient only
Any order, after fillDeadlineAnyone - enables permissionless refunds

Only the recipient can cancel a cross-chain order before the deadline, because the sender lives on the origin chain and may not control the same address on the destination.

Order typeWhat happens on cancel
Same-chain (origin = dest)The unfilled remainder of amountIn transfers to the order's sender immediately. msg.value must be 0.
Cross-chainStatus becomes Cancelled and a CancelReport is sent via the Portal. msg.value pays the bridge fee, refunded to msg.sender. On the origin chain, reportCancel() releases the remainder to the sender.
Because cross-chain messages can arrive out of order, reportFill() accepts fills against orders in either CreatedorCancelled status. The invariant amountInReleased + amountInRefunded <= amountIn prevents over-distribution, so a fill that was in flight when the cancel landed still settles correctly.

Edge Cases

Concurrent cancel and fill: If a fill is in-flight when an order is cancelled, the fill can still settle on the origin chain. reportFill() accepts orders in Cancelled status, and the invariant amountInReleased + amountInRefunded <= amountIn prevents over-distribution.

Order Identification

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

orderId = keccak256(abi.encodePacked(
    version,
    sender,
    nonce,
    originChainId,
    destChainId,
    createdAt,
    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.

The EVM and SVM implementations produce identical order IDs.

Solver Integration

Solvers monitor OrderBook events to discover fillable orders:

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

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.

Bridge Fees

For cross-chain fills and cancels, msg.value covers the Portal V2 bridge fee. Hyperlane and LayerZero adapters expose an onchain quote() function; Wormhole uses its Executor API. The full solver-facing walkthrough lives in the solver integration guide in the protocol repository.

Security Considerations

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)While the token is paused, fills and cancel settlement revert, so an order cannot complete until the token unpauses. Funds are not at risk.LOWSafe to whitelist - nearly every major stablecoin is pausable. Expect fills and refunds to stall for the duration of a pause.
Rebase tokensIf token rebases downward after order creation, reportFill() and reportCancel() 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 safeTransferExactMEDIUMDo not use

Solver Safety

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

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 and cancel reports between chains. M0 uses its Portal V2 infrastructure for secure cross-chain messaging.

Deployments

The OrderBook is deployed behind an OpenZeppelin TransparentUpgradeableProxy whose address is derived via CREATE3, giving it the same address on every EVM chain. Solana runs an equivalent order_book program.

OrderBook addresses

Deployed OrderBook addresses, listed under each chain alongside the rest of the M0 platform contracts.

The OrderBook uses the same internal chain IDs as Portal V2 in its cross-chain payloads. These match standard EVM chain IDs on EVM networks; bridge adapters translate them into the underlying messaging-network IDs.

NetworkInternal Chain ID
Ethereum1
Base8453
Arbitrum One42161
Monad143
Moca Chain2288
Citrea4114
Rise4153
Solana1399811149
A deployment is not the same as a routable chainThese are the chains where the OrderBook contract itself is deployed — useful if you are integrating the contract directly. The Orchestration API enables limit order routing on a subset of these chains, and reaches other chains through Portal V2 and the M0 Swap Facility instead. Before you rely on a limit order route, check /supported-assets rather than this list.

Contract Reference

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

Key Functions

FunctionDescription
openOrder()Create a new limit order
openOrderWithPermit()Create an order with an EIP-2612 permit (2 overloads)
fillOrder()Fill an order, fully or partially (3 overloads)
cancelOrder()Cancel an order on the destination chain (3 overloads)
cancelOrderFor()Cancel gaslessly with the recipient's signature (3 overloads)
reportFill()Process a cross-chain fill report (portal only)
reportCancel()Process a cross-chain cancel report (portal only)
setDestinationSupported()Enable or disable a destination chain (DEFAULT_ADMIN_ROLE)
pause() / unpause()Pause or resume order actions (PAUSER_ROLE)

View Functions

FunctionDescription
getOrder()Get stored order details by ID
getOrderData()Get the OrderData payload solvers need to fill
getOrderId()Compute an order ID from OrderData
getFilledAmounts()Get filled and refunded amounts for an order
getSenderNonce()Get the next nonce for an address
isDestinationSupported()Check whether a chain is a supported destination
getCancelOrderDigest()Get the EIP-712 digest for a gasless cancellation
VERSIONOrder format version (1)
portalImmutable Portal V2 address used for messaging

Data Structures

OrderStatus

enum OrderStatus {
    DoesNotExist,  // Order has never been created on this chain
    Created,       // Order is active and fillable
    Cancelled,     // Order was cancelled; in-flight fills may still report
    Completed      // Order fully filled
}

Order

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

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

OrderData

The cross-chain payload used to compute the order ID and to fill or cancel an order on the destination chain. All addresses are 32 bytes and timestamps are 64-bit so the encoding is well-defined across chains:

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

FillParams

struct FillParams {
    uint128 amountOutToFill;
    bytes32 originRecipient;  // receives released tokenIn on the origin chain
    bytes32 refundAddress;    // bridge-fee refund address; zero == msg.sender
}

FilledAmounts

Tracked on both origin and destination chains:

struct FilledAmounts {
    uint128 amountInRefunded;  // released back to the order sender
    uint128 amountInReleased;  // released to solver(s) after fills
    uint128 amountOutFilled;   // tokenOut delivered to the recipient
}

FillReport

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

CancelReport

struct CancelReport {
    bytes32 orderId;
    bytes32 orderSender;
    bytes32 tokenIn;
    uint128 amountInToRefund;
}

Events

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

event OrderFilled(
    bytes32 indexed orderId,
    address indexed solver,
    uint128 amountInToRelease,
    uint128 amountOutFilled,
    bytes32 indexed messageId  // zero for same-chain fills
);

event OrderCompleted(bytes32 orderId);

// Cancellation and refunds
event OrderCancelled(bytes32 indexed orderId, bytes32 indexed messageId);

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

// Cross-chain reports received on the origin chain
event FillReported(
    bytes32 indexed orderId,
    address indexed originRecipient,
    uint128 amountInToRelease,
    uint128 amountOutFilled
);

event CancelReported(bytes32 indexed orderId);

// Configuration
event DestinationSupportUpdated(uint32 indexed destChainId, bool isSupported);

Error Codes

ErrorDescription
AmountInZeroOrder input amount cannot be zero
AmountOutZeroOrder output amount cannot be zero
FillAmountZeroFill amount must be greater than zero
InvalidDeadlineFill deadline is in the past
InvalidDestinationChainDestination chain is not supported, or is not the current chain
InvalidMsgValuemsg.value must be zero for same-chain fills and cancels
InvalidOrderStatusOrder is not in a valid status for this operation
InvalidOrderVersionOrder version doesn't match the contract version
InvalidRecipientRecipient or originRecipient is the zero address
InvalidReportFill or cancel report data is invalid, or breaks an invariant
InvalidReportSourceReport arrived from a chain other than the order's destination
InvalidSolverSolver cannot be the same address as the recipient
InvalidTimestampOrder createdAt is in the future
NotAuthorizedCaller is not authorized for this operation
OrderAlreadyExistsAn order with this ID already exists
OrderExpiredOrder fill deadline has passed
OrderIdMismatchComputed order ID doesn't match the provided ID
SameTokenOrderSame-chain orders cannot use the same token in and out
ZeroAdminAdmin address cannot be zero at initialization
ZeroPauserPauser address cannot be zero at initialization
ZeroPortalPortal address cannot be zero at deployment
ZeroSenderOrder sender cannot be the zero address
Copyright © M0 Foundation 2026