DEV Community

Cover image for Building an ICO on Canton Network
Dennison Bertram
Dennison Bertram

Posted on

Building an ICO on Canton Network

A hands-on guide to creating privacy-preserving token sales on Canton Network

Ever wanted to build a token sale where only the buyer, seller, and maybe a regulator can see the details? That's exactly what we're going to create today. We'll build a complete ICO (Initial Coin Offering) smart contract that sells one token for another, with privacy built-in and atomic operations that can't fail halfway through.


What's an ICO Anyway?

ICO's are back! From Monad to MegaETH or projects launching their token on Tally.xyz, capital formation as product market fit for crypto is the meta.

Before we dive into code, let's make sure we're on the same page. An ICO (Initial Coin Offering) is when a project sells their tokens to raise funds. Traditionally this happens on public blockchains where everyone can see:

  • Who bought what
  • How much they paid
  • All the transaction details

But what if you want to keep those details private? That's where Canton Network comes in.


Why Canton Network?

Canton Network is different. It gives you:

🔒 Privacy by Default
Only the parties directly involved in a transaction can see the details. No more public order books!

Atomic Operations
Either everything happens (buyer pays, seller delivers) or nothing happens. No failed transactions leaving things in a weird state.

🤝 Multi-Party Authorization
Smart contracts can require multiple parties to agree before anything happens. Perfect for regulated token sales.

🏗️ Enterprise Ready
Built for serious applications with proper tooling, monitoring, and compliance features.


What We're Building

We'll create a complete ICO system with three parts:

  1. Token Contract - A simple but secure token that can be issued and transferred
  2. ICO Contract - The main sale logic with atomic token-for-token swaps
  3. Test Suite - Scripts to verify everything works correctly

The flow will be:

Buyer pays with USDC → ICO contract validates → MYCOIN tokens created → All in one atomic transaction
Enter fullscreen mode Exit fullscreen mode

Quick Setup

Let's get your environment ready. You'll need:

  • Daml SDK (version 3.3.0-snapshot...)
  • Java 21 (for the build tools)
  • Docker (for running the local network)
  • tmux (for managing long-running processes)

The easiest way is to clone the Canton quickstart repo and run their setup:

git clone https://github.com/digital-asset/cn-quickstart.git
cd cn-quickstart/quickstart
make setup
Enter fullscreen mode Exit fullscreen mode

This gives you a complete development environment with Canton nodes, wallets, and monitoring tools.

Want to get started with AI? Use this llms.txt to get started fast.


Your First ICO Contract

Let's start with a simple token contract. Create a file called Token.daml:

module Token where

import DA.Assert

-- A simple token that can be issued and transferred
template Token
  with
    issuer : Party     -- Who created this token
    owner : Party      -- Who currently owns it
    symbol : Text      -- Like "USD" or "MYCOIN"
    amount : Decimal   -- How much
  where
    ensure amount > 0.0
    signatory issuer, owner  -- Both must agree to create it

    -- Transfer ownership to someone else
    choice Transfer : ContractId Token
      with
        newOwner : Party
      controller owner, newOwner  -- Both current and new owner must agree
      do
        create this with owner = newOwner

-- The authority that can create new tokens
template TokenIssuer
  with
    issuer : Party
    symbol : Text
    totalSupply : Decimal
  where
    signatory issuer

    -- Create new tokens for someone (doesn't consume this contract)
    nonconsuming choice IssueTokens : ContractId Token
      with
        recipient : Party
        amount : Decimal
      controller issuer, recipient  -- Both issuer and recipient must agree
      do
        assert (amount > 0.0)
        create Token with
          issuer = issuer
          owner = recipient
          symbol = symbol
          amount = amount
Enter fullscreen mode Exit fullscreen mode

Now let's create the ICO contract in Ico.daml:

module Ico where

import DA.Assert
import DA.Time
import Token

-- An active ICO where people can buy tokens
template IcoOffering
  with
    issuer : Party                    -- The company running the ICO
    saleTokenIssuer : Party           -- Who creates the tokens being sold
    saleTokenSymbol : Text            -- What token is being sold
    paymentTokenIssuer : Party        -- Who creates the payment tokens
    paymentTokenSymbol : Text         -- What token buyers pay with
    exchangeRate : Decimal            -- How many sale tokens per payment token
    totalSaleTokens : Decimal         -- Total available for sale
    tokensSold : Decimal              -- How many sold so far
    totalRaised : Decimal             -- Total payment collected
    startTime : Time                  -- When ICO starts
    endTime : Time                    -- When ICO ends
    minPurchase : Decimal             -- Minimum purchase amount
    maxPurchase : Decimal             -- Maximum purchase (0 = no limit)
  where
    ensure totalSaleTokens > 0.0 && exchangeRate > 0.0
    signatory issuer

    -- Buy tokens! This is the magic atomic swap
    choice Purchase : (ContractId Token.Token, ContractId IcoOffering)
      with
        buyer : Party
        paymentTokenCid : ContractId Token.Token  -- Buyer's payment token
        paymentAmount : Decimal                   -- How much they're paying
      controller buyer, issuer, saleTokenIssuer   -- All three parties must agree!
      do
        -- Check if ICO is active
        now <- getTime
        assert (now >= startTime)
        assert (now < endTime)

        -- Validate purchase amount
        assert (paymentAmount >= minPurchase)
        assert (maxPurchase == 0.0 || paymentAmount <= maxPurchase)

        -- Calculate how many tokens they get
        let saleTokenAmount = paymentAmount * exchangeRate

        -- Make sure we have enough tokens left
        let remainingTokens = totalSaleTokens - tokensSold
        assert (saleTokenAmount <= remainingTokens)

        -- Verify they own the payment token
        paymentToken <- fetch paymentTokenCid
        assert (paymentToken.issuer == paymentTokenIssuer)
        assert (paymentToken.symbol == paymentTokenSymbol)
        assert (paymentToken.owner == buyer)
        assert (paymentToken.amount >= paymentAmount)

        -- Handle payment (split if needed, then transfer)
        if paymentToken.amount == paymentAmount
          then do
            -- Exact amount: transfer the whole token
            _ <- exercise paymentTokenCid Token.Transfer with
                newOwner = issuer
            return ()
          else do
            -- Partial payment: split and transfer just the payment amount
            (paymentPortionCid, _changeCid) <- exercise paymentTokenCid Token.Split with
                splitAmount = paymentAmount
            _ <- exercise paymentPortionCid Token.Transfer with
                newOwner = issuer
            return ()

        -- Create the sale tokens for the buyer
        saleTokenCid <- create Token.Token with
          issuer = saleTokenIssuer
          owner = buyer
          symbol = saleTokenSymbol
          amount = saleTokenAmount

        -- Update the ICO with new totals
        updatedIcoCid <- create this with
          tokensSold = tokensSold + saleTokenAmount
          totalRaised = totalRaised + paymentAmount

        return (saleTokenCid, updatedIcoCid)

    -- Close the ICO when it's done
    choice Close : ContractId IcoCompleted
      controller issuer
      do
        now <- getTime
        assert (now >= endTime || tokensSold >= totalSaleTokens)
        create IcoCompleted with
          issuer = issuer
          saleTokenIssuer = saleTokenIssuer
          saleTokenSymbol = saleTokenSymbol
          paymentTokenIssuer = paymentTokenIssuer
          paymentTokenSymbol = paymentTokenSymbol
          totalSaleTokens = totalSaleTokens
          tokensSold = tokensSold
          totalRaised = totalRaised
          finalExchangeRate = exchangeRate

-- Record of a completed ICO
template IcoCompleted
  with
    issuer : Party
    saleTokenIssuer : Party
    saleTokenSymbol : Text
    paymentTokenIssuer : Party
    paymentTokenSymbol : Text
    totalSaleTokens : Decimal
    tokensSold : Decimal
    totalRaised : Decimal
    finalExchangeRate : Decimal
  where
    signatory issuer

    -- Get final statistics
    nonconsuming choice GetStats : (Decimal, Decimal, Decimal)
      controller issuer
      do return (tokensSold, totalRaised, finalExchangeRate)

-- Helper to create ICOs
template IcoFactory
  with
    issuer : Party
  where
    signatory issuer

    choice CreateIco : ContractId IcoOffering
      with
        saleTokenIssuer : Party
        saleTokenSymbol : Text
        paymentTokenIssuer : Party
        paymentTokenSymbol : Text
        exchangeRate : Decimal
        totalSaleTokens : Decimal
        startTime : Time
        endTime : Time
        minPurchase : Decimal
        maxPurchase : Decimal
      controller issuer
      do
        assert (totalSaleTokens > 0.0)
        assert (exchangeRate > 0.0)
        assert (endTime > startTime)
        assert (minPurchase > 0.0)

        create IcoOffering with
          issuer = issuer
          saleTokenIssuer = saleTokenIssuer
          saleTokenSymbol = saleTokenSymbol
          paymentTokenIssuer = paymentTokenIssuer
          paymentTokenSymbol = paymentTokenSymbol
          exchangeRate = exchangeRate
          totalSaleTokens = totalSaleTokens
          tokensSold = 0.0
          totalRaised = 0.0
          startTime = startTime
          endTime = endTime
          minPurchase = minPurchase
          maxPurchase = maxPurchase
Enter fullscreen mode Exit fullscreen mode

Testing It Out

Let's create a test script to verify our ICO works. Create IcoTest.daml:

module IcoTest where

import DA.Assert
import DA.Time
import Daml.Script
import Ico
import Token

test_ico_lifecycle = script do
  -- Create all the parties
  icoIssuer <- allocateParty "Company"
  saleTokenIssuer <- allocateParty "Company"
  paymentTokenIssuer <- allocateParty "USDCIssuer"
  buyer1 <- allocateParty "Alice"
  buyer2 <- allocateParty "Bob"

  -- Create token issuers
  saleIssuerCid <- submit saleTokenIssuer do
    createCmd TokenIssuer with
      issuer = saleTokenIssuer
      symbol = "MYCOIN"
      totalSupply = 1000000.0

  paymentIssuerCid <- submit paymentTokenIssuer do
    createCmd TokenIssuer with
      issuer = paymentTokenIssuer
      symbol = "USDC"
      totalSupply = 1000000.0

  -- Give buyers some USDC to spend
  buyer1PaymentCid <- submitMulti [paymentTokenIssuer, buyer1] [] do
    exerciseCmd paymentIssuerCid IssueTokens with
      recipient = buyer1
      amount = 1000.0

  buyer2PaymentCid <- submitMulti [paymentTokenIssuer, buyer2] [] do
    exerciseCmd paymentIssuerCid IssueTokens with
      recipient = buyer2
      amount = 500.0

  -- Set up the ICO
  now <- getTime
  factoryCid <- submit icoIssuer do
    createCmd IcoFactory with issuer = icoIssuer

  icoCid <- submit icoIssuer do
    exerciseCmd factoryCid CreateIco with
      saleTokenIssuer = saleTokenIssuer
      saleTokenSymbol = "MYCOIN"
      paymentTokenIssuer = paymentTokenIssuer
      paymentTokenSymbol = "USDC"
      exchangeRate = 100.0  -- 1 USDC = 100 MYCOIN
      totalSaleTokens = 50000.0
      startTime = now
      endTime = addRelTime now (days 1)
      minPurchase = 10.0
      maxPurchase = 0.0

  -- Alice buys 100 USDC worth (gets 10,000 MYCOIN)
  (aliceTokensCid, icoCid1) <- submitMulti [buyer1, icoIssuer, saleTokenIssuer] [] do
    exerciseCmd icoCid Purchase with
      buyer = buyer1
      paymentTokenCid = buyer1PaymentCid
      paymentAmount = 100.0

  -- Verify Alice got her tokens
  Some aliceTokens <- queryContractId buyer1 aliceTokensCid
  assert (aliceTokens.amount == 10000.0)
  assert (aliceTokens.symbol == "MYCOIN")

  -- Bob buys 50 USDC worth (gets 5,000 MYCOIN)
  (bobTokensCid, icoCid2) <- submitMulti [buyer2, icoIssuer, saleTokenIssuer] [] do
    exerciseCmd icoCid1 Purchase with
      buyer = buyer2
      paymentTokenCid = buyer2PaymentCid
      paymentAmount = 50.0

  -- Verify Bob got his tokens
  Some bobTokens <- queryContractId buyer2 bobTokensCid
  assert (bobTokens.amount == 5000.0)

  -- Check ICO totals
  Some finalIco <- queryContractId icoIssuer icoCid2
  assert (finalIco.tokensSold == 15000.0)  -- 10k + 5k
  assert (finalIco.totalRaised == 150.0)   -- 100 + 50

  return ()
Enter fullscreen mode Exit fullscreen mode

Testing It Out

Run the tests to make sure everything works:

# In the cn-quickstart/quickstart directory
cd daml/ico-token
daml test
Enter fullscreen mode Exit fullscreen mode

You should see something like:

Test suite passed with 8 transactions and 9 active contracts
Enter fullscreen mode Exit fullscreen mode

Deploying to LocalNet

Time to see your ICO running on a real Canton network! The build scripts handle all the complexity:

# Build the contracts
cd examples/ico-token
./scripts/build.sh

# Deploy to local network
./scripts/deploy.sh
Enter fullscreen mode Exit fullscreen mode

This will:

  1. Compile your Daml contracts into a DAR file
  2. Start Canton participant nodes
  3. Launch web interfaces for exploration
  4. Deploy your contracts to the ledger

Once running, you can access:


Making It Real

Your ICO is now live on LocalNet! Here's how to interact with it:

  1. Create parties through the wallet interfaces
  2. Issue tokens using the TokenIssuer contracts
  3. Set up an ICO using the IcoFactory
  4. Make purchases through the Purchase choice
  5. Watch the magic happen atomically

The best part? Only the buyer, seller, and sale token issuer can see the transaction details. Everyone else just sees that a transaction happened.


What's Next?

🎉 Congratulations! You just built a privacy-preserving ICO on Canton Network.

Want to take it further? Here are some ideas:

Add Features

  • Tiered pricing: Different rates for different purchase amounts
  • Time bonuses: Better rates for early buyers
  • Vesting: Lock purchased tokens for a period
  • KYC integration: Verify buyer identities

Go Production

  • Multi-party deployments: Run across different Canton domains
  • Compliance: Add regulator observers
  • Monitoring: Set up proper observability
  • Scaling: Handle thousands of concurrent buyers

Learn More

  • Daml patterns: Explore more Canton-specific smart contract patterns
  • Privacy models: Deep dive into sub-transaction privacy
  • Integration: Connect with existing DeFi protocols

The Canton ecosystem is growing fast. Your ICO contract is a great foundation for building the next generation of token sales!


About the Author

Dennison Bertram is the co-founder and CEO of Tally.xyz, the infrastructure layer for tokens—from ICO and airdrop to governance, staking, and value accrual. Tally delivers the complete framework for operating tokens at scale, powering the largest teams in the ecosystem with billions under management.

Top comments (0)