Skip to main content

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:
Properties:
  • 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:
Properties:
  • 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:
Finalizer:
How it works:
  1. Function receives receiver address and amount (both public)
  2. Returns a finalizer that executes on-chain
  3. Finalizer reads current balance (or 0 if none exists)
  4. Adds the minted amount to the balance
  5. Stores the updated balance on-chain
Usage:

Private Mint

Mint tokens as a private record:
How it works:
  1. Creates a new token record
  2. Sets the owner to the receiver
  3. Sets the amount
  4. Returns the record (encrypted on-chain)
Key Difference:
  • No finalizer needed (no on-chain state update)
  • Record is returned to the receiver
  • Amount remains private
Usage:

Transfer Functions

Public Transfer

Transfer tokens between public balances:
Finalizer:
How it works:
  1. Uses self.caller to identify the sender
  2. Finalizer decrements sender’s balance
  3. Finalizer increments receiver’s balance
  4. Transaction fails if sender has insufficient balance
Usage:

Private Transfer

Transfer tokens using private records:
How it works:
  1. Consumes the sender’s token record
  2. Calculates the change amount
  3. Creates a new record for the sender with the remaining balance
  4. Creates a new record for the receiver with the transferred amount
  5. Returns both records
Key Features:
  • Completely private (amounts hidden)
  • No on-chain state updates
  • Proof verifies sender has sufficient balance
  • Fails if sender.amount < amount (underflow protection)
Usage:

Private to Public Transfer

Convert private tokens to public balance:
Finalizer:
How it works:
  1. Consumes private record
  2. Creates change record for sender (private)
  3. Finalizer adds amount to receiver’s public balance
  4. Receiver address and amount become public
Privacy Trade-offs:
  • Sender’s identity remains private
  • Receiver address is public
  • Amount is public

Public to Private Transfer

Convert public balance to private record:
Finalizer:
How it works:
  1. Creates private record for receiver
  2. Finalizer deducts amount from sender’s public balance
  3. Receiver gets private record
Privacy Trade-offs:
  • 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
Use Private (Records) when:
  • Privacy is paramount
  • Hiding transaction amounts
  • Competitive advantages
  • User preference for privacy

Finalizers

Finalizers execute on-chain after the main function:
  1. Main function generates a proof
  2. Proof is verified
  3. Finalizer executes on-chain
  4. Finalizer can access and modify mappings
  5. Finalizer can use block.height and other on-chain data

Security Considerations

Overflow Protection

Addition can overflow! If current_amount + amount > u64::MAX, the transaction fails.

Underflow Protection

Subtraction can underflow! If sender.amount < amount, the proof generation fails.

Authorization

The self.caller value identifies the transaction initiator:
This ensures only the account owner can transfer their tokens.

Testing

Unit Tests

Create test cases in inputs/token.in:

Run Tests

Extensions and Improvements

Add Total Supply Tracking

Add Burn Functionality

Add Transfer Limits

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