Limit Order Protocol
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
amountInandamountOut - 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:
OrderBookcontracts 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:
- User opens order on Chain A
- Solver fills order on Chain A
- Tokens are exchanged atomically
For cross-chain orders:
- User opens order on Chain A (origin), locking input tokens
- Solver fills order on Chain B (destination), delivering output tokens to recipient
- Fill report is sent back to Chain A via the messaging layer
- 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:
| Parameter | Description |
|---|---|
destChainId | Destination chain where tokens will be delivered |
tokenIn | Input token address on origin chain |
tokenOut | Output token address on destination chain |
amountIn | Amount of input token to exchange |
amountOut | Expected amount of output token |
recipient | Address to receive tokens on destination chain |
fillDeadline | Timestamp 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
orderIdis 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
amountOutToFillin their fill parameters - A proportional amount of
tokenInis 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
amountOuthas 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;
| Scenario | Refund Available |
|---|---|
| Same-chain order cancelled | Immediately |
| Cross-chain order cancelled | After cancelRequestedAt + finalityBuffer |
| Order deadline expired | After fillDeadline + finalityBuffer |
The finality buffer is configured per destination chain and accounts for messaging latency and potential reorgs.
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
OrderBook has known edge cases with non-standard tokens. Review this
section carefully before whitelisting tokens.| Token Type | Behavior | Severity | Recommendation |
|---|---|---|---|
| Pausable tokens (USDC/USDT) | If paused during cross-chain fill, user may receive destination tokens AND claim origin refund (double-spend) | CRITICAL | Avoid for cross-chain orders or implement pause monitoring |
| Rebase tokens | If token rebases downward after order creation, reportFill() and claimRefund() may revert permanently, locking funds | HIGH | Do not use |
| Yield-bearing tokens | Yield accrues to OrderBook contract, not recoverable by user or solver | MEDIUM | Use short order lifetimes; configure fee recovery via MEarnerManager |
| Fee-on-transfer tokens | fillOrder() reverts via safeTransferExact; claimRefund() may lose fee amount | MEDIUM | Do not use |
Solver Safety
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
| Function | Description |
|---|---|
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
| Function | Description |
|---|---|
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
| Error | Description |
|---|---|
AmountInZero | Order input amount cannot be zero |
AmountOutZero | Order output amount cannot be zero |
FillAmountZero | Fill amount must be greater than zero |
FinalityPending | Finality buffer has not elapsed; cannot claim refund yet |
InvalidDeadline | Fill deadline is in the past or invalid |
InvalidDestinationChain | Destination chain is not supported |
InvalidFinalityBuffer | Finality buffer value is invalid |
InvalidNonce | Nonce doesn't match sender's expected nonce |
InvalidOrderStatus | Order is not in a valid status for this operation |
InvalidOrderVersion | Order version doesn't match contract version |
InvalidOriginChain | Origin chain ID doesn't match this chain |
InvalidRecipient | Recipient address is invalid |
InvalidReport | Fill report data is invalid |
NotAuthorized | Caller is not authorized for this operation |
OrderExpired | Order fill deadline has passed |
OrderAlreadyFilled | Order has already been completely filled |
OrderIdMismatch | Computed order ID doesn't match provided ID |
Related
- Accessing Liquidity - Overview of M0's liquidity infrastructure
- Portal V2 - Cross-chain bridge and messaging system
- M Portals (V1) - Previous portal architecture reference
- Orchestration API - API for requesting quotes and transaction payloads
- Source Code - Smart contract repository
M0 Extensions
Documentation of M0 Extensions, the application layer for building custom stablecoins on the M0 platform, including the SwapFacility for seamless 1:1 conversions.
M0 Portals
Documentation of M0's cross-chain architecture, including the M Portal (Wormhole) and M Portal Lite (Hyperlane) implementations for bridging M across blockchains.