Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Token (ERC20-like)

A fungible token with HashMap balances, nested allowances, named errors, and event / emit. The chapter below is a pedagogical merge of patterns from several fixtures — it is not a line-for-line copy of a single file in the tree.

Source fixtureWhat this chapter borrows
contracts/token.camCore transfer / approve / balance surface
contracts/erc20_events_evm.camevent + emit
contracts/erc20_errors_evm.camNamed error + throw Error(…)

The canonical minimal token in the workspace still uses numeric throws and no events — open contracts/token.cam when you need that baseline.

Helpers and surface

pure fn balance_of(balances: HashMap<address, U256>, owner: address) -> U256 {
    if balances.exists(owner) { balances[owner] } else { 0 }
}

entity Token {
    event Transfer(indexed src: address, indexed dst: address, value: U256);
    event Approval(indexed owner: address, indexed spender: address, value: U256);

    error InsufficientBalance(have: U256, need: U256);
    error Unauthorized();
    error ZeroAmount();

    routes {
        constructor(name: String, symbol: String, decimals: u8,
                    initial_supply: U256) => []

        transfer(to: address, amount: U256)
            where amount > 0 : throw ZeroAmount() => [
                if balance_of(m_balances, msg::sender) < amount => [
                    throw InsufficientBalance(
                        balance_of(m_balances, msg::sender), amount)
                ]
                emit Transfer(msg::sender, to, amount);
            ]

        approve(spender: address, amount: U256) => [
            emit Approval(msg::sender, spender, amount);
        ]

        view balanceOf(owner: address) -> U256 => [
            return(balance_of(m_balances, owner))
        ]

        view totalSupply() -> U256 => [
            return(m_total_supply)
        ]
    }
    // members below…
}

where / throw Name(…) pair with declared errors; emit pairs with declared events. Up to three indexed fields per event on EVM (V36).

HashMap balances

m_balances: HashMap<address, U256> {
    in constructor(_, _, _, initial_supply) => {
        let sender = msg::sender;
        {}.insert(sender, initial_supply)
    }
    in transfer(to, amount) => {
        let sender = msg::sender;
        let sender_bal = balance_of(m_balances, sender);
        let to_bal = balance_of(m_balances, to);
        m_balances
            .update(sender, sender_bal - amount)
            .update(to, to_bal + amount)
    }
}

Maps are persistent values: .insert / .update return a new map; the transform’s final expression becomes the stored member. Empty map literal: {}.

Nested allowances

m_allowances: HashMap<address, HashMap<address, U256>> {
    in approve(spender, amount) => {
        let sender = msg::sender;
        let inner = if m_allowances.exists(sender) {
            m_allowances[sender]
        } else {
            {}
        };
        m_allowances.update(sender, inner.update(spender, amount))
    }
}

transferFrom decrements the inner map the same way (see contracts/token.cam for the full mint / burn / allowance path).

Patterns worth copying

PatternWhere
pure fn for map lookups used in where and transformsbalance_of
Named error + throw Error(…)erc20_errors_evm.cam
event + emiterc20_events_evm.cam
Unphased CEI: update maps, then emittransfer body above

For a production-shaped ERC-20 with EIP-2612 permit and optional multi-instance identity, use the shipped Contract Standard Library (stdlib/token/ERC20.cam / ERC20Multi.cam). The Uniswap V2 example keeps its own pair token in examples/uniswap-v2/ERC20.cam.

Tests in the workspace

FileRole
contracts/token.camBaseline entity (numeric throws, no events)
contracts/erc20_events_evm.camEvent / emit patterns
contracts/erc20_errors_evm.camNamed errors

Add these paths to a project.yaml with target: evm to generate Foundry harnesses — same workflow as Counter.