BurnMint Token Pool Deployment (Canton)
Deploy a BurnMint token pool on Canton, register it on the Token Admin Registry (TAR), and enable a cross-chain lane. Use the ccip-starter-kit-canton for pool deployment and TAR registration, or submit Ledger API commands directly.
This guide assumes you already have a token instrument live on Canton — for example a Registry instrument from Digital Asset's Registry utility, or any instrument supporting CIP-56 with the BurnMintFactory interface.
Scope: On-ledger pool connection — deploy, TAR registration (including the BurnMintFactory for custom tokens), and lane enablement. Does not cover end-to-end transfer tests.
Prerequisites
- Instrument (token managed on Canton Ledger) with known
InstrumentId = { admin, id }. The admin party must be the same party that will own the token pool. - Burn/mint authority —
instrumentId.admin == poolOwner(enforced by the BurnMint template and by the starter kit). - BurnMint token pool DAR uploaded and vetted on your participant —
ccip-burn-mint-token-pool-2.1.0.darfrom thecontracts/dars/releasedfolder (bundles its CCIP dependencies — the only DAR you upload). Vet every released version you depend on — see Upgradability. - To execute incoming transfers, the CommitteeVerifier DAR —
ccip-committee-verifier-v2-2.0.0.dar— uploaded and vetted on your participant as well. - CCIP contract references from Chainlink ops: CCIP owner party, Token Admin Registry, RMNRemote, FeeQuoter.
- Starter kit setup (if using
cct:*scripts) — complete Canton as Source prerequisites and the CCT overview.
Deployment overview
| Step | Action | Starter kit | Manual |
|---|---|---|---|
| 1 | Deploy BurnMintTokenPool | cct:deploy-burn-mint | Ledger API CreateCommand |
| 2 | Register on TAR | cct:set-token | TAR choices via Ledger API |
| 3 | Enable lane | — | Rate limiters + ApplyChainUpdates |
| 4 | Go live | — | EDS + verification |
Step 1 — Deploy the BurnMint Token Pool
Create one BurnMintTokenPool. poolOwner + instanceId uniquely identifies the pool. Signatory: poolOwner.
From the starter kit root:
npm run cct:deploy-burn-mint -- \
--instanceId acme-eur-bm-pool \
--instrument 'yourParty::1220…::token-id'
Flags:
| Flag | Required | Description |
|---|---|---|
--instanceId | Yes | Unique pool instance ID for your party (e.g. acme-eur-bm-pool) |
--instrument | Yes | Canton instrument ID: party::fingerprint::tokenId |
--poolOwner | No | Defaults to party in canton-config.json (must equal instrument admin) |
--ccipOwner | No | Defaults to ccipParty in canton-config.json |
--decimals | No | Token decimals on Canton (default 10) |
Expected output:
Deploying BurnMint token pool on Canton
instanceId: acme-eur-bm-pool
instrument: yourParty::1220…::token-id
poolOwner: yourParty::1220…
Canton bearer token obtained via OIDC client credentials.
✅ BurnMintTokenPool contract ID: 00abc…
Pool address: acme-eur-bm-pool@yourParty::1220…
Update: https://lighthouse.testnet.cantonloop.com/transactions/…
Next step: register the pool on the Token Admin Registry:
npm run cct:set-token -- --instrument "yourParty::1220…::token-id" --poolInstanceId "acme-eur-bm-pool" --poolOwner "yourParty::1220…"
Save the pool address ({instanceId}@{poolOwner}) and contract ID for verification.
Submit a CreateCommand for BurnMintTokenPool via your participant's JSON Ledger API (submit-and-wait-for-transaction-tree).
| Argument | Guidance |
|---|---|
instanceId | Unique string for your party, e.g. acme-eur-bm-pool |
poolOwner | Party that owns the pool. Must be the same as instrumentId.admin. |
ccipOwner | Chainlink CCIP owner party from ops |
instrumentId | Your underlying asset's InstrumentId |
decimals | Standard value: 10 |
rateLimitAdmin | Optional — leave unset unless a separate party manages limits |
remoteChainConfigs | Leave empty — configure via ApplyChainUpdates (Step 3) |
tokenTransferFeeConfigs | Optional per-destination fees — set at deploy or via ApplyTokenTransferFeeConfigUpdates |
poolReceiveContext | Leave empty at creation |
transferTimeout | Indefinite to disable expiry on pending outgoing transfers |
deps | tokenAdminRegistry, rmnRemote, feeQuoter references from ops |
After creation, your pool address is {instanceId}@{poolOwner}.
Transfer-fee fields
When setting tokenTransferFeeConfigs (map keyed by destination chain selector):
| Field | Meaning |
|---|---|
isEnabled | Must be true to apply the config |
feeUSDCents | Flat fee in USD cents (non-negative) |
destGasOverhead | Gas for destination execution (> 0 when set via choice) |
destBytesOverhead | Data-availability bytes overhead (≥ 32) |
feeBps | Proportional fee in basis points (< 10000) |
CCIP hosted addresses
| Field | Testnet | Mainnet |
|---|---|---|
| Token Admin Registry | tokenadminregistry-nbehb@ccipOwner::1220e382f4e57b0815e6be737006e381e6b7de448e06bd033ece6df498017879f551 | Contact the CCIP team for the Token Admin Registry address to register a token pool |
| RMNRemote | rmn_remote-pttst@rmnOwner::1220e382f4e57b0815e6be737006e381e6b7de448e06bd033ece6df498017879f551 | rmn_remote-zqxpl@rmnOwner::122012714685760dc1927c4cfe119ce2126c48756154e95c06f5c181da05a5519093 |
| FeeQuoter | feequoter-koyox@ccipOwner::1220e382f4e57b0815e6be737006e381e6b7de448e06bd033ece6df498017879f551 | feequoter-dwxyx@ccipOwner::122012714685760dc1927c4cfe119ce2126c48756154e95c06f5c181da05a5519093 |
| CCIP owner party | ccipOwner::1220e382f4e57b0815e6be737006e381e6b7de448e06bd033ece6df498017879f551 | ccipOwner::122012714685760dc1927c4cfe119ce2126c48756154e95c06f5c181da05a5519093 |
| Global EDS | https://eds.testnet.ccip.chain.link | https://eds.ccip.chain.link |
| Indexer | https://indexer-1.testnet.ccip.chain.link | Primary: https://indexer-1.ccip.chain.link · Backup: https://indexer-2.ccip.chain.link |
| Canton chain selector | 9268731218649498074 | 2308837218439511688 |
| Default CommitteeVerifier | committeeverifier-pttst@ccvOwner::1220e382f4e57b0815e6be737006e381e6b7de448e06bd033ece6df498017879f551 | committeeverifier-vnmkd@ccvOwner::122096accf0a84fc7d80d5fce5ea3135317a03eb22e62e0d8cdd7548865f984f11ff |
Step 2 — Register on the Token Admin Registry
TAR maps each instrument to a single token pool. Registration: propose → accept → set pool.
npm run cct:set-token -- \
--instrument 'yourParty::1220…::token-id' \
--poolInstanceId acme-eur-bm-pool
Flags:
| Flag | Required | Description |
|---|---|---|
--instrument | Yes | Same instrument ID used in Step 1 |
--poolInstanceId | Yes | instanceId from Step 1 |
--poolOwner | No | Party that owns the pool contract (defaults to party in canton-config.json) |
--poolAdmin | No | TAR admin party (defaults to poolOwner) |
--proposeCaller | No | Party that calls ProposeAdministrator (defaults to instrument admin) |
--tokenConfigCid | No | Resume at AcceptAdminRole if ProposeAdministrator already succeeded |
--skipAccept | No | Resume at SetPool only (skip propose and accept) |
The script fetches TAR disclosures from Global EDS, then submits all three TAR choices in sequence.
Expected output:
Registering token pool on the Token Admin Registry (TAR)
instrument: yourParty::1220…::token-id
poolInstanceId: acme-eur-bm-pool
pool address: acme-eur-bm-pool@yourParty::1220…
Step 1/3 — ProposeAdministrator on Token Admin Registry
tokenConfigCid: 00def…
Step 2/3 — AcceptAdminRole on Token Admin Registry
Step 3/3 — SetPool on Token Admin Registry
✅ Token pool registered for yourParty::1220…::token-id
tokenConfigCid: 00ghi…
Next step: enable a cross-chain lane (rate limiters + ApplyChainUpdates on the pool).
Fetch TAR explicit disclosure
Your party is not an observer on TAR. Query the Global EDS batch endpoint:
curl --request POST \
--url https://eds.testnet.ccip.chain.link/ccip/v1/global/disclosure/batch \
--header 'content-type: application/json' \
--data '{
"addresses": [
"tokenadminregistry-nbehb@ccipOwner::1220e382f4e57b0815e6be737006e381e6b7de448e06bd033ece6df498017879f551"
]
}'
Include disclosed contracts in subsequent ledger submissions. OpenAPI: eds-global.yaml.
TAR choices
All three choices take a context argument (Splice.Api.Token.MetadataV1.ChoiceContext) — pass an empty context ({"values": {}}) unless your token's tooling supplies one.
ProposeAdministrator— CCIP owner or instrument admin. PassinstrumentId,newAdmin,context,caller. For new instruments, leavetokenConfigCidasNone. ReturnsTokenConfigCID for next steps.AcceptAdminRole— Proposed admin exercises withtokenConfigCid,instrumentId,context,caller.SetPool— Admin links instrument to pool viaPoolRegistration(poolOwner,poolInstanceIdfrom Step 1), pluscontextandcaller.
Admin role can be transferred with TransferAdminRole → AcceptAdminRole.
Pin the BurnMintFactory on the TAR
BurnMint pools do not burn or mint instrument supply themselves — they call a BurnMintFactory (Splice.Api.Token.BurnMintV1.BurnMintFactory) recorded on the instrument's TokenConfig. The factory contract implements the actual burn (send) and mint (execute) operations against your token.
- Registry instruments (created via Digital Asset's Registry utility) already have a factory — the registry resolves it automatically. No action needed.
- Custom tokens must deploy their own
BurnMintFactorycontract and pin it on the TAR via theSetBurnMintFactorychoice. The reference implementation isLinkRegistry(#link:Link.Token:LinkRegistry) — the factory backing LINK on Canton, which implements bothTransferFactoryandBurnMintFactory.
SetBurnMintFactory is exercised by the token admin on the TAR (same disclosure flow as the other TAR choices):
| Argument | Value |
|---|---|
tokenConfigCid | Current TokenConfig CID (re-read it — it rotates on every TAR write) |
instrumentId | Your instrument's InstrumentId |
burnMintFactory | Some + your factory's ContractId, or None to clear |
context | Empty {"values": {}} unless your tooling supplies one |
caller | The token admin party |
Serve the factory from your EDS
Senders and executors cannot see your factory contract — your pool operator's EDS must supply it in the token pool API response. The reference EDS supports two factory modes per pool ([token_pool_api.token_pools.<address>.factory]):
| Mode | Config | When to use |
|---|---|---|
address | type = "address", template_id (#package-name:Module:Entity), party, instance_address (0x… hashed form) | Your factory is visible to the EDS's participant — EDS reads it directly from the Active Contract Set. |
url | type = "url", token_standard_url — on testnet, the DA token-standard backend: https://api.utilities.digitalasset-staging.com (not a validator scan-proxy URL) | The DA registry resolves the factory for you over HTTP — EDS needs no ledger visibility into the factory. |
See the reference configs: eds-token-pools.toml (URL mode) and the EDS config tests (address mode).
Step 3 — Enable a lane
Lane enablement is not automated by the starter kit. Deploy rate limiters and call ApplyChainUpdates on your pool via the Ledger API.
1. Deploy three rate limiters
Each lane requires RateLimiter contracts (for testing, deploy disabled with zero capacity/rate):
- Inbound (default finality)
- Inbound (custom finality)
- Outbound
Created by poolOwner with matching poolInstanceId, poolOwner, and remoteChainSelector.
2. Apply the chain update
Call ApplyChainUpdates on the pool:
The choice argument is not the pool's remoteChainConfigs map — it takes remoteChainSelectorsToRemove (a list) and chainsToAdd (a list of ChainUpdate records, each carrying its own remoteChainSelector):
{
"commands": [
{
"ExerciseCommand": {
"templateId": "#ccip-burn-mint-token-pool-v2:CCIP.BurnMintTokenPoolV2:BurnMintTokenPool",
"contractId": "<POOL_CONTRACT_ID>",
"choice": "ApplyChainUpdates",
"choiceArgument": {
"remoteChainSelectorsToRemove": [],
"chainsToAdd": [
{
"remoteChainSelector": "9268731218649498074",
"remotePools": ["<REMOTE_POOL_ADDRESS_32_BYTES>"],
"remoteTokenAddress": "<REMOTE_TOKEN_ADDRESS_32_BYTES>",
"inboundCCVs": [
{
"unpack": "committeeverifier-pttst@ccvOwner::1220e382f4e57b0815e6be737006e381e6b7de448e06bd033ece6df498017879f551"
}
],
"outboundCCVs": [
{
"unpack": "committeeverifier-pttst@ccvOwner::1220e382f4e57b0815e6be737006e381e6b7de448e06bd033ece6df498017879f551"
}
],
"finalityConfig": { "tag": "WaitForFinality", "value": {} },
"inboundRateLimiter": { "unpack": "<INBOUND_LIMITER>@<POOL_OWNER>" },
"inboundCustomBlockConfirmationsRateLimiter": { "unpack": "<INBOUND_CUSTOM_LIMITER>@<POOL_OWNER>" },
"outboundRateLimiter": { "unpack": "<OUTBOUND_LIMITER>@<POOL_OWNER>" }
}
]
}
}
}
],
"commandId": "apply-chain-updates",
"actAs": ["<POOL_OWNER_PARTY>"]
}
| ChainUpdate field | Guidance |
|---|---|
remoteChainSelector | Destination chain selector — see CCIP Directory |
remotePools | Remote token pool address(es). Padded to 32 bytes in the case of EVM addresses. |
remoteTokenAddress | Remote token address. Padded to 32 bytes in the case of EVM addresses. |
inboundCCVs / outboundCCVs | Default CommitteeVerifier (testnet address above) unless custom |
finalityConfig | WaitForFinality for default finalized source tx |
inboundRateLimiter | Inbound default limiter address |
inboundCustomBlockConfirmationsRateLimiter | Inbound custom limiter address |
outboundRateLimiter | Outbound limiter address |
Once applied, the on-ledger lane is complete.
Step 4 — Go live
Verification checklist
InstrumentId.adminequals pool owner.- TAR maps your instrument to your pool with your party as admin.
- Pool exists at
{instanceId}@{poolOwner}with matchingInstrumentId. - Lane rate limiters are in place.
Stand up EDS for your pool
Users need explicit disclosures for your pool to send and execute transfers. The reference EDS Docker image supports BurnMint out of the box.
Test transactions
See CCIP on Canton — Overview for user flows. Track messages in CCIP Explorer.