Cross-Chain Transfers
Move value between parent and child chains using withdrawal-deposit pairs and Merkle proofs. Trustless, no bridge needed.
How Cross-Chain Transfers Work
Unlike bridge-based systems (where you trust a third party to relay), Lattice's cross-chain transfers are native. The protocol itself verifies transfers using Merkle proofs embedded in the chain hierarchy.
The flow has three steps:
Lock funds on the source chain
Claim funds on the destination chain
Finalize and prove the transfer completed
Step 1: Create a Withdrawal on the Parent Chain
A WithdrawalAction locks funds on the parent chain. The nonce uniquely identifies this transfer, and the demander specifies who can claim the funds on the other side.
import Foundation
import Lattice
let sender = CryptoUtils.generateKeyPair()
let receiver = CryptoUtils.generateKeyPair()
// The demander hash identifies who can claim these funds
let demanderHash = CryptoUtils.sha256(receiver.publicKey)
// Create a unique nonce for this transfer
let transferNonce: UInt128 = 1
// Step 1: Withdraw 500 from the parent chain
let withdrawal = WithdrawalAction(
withdrawer: CryptoUtils.sha256(sender.publicKey),
nonce: transferNonce,
demander: demanderHash,
amountDemanded: 500,
amountWithdrawn: 500
)
let withdrawBody = TransactionBody(
accountActions: [],
actions: [],
depositActions: [],
genesisActions: [],
peerActions: [],
receiptActions: [],
withdrawalActions: [withdrawal],
signers: [CryptoUtils.sha256(sender.publicKey)],
fee: 1,
nonce: 0
)
// Sign and submit to the parent chain
let wData = withdrawBody.toData()
let wHash = CryptoUtils.sha256(String(data: wData, encoding: .utf8)!)
let wSig = CryptoUtils.sign(message: wHash, privateKeyHex: sender.privateKey)!
let withdrawTx = Transaction(
signatures: [sender.publicKey: wSig],
body: withdrawBody
)
await node.submitTransaction(directory: "Nexus", transaction: withdrawTx)
print("Withdrawal submitted to parent chain")
Once mined, the WithdrawalAction locks the specified amount in the parent chain's withdrawal state. The funds are debited from the sender's account but not yet credited anywhere — they're in limbo until the deposit side completes.
Step 2: Deposit on the Child Chain
The receiver creates a DepositAction on the child chain. The nonce must match the withdrawal, proving these are two halves of the same transfer.
// Step 2: Deposit the same amount on the child chain
let deposit = DepositAction(
nonce: transferNonce, // must match the withdrawal
demander: demanderHash, // must match the withdrawal
amountDemanded: 500, // must match the withdrawal
amountDeposited: 500
)
let depositBody = TransactionBody(
accountActions: [],
actions: [],
depositActions: [deposit],
genesisActions: [],
peerActions: [],
receiptActions: [],
withdrawalActions: [],
signers: [CryptoUtils.sha256(receiver.publicKey)],
fee: 1,
nonce: 0
)
let dData = depositBody.toData()
let dHash = CryptoUtils.sha256(String(data: dData, encoding: .utf8)!)
let dSig = CryptoUtils.sign(message: dHash, privateKeyHex: receiver.privateKey)!
let depositTx = Transaction(
signatures: [receiver.publicKey: dSig],
body: depositBody
)
await node.submitTransaction(directory: "TokenLedger", transaction: depositTx)
print("Deposit submitted to child chain")
Step 3: Receipt on the Parent Chain
The final step: a ReceiptAction on the parent chain that proves the deposit was completed. This closes the loop and finalizes the locked withdrawal.
// Step 3: Receipt proves the deposit completed
let receipt = ReceiptAction(
withdrawer: CryptoUtils.sha256(sender.publicKey),
nonce: transferNonce,
demander: demanderHash,
amountDemanded: 500,
directory: "TokenLedger" // identifies which child chain received the deposit
)
let receiptBody = TransactionBody(
accountActions: [],
actions: [],
depositActions: [],
genesisActions: [],
peerActions: [],
receiptActions: [receipt],
withdrawalActions: [],
signers: [CryptoUtils.sha256(sender.publicKey)],
fee: 1,
nonce: 1
)
let rData = receiptBody.toData()
let rHash = CryptoUtils.sha256(String(data: rData, encoding: .utf8)!)
let rSig = CryptoUtils.sign(message: rHash, privateKeyHex: sender.privateKey)!
let receiptTx = Transaction(
signatures: [sender.publicKey: rSig],
body: receiptBody
)
await node.submitTransaction(directory: "Nexus", transaction: receiptTx)
print("Receipt submitted — transfer complete")
The Verification Flow
Each step is verified by the protocol before inclusion in a block:
| Step | Chain | Verification |
|---|---|---|
| Withdrawal | Parent | Sender has sufficient balance. Nonce is unique. Funds are locked. |
| Deposit | Child | Matching withdrawal exists in parent state (verified via Merkle proof from parent block anchored in child). Nonce, demander, and amount match. |
| Receipt | Parent | Matching deposit exists in child state (verified via Merkle proof from child block referenced in parent). Finalizes the locked withdrawal. |
The child chain can verify the withdrawal exists on the parent without trusting anyone — it reads the proof directly from the parent block that's anchored in the child chain. Similarly, the parent verifies the deposit via child block references.
This is fundamentally different from bridge-based systems: there's no multisig, no validator committee, no trust assumption beyond the chains themselves.
Transfer Lifecycle Diagram
Parent (Nexus) Child (TokenLedger)
│ │
│ ① Withdrawal │
│ Lock 500 tokens │
│ nonce: 1 │
│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶│
│ │ ② Deposit
│ Merkle proof │ Credit 500 tokens
│ of withdrawal │ Verify nonce: 1
│◀─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│
│ ③ Receipt │
│ Finalize lock │
│ Verify deposit proof │
│ │
▼ ▼
Withdrawal closed Funds available
Key Constraints
- Nonce uniqueness — each transfer gets a unique
UInt128nonce. A nonce can only be used once across the withdrawal-deposit-receipt triplet. - Amount matching —
amountDemandedmust be identical across all three actions. The protocol rejects mismatches. - Demander binding — only the keyholder whose hash matches
demandercan create the deposit. This prevents front-running. - Ordering — withdrawal must be mined before deposit can be verified; deposit must be mined before receipt can be verified. The protocol enforces this via state checks.