Overview
The Token example demonstrates how to build a custom token with both public (transparent) and private (shielded) functionality. This is one of the most comprehensive examples, showcasing records, mappings, and finalizers.This example is located at
.circleci/token/ in the Leo repository and is actively tested in CI.Program Structure
Data Structures
Mapping: Public Balances
Public token balances are stored on-chain in a mapping:- Publicly visible on the blockchain
- Accessed in finalizer functions
- Key:
address(account owner) - Value:
u64(token balance)
Record: Private Tokens
Private tokens are stored as records:- Private by default (encrypted on-chain)
- Owner controls spending
- Can be split and combined
- Zero-knowledge proofs hide amounts
Minting Functions
Public Mint
Mint tokens directly to a public balance:- Function receives receiver address and amount (both public)
- Returns a finalizer that executes on-chain
- Finalizer reads current balance (or 0 if none exists)
- Adds the minted amount to the balance
- Stores the updated balance on-chain
Private Mint
Mint tokens as a private record:- Creates a new token record
- Sets the owner to the receiver
- Sets the amount
- Returns the record (encrypted on-chain)
- No finalizer needed (no on-chain state update)
- Record is returned to the receiver
- Amount remains private
Transfer Functions
Public Transfer
Transfer tokens between public balances:- Uses
self.callerto identify the sender - Finalizer decrements sender’s balance
- Finalizer increments receiver’s balance
- Transaction fails if sender has insufficient balance
Private Transfer
Transfer tokens using private records:- Consumes the sender’s token record
- Calculates the change amount
- Creates a new record for the sender with the remaining balance
- Creates a new record for the receiver with the transferred amount
- Returns both records
- Completely private (amounts hidden)
- No on-chain state updates
- Proof verifies sender has sufficient balance
- Fails if
sender.amount < amount(underflow protection)
Private to Public Transfer
Convert private tokens to public balance:- Consumes private record
- Creates change record for sender (private)
- Finalizer adds amount to receiver’s public balance
- Receiver address and amount become public
- Sender’s identity remains private
- Receiver address is public
- Amount is public
Public to Private Transfer
Convert public balance to private record:- Creates private record for receiver
- Finalizer deducts amount from sender’s public balance
- Receiver gets private record
- Sender address is public (
self.caller) - Receiver address is public
- Amount is public
- Resulting record is private
Running the Example
Build the Program
Run Functions
Use the Demo Script
Key Concepts
Records vs Mappings
When to Use Public vs Private
Use Public (Mappings) when:- Transparency is required
- Regulatory compliance needs
- Public audit trails
- Simpler user experience
- Privacy is paramount
- Hiding transaction amounts
- Competitive advantages
- User preference for privacy
Finalizers
Finalizers execute on-chain after the main function:- Main function generates a proof
- Proof is verified
- Finalizer executes on-chain
- Finalizer can access and modify mappings
- Finalizer can use
block.heightand other on-chain data
Security Considerations
Overflow Protection
Underflow Protection
Authorization
Theself.caller value identifies the transaction initiator:
Testing
Unit Tests
Create test cases ininputs/token.in:
Run Tests
Extensions and Improvements
Add Total Supply Tracking
Add Burn Functionality
Add Transfer Limits
Related Examples
Lottery
Simpler example with randomness
Tic-Tac-Toe
Game logic with structs
Further Reading
Records
Learn more about records
Mappings
Deep dive into mappings
Finalize
Understanding finalize blocks
Built-in Types
Type reference