Research

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

  1. What Are We Actually Building?
  2. How to Think About Daml Tokens
  3. How to Split Your Files Correctly
  4. Step 1: Install the Tools
  5. Step 2: Create Your Project
  6. Step 3: Write the Token Contract
  7. Step 4: Write the Mint Request Contract
  8. Step 5: Write the Test Script
  9. Step 6: Build and Test Locally
  10. Step 7: Upload to Canton
  11. Common Mistakes
  12. Summary
  13. About Us
  14. Tags
  15. 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 Token contract (the actual token holding)
  • A MintRequest contract (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 tokens

Keep 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 --version

Step 2: Create Your Project

01dpm new my-token02cd my-token

This 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-script

Now create the folder for the test file:

01mkdir -p daml/Token

Your 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 build

A successful build prints something like:

01Created .daml/dist/my-token-1.0.0.dar

Run the tests:

01dpm test

You should see:

01daml/Token/Test.daml:tokenTest: ok

You can also open Daml Studio to see a visual ledger view of the test:

01dpm studio

In 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 sandbox

In 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.dar

A 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!