Tenzro Testnet is live —request testnet TNZO
← Back to Tutorials

Canton Enterprise DvP Settlement & RWA Tokenization

DeFiAdvanced40 min

Build an institutional DeFi agent on Canton (DAML 3.x) using the Tenzro Rust SDK and the Canton MCP tools. This tutorial covers the complete lifecycle of a Delivery-vs-Payment transaction: allocating DAML parties, tokenizing a US Treasury bond via CIP-56, funding participants with Canton Coin, executing an atomic DvP settlement where both legs (delivery + payment) settle or neither does, and finalizing the record on the Tenzro Ledger with a verifiable credential.

What You'll Build

  • A TDIP identity and MPC wallet via the Rust SDK
  • An enterprise agent with Canton DvP capabilities
  • DAML party allocation for buyer and seller counterparties
  • Real-world asset (RWA) tokenization of a US Treasury bond via CIP-56
  • Canton Coin balance queries and transfers
  • Atomic Delivery-vs-Payment settlement (bond + cash, all-or-nothing)
  • DAML escrow contract creation and lifecycle management
  • Contract event queries and fee schedule inspection
  • DAR package upload for custom settlement logic
  • Settlement finality on the Tenzro Ledger with a verifiable credential

Canton MCP Tools Used

This tutorial uses all 14 tools available on the Canton MCP server (port 3005):

  • canton_get_health — participant health check
  • canton_list_domains — available synchronizer domains
  • canton_allocate_party — create DAML parties
  • canton_list_parties — list all parties
  • canton_create_asset — CIP-56 asset tokenization
  • canton_get_balance — Canton Coin balances
  • canton_transfer — Canton Coin transfers
  • canton_dvp_settle — atomic Delivery-vs-Payment
  • canton_submit_command — DAML create/exercise commands
  • canton_list_contracts — query active contracts
  • canton_get_events — contract event stream
  • canton_get_fee_schedule — synchronizer fees
  • canton_upload_dar — upload DAML packages
  • canton_get_transaction — transaction lookup

What is Delivery-vs-Payment?

DvP (Delivery-vs-Payment) is the gold standard for securities settlement. It guarantees that the delivery of the security (e.g., a bond) and the payment (e.g., cash) happen atomically — either both legs complete or neither does. This eliminates counterparty risk: the buyer never pays without receiving the bond, and the seller never delivers without receiving payment. Canton's DAML runtime enforces this atomicity at the smart contract level, and CIP-56 provides the tokenization standard for the underlying assets. CIP-56 tokens used in DvP flows are cross-VM accessible via the Sei V2 pointer model — the same asset is visible as an ERC-20 on EVM, an SPL token on SVM, and a CIP-56 holding on Canton, all sharing a single native balance.

Prerequisites

[dependencies]
tenzro-sdk = "0.1"
tokio = { version = "1", features = ["full"] }
tracing-subscriber = "0.3"
uuid = { version = "1", features = ["v4"] }
serde_json = "1"

Step 1: Connect & Provision Identity

use tenzro_sdk::{TenzroClient, SettlementRequest, config::SdkConfig};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = SdkConfig::testnet();
    let client = TenzroClient::connect(config).await?;
// Register a human identity via TDIP
let identity = client.identity().register_human("Canton Fund Manager").await?;
println!("DID: {}", identity.did);

// Create an MPC wallet
let wallet = client.wallet().create_wallet().await?;
println!("Wallet: {}", wallet.address);

Step 2: Register the Canton Agent

// Register the Canton DvP agent
let agent = client.agent().register(
    "canton-dvp-agent",
    "Canton DvP Settler",
    &["enterprise", "canton", "daml", "dvp"],
).await?;
println!("Agent ID: {}", agent.agent_id);

Step 3: Check Canton Health & Domains

// Check Canton network health and available domains
let health = client.agent().send_message(
    &agent.agent_id,
    "Use canton_get_health tool",
).await?;
println!("Canton health: {}", health.payload);

let domains = client.agent().send_message(
    &agent.agent_id,
    "Use canton_list_domains tool",
).await?;
println!("Domains: {}", domains.payload);
Canton health: { "status": "healthy", "participant": "tenzro-participant-1", "api_version": "v2" }
Domains: [
  { "id": "tenzro-domain-1", "status": "active", "participants": 3 },
  { "id": "global-domain",   "status": "active", "participants": 12 }
]

Step 4: Allocate DAML Parties

Every Canton transaction involves parties. Allocate a buyer and seller party for the DvP:

// Allocate DAML parties for the transaction
let buyer = client.agent().send_message(
    &agent.agent_id,
    "Use canton_allocate_party tool: \
     party_id_hint=buyer-fund-a, \
     display_name=Fund A (Buyer)",
).await?;
println!("Buyer party: {}", buyer.payload);

let seller = client.agent().send_message(
    &agent.agent_id,
    "Use canton_allocate_party tool: \
     party_id_hint=seller-bank-b, \
     display_name=Bank B (Seller)",
).await?;
println!("Seller party: {}", seller.payload);

// List all allocated parties
let parties = client.agent().send_message(
    &agent.agent_id,
    "Use canton_list_parties tool",
).await?;
println!("All parties: {}", parties.payload);
Buyer party:  { "party_id": "buyer-fund-a::tenzro-participant-1", "display_name": "Fund A (Buyer)" }
Seller party: { "party_id": "seller-bank-b::tenzro-participant-1", "display_name": "Bank B (Seller)" }
All parties: [
  { "party_id": "buyer-fund-a::tenzro-participant-1",  "display_name": "Fund A (Buyer)" },
  { "party_id": "seller-bank-b::tenzro-participant-1", "display_name": "Bank B (Seller)" }
]

Step 5: Tokenize a Bond (CIP-56)

CIP-56 is Canton's standard for tokenizing real-world assets. Tokenize a US Treasury bond with full ISIN metadata:

// Tokenize a US Treasury bond via CIP-56
let asset = client.agent().send_message(
    &agent.agent_id,
    "Use canton_create_asset tool: \
     owner=seller-bank-b, \
     asset_type=bond, \
     quantity=1000000, \
     metadata={\"isin\": \"US912828ZT58\", \"coupon\": \"2.5%\", \"maturity\": \"2030-11-15\"}",
).await?;
println!("Asset created: {}", asset.payload);
Asset created: {
  "asset_id": "bond-us912828zt58",
  "owner": "seller-bank-b",
  "type": "bond",
  "quantity": 1000000,
  "isin": "US912828ZT58",
  "coupon": "2.5%",
  "maturity": "2030-11-15"
}

Cross-VM Token Visibility

In the cross-VM architecture, CIP-56 assets created on Canton are automatically registered in the unified TokenRegistry. This means the bond tokenized above is immediately queryable from EVM (via the wTNZO ERC-20 pointer contract) and SVM (via the SPL adapter) without any manual bridging. All VMs share the same underlying native balance — there is no liquidity fragmentation. See the Cross-VM Token Architecture documentation for details.

Step 6: Check Balances & Fund the Buyer

// Check Canton Coin balances for both parties
let buyer_balance = client.agent().send_message(
    &agent.agent_id,
    "Use canton_get_balance tool: party=buyer-fund-a",
).await?;
println!("Buyer balance: {}", buyer_balance.payload);

let seller_balance = client.agent().send_message(
    &agent.agent_id,
    "Use canton_get_balance tool: party=seller-bank-b",
).await?;
println!("Seller balance: {}", seller_balance.payload);
// Fund the buyer with Canton Coin
let transfer = client.agent().send_message(
    &agent.agent_id,
    "Use canton_transfer tool: \
     sender=seller-bank-b, \
     receiver=buyer-fund-a, \
     amount=5000000",
).await?;
println!("Transfer: {}", transfer.payload);
Transfer: {
  "tx_id": "txn-a1b2c3d4",
  "sender": "seller-bank-b",
  "receiver": "buyer-fund-a",
  "amount": 5000000,
  "status": "committed"
}

Step 7: Execute Atomic DvP Settlement

This is the core of the tutorial. The DvP settles both legs atomically — the bond transfers from seller to buyer, and the payment transfers from buyer to seller. If either leg fails, neither settles:

// Execute atomic Delivery-vs-Payment settlement
// Bond moves from seller to buyer; payment moves from buyer to seller
// Both legs settle atomically — neither settles unless both do
let dvp = client.agent().send_message(
    &agent.agent_id,
    "Use canton_dvp_settle tool: \
     buyer=buyer-fund-a, \
     seller=seller-bank-b, \
     asset_id=bond-us912828zt58, \
     quantity=100, \
     price=5000000",
).await?;
println!("DvP settlement: {}", dvp.payload);
DvP settlement: {
  "settlement_id": "dvp-settle-001",
  "status": "settled",
  "delivery": {
    "asset_id": "bond-us912828zt58",
    "quantity": 100,
    "from": "seller-bank-b",
    "to": "buyer-fund-a"
  },
  "payment": {
    "amount": 5000000,
    "from": "buyer-fund-a",
    "to": "seller-bank-b"
  }
}

Step 8: DAML Escrow Contract

For additional protection, create a DAML escrow contract. The canton_submit_command tool sends a DAML Create command via the JSON Ledger API v2:

// Create a DAML escrow contract for additional protection
let escrow = client.agent().send_message(
    &agent.agent_id,
    "Use canton_submit_command tool: \
     command_type=create, \
     template_id=Tenzro.Escrow:EscrowContract, \
     payload={\"buyer\": \"buyer-fund-a\", \"seller\": \"seller-bank-b\", \
     \"amount\": 1000000, \"asset_ref\": \"bond-us912828zt58\"}, \
     act_as=buyer-fund-a",
).await?;
println!("Escrow contract: {}", escrow.payload);
Escrow contract: {
  "contract_id": "#1:0",
  "template_id": "Tenzro.Escrow:EscrowContract",
  "status": "active",
  "created_at": "2026-04-08T14:30:00Z"
}

Step 9: Query Contracts & Events

// Query active DAML contracts
let contracts = client.agent().send_message(
    &agent.agent_id,
    "Use canton_list_contracts tool: \
     template_id=Tenzro.Escrow:EscrowContract, \
     party=buyer-fund-a",
).await?;
println!("Active contracts: {}", contracts.payload);

// Get contract events
let events = client.agent().send_message(
    &agent.agent_id,
    "Use canton_get_events tool: \
     contract_id=escrow-001, \
     party=buyer-fund-a",
).await?;
println!("Events: {}", events.payload);

Step 10: Fee Schedule & DAR Upload

// Check synchronizer fee schedule
let fees = client.agent().send_message(
    &agent.agent_id,
    "Use canton_get_fee_schedule tool",
).await?;
println!("Fee schedule: {}", fees.payload);
Fee schedule: {
  "base_fee": 1000,
  "per_byte_fee": 10,
  "currency": "Canton Coin",
  "synchronizer": "tenzro-domain-1"
}
// Upload a custom DAR package for specialized settlement logic
let dar = client.agent().send_message(
    &agent.agent_id,
    "Use canton_upload_dar tool: \
     dar_path=/packages/tenzro-settlement-1.0.dar",
).await?;
println!("DAR upload: {}", dar.payload);

Step 11: Settle on Tenzro Ledger

Finalize the DvP on the Tenzro Ledger and add a verifiable credential for audit trail:

// Settle the DvP on the Tenzro Ledger
let settlement = client.settlement().settle(SettlementRequest {
    request_id: format!("canton-dvp-{}", uuid::Uuid::new_v4()),
    provider: wallet.address.clone(),
    customer: "0x0000000000000000000000000000000000000000".to_string(),
    amount: 5000000,
    asset: "TNZO".to_string(),
}).await?;
println!("Settlement: {}", settlement.receipt_id);
println!("Status:     {}", settlement.status);

The "TNZO" asset settles on the native Tenzro Ledger. When settlement involves cross-VM parties (e.g., a Canton seller and an EVM buyer), the CROSS_VM_BRIDGE precompile (0x1003) handles atomic transfers between VMs automatically — no manual bridging step is required.

// Add a settlement attestation credential to the identity
let cred = client.identity().add_credential(
    &identity.did,
    "DvPSettlementAttestation",
    None,
    Some(serde_json::json!({
        "settlement_id": settlement.receipt_id,
        "asset": "US912828ZT58",
        "quantity": 100,
        "counterparty": "seller-bank-b",
        "chain": "canton",
    })),
).await?;
println!("Credential: {}", cred);

Run the Full Example

The complete example is available at sdk/tenzro-sdk/examples/defi_canton_dvp.rs:

cargo run --example defi_canton_dvp

What You Learned

Next Steps