/Education. · 04.

Intent 104: Hub & Spoke, how an order travels

Two contracts on every chain, one message each way. Follow an order from createOrder() on the Hub to fillOrder() on the Spoke and back.

iLayer.io~8 min5 chapters
OrderHubOrderSpokeRouter
/Hub & Spoke./One order, two chains./01.createOrder()Hub verifies, takes custody, ACTIVE./02.RouterLayerZero or Axelar carries the id./03.fillOrder()Spoke checks, pays the recipient./04.SettledHub releases inputs to the solver.FILLEDcustody releasedPENDINGoutput deliveredCreate3 addressesno counterparty risk
/Hub & Spoke./01.createOrder()Hub verifies, takescustody, ACTIVE./02.RouterLayerZero or Axelarcarries the id./03.fillOrder()Spoke checks, paysthe recipient./04.SettledHub releases inputsto the solver.FILLEDcustody releasedPENDINGoutput deliveredCreate3 addressesno counterparty risk

iLayer’s smart contracts are the backbone of the network. They let a user and a solver exchange value without counterparty risk, through a locking mechanism that behaves like an atomic swap, mediated by the Router cross-chain messaging adapter.

The system is split into two contracts, both deployed on every supported chain:

  • OrderHub: order creation and withdrawal, custody of input tokens, settlement.
  • OrderSpoke: order filling and disbursement of output tokens.

On EVM chains both are deployed with Create3, so the Hub and the Spoke sit at the same deterministic address on every chain. Let’s follow one order through them.

/01 · Chapter.

Creating an order on the Hub

The user builds an Order: input tokens (ERC-20, ERC-721 or ERC-1155), the desired outputs on the destination chain, deadlines, destination chain id, the optional sponsored flag and the optional execution hook. Then the user signs it off-chain.

createOrder() on the OrderHub does four things in sequence:

  1. Verifies the signature (EIP-712 for wallets, EIP-1271 for smart contract accounts) and the request nonce, which blocks replay.
  2. Transfers the inputs from the user into the Hub. From this moment the Hub holds custody.
  3. Marks the order ACTIVE and emits OrderCreated with the orderId and nonce.
  4. Sends a cross-chain message through the chosen Router adapter (LayerZero or Axelar today) to the OrderSpoke on the destination chain, which registers the id as PENDING.

Nothing has crossed a bridge except a message. Your tokens are exactly where you left them, held by a contract you can withdraw from if the order expires.

/02 · Chapter.

The Router in the middle

Hub and Spoke never talk to LayerZero or Axelar directly. They hand a Message struct to the Router: which bridge, target chain id, receiver, payload, extra bridge data and sender. The Router dispatches it, or reverts with UnsupportedBridgingRoute() if that bridge is not implemented on this chain.

Only whitelisted contracts may call the Router. That keeps attackers away from the privileged messaging path and keeps the event log clean. On the receiving side the Router invokes onMessageReceived on the target contract, which is how the Spoke learns about a new pending order and how the Hub learns about a fill.

/03 · Chapter.

Filling an order on the Spoke

A solver calls fillOrder() on the OrderSpoke of the destination chain. Before anything moves, the Spoke checks that the order was registered, is valid, has not expired, has not already been filled, and that the caller is allowed: either it is the designated primary filler, or the primary deadline has passed and the order is open to anyone.

The solver then provides the output tokens. The Spoke verifies that balances match the amounts in the Order; any positive slippage stays in the Spoke. If the Order carries a hook, the Spoke executes it through the Executor, isolated against reentrancy. Finally the Spoke emits OrderFilled and sends a message back to the origin chain.

/04 · Chapter.

Settlement on the Hub

When the fill message arrives, the Hub marks the order FILLED, emits OrderSettled, and releases the input tokens it was holding to the solver’s funding wallet.

That closes the loop: the user has the outputs on the destination chain, the solver has the inputs on the source chain, and at no point did either side depend on the other’s honesty. The user could not take the inputs back after the fill, and the solver could not take them before it.

/05 · Chapter.

What can go wrong, and what the contracts do about it

The Hub and Spoke expose a list of named errors, and reading them is the fastest way to understand the guarantees:

  • RequestNonceReused, InvalidOrderSignature: no replay, no forged orders.
  • OrderDeadlinesMismatch, InvalidDeadline: the primary window must end before the deadline, and the deadline must respect the maximum span.
  • RestrictedToPrimaryFiller, OrderAlreadyFilled, OrderExpired: exactly one fill, by the right party, in time.
  • OrderCannotBeWithdrawn: the time buffer after expiry protects the solver from a withdraw racing a late fill.
  • ExternalCallFailed: a broken hook reverts the fill instead of leaving a half-executed order.

Every one of these is a rule the code enforces, not a promise in a document. Next we look at how quotes reach you before any of this starts.

/Read next.

How quotes reach you.

Before an order exists there is a request for quote. Next: the RFQ, and why it runs on Waku instead of a server.

All posts