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}