> For the complete documentation index, see [llms.txt](https://docs.glacislabs.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.glacislabs.com/zero-delta/integration-guide/smart-contracts.md).

# Smart Contracts

Contract interface, data types, events, error codes, deployments, and coverage. For the REST API, see the API Reference. Compiler: Solidity 0.8.19 (optimizer enabled, 200 runs). License: BUSL-1.1

The contracts and error codes use the **ZDLite** identifier (the on-chain name for the product branded ZeroDelta). Contract sources carry an SPDX `BUSL-1.1` header (source-available); test sources are MIT. Integration and use are permitted.

> **Per-(chain, token) addresses:** the canonical source for token contract addresses on each chain is `GET /api/v1/tokens` (see Coverage and the Integration Guide). Addresses change as chains/tokens are onboarded; do not hardcode them.

### Data types

```solidity
struct OrderRequest {
    address owner;                // Order owner (the user); need not equal msg.sender
    address bidToken;             // Token being sold — always a local EVM address on the source chain
    uint256 bidTokenAmount;       // Amount of bidToken to sell
    bytes   askToken;             // Token being bought, on destinationChainId (20 packed bytes for EVM)
    uint256 askTokenAmount;       // Minimum acceptable output (fill threshold)
    bytes   destinationReceiver;  // Recipient on the destination chain (20 bytes EVM, 32 bytes Solana-style)
    uint64  destinationChainId;   // Target chain ID
    uint64  executionChainId;     // Chain this order clears on; must be in allowedExecutionChains
    uint64  deadline;             // Unix expiry (0 = no deadline)
}

struct Order {
    address owner;
    uint64  nonce;                // Auto-incremented, set by the contract (the first order from a chain is nonce 1)
    uint64  deadline;
    uint64  sourceChainId;        // Set by the contract
    uint64  destinationChainId;
    bytes   bidToken;
    uint256 bidTokenAmount;       // Actual amount escrowed (balance-based accounting)
    bytes   askToken;
    uint256 askTokenAmount;       // Minimum acceptable output (fill threshold)
    bytes   destinationReceiver;
    bytes   nextOrders;           // abi.encode(OrderRequest[]) of chained follow-ups; empty for normal orders
}

// Returned by getPendingChain(prevOrderId) for a chained order whose leg 1 has
// landed and whose funds are parked awaiting leg 2.
struct PendingChain {
    address owner;                // Who may claimChain() the parked funds
    address token;                // The parked asset (leg 1's output, leg 2's bid token)
    uint256 amount;               // Parked amount. ZERO means already continued or claimed.
    bytes   requests;             // abi.encode(OrderRequest[]) of the follow-up leg
}

enum OrderStatus {
    Nonexistent,
    Pending,
    Filled,
    Cancelled
}
```

**The ask side is `bytes`, not `address`.** `askToken` and `destinationReceiver` are chain-agnostic byte strings so a non-EVM destination can be expressed at all. For every EVM destination you pass the plain 20-byte address as `bytes` — a `0x`-prefixed 20-byte hex string in ethers/viem, exactly what the API returns in `orderRequests[]`. The contract rejects any other width than 20 or 32 bytes (`ZDLite__UnsupportedReceiver`), and requires exactly 20 when the destination chain is the order's own execution chain. `bidToken` stays an `address`: it is pulled with `IERC20` from `msg.sender`, so the bid side is always local and always EVM.

**Chain IDs are `uint64`** on both structs, and there is no `data` field: the old pass-through `bytes data` is now `Order.nextOrders`, which the contract owns (it carries the chained follow-up requests and is empty for a normal order).

**`PendingChain.amount == 0` is the resolved sentinel.** `getPendingChain` returns a zeroed struct for a chain that was never parked *and* for one already continued or claimed — the two are indistinguishable from the return value alone. Treat a zero `amount` as "nothing to act on" rather than as "claimable", and drive UI from the `ChainedOrderParked` / `ChainedOrderContinued` / `ChainedOrderClaimed` events when you need to tell those cases apart.

**`executionChainId` is per order.** Each request names the chain it clears on, and the contract checks it against the `allowedExecutionChains` registry — there is no single protocol-wide setting. The API fills this in for you (`orderRequests[].executionChainId`); a `0` or non-allowlisted value reverts (`ZDLite__InvalidExecutionChainId` / `ZDLite__ExecutionChainNotAllowed(uint256)`).

**On-chain enum vs. read-API status — two different vocabularies.** The on-chain `OrderStatus` enum above (`Nonexistent` / `Pending` / `Filled` / `Cancelled`) is what `orderStatuses(orderId)` returns. The **read API** (`GET /api/v1/transactions/{txHash}`) returns its own lowercase status string, **not** the enum names — if you poll the API, compare against the lowercase values, not `Filled` / `Cancelled`. The full read-API set (code-verified), with terminal classification:

<table data-search="false"><thead><tr><th>Status</th><th>Terminal?</th></tr></thead><tbody><tr><td><code>pending</code></td><td>no</td></tr><tr><td><code>in_progress</code></td><td>no</td></tr><tr><td><code>outbound_bridging</code></td><td>no</td></tr><tr><td><code>cancel_pending</code></td><td>no</td></tr><tr><td><code>awaiting_claim</code></td><td>no</td></tr><tr><td><code>continuing</code></td><td>no</td></tr><tr><td><code>delivered</code></td><td>yes (success)</td></tr><tr><td><code>expired</code></td><td>yes</td></tr><tr><td><code>cancelled</code></td><td>yes</td></tr><tr><td><code>fill_failed</code></td><td>yes</td></tr><tr><td><code>delivery_failed</code></td><td>yes</td></tr><tr><td><code>failed</code></td><td>yes</td></tr></tbody></table>

Happy path: `pending → in_progress → outbound_bridging → delivered`. This is the canonical set the Integration Guide poller mirrors; treat anything outside the non-terminal set (the first six) as terminal.

> **`continuing` belongs to `journeyStatus`, not `status`.** On a chained order the parent's per-order `status` reads `delivered` the moment leg 1 lands — that leg really is done — while the journey-scoped `journeyStatus` reads `continuing` because the value is parked awaiting leg 2, not in the receiver's wallet. Branch completion checks on `journeyStatus` (falling back to `status` when it is absent, as it is on non-chained orders). See the Integration Guide poller.

The order is identified by `orderId`, an immutable identity derived by the contract from `owner`, `nonce`, and `sourceChainId` (not a function of amounts). The contract computes it as `keccak256(abi.encode(owner, nonce, sourceChainId))` (code-verified against the live contract).

Do not precompute or reconstruct `orderId`. `nonce` and `sourceChainId` are assigned by the contract at submit, so an off-chain caller cannot know them in advance, and under concurrency a precomputed id can be wrong. Obtain `orderId` after submission from the `OrderSubmitted` event in the transaction receipt, or from an `eth_call` simulation of `submitOrder`. A normal state-changing send does not return the value to an off-chain caller.

On-chain events key on `orderId`; the read API is queryable by source transaction hash. Persist both: `orderId` for the on-chain cancel/read path (`getOrder`, `orderStatuses`), the source tx hash for API status polling. Note that the read API's `id` field is a **deployment-scoped uid** (for example `"rwa:0x52b1…"`), not `orderId` — the escrow's cancel/claim calls take `orderId`, and passing `id` there reverts.

***

### Integrator-relevant functions

```solidity
// ── Source chain ───────────────────────────────────────────
// Submit one or more chained orders. Pulls requests[0].bidToken from msg.sender,
// bridges it to that request's execution chain, emits OrderSubmitted.
// msg.value covers the bridge fee. Returns (orderId, order) on-chain; off-chain
// callers do NOT receive these from a normal send — read orderId from the
// OrderSubmitted event, or use eth_call to simulate.
//
//   requests   — length 1 (normal order) or 2 (chained: request 0's swap output
//                funds request 1). The API returns exactly this array as
//                `orderRequests`; pass it through unchanged.
//   partnerId  — pass the quote's 32-byte string marker when it returns one;
//                otherwise pass bytes32(0). The escrow emits a non-zero
//                value as PartnerOrder(partnerId); the off-chain listener verifies
//                the marker against the submitted order.
function submitOrder(OrderRequest[] calldata requests, bytes32 partnerId)
    external payable returns (bytes32 orderId, Order memory order);

// ── Execution chain: cancel / claim ───────────────────────────────
function cancel(Order calldata order) external;                  // owner cancels; funds released once arrived
function claimCancellation(Order calldata order) external;       // withdraw a pre-arrival cancellation once it lands
function cancelFor(Order calldata order, bytes calldata signature) external; // gasless: a relayer submits the owner's signed cancel

// ── Execution chain: chained orders (only when order.nextOrders is non-empty) ─
// continueChain is PERMISSIONLESS and PAYABLE: anyone may push the journey
// forward, and the caller pays leg 2's bridge fee out of their own msg.value.
// claimChain is OWNER-ONLY: only the parked order's owner can take the funds back.
function continueChain(bytes32 prevOrderId) external payable returns (bytes32 orderId, Order memory order);
function claimChain(bytes32 prevOrderId) external;               // owner takes the parked funds back instead

// ── Views ───────────────────────────────────────────────
// getOrder / orderStatuses resolve the canonical order state on the order's
// EXECUTION chain. A source-chain instance will not hold the canonical record
// post-bridge (returns Nonexistent).
function getOrder(bytes32 orderId) external view returns (Order memory);
function orderStatuses(bytes32 orderId) external view returns (OrderStatus);
function getPendingChain(bytes32 prevOrderId) external view returns (PendingChain memory);
function isSupportedToken(uint256 chainId, address token) external view returns (bool);
function isSupportedToken(uint256 chainId, bytes memory token) external view returns (bool);
function getBridgeAdapter(address token, uint256 destChainId) external view returns (address);
function getEquivalentToken(uint256 destChainId, address destToken) external view returns (address);
function getEquivalentToken(uint256 destChainId, bytes memory destToken) external view returns (address);
function allowedExecutionChains(uint256 chainId) external view returns (bool);
function domainSeparator() external view returns (bytes32);      // EIP-712 domain, for cancelFor relayers
```

`getOrder` and `orderStatuses` are **execution-chain** views. Every order names its own execution chain in `OrderRequest.executionChainId`; the quote surfaces the resolved pair as `executionChainId` + `executionEscrowAddress`, and that is the contract to read and to cancel against. Point a provider at that chain — not at the source chain — before calling. See the cancel snippet in the Integration Guide.

> **`getOrder` is only populated after arrival.** Before the order bridges and arrives on its execution chain, `getOrder(orderId)` returns a zeroed struct. Passing that to `cancel()` derives `keccak256(0, 0, 0)` and reverts with `ZDLite__UnauthorizedCaller` — which looks like a permissions problem and is not one. Persist the `Order` tuple from the `OrderSubmitted` event at submit time; pre-arrival it is the only copy in existence.

**Chained orders: who may call what.** `continueChain(prevOrderId)` is **permissionless** — any address may call it to push a parked journey forward — and it is **`payable`**, because the caller funds leg 2's bridge messaging fee from their own `msg.value`. That is deliberate: a relayer or the counterparty can complete a journey the user has walked away from. `claimChain(prevOrderId)` is the opposite: **owner-only**, and it abandons the follow-up leg to return the parked funds. Both are no-ops once the chain is resolved (`PendingChain.amount == 0`).

**Direct-bridged orders store no state.** When a single request's ask token is the same asset as its bid token on the destination chain (a same-ticker lane, e.g. USDC → USDC), `submitOrder` bridges straight to `destinationReceiver` and emits **only** `OrderDirectBridged`. No order is stored, nothing arrives, nothing fills — and there is nothing to cancel: `cancel()` on such an id registers a harmless pre-cancellation and `claimCancellation` reverts `ZDLite__OrderNotArrived`. `askTokenAmount` acts as a minimum-delivered floor. The API flags these quotes with `isDirect: true`; suppress cancel affordances for them.

**Gasless cancel (`cancelFor`).** The owner signs an EIP-712 `Cancel` message and any relayer submits it. Domain: `name = "ZDLiteEscrow"`, `version = "1"`, `chainId` = the execution chain, `verifyingContract` = that chain's escrow (read `domainSeparator()` to verify). Typed struct: `Cancel(address owner,uint64 orderNonce,uint64 sourceChainId)` — the same triple that derives `orderId`. Replay protection is the order's one-way status machine, not a separate nonce: once the cancel lands the status is no longer `Pending`/`Nonexistent`, so a replay reverts. Contract signatures (EIP-1271) are accepted.

The contract also exposes a `nonce()` view, but do not use it to precompute `orderId`: `nonce` is contract-assigned and advances as orders are submitted, so a read-then-derive sequence is racy under concurrency. Always read `orderId` back from the `OrderSubmitted` event instead.

The contract also exposes administrative functions (token/adapter registration, execution-chain allowlisting, pause, role management) that are operator-only and not part of the integration surface. The full interface is available in the OpenAPI/ABI package provided on onboarding.

***

### Events

| Event                                              | Indexed     | Emitted when                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| -------------------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `OrderSubmitted(bytes32 orderId, Order order)`     | `orderId`   | An order is submitted on the source chain.                                                                                                                                                                                                                                                                                                                                                                                                               |
| `OrderDirectBridged(bytes32 orderId, Order order)` | `orderId`   | A same-token direct bridge is submitted — emitted **instead of** `OrderSubmitted`, and terminal at submit.                                                                                                                                                                                                                                                                                                                                               |
| `OrderArrived(bytes32 orderId, Order order)`       | `orderId`   | An order arrives on the execution chain (or same-chain submit).                                                                                                                                                                                                                                                                                                                                                                                          |
| `OrderFilled(bytes32 orderId, Order order)`        | `orderId`   | An order is filled.                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `OrderCancelled(bytes32 orderId, Order order)`     | `orderId`   | A cancel returns funds.                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `PartnerOrder(bytes32 partnerId)`                  | `partnerId` | A submit carried a non-zero `partnerId`. The off-chain listener verifies the quoted marker against the submitted order; the escrow does not verify it. A marker from a quote that carried `feeRecipients` is an **integrator-fee marker**: that is how the fee is attributed to the order. No contract change is involved: the escrow is unchanged and the fee never touches the contracts — it is taken from the fill's output and accounted off-chain. |

Pre-arrival cancellations and claims emit their own events (`OrderPreCancelled`, `EscrowClaimed`), and chained orders emit `ChainedOrderParked` / `ChainedOrderContinued` / `ChainedOrderClaimed`, all keyed by the funding order's id. See the ABI package for the complete list.

***

### Error codes

The errors integrators encounter most often:

All names below are code-verified against the deployed contract source. Match by selector against these exact signatures — **including parameters**: an error that carries arguments hashes to a different selector than its bare name, so deriving a selector from the name alone will never match the revert data.

<table data-search="false"><thead><tr><th>Error</th><th>Meaning</th></tr></thead><tbody><tr><td><code>ZDLite__UnsupportedToken</code></td><td>The bid or ask token is not registered on that chain.</td></tr><tr><td><code>ZDLite__BridgeAdapterNotFound</code></td><td>No adapter is registered for the <code>(token, destChain)</code> route.</td></tr><tr><td><code>ZDLite__ZeroAddress</code></td><td>A required address (<code>owner</code> or <code>destinationReceiver</code>) is the zero address; both must be real addresses.</td></tr><tr><td><code>ZDLite__ZeroAmount</code></td><td><code>bidTokenAmount</code> (or a required amount) is zero.</td></tr><tr><td><code>ZDLite__UnsupportedReceiver</code></td><td><code>destinationReceiver</code> is not 20 or 32 bytes — or not exactly 20 when the destination chain is the order's execution chain.</td></tr><tr><td><code>ZDLite__InvalidExecutionChainId</code></td><td><code>executionChainId</code> is <code>0</code>.</td></tr><tr><td><code>ZDLite__ExecutionChainNotAllowed(uint256)</code></td><td><code>executionChainId</code> is not in the escrow's allowlist. The revert data carries the rejected chain id, so the selector is that of the <code>(uint256)</code> signature — not of the bare name.</td></tr><tr><td><code>ZDLite__NoOrderRequests</code></td><td>The <code>requests</code> array is empty.</td></tr><tr><td><code>ZDLite__TooManyOrderRequests</code></td><td>More than two requests were submitted (max chain length is 2).</td></tr><tr><td><code>ZDLite__InvalidDeadline</code></td><td><code>deadline</code> is in the past (and not <code>0</code>).</td></tr><tr><td><code>ZDLite__OrderExpired</code></td><td>The order's deadline passed before it could be filled.</td></tr><tr><td><code>ZDLite__InsufficientAskToken(expected, actual)</code></td><td>A fill could not meet the user's minimum output; the fill reverts.</td></tr><tr><td><code>ZDLite__DirectBridgeInsufficientAmount(askTokenAmount, actualAmount)</code></td><td>A direct bridge would deliver less than the <code>askTokenAmount</code> floor.</td></tr><tr><td><code>ZDLite__OrderNotPending</code></td><td>The order is not in <code>Pending</code> state for the requested action.</td></tr><tr><td><code>ZDLite__OrderNotArrived</code></td><td>A cancel/claim was attempted before the order arrived on the execution chain.</td></tr><tr><td><code>ZDLite__OrderAlreadyArrived</code></td><td>Re-delivery guard (bridge replay protection).</td></tr><tr><td><code>ZDLite__OrderAlreadyFilled</code></td><td>The order was already filled.</td></tr><tr><td><code>ZDLite__OrderNotCancellable</code></td><td>The order is not in a cancellable state.</td></tr><tr><td><code>ZDLite__UnauthorizedCaller</code></td><td>Caller is not permitted for that action (e.g. cancel by non-owner) — also what a zeroed <code>Order</code> struct produces, see the <code>getOrder</code> note above.</td></tr><tr><td><code>ZDLite__InvalidSignature</code></td><td>A <code>cancelFor</code> signature did not validate against <code>owner</code>.</td></tr><tr><td><code>ZDLite__ChainedTokenMismatch</code> / <code>ZDLite__ChainedChainMismatch</code></td><td>A two-request submit does not satisfy <code>askToken[0] == bidToken[1]</code> and <code>destinationChainId[0] == executionChainId[1]</code>.</td></tr><tr><td><code>ZDLite__ChainAlreadyParked</code></td><td>A chained order's funds are already parked for that funding order id — the park step cannot run twice.</td></tr><tr><td><code>ZDLite__ChainNotParked</code> / <code>ZDLite__ChainAlreadyResolved</code></td><td><code>continueChain</code> / <code>claimChain</code> was called for a chain that is not parked, or was already continued or claimed.</td></tr></tbody></table>

These are the common subset an integrator hits; the complete error set ships in the ABI package.

**On underpaid `msg.value`:** there is no `ZDLite__` error for an insufficient bridge fee. If `msg.value` does not cover the bridge adapter's native messaging fee, `submitOrder` reverts with the bridge adapter's own error, which is adapter-specific (CCTP vs LayerZero) — do not match it by a `ZDLite__` name. Send `gasCostSource` from the quote. See the Integration Guide.

> **Buffer the fee on LayerZero lanes only — never on CCTP.** On a LayerZero-source order the escrow passes `msg.sender` as the OFT `refundAddress`, so the adapter refunds whatever excess you send; a small buffer (say +10–20%) is cheap insurance against the fee moving between quote and inclusion. CCTP (USDC) lanes are the opposite: they quote `gasCostSource: 0`, and the CCTP adapter's refund-address parameter is declared but **unused**, so native value attached to a USDC order is **stranded in the contract, not refunded**. Send exactly the quoted `gasCostSource` on CCTP lanes.

**On a paused contract:** pause uses OpenZeppelin `PausableUpgradeable` (`whenNotPaused`), so a submission against a paused contract reverts with OpenZeppelin's `EnforcedPause` selector (older deployments: the `"Pausable: paused"` revert string), not a `ZDLite__` error. Note that the pause also freezes `cancel`, `claimCancellation`, `continueChain`, `claimChain`, and the bridge-delivery entrypoints.

***

### Deployments

ZeroDelta runs two live deployments: **`prod`** (production) and **`dev`** (pre-production). They are independent on-chain deployments — same nine chains, same execution-chain allowlist, entirely different addresses — and are reached through different API base URLs and API keys. The addresses below are the **`prod` deployment**; `dev` addresses are not published here, and integrators read them from `GET /api/v1/chains` on their `dev` base URL.

The escrow is deployed at the same address across all 9 supported chains, and Ethereum is the default execution chain.

* **Escrow (`prod`, all chains):** `0x5e25c8ABc19b88d6A7Ab0D805C77A34987a68b40`
* **Roles (`prod`, all chains):** `0x4248EA01aB83541d1406e23250FCAE6c29D2fF13`
* **Default execution chain:** Ethereum (`chainId 1`).
* **Allowed execution chains:** Ethereum (`1`), Base (`8453`), Arbitrum (`42161`), Plume (`98866`). An order's `executionChainId` must be one of these; the API picks it for you.

> **Confirm your environment before you wire addresses in.** The deployment name scopes the CREATE2 salt, so **every address rotates between deployments** — the `dev` deployment does not share a single address with `prod`, even for the same contract on the same chain. Use the base URL and deployment you were given at onboarding, and read addresses from `GET /api/v1/chains` for that environment rather than hardcoding them.

`GET /api/v1/chains` is canonical: per chain it returns the escrow address, the bridge adapter addresses, the roles-contract address, and whether the chain is an execution chain. That makes the `submitOrder` requirement "a bridge adapter must exist for `(bidToken, executionChainId)`" checkable from the API before you submit. Keep the chain set in sync with the Integration Guide coverage matrix.

***

### Coverage

* **Bridged bid stablecoins:** USDC (Circle CCTP V2), USDT and USDe (LayerZero V2 OFT) are the core set; USDtb, AUSD, PYUSD and USDG are also bridge-configured over LayerZero OFT.
* **Ask-only outputs:** additional tokens can be bought but not bridged — DAI, RLUSD, USD1, USDS, wM, rUSDY, PAXG, USDY, sUSDf, USDf, USD3 and PRIME — plus the tokenized-asset set (13: eleven Nest `n*` vault tokens and two Centrifuge `de*` tokens). They are delivered on the chain the order executes on, so `destinationChainId` must equal `executionChainId` for those. XAUT is the exception among tokenized assets — it carries its own OFT mesh and can be delivered cross-chain.
* **Chains:** Ethereum (default execution chain), Optimism, Arbitrum, Base, Ink, Unichain, Plasma, Sonic, Plume. Chain IDs and the per-cell bridge matrix are in the Integration Guide.
* **Per-(chain, token) addresses:** retrieved from `GET /api/v1/tokens` (e.g. `?chainId=42161`), which returns `symbol`, `chainId`, `address`, and `decimals` per token. This is the canonical source; addresses are not hardcoded here because they change as coverage expands.
* Coverage is not uniform per (chain, token) cell — `GET /api/v1/routes` returns the live per-cell set. New chains, tokens, and routes are onboarded on partner request. The live `/chains`, `/tokens`, and `/routes` endpoints are the source of truth for what is enabled in your environment.

***

### Audit

Halborn and Sherlock have audited the ZeroDelta (ZDLite) smart contracts; all important findings addressed. Report available on request.
