Skip to content
LogoLogo

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; };
}
  • transferFrom treats an allowance of Uint.max as infinite, like OpenZeppelin.
  • move is internal and shared by transfer and transferFrom, and its errors flow into both.
  • For a modular version, move the balances into a module ERC20 with named storage. The Modules example shows the pattern.