ERC-20 token
A standard fungible token. Function, event, and error names match the ERC-20 and ERC-6093 specifications, so wallets, explorers, and other contracts treat it like any other ERC-20.
erc20.lithe
(* A complete ERC-20 with an owner who can mint. Names match the standard exactly,
and errors follow OpenZeppelin's ERC-6093 conventions. *)
import "std/address";
import "std/evm";
import "std/uint";
error ERC20InsufficientBalance of record {
sender : address;
balance : uint;
needed : uint;
}
error ERC20InsufficientAllowance of record {
spender : address;
allowance : uint;
needed : uint;
}
error ERC20InvalidReceiver of address;
error OwnableUnauthorizedAccount of address;
error AlreadyInitialized;
event Transfer of record {
indexed from : address;
indexed to : address;
value : uint;
}
event Approval of record {
indexed owner : address;
indexed spender : address;
value : uint;
}
storage is record {
owner : address;
name : string;
symbol : string;
totalSupply : uint;
balances : map(address, uint);
allowances : map(address, map(address, uint));
}
external function initialize(const name : string; const symbol : string)
raises AlreadyInitialized
is {
assert(storage.owner = Address.zero, AlreadyInitialized);
storage.owner := Evm.sender();
storage.name := name;
storage.symbol := symbol;
}
external view function name() : string is storage.name
external view function symbol() : string is storage.symbol
external pure function decimals() : uint8 is 18
external view function totalSupply() : uint is storage.totalSupply
external view function balanceOf(const account : address) : uint is storage.balances[account]
external view function allowance(const owner : address; const spender : address) : uint is
storage.allowances[owner][spender]
function move(const from : address; const to : address; const value : uint)
raises ERC20InsufficientBalance | ERC20InvalidReceiver
is {
assert(to <> Address.zero, ERC20InvalidReceiver(to));
const balance : uint := storage.balances[from];
assert(
balance >= value,
ERC20InsufficientBalance { sender = from; balance = balance; needed = value; }
);
storage.balances[from] := balance - value;
storage.balances[to] := storage.balances[to] + value;
emit Transfer { from = from; to = to; value = value; };
}
external function transfer(const to : address; const value : uint) : bool
raises ERC20InsufficientBalance | ERC20InvalidReceiver
is {
move(Evm.sender(), to, value);
} with true
external function approve(const spender : address; const value : uint) : bool is {
storage.allowances[Evm.sender()][spender] := value;
emit Approval { owner = Evm.sender(); spender = spender; value = value; };
} with true
external function transferFrom(const from : address; const to : address; const value : uint) : bool
raises ERC20InsufficientAllowance | ERC20InsufficientBalance | ERC20InvalidReceiver
is {
const allowed : uint := storage.allowances[from][Evm.sender()];
(* An allowance of Uint.max never decreases, as in OpenZeppelin. *)
if allowed < Uint.max then {
assert(
allowed >= value,
ERC20InsufficientAllowance { spender = Evm.sender(); allowance = allowed; needed = value; }
);
storage.allowances[from][Evm.sender()] := allowed - value;
}
move(from, to, value);
} with true
external function mint(const to : address; const value : uint)
raises OwnableUnauthorizedAccount | ERC20InvalidReceiver
is {
assert(Evm.sender() = storage.owner, OwnableUnauthorizedAccount(Evm.sender()));
assert(to <> Address.zero, ERC20InvalidReceiver(to));
storage.totalSupply := storage.totalSupply + value;
storage.balances[to] := storage.balances[to] + value;
emit Transfer { from = Address.zero; to = to; value = value; };
}transferFromtreats an allowance ofUint.maxas infinite, like OpenZeppelin.moveis internal and shared bytransferandtransferFrom, and its errors flow into both.- For a modular version, move the balances into a
module ERC20with named storage. The Modules example shows the pattern.

