cSOL Whitepaper

Token issuing protocol for bringing Solana assets onto the Canton Network — lock/mint and burn/release coordinated by a multi-signature relayer, with an immutable on-chain vault and replay protection on both sides.

September 2026

Abstract

This paper describes a token issuing protocol that enables Solana assets to be brought onto the Canton Network.

The protocol uses a lock/mint and burn/release model coordinated by a multi-signature off-chain relayer system, which observes source-chain events, validates transaction finality and validity, prevents replay attacks, and executes the corresponding action on the destination chain.

Native SOL is locked in an immutable on-chain vault, the relayer observes finalized deposits and issues cSOL on Canton, and cSOL burns on Canton are released from the vault after relayer validation. Every cross-chain operation is identified by a unique message_id that prevents replay on both sides.

1. Introduction

1.1 Problem Statement

Canton applications need bridged assets they can use directly as collateral, for borrowing and lending, and as liquidity on Canton trading platforms. That starts with cSOL and extends to other Solana tokens in the future. The lack of tokens issued on Canton limits the capabilities for DeFi development on the network.

1.2 Solution

The protocol issues and redeems cSOL against native SOL held in a vault on Solana, and uses the same token issuing model for future tokens. Two Anchor programs split the work: the router manages execution flow, fees and destination configuration; the vault holds funds and enforces release controls. Both programs are deployed immutable. Operations are controlled through three multisig-managed roles (Admin, Manager, Relayer), held by Squads multisigs.

2. Protocol Overview

The router and vault handle deposits and releases on Solana, DA Utilities workflows handle issuance and burn on Canton, and the multisig relayer system connects the two.

2.1 Components

LayerStackComponents
SolanaAnchorrouter — user and relayer entry point; fees, minimum amounts, destination configuration, per-direction pauses, role-based access.
vault — custodies native SOL, derives every message_id, emits bridge events, stores replay protection, enforces daily_limit and total_locked.
CantonDA UtilitiescSOL — instrument of DA Utilities implementing the Canton Network Token Standard (CIP-0056 and CIP-0112).
Mint, transfer and burn via the registry's BurnMintFactory, TransferFactory and AllocationFactory choices.
Off-chainRelayerMulti-signature relayer system: Solana watcher (finalized transactions), Solana executor (releases), registrar automation on Canton, and the state store keyed by message_id.

2.1.1 Solana design

On Solana, the router performs validations, collects fees and controls routing, while the vault holds funds and handles releases. The router is the only caller the vault accepts: it signs vault instructions with a program-derived address (router_authority) that only the configured router program can produce.

  • vault: custodies native SOL in a program-owned treasury account; accepts deposit and release only from the configured router; derives every message_id; emits the Deposit and Withdraw bridge events; stores replay protection as one processed account per released message_id; optionally enforces daily_limit; tracks total_locked so it can never release more than was deposited through it.
  • router: user and relayer entry point; enforces fees, minimum amounts, active destination configuration, per-direction pauses and role-based access. Accumulated protocol fees sit in the router's fee treasury (fee_balance) until collect_fee is called.

The vault is bound to a router program id, not to a router instance. Replacing the router means deploying a new non-upgradeable router and calling vault.set_router; the nonce, the replay state, the limits and total_locked live in the vault and survive the switch.

2.1.2 Canton design (DA Utilities)

On Canton, cSOL is an instrument of DA Utilities (documentation), a managed service operated by Digital Asset. Its Daml models and backend are upgraded by Digital Asset on its own release schedule; this paper describes release 0.14. The registry implements the Canton Network Token Standard, both V1 (CIP-0056) and V2 (CIP-0112); cSOL holdings are directly usable in any Canton application that integrates the standard Holding interface of either version.

The concrete choices used are:

  • Mint (registrar → bridge party): BurnMintFactory_BurnMint on the registry's AllocationFactory, with no inputs and one output owned by the bridge party. The choice is controlled by the provider and the registrar plus the output owners; all three are the operator's own parties, so a single transaction from the operator's node performs the mint.
  • Delivery (bridge party → recipient): TransferFactory_Transfer (Token Standard). If the recipient has a TransferPreapproval covering cSOL, the transfer settles in the same transaction. Otherwise it creates a TransferOffer that the recipient accepts (TransferInstruction_Accept) or rejects; the bridge party can withdraw it (TransferInstruction_Withdraw).
  • Burn (holder): the holder exercises AllocationFactory_RequestBurn, which locks the holder's holdings and creates a BurnRequest signed by the holder, the provider and the registrar. The registrar accepts it with BurnRequest_Accept, which archives the locked holdings in the same transaction (the holder's authority comes from the request they signed). The holder may cancel (BurnRequest_Cancel) and the registrar may reject (BurnRequest_Reject) before acceptance; both unlock the holdings.

2.1.3 Off-chain design

The multi-signature relayer system watches finalized deposits, records and validates them, and submits the matching Canton action or Solana action. A Solana watcher reads finalized transactions and a Solana executor submits releases.

2.2 Message Model

Each cross-chain operation is a message carrying a unique 32-byte message_id from start to finish, so the relayer can track it, retry safely and prevent replay. The message_id is also the main correlation key in logs and in the database.

For Solana deposits, the vault derives message_id as:

message_id derivation
keccak256( "canton-bridge/sol/v1" ‖ sender[32] ‖ src_chain_id u64le ‖ nonce u64le
         ‖ amount u64le ‖ dst_chain_id u64le ‖ dst_output_token[32]
         ‖ recipient_len u16le ‖ recipient )

sender is the depositor's public key, nonce is the vault's monotonic deposit counter, amount is the net amount locked after fees and src_chain_id identifies the cluster (101 mainnet, 103 devnet) and is fixed when the vault is initialized. The domain prefix keeps Solana message ids disjoint from those of any other source chain. The layout is part of the wire format and is pinned by reference vectors shared with the relayer.

2.2.1 — Solana Deposit eventRust
/// Emitted when SOL is locked
pub struct Deposit {
    pub message_id: [u8; 32],       // Unique bridge message
    pub sender: Pubkey,             // Depositor
    pub nonce: u64,                 // Vault deposit counter
    pub bridge_amount: u64,         // Net bridged amount in lamports
    pub src_chain_id: u64,          // Source cluster
    pub dst_chain_id: u64,          // Destination chain
    pub dst_output_token: [u8; 32], // Destination token
    pub recipient: Vec<u8>,         // Destination recipient (Canton party id)
}

The source asset is implicitly native SOL, so the event does not carry a separate src_input_token field.

2.2.2 — Solana Withdraw eventRust
/// Emitted when SOL is released for a cross-chain withdrawal
pub struct Withdraw {
    pub message_id: [u8; 32], // Unique bridge message
    pub recipient: Pubkey,    // Recipient account on Solana
    pub amount: u64,          // Released amount in lamports
}

Both events are emitted through a self-invocation of the program (Anchor emit_cpi!), so they are recorded in the transaction's inner instructions rather than in program logs. Logs can be truncated by the runtime; inner instruction data cannot, and the relayer reads events from there.

2.2.3 DA Utilities mint and burn workflows

On Canton, issuance is a direct mint by the operator and redemption is a request/accept workflow.

Mint: the relayer, holding the registrar role, exercises BurnMintFactory_BurnMint with a single output of bridge_amount owned by the bridge party, then transfers the holding to the recipient with TransferFactory_Transfer (section 3.1). The registry records both as EventLog events (CIP-0112), which the relayer uses to confirm delivery.

Burn: the holder exercises AllocationFactory_RequestBurn, which locks the holdings to burn into a BurnRequest carrying the amount, the holder and a reference naming the Solana recipient. The registrar validates the request and accepts it with BurnRequest_Accept; acceptance archives the locked holdings and records the burn as an EventLog event. Release 0.14 no longer creates ExecutedBurn or ExecutedMint contracts, so the relayer reads burns from the event log rather than from the active contract set and turns each accepted burn into a Solana release.

2.3 Trust Model

The protocol depends on the Solana privileged roles, the relayer, and the DA Utilities operator and registrar workflow. Users rely on the operator backend to provide correct choice context and disclosed contracts, on the registrar to mint only against finalized deposits and to accept only valid burn requests, and on the relayer not to mint unbacked assets or release SOL without a valid Canton burn.

Each role is a single Solana public key, meant to be a Squads multisig vault; thresholds and members live in Squads, the programs are multisig-agnostic.

RoleProgramsPowers
Adminrouter, vaultRouter: unpause, ceiling on the flat fee, set manager and relayer, collect fees, sweep surplus. Vault: set router, set daily limit, sweep surplus. Transferred in two steps (propose_admin → accept_admin), separately per program.
ManagerrouterPause either direction, fees within the admin's ceilings, minimum amounts, destination. Cannot unpause and cannot move funds. The admin can do everything the manager can.
Relayerrouterfinalize_withdraw only.

The router admin, vault admin, manager, registrar and relayer roles have different powers, and those powers need to stay separated in operation. Key management and operational procedures are part of that trust boundary, and the protocol also depends on reliable RPC sources.

These risks are significantly reduced by separating powers across roles, requiring multisignature approval for privileged actions, and applying operational security practices around key custody, access control, monitoring and key rotation.

2.4 Fee Structure

The protocol can charge two kinds of fees on deposits. The first is crypto_fee, a fixed fee per deposit in lamports covering the token issuance cost on the destination chain. The second is protocol_fee_bps, a percentage of the bridged amount charged on the remainder after the fixed fee.

Both fees are taken on the router before funds are forwarded to the vault, and the amount left after fees is the amount that is actually locked and issued. The minimum amount (min_amount) is checked against the post-fee amount, not the original deposit; it protects the protocol from DoS and is never below Solana's rent-exempt minimum for an empty account, so that a later release of that amount to a fresh account cannot fail.

Collected fees stay in the router's fee treasury until the admin withdraws them with collect_fee. The percentage fee is capped at 10%, the fixed fee is capped by an admin-set ceiling (max_crypto_fee) the manager cannot exceed, and if both fees are set to zero the full deposit amount is issued.

3. Protocol Flows

3.1 Solana → Canton (Lock / Mint)

1.A user calls router.deposit(amount, max_fee, recipient) with a Canton party id as recipient and, as max_fee, the total fee quoted to them. The router only checks that the recipient is a plausible party id (UTF-8 containing ::); the relayer rejects any recipient that is not well formed before minting.
2.The router verifies that deposits are unpaused and that a destination is configured.
3.The router applies fees, fails if they exceed max_fee (so a fee change that lands before the deposit cannot take more than the user agreed to), checks min_amount on the net amount, adds the fees to fee_balance and transfers them to its fee treasury, then invokes vault.deposit with the net amount.
4.The vault verifies that the caller is the configured router, derives message_id from the current nonce, increments nonce and total_locked, transfers the net amount from the depositor to its treasury and emits Deposit.
5.The relayer observes the transaction once it is finalized and records the message in its database.
6.The relayer, acting as the registrar, mints bridge_amount × 10⁻⁹ cSOL to the bridge party with BurnMintFactory_BurnMint.
7.The relayer delivers the cSOL to the recipient with TransferFactory_Transfer. If the recipient holds a TransferPreapproval for cSOL, the mint and the transfer are submitted as one transaction and the recipient's holding exists as soon as it commits. Otherwise the transfer creates a TransferOffer and the recipient must accept it from their wallet.
8.The relayer updates the message status from the command completions.
┌──────────────────────────────────────────────────────────────────────────────────────────┐
│                              Solana -> Canton (Lock / Mint)                              │
├──────────────────────────────────────────────────────────────────────────────────────────┤
│  User          router               vault              Relayer           DA Utilities    │
│    │              │                   │                   │                    │         │
│    │-- deposit -->│                   │                   │                    │         │
│    │              │ pause, destination│                   │                    │         │
│    │              │ fees <= max_fee,  │                   │                    │         │
│    │              │ min_amount        │                   │                    │         │
│    │              │-- deposit(net) -->│                   │                    │         │
│    │              │ (router_authority)│                   │                    │         │
│    │              │                   │ message_id = keccak(...nonce...)       │         │
│    │              │                   │ nonce++, total_locked += net           │         │
│    │              │                   │---- Deposit ----->│ (finalized)        │         │
│    │              │                   │                   │---- BurnMint ----->│         │
│    │              │                   │                   │    Holding(bridge) │         │
│    │              │                   │                   │---- Transfer ----->│         │
│    │              │                   │ preapproval: Holding(recipient), same tx         │
│    │              │                   │ else: TransferOffer, recipient accepts │         │
│    │              │                   │                   │                    │         │
└──────────────────────────────────────────────────────────────────────────────────────────┘

3.2 Canton → Solana (Burn / Release)

1.The holder exercises AllocationFactory_RequestBurn through the operator backend, locking the relevant Holding contracts into a BurnRequest whose reference names the Solana recipient. The request stays pending until the registrar accepts or rejects it, or the holder cancels it.
2.The relayer, as the registrar, validates the request (reference, lamport granularity, min_withdraw_amount) and exercises BurnRequest_Accept. The locked holdings are archived and the burn is recorded as an EventLog event in the same transaction.
3.The relayer reads the completion (update id and offset), derives message_id and inserts the request into the database.
4.The multisignature relayer system takes the request, validates and signs it.
5.After all of the signatures are collected, the relayer role calls router.finalize_withdraw(message_id, amount) with the Solana recipient account.
6.The router checks the relayer signature, that withdrawals are not paused and that the amount is non-zero and at least min_withdraw_amount (itself never below the rent-exempt minimum, so a release to a fresh account cannot fail), then invokes vault.release.
7.The vault creates the processed account for message_id (a replay fails because the account already exists), applies daily_limit, subtracts the amount from total_locked, emits Withdraw and transfers the SOL.
8.The off-chain module tracks this process and updates the database status of the request.
┌──────────────────────────────────────────────────────────────────────────────────────────┐
│                            Canton -> Solana (Burn / Release)                             │
├──────────────────────────────────────────────────────────────────────────────────────────┤
│ Holder         DA Utilities          Relayer      Multisig       router           vault  │
│    │                 │                  │             │             │               │    │
│    │- RequestBurn -->│                  │             │             │               │    │
│    │ (amount, reference=sol:<recipient>)│             │             │               │    │
│    │                 │ holdings locked, BurnRequest   │             │               │    │
│    │                 │---- visible ---->│             │             │               │    │
│    │                 │                  │ validate reference,       │               │    │
│    │                 │                  │ lamport grain, min amount │               │    │
│    │                 │<---- Accept -----│             │             │               │    │
│    │                 │ archived, EventLog burn        │             │               │    │
│    │                 │-- completion --->│             │             │               │    │
│    │                 │                  │ message_id = keccak(      │               │    │
│    │                 │                  │ update_id, burn_cid,      │               │    │
│    │                 │                  │ amount, recipient)        │               │    │
│    │                 │                  │- propose -->│             │               │    │
│    │                 │                  │             │- finalize ->│               │    │
│    │                 │                  │             │ (threshold) │               │    │
│    │                 │                  │             │             │-- release --->│    │
│    │                 │                  │             │             │ processed[id] │    │
│    │                 │                  │             │             │   daily_limit │    │
│    │                 │                  │             │             │total_locked - │    │
│    │<------------------------ Withdraw event, transfer SOL -------------------------│    │
│    │                 │                  │             │             │               │    │
└──────────────────────────────────────────────────────────────────────────────────────────┘

4. State, Finality, and Replay

4.1 Idempotency

On Solana, replay protection is an account rather than a mapping: releasing message_id creates the program-derived account ["processed", message_id], which is never closed, and a second release of the same id fails at account creation before any state changes. Safe retries and restarts follow from this. The DB is the source of truth for operational state, and the relayer always queries and updates by message_id, dismissing all duplicates.

The replay state lives in the vault, so it survives a router replacement. Each processed account costs a small, permanent rent deposit paid by the transaction's payer, normally the relayer.

4.2 Finality Policy

Solana has no block reorganizations past finalized commitment: a finalized slot has been rooted by a supermajority of stake and cannot be rolled back. The relayer therefore separates:

  • Inclusion — a transaction is confirmed by a supermajority (confirmed commitment).
  • Finality — the transaction's slot is rooted (finalized commitment).

The off-chain module picks up deposits only at finalized commitment and reads Deposit events from inner instructions rather than from logs.

5. Security Considerations

The security of the protocol depends on program logic, how privileged actions are governed, and how the off-chain system processes transactions. Token issuance is not controlled by a single unchecked key: a valid issuance requires a finalized deposit on Solana, relayer validation, and the registrar's mint on the Canton registry. When those roles are operated through multisig-controlled processes, unauthorized issuance requires compromising multiple approvers across separate roles rather than a single operator key.

The router and vault expose narrow privileged actions through role-based access controls, pause switches and destination controls. On the Solana release path, a daily withdraw limit caps the theoretical damage even in the unlikely event of relayer compromise.

Security risks are mitigated by:

  • Multisig-controlled operational roles require the compromise of multiple signers at the same time rather than one key.
  • Role-based access on the router and vault limits what each role can do; the manager can pause but not unpause, and only the admin can unpause, so an emergency stop cannot be quietly reverted by the same key. Admin transfer is two-step on both programs.
  • Router authentication — the vault authenticates the router by a program-derived signer that only the configured router program can produce, and it moves lamports itself instead of trusting the router's accounting.
  • Solvency invariant — total_locked guarantees the vault never releases more than was deposited through it, whatever the router says. sweep_surplus reaches only lamports above total_locked (or above fee_balance on the router).
  • Daily limit — daily_limit constrains the maximum loss on the Solana release path if the relayer multisignature credentials are compromised.
  • Replay protection and unique message_ids (domain prefix, chain id and monotonic nonce) make each message processable once.
  • Pause controls and the rent-aware minimum amount reduce risks and support emergency response to incidents.
  • Immutable programs — both are finalized after deployment and verified against the public repository, and the only upgrade path is a new router selected by the vault admin.