Launching a Token on Canton Network: A Beginner's Guide
A walkthrough covering how to think about token contracts in Daml, how to structure your files, write your first token, test it locally, and upload it to Canton, no blockchain experience required.
A walkthrough covering how to think about token contracts in Daml, how to structure your files, write your first token, test it locally, and upload it to Canton, no blockchain experience required.
Table of Contents
- What Are We Actually Building?
- How to Think About Daml Tokens
- How to Split Your Files Correctly
- Step 1: Install the Tools
- Step 2: Create Your Project
- Step 3: Write the Token Contract
- Step 4: Write the Mint Request Contract
- Step 5: Write the Test Script
- Step 6: Build and Test Locally
- Step 7: Upload to Canton
- Common Mistakes
- Summary
- About Us
- Tags
- FAQ
What Are We Actually Building?
A token on Canton is just a Daml smart contract that says: "this party owns this amount of this asset." That's it.
Unlike Ethereum where a token is a global ledger entry visible to everyone, a Canton token is a private contract between specific parties. Only the people named in the contract can see it. The Canton Network enforces this automatically, you don't have to write any privacy logic yourself.
By the end of this guide you will have:
- A
Tokencontract (the actual token holding) - A
MintRequestcontract (how new tokens get created safely) - A test script that proves it all works
- Your compiled contract uploaded to a local Canton node
How to Think About Daml Tokens
Before writing a single line of code, it helps to understand the core mental model.
Everything is a contract. A token is not a number in a database. It is a contract on the ledger that says: issuer X created this, owner Y holds it, and the amount is Z. When Alice sends tokens to Bob, the old contract is archived (deleted) and a new one is created with Bob as the owner. There is no "update", only archive and create.
Someone has to be in charge. In Daml, every contract has a signatory, the party whose authority backs the contract's existence. For tokens, the issuer is the signatory. This means only the issuer can create token contracts. Owners can exercise choices on their tokens (transfer, burn, split) but they cannot forge new ones from thin air.
The issuer signs, the owner acts. Think of it like a banknote. The central bank (issuer) prints it and guarantees its value. You (owner) can spend it, give it away, or destroy it. But you cannot print more.
01Issuer Owner02 | |03 | -- creates Token -------> | (issuer is signatory)04 | |05 | | -- Transfer --> New Owner06 | | -- Burn --> (destroyed)07 | | -- Split --> Two tokensKeep this mental model in mind as you read the code below and it will all make sense.
How to Split Your Files Correctly
This trips up beginners more than anything else. Here is the rule:
One module per file. The file name must match the module name.
If your module is called Token, the file must be called Token.daml. If your module is called Token.Mint, the file must be at daml/Token/Mint.daml. Daml treats dots as directory separators.
For a simple token project, the recommended structure is:
01my-token/02├── daml.yaml ← project config (SDK version, dependencies)03├── daml/04│ ├── Token.daml ← the Token template05│ ├── MintRequest.daml ← the MintRequest template 06│ └── Token/07│ └── Test.daml ← test scripts (module: Token.Test)Why split at all? Because test scripts import Daml.Script, which is a dev dependency. Keeping test code in a separate file means your production .dar file stays clean and your contract logic stays readable. As your project grows, separating concerns (token logic, minting logic, tests) makes maintenance far easier.
For this beginner guide, we will keep everything in two files: Token.daml for the contracts, and Token/Test.daml for the tests.
Step 1: Install the Tools
You need Java 17+ and the Digital Asset Package Manager (dpm).
01# Install Java02sudo apt update && sudo apt install openjdk-21-jdk -y03 04# Install dpm05curl https://get.digitalasset.com/install/install.sh | sh06 07# Add dpm to your PATH08echo 'export PATH="/home/$USER/.dpm/bin:$PATH"' >> ~/.bashrc09source ~/.bashrc10 11# Verify both work12java -version13dpm --versionStep 2: Create Your Project
01dpm new my-token02cd my-tokenThis creates a daml.yaml config file and a daml/ folder. Open daml.yaml and make sure it looks like this:
01sdk-version: 3.4.0 # run `dpm version` and use your installed version02name: my-token03version: 1.0.004source: daml05dependencies:06 - daml-prim07 - daml-stdlib08 - daml-scriptNow create the folder for the test file:
01mkdir -p daml/TokenYour project is ready.
Step 3: Write the Token Contract
Create daml/Token.daml with the following content:
01module Token where02 03-- A token on Canton. Represents ownership of an amount of some asset.04template Token05 with06 issuer : Party -- the authority backing this token07 owner : Party -- who currently holds it08 amount : Decimal -- how much09 symbol : Text -- e.g. "MYT" (My Token)10 where11 signatory issuer -- only the issuer authorizes token creation12 observer owner -- the owner can see their token13 14 -- Transfer to a new owner. Only the current owner can do this.15 choice Transfer : ContractId Token16 with newOwner : Party17 controller owner18 do create this with owner = newOwner19 20 -- Destroy the token entirely.21 choice Burn : ()22 controller owner23 do return ()24 25 -- Split into two tokens. Useful for partial payments.26 choice Split : (ContractId Token, ContractId Token)27 with splitAmount : Decimal28 controller owner29 do30 assert (splitAmount > 0.0 && splitAmount < amount)31 t1 <- create this with amount = splitAmount32 t2 <- create this with amount = amount - splitAmount33 return (t1, t2)Key decisions explained:
signatory issuer, the issuer is the sole authority. Without this, anyone could create tokens and claim they're backed by your issuer. With it, only a transaction that carries the issuer's signature can create a Token contract.
observer owner, the owner does not co-sign creation (which would block transfers, since new owners haven't agreed to anything yet). They are an observer: they can see the contract and exercise choices on it, but they don't sign it into existence.
controller owner on all choices, the owner is in full control of what happens to their token. The issuer has no power to burn or transfer an owner's tokens without their consent.
Step 4: Write the Mint Request Contract
Create daml/MintRequest.daml:
01module MintRequest where02 03import Token (Token(..))04 05-- A request from a user to the issuer to mint tokens for them.06-- This two-step pattern means minting always requires the issuer's approval.07template MintRequest08 with09 issuer : Party10 owner : Party11 amount : Decimal12 symbol : Text13 where14 signatory owner -- the requester backs this request15 observer issuer -- the issuer can see and act on it16 17 -- Issuer approves: the token gets created.18 choice Approve : ContractId Token19 controller issuer20 do21 create Token with22 issuer = issuer23 owner = owner24 amount = amount25 symbol = symbol26 27 -- Requester changes their mind.28 choice Cancel : ()29 controller owner30 do return ()Why use a separate MintRequest?
In a real application the issuer is a backend service, not a human sitting at a keyboard. The MintRequest pattern lets the user initiate the process from a frontend and requires the issuer to approve before any tokens exist. This is the standard Propose and Accept pattern in Daml, and it maps cleanly to real-world authorization workflows.
Step 5: Write the Test Script
Create daml/Token/Test.daml:
01module Token.Test where02 03import Daml.Script04import Token05import MintRequest06 07tokenTest : Script ()08tokenTest = script do09 10 -- Set up three parties11 issuer <- allocateParty "Issuer"12 alice <- allocateParty "Alice"13 bob <- allocateParty "Bob"14 15 -- Alice asks the issuer to mint her 1000 tokens16 mintReqId <- submit alice do17 createCmd MintRequest with18 issuer = issuer19 owner = alice20 amount = 1000.021 symbol = "MYT"22 23 -- Issuer approves, tokens now exist24 tokenId <- submit issuer do25 exerciseCmd mintReqId Approve26 27 -- Alice splits off 300 tokens28 (small, _large) <- submit alice do29 exerciseCmd tokenId Split with splitAmount = 300.030 31 -- Alice sends the 300-token slice to Bob32 bobTokenId <- submit alice do33 exerciseCmd small Transfer with newOwner = bob34 35 -- Bob burns his tokens36 submit bob do37 exerciseCmd bobTokenId Burn38 39 -- Nobody can forge tokens by pretending to be the issuer40 submitMustFail alice do41 createCmd Token with42 issuer = issuer -- alice submits but can't carry issuer's signature43 owner = alice44 amount = 9999.045 symbol = "FAKE"46 47 return ()The test covers the full lifecycle: mint → split → transfer → burn, plus a security check that forgery fails.
Step 6: Build and Test Locally
Compile the project:
01dpm buildA successful build prints something like:
01Created .daml/dist/my-token-1.0.0.darRun the tests:
01dpm testYou should see:
01daml/Token/Test.daml:tokenTest: okYou can also open Daml Studio to see a visual ledger view of the test:
01dpm studioIn VS Code, click the "Script Results" link that appears above tokenTest in the editor. It shows you a table of every contract created, who can see it, and which ones are archived, a live visualization of your token's lifecycle.
Step 7: Upload to Canton
Start a local Canton sandbox (simulates a real Canton node on your machine):
01dpm sandboxIn a second terminal, upload your compiled .dar file:
01curl -X POST http://localhost:7575/v2/packages \02 -H "Content-Type: application/octet-stream" \03 --data-binary @.daml/dist/my-token-1.0.0.darA 200 OK response means your token contract is now live on the local Canton node. Any application with access to this node can now create MintRequest contracts, have the issuer approve them, and start transferring tokens between parties.
For deployment to DevNet, TestNet, or MainNet, follow the full validator node setup (spinning up a Canton participant node, getting sponsored, and uploading your .dar via the Ledger API). The upload command is the same, only the endpoint URL changes.
Common Mistakes
Putting signatory issuer, owner on the Token. This requires the new owner's signature whenever a token is created, including during transfers. Since the new owner isn't part of the transaction yet, the transfer fails. Use signatory issuer and observer owner.
Module name doesn't match file name. If your file is daml/Token.daml but the first line says module Tokens where, Daml will refuse to compile. They must match exactly.
Forgetting to import. If Token/Test.daml uses Token and MintRequest templates, it must import Token and import MintRequest at the top. Daml does not auto-import anything.
Reusing the same .dar across networks. Each Canton network (DevNet, TestNet, MainNet) is fully isolated. Upload your .dar separately to each one. The same file works everywhere, the endpoint URL is the only thing that changes.
Using = for ledger actions. In scripts, <- means "run this action and give me the result." = is for pure values. tokenId <- submit ... is correct. tokenId = submit ... will not compile.
Summary
A Canton token is a private Daml contract with an issuer (the authority) and an owner (the holder). The issuer signs tokens into existence; the owner controls what happens to them via Transfer, Burn, and Split choices.
Split your code into focused files: one for the token template, one for the mint request template, one for tests. Keep test code separate from production code so your .dar stays clean.
The workflow is: write → dpm build → dpm test → dpm sandbox → upload .dar. Once your .dar is on a Canton node, your token is live.
Daml Documentation: docs.digitalasset.com/build/3.4
Canton Token Standard (CIP-0056): github.com/hyperledger-labs/splice/tree/main/token-standard
Validator Setup (next step): docs.dev.sync.global
About Us
At SC Audit Studio, we specialize in protocol security assessments. Our team of experts has worked with companies like Aave, 1inch, and many more to conduct thorough security assessments across EVM and non-EVM environments.
Reach out to us for queries and security assessments!