Search Apps Documentation Source Content File Folder Download Copy Actions Download

erc20.gno

5.31 Kb · 190 lines
  1// Package erc20 implements MiniToken, a minimal fungible token modeled on the
  2// Solidity ERC-20 idea, ported to gno.land (gno 0.9 / test13).
  3//
  4// State is package-level and persistent. All state-mutating exported functions
  5// are crossing functions (they take `cur realm` as the first parameter) per the
  6// gno 0.9 interrealm convention, and identify the caller via
  7// cur.Previous().Address() after checking cur.IsCurrent().
  8package erc20
  9
 10import (
 11	"errors"
 12	"sort"
 13	"strconv"
 14	"strings"
 15)
 16
 17const (
 18	name   = "MiniToken"
 19	symbol = "MINI"
 20)
 21
 22var (
 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 uint64
 29)
 30
 31var (
 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)
 39
 40// caller returns the immediate caller's address for a crossing function.
 41// It enforces the gno 0.9 authentication primitive: cur must be the live
 42// 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}
 53
 54// Mint creates `amount` new tokens and credits them to `to`, increasing the
 55// total supply. Crossing function.
 56func Mint(cur realm, to address, amount uint64) {
 57	_ = caller(cur) // authenticate the crossing frame; anyone may mint in this demo
 58	if !to.IsValid() {
 59		panic(errZeroAddress)
 60	}
 61	if amount == 0 {
 62		return
 63	}
 64	if totalSupply+amount < totalSupply {
 65		panic(errOverflow)
 66	}
 67	totalSupply += amount
 68	balances[to] += amount
 69}
 70
 71// 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}
 81
 82// 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] = m
 93	}
 94	m[spender] = amount
 95}
 96
 97// TransferFrom moves `amount` tokens from `from` to `to`, spending the caller's
 98// 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 - amount
112}
113
114// transfer is the internal, non-crossing balance mover. Pure logic on package
115// 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 nil
119	}
120	if balances[from] < amount {
121		return errInsufficient
122	}
123	balances[from] -= amount
124	balances[to] += amount
125	return nil
126}
127
128// BalanceOf returns the token balance of `holder`. Read-only, non-crossing.
129func BalanceOf(holder address) uint64 {
130	return balances[holder]
131}
132
133// Allowance returns how much `spender` may still pull from `owner`.
134func Allowance(owner, spender address) uint64 {
135	return allowances[owner][spender]
136}
137
138// TotalSupply returns the total number of tokens in existence.
139func TotalSupply() uint64 {
140	return totalSupply
141}
142
143// Name returns the token name.
144func Name() string { return name }
145
146// Symbol returns the token symbol.
147func Symbol() string { return symbol }
148
149// Render produces the gnoweb Markdown view of the token: metadata plus a
150// deterministically-ordered table of non-zero balances. Render is NOT a
151// crossing function (no `cur realm` parameter).
152func Render(path string) string {
153	var b strings.Builder
154
155	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")
160
161	// Collect and sort holder addresses for deterministic output. Map iteration
162	// order is not a stable contract, so we sort the string keys (gno's sort
163	// 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			continue
169		}
170		s := addr.String()
171		keys = append(keys, s)
172		byKey[s] = bal
173	}
174	sort.Strings(keys)
175
176	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	}
181
182	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	}
188
189	return b.String()
190}