Tutorials → 03

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:

1 Withdrawal

Lock funds on the source chain

2 Deposit

Claim funds on the destination chain

3 Receipt

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")
Withdrawal locking

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:

StepChainVerification
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.
Why Merkle proofs matter

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