1// Package erc20 implements MiniToken, a minimal fungible token modeled on the2// Solidity ERC-20 idea, ported to gno.land (gno 0.9 / sapphire).3//4// State is package-level and persistent. All state-mutating exported functions5// are crossing functions (they take `cur realm` as the first parameter) per the6// gno 0.9 interrealm convention, and identify the caller via7// cur.Previous().Address() after checking cur.IsCurrent().8package erc20910import (11 "errors"12 "sort"13 "strconv"14 "strings"15)1617const (18 name = "MiniToken"19 symbol = "MINI"20)2122var (23 // balances maps holder address -> token balance.24 balances = map[address]uint64{}25 // allowances[owner][spender] = amount the spender may pull from owner.26 allowances = map[address]map[address]uint64{}27 // totalSupply is the sum of all balances.28 totalSupply uint6429)3031var (32 errZeroAddress = errors.New("erc20: zero address")33 errInsufficient = errors.New("erc20: insufficient balance")34 errAllowanceLow = errors.New("erc20: insufficient allowance")35 errOverflow = errors.New("erc20: supply overflow")36 errSpoofedRealm = errors.New("erc20: spoofed realm")37 errNotUser = errors.New("erc20: caller is not a user account")38)3940// caller returns the immediate caller's address for a crossing function.41// It enforces the gno 0.9 authentication primitive: cur must be the live42// crossing frame before its Previous() is trusted.43func caller(cur realm) address {44 if !cur.IsCurrent() {45 panic(errSpoofedRealm)46 }47 prev := cur.Previous()48 if !prev.IsUser() {49 panic(errNotUser)50 }51 return prev.Address()52}5354// Mint creates `amount` new tokens and credits them to `to`, increasing the55// total supply. Crossing function.56func Mint(cur realm, to address, amount uint64) {57 _ = caller(cur) // authenticate the crossing frame; anyone may mint in this demo58 if !to.IsValid() {59 panic(errZeroAddress)60 }61 if amount == 0 {62 return63 }64 if totalSupply+amount < totalSupply {65 panic(errOverflow)66 }67 totalSupply += amount68 balances[to] += amount69}7071// Transfer moves `amount` tokens from the caller to `to`. Crossing function.72func Transfer(cur realm, to address, amount uint64) {73 from := caller(cur)74 if !to.IsValid() {75 panic(errZeroAddress)76 }77 if err := transfer(from, to, amount); err != nil {78 panic(err)79 }80}8182// Approve authorizes `spender` to pull up to `amount` tokens from the caller.83// Crossing function.84func Approve(cur realm, spender address, amount uint64) {85 owner := caller(cur)86 if !spender.IsValid() {87 panic(errZeroAddress)88 }89 m := allowances[owner]90 if m == nil {91 m = map[address]uint64{}92 allowances[owner] = m93 }94 m[spender] = amount95}9697// TransferFrom moves `amount` tokens from `from` to `to`, spending the caller's98// allowance granted by `from`. Crossing function.99func TransferFrom(cur realm, from, to address, amount uint64) {100 spender := caller(cur)101 if !from.IsValid() || !to.IsValid() {102 panic(errZeroAddress)103 }104 allowed := allowances[from][spender]105 if allowed < amount {106 panic(errAllowanceLow)107 }108 if err := transfer(from, to, amount); err != nil {109 panic(err)110 }111 allowances[from][spender] = allowed - amount112}113114// transfer is the internal, non-crossing balance mover. Pure logic on package115// state; returns an error rather than panicking so callers control reverts.116func transfer(from, to address, amount uint64) error {117 if amount == 0 {118 return nil119 }120 if balances[from] < amount {121 return errInsufficient122 }123 balances[from] -= amount124 balances[to] += amount125 return nil126}127128// BalanceOf returns the token balance of `holder`. Read-only, non-crossing.129func BalanceOf(holder address) uint64 {130 return balances[holder]131}132133// Allowance returns how much `spender` may still pull from `owner`.134func Allowance(owner, spender address) uint64 {135 return allowances[owner][spender]136}137138// TotalSupply returns the total number of tokens in existence.139func TotalSupply() uint64 {140 return totalSupply141}142143// Name returns the token name.144func Name() string { return name }145146// Symbol returns the token symbol.147func Symbol() string { return symbol }148149// Render produces the gnoweb Markdown view of the token: metadata plus a150// deterministically-ordered table of non-zero balances. Render is NOT a151// crossing function (no `cur realm` parameter).152func Render(path string) string {153 var b strings.Builder154155 b.WriteString("# " + name + " (" + symbol + ")\n\n")156 b.WriteString("A minimal ERC-20-style fungible token on gno.land.\n\n")157 b.WriteString("- **Name:** " + name + "\n")158 b.WriteString("- **Symbol:** " + symbol + "\n")159 b.WriteString("- **Total supply:** " + strconv.FormatUint(totalSupply, 10) + "\n\n")160161 // Collect and sort holder addresses for deterministic output. Map iteration162 // order is not a stable contract, so we sort the string keys (gno's sort163 // package has no generic Slice; Strings is the deterministic primitive).164 keys := make([]string, 0, len(balances))165 byKey := make(map[string]uint64, len(balances))166 for addr, bal := range balances {167 if bal == 0 {168 continue169 }170 s := addr.String()171 keys = append(keys, s)172 byKey[s] = bal173 }174 sort.Strings(keys)175176 b.WriteString("## Balances\n\n")177 if len(keys) == 0 {178 b.WriteString("_No holders yet — call `Mint` to create tokens._\n")179 return b.String()180 }181182 b.WriteString("| Address | Amount |\n")183 b.WriteString("| --- | ---: |\n")184 for _, k := range keys {185 b.WriteString("| `" + k + "` | " +186 strconv.FormatUint(byKey[k], 10) + " |\n")187 }188189 return b.String()190}191Allowance(owner string, spender string) uint64
Approve(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, spender string, amount uint64)
BalanceOf(holder string) uint64
Mint(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, to string, amount uint64)
Name() string
Render(path string) string
Symbol() string
TotalSupply() uint64
Transfer(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, to string, amount uint64)
TransferFrom(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, from string, to string, amount uint64)
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.