Architecture at a glance
What each component does
Frontend (React)
Connects wallets, authenticates via SIWE (EVM) or SEP-10 (Stellar), and lets users create ads, submit orders, and monitor status.
OrderPortal
On the source chain. Bridgers call
createOrder to deposit tokens and register the order hash with the MerkleManager. unlock pays the Maker’s recipient once the ZK proof verifies.AdManager
On the destination chain. Makers call
createAd and fundAd to offer liquidity, then lockForOrder to reserve funds against a specific order. unlock pays the Bridger’s recipient once the ZK proof verifies.MerkleManager
Maintains an append-only Merkle Mountain Range of every order hash. Both AdManager and OrderPortal hold
MANAGER_ROLE to append entries.Verifier
Checks UltraHonk zero-knowledge proofs on-chain. AdManager and OrderPortal call it internally during every
unlock.wNativeToken
Wraps the chain’s native asset (ETH, XLM) on deposit and unwraps on withdrawal, so native-token bridging works through the same ERC-20-style interface.
Backend Relayer
During the pre-auth phase, coordinates the flow: pre-authorizes trades, watches for on-chain callbacks, triggers proof generation, and submits proofs to both chains. Cannot redirect funds — only submit proofs.
Proof Circuits (Noir)
The deposit circuit validates the order hash, checks MMR inclusion, and verifies nullifiers. The auth circuit (in progress) aggregates Maker + Bridger BLS signatures.
The 12-step cross-chain flow
The scenario below follows a single transfer end-to-end. Each step is what actually happens on-chain or in the relayer.Maker creates an ad on the destination chain
The Maker calls
createAd on AdManager, locking their liquidity against a specific route (for example, wETH on Stellar for ETH on Sepolia). The ad becomes discoverable in the marketplace.Bridger requests pre-authorization
Before submitting an on-chain order, the Bridger asks the backend relayer to pre-authorize the trade. The relayer validates that the ad is still open, the route is configured, and the parameters are sane.
Relayer returns a signed approval
The relayer returns a pre-authorization back to the user, which they include when creating the order on-chain. This is the transitional gate that the upcoming BLS aggregation layer replaces — see the roadmap.
Bridger creates the order on the source chain
The Bridger calls
createOrder on OrderPortal, depositing source-chain tokens. Native tokens are automatically wrapped through wNativeToken. The deposit is now locked in the contract.OrderPortal appends the order hash to the source-chain MMR
Inside the same transaction, OrderPortal appends the EIP-712 order hash to the source chain’s MerkleManager MMR tree. The tree is append-only, so the record is permanent and Merkle-provable.
User triggers the relayer callback
Once the source-chain transaction confirms, the user triggers a callback to the relayer. The relayer checks the confirmation on-chain — it does not trust a self-reported status.
Relayer fetches Merkle proof + order data
The relayer pulls the MMR inclusion proof for the order hash plus the raw order data from the source chain. These are the inputs the proof circuit needs.
Relayer runs the deposit proof circuit
The relayer invokes the Noir deposit circuit. The circuit:
- Recomputes the order hash from the struct fields and checks it matches.
- Validates the MMR inclusion proof against the claimed root.
- Verifies the nullifier is unused.
- Outputs a compact ZK proof.
Relayer submits the proof to AdManager (destination chain)
The relayer calls
AdManager.unlock(proof, ...) on the destination chain.Destination chain verifies and pays the Bridger
AdManager’s internal Verifier checks the proof. On success, the Maker’s locked liquidity is released to the Bridger’s recipient address on the destination chain.
Relayer submits the proof to OrderPortal (source chain)
The relayer calls
OrderPortal.unlock(proof, ...) on the source chain with the corresponding proof for the other side of the trade.Settlement on the two chains is not a single atomic transaction — the relayer submits a proof to each chain independently. Atomicity comes from the fact that both proofs reference the same order hash and nullifier: neither party can receive funds twice, and neither can receive funds without the opposite side’s deposit being proven.
What zero-knowledge proofs give you
Trustless verification
The on-chain Verifier confirms that the deposit really happened and the terms match, without trusting the relayer. The proof is the only thing that unlocks funds.
Replay prevention
Each proof consumes a unique nullifier. Once used, the same proof cannot be reused to drain funds a second time.
Tamper-proof history
Deposits are recorded in an append-only MMR tree per chain. The circuit checks inclusion against this tree, so tampering with the record would invalidate every proof that follows.
Chain-bound orders
EIP-712 order hashes bind the trade to a specific chain ID and contract address. A proof valid on one chain cannot be replayed on another.
Security guarantees
- EIP-712 order hashes bind every trade to specific chain IDs and contract addresses, preventing cross-chain or cross-contract replay.
- Bidirectional chain linking means each contract only accepts proofs from its configured counterpart chain.
- Manager role permissions on MerkleManager restrict which contracts can append new order hashes.
- Collateral locking requires Makers to have funds locked before any order is matched — so a Bridger’s destination tokens exist before the trade starts.
Next steps
Quickstart
Run your first cross-chain transfer on testnet.
Smart contracts
Full contract reference, functions, and deployed addresses.
MMR commitments
How the append-only deposit log works.
Run locally
Bootstrap the full stack on your machine.