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, 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
amountInandamountOut - 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:
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 |
sender | Address 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 theOrderBook - A unique
orderIdis 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
tokenInis 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.valueis forwarded to the Portal as the bridge feefillerParams_.refundAddressreceives any bridge overpayment; if zero, it defaults tomsg.sender- When the report is delivered,
reportFill()releases input tokens to the solver'soriginRecipient
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:
| Scenario | Who can cancel |
|---|---|
| Same-chain order, before deadline | The order's sender or recipient |
| Cross-chain order, before deadline | The order's recipient only |
Any order, after fillDeadline | Anyone - 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 type | What 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-chain | Status 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. |
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
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) | While the token is paused, fills and cancel settlement revert, so an order cannot complete until the token unpauses. Funds are not at risk. | LOW | Safe to whitelist - nearly every major stablecoin is pausable. Expect fills and refunds to stall for the duration of a pause. |
| Rebase tokens | If token rebases downward after order creation, reportFill() and reportCancel() 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 | MEDIUM | Do not use |
Solver Safety
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.
| Network | Internal Chain ID |
|---|---|
| Ethereum | 1 |
| Base | 8453 |
| Arbitrum One | 42161 |
| Monad | 143 |
| Moca Chain | 2288 |
| Citrea | 4114 |
| Rise | 4153 |
| Solana | 1399811149 |
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
| Function | Description |
|---|---|
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
| Function | Description |
|---|---|
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 |
VERSION | Order format version (1) |
portal | Immutable 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
| Error | Description |
|---|---|
AmountInZero | Order input amount cannot be zero |
AmountOutZero | Order output amount cannot be zero |
FillAmountZero | Fill amount must be greater than zero |
InvalidDeadline | Fill deadline is in the past |
InvalidDestinationChain | Destination chain is not supported, or is not the current chain |
InvalidMsgValue | msg.value must be zero for same-chain fills and cancels |
InvalidOrderStatus | Order is not in a valid status for this operation |
InvalidOrderVersion | Order version doesn't match the contract version |
InvalidRecipient | Recipient or originRecipient is the zero address |
InvalidReport | Fill or cancel report data is invalid, or breaks an invariant |
InvalidReportSource | Report arrived from a chain other than the order's destination |
InvalidSolver | Solver cannot be the same address as the recipient |
InvalidTimestamp | Order createdAt is in the future |
NotAuthorized | Caller is not authorized for this operation |
OrderAlreadyExists | An order with this ID already exists |
OrderExpired | Order fill deadline has passed |
OrderIdMismatch | Computed order ID doesn't match the provided ID |
SameTokenOrder | Same-chain orders cannot use the same token in and out |
ZeroAdmin | Admin address cannot be zero at initialization |
ZeroPauser | Pauser address cannot be zero at initialization |
ZeroPortal | Portal address cannot be zero at deployment |
ZeroSender | Order sender cannot be the zero address |
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.