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)