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 fixture | What this chapter borrows |
|---|---|
contracts/token.cam | Core transfer / approve / balance surface |
contracts/erc20_events_evm.cam | event + emit |
contracts/erc20_errors_evm.cam | Named 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
| Pattern | Where |
|---|---|
pure fn for map lookups used in where and transforms | balance_of |
Named error + throw Error(…) | erc20_errors_evm.cam |
event + emit | erc20_events_evm.cam |
Unphased CEI: update maps, then emit | transfer 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
| File | Role |
|---|---|
contracts/token.cam | Baseline entity (numeric throws, no events) |
contracts/erc20_events_evm.cam | Event / emit patterns |
contracts/erc20_errors_evm.cam | Named errors |
Add these paths to a project.yaml with target: evm to generate Foundry
harnesses — same workflow as Counter.