token.gno
9.05 Kb · 357 lines
1package grc20
2
3import (
4 "chain"
5 "math"
6 "math/overflow"
7 "strconv"
8
9 "gno.land/p/nt/ufmt/v0"
10)
11
12// NewToken creates a Token whose origRealm is bound to the calling realm.
13// rlm must be the caller's own captured cur (asserted via rlm.IsCurrent()),
14// and rlm.PkgPath() — the calling realm itself — becomes the Token's
15// origRealm. Token.ID() returns origRealm + "." + symbol.
16//
17// Because IsCurrent runtime-validates that rlm came from the live
18// crossing frame, origRealm is unforgeable: an external realm cannot
19// fabricate a Token claiming to belong to a different package.
20//
21// Typical call from a realm's init(cur realm) or other crossing function:
22//
23// Token, ledger := grc20.NewToken(0, cur, "Foo", "FOO", 4)
24//
25// If the Token should be discoverable, follow up with
26// grc20reg.Register(cross, Token, slug).
27func NewToken(_ int, rlm realm, name, symbol string, decimals int) (*Token, *PrivateLedger) {
28 if !rlm.IsCurrent() {
29 panic(ErrSpoofedRealm)
30 }
31 pkgPath := rlm.PkgPath()
32 if pkgPath == "" {
33 panic(ErrNotRealm)
34 }
35 if !validName(name) {
36 panic(ErrInvalidName)
37 }
38 if !validSymbol(symbol) {
39 panic(ErrInvalidSymbol)
40 }
41 if decimals < 0 || decimals > MaxDecimals {
42 panic(ErrInvalidDecimals)
43 }
44 ledger := &PrivateLedger{}
45 token := &Token{
46 name: name,
47 symbol: symbol,
48 decimals: decimals,
49 origRealm: pkgPath,
50 ledger: ledger,
51 }
52 ledger.token = token
53 return token, ledger
54}
55
56// validName reports whether name is a valid display name: non-empty,
57// within MaxNameLen, and contains no control characters (any rune
58// below 0x20 or 0x7f). Permits Unicode letters, digits, punctuation,
59// and spaces — name is purely a display field.
60func validName(name string) bool {
61 if name == "" || len(name) > MaxNameLen {
62 return false
63 }
64 for _, c := range name {
65 if c < 0x20 || c == 0x7f {
66 return false
67 }
68 }
69 return true
70}
71
72// validSymbol reports whether symbol is a valid token symbol: non-empty,
73// within MaxSymbolLen, and consists only of [A-Za-z0-9_-]. Matches
74// grc20reg.validateSlug so the symbol round-trips cleanly through
75// Token.ID() and any registry binding.
76func validSymbol(symbol string) bool {
77 if symbol == "" || len(symbol) > MaxSymbolLen {
78 return false
79 }
80 for _, c := range symbol {
81 if !isAlnum(c) && c != '_' && c != '-' {
82 return false
83 }
84 }
85 return true
86}
87
88func isAlnum(c rune) bool {
89 return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
90}
91
92// GetName returns the name of the token.
93func (tok Token) GetName() string { return tok.name }
94
95// GetSymbol returns the symbol of the token.
96func (tok Token) GetSymbol() string { return tok.symbol }
97
98// GetDecimals returns the number of decimals used to get the token's precision.
99func (tok Token) GetDecimals() int { return tok.decimals }
100
101// TotalSupply returns the total supply of the token.
102func (tok Token) TotalSupply() int64 { return tok.ledger.totalSupply }
103
104// KnownAccounts returns the number of known accounts in the bank.
105func (tok Token) KnownAccounts() int { return tok.ledger.balances.Size() }
106
107// ID returns the Identifier of the token.
108// It is composed of the original realm and the provided symbol.
109func (tok *Token) ID() string {
110 return tok.origRealm + "." + tok.symbol
111}
112
113// HasAddr checks if the specified address is a known account in the bank.
114func (tok Token) HasAddr(addr address) bool {
115 return tok.ledger.hasAddr(addr)
116}
117
118// BalanceOf returns the balance of the specified address.
119func (tok Token) BalanceOf(addr address) int64 {
120 return tok.ledger.balanceOf(addr)
121}
122
123// Allowance returns the allowance of the specified owner and spender.
124func (tok Token) Allowance(owner, spender address) int64 {
125 return tok.ledger.allowance(owner, spender)
126}
127
128func (tok Token) RenderHome() string {
129 str := ""
130 str += ufmt.Sprintf("# %s ($%s)\n\n", tok.name, tok.symbol)
131 str += ufmt.Sprintf("* **Decimals**: %d\n", tok.decimals)
132 str += ufmt.Sprintf("* **Total supply**: %d\n", tok.ledger.totalSupply)
133 str += ufmt.Sprintf("* **Known accounts**: %d\n", tok.KnownAccounts())
134 return str
135}
136
137// SpendAllowance decreases the allowance of the specified owner and spender.
138func (led *PrivateLedger) SpendAllowance(owner, spender address, amount int64) error {
139 if !owner.IsValid() || !spender.IsValid() {
140 return ErrInvalidAddress
141 }
142
143 if amount < 0 {
144 return ErrInvalidAmount
145 }
146 // do nothing
147 if amount == 0 {
148 return nil
149 }
150
151 currentAllowance := led.allowance(owner, spender)
152 if currentAllowance < amount {
153 return ErrInsufficientAllowance
154 }
155
156 key := allowanceKey(owner, spender)
157 newAllowance := overflow.Sub64p(currentAllowance, amount)
158
159 if newAllowance == 0 {
160 led.allowances.Remove(key)
161 } else {
162 led.allowances.Set(key, newAllowance)
163 }
164
165 return nil
166}
167
168// Transfer transfers tokens from the specified from address to the specified to address.
169func (led *PrivateLedger) Transfer(from, to address, amount int64) error {
170 if !from.IsValid() {
171 return ErrInvalidAddress
172 }
173 if !to.IsValid() {
174 return ErrInvalidAddress
175 }
176 if from == to {
177 return ErrCannotTransferToSelf
178 }
179 if amount < 0 {
180 return ErrInvalidAmount
181 }
182
183 var (
184 toBalance = led.balanceOf(to)
185 fromBalance = led.balanceOf(from)
186 )
187
188 if fromBalance < amount {
189 return ErrInsufficientBalance
190 }
191
192 var (
193 newToBalance = overflow.Add64p(toBalance, amount)
194 newFromBalance = overflow.Sub64p(fromBalance, amount)
195 )
196
197 led.balances.Set(string(to), newToBalance)
198
199 if newFromBalance == 0 {
200 led.balances.Remove(string(from))
201 } else {
202 led.balances.Set(string(from), newFromBalance)
203 }
204
205 chain.Emit(
206 TransferEvent,
207 "token", led.token.ID(),
208 "from", from.String(),
209 "to", to.String(),
210 "value", strconv.Itoa(int(amount)),
211 )
212
213 return nil
214}
215
216// TransferFrom transfers tokens from the specified owner to the specified to address.
217// It first checks if the owner has sufficient balance and then decreases the allowance.
218func (led *PrivateLedger) TransferFrom(owner, spender, to address, amount int64) error {
219 if amount < 0 {
220 return ErrInvalidAmount
221 }
222
223 if !owner.IsValid() || !to.IsValid() {
224 return ErrInvalidAddress
225 }
226
227 if led.balanceOf(owner) < amount {
228 return ErrInsufficientBalance
229 }
230
231 // The check above guarantees that Transfer will succeed, ensuring
232 // atomicity for the subsequent operations.
233 if err := led.SpendAllowance(owner, spender, amount); err != nil {
234 return err
235 }
236
237 if err := led.Transfer(owner, to, amount); err != nil {
238 return err
239 }
240
241 return nil
242}
243
244// Approve sets the allowance of the specified owner and spender.
245func (led *PrivateLedger) Approve(owner, spender address, amount int64) error {
246 if !owner.IsValid() || !spender.IsValid() {
247 return ErrInvalidAddress
248 }
249 if amount < 0 {
250 return ErrInvalidAmount
251 }
252
253 led.allowances.Set(allowanceKey(owner, spender), amount)
254
255 chain.Emit(
256 ApprovalEvent,
257 "token", led.token.ID(),
258 "owner", string(owner),
259 "spender", string(spender),
260 "value", strconv.Itoa(int(amount)),
261 )
262
263 return nil
264}
265
266// Mint increases the total supply of the token and adds the specified amount to the specified address.
267func (led *PrivateLedger) Mint(addr address, amount int64) error {
268 if !addr.IsValid() {
269 return ErrInvalidAddress
270 }
271 if amount < 0 {
272 return ErrInvalidAmount
273 }
274
275 // limit amount to MaxInt64 - totalSupply
276 if amount > overflow.Sub64p(math.MaxInt64, led.totalSupply) {
277 return ErrMintOverflow
278 }
279
280 led.totalSupply += amount
281 currentBalance := led.balanceOf(addr)
282 newBalance := overflow.Add64p(currentBalance, amount)
283
284 led.balances.Set(string(addr), newBalance)
285
286 chain.Emit(
287 TransferEvent,
288 "token", led.token.ID(),
289 "from", "",
290 "to", string(addr),
291 "value", strconv.Itoa(int(amount)),
292 )
293
294 return nil
295}
296
297// Burn decreases the total supply of the token and subtracts the specified amount from the specified address.
298func (led *PrivateLedger) Burn(addr address, amount int64) error {
299 if !addr.IsValid() {
300 return ErrInvalidAddress
301 }
302 if amount < 0 {
303 return ErrInvalidAmount
304 }
305
306 currentBalance := led.balanceOf(addr)
307 if currentBalance < amount {
308 return ErrInsufficientBalance
309 }
310
311 led.totalSupply = overflow.Sub64p(led.totalSupply, amount)
312 newBalance := overflow.Sub64p(currentBalance, amount)
313
314 if newBalance == 0 {
315 led.balances.Remove(string(addr))
316 } else {
317 led.balances.Set(string(addr), newBalance)
318 }
319
320 chain.Emit(
321 TransferEvent,
322 "token", led.token.ID(),
323 "from", string(addr),
324 "to", "",
325 "value", strconv.Itoa(int(amount)),
326 )
327
328 return nil
329}
330
331// hasAddr checks if the specified address is a known account in the ledger.
332func (led PrivateLedger) hasAddr(addr address) bool {
333 return led.balances.Has(addr.String())
334}
335
336// balanceOf returns the balance of the specified address.
337func (led PrivateLedger) balanceOf(addr address) int64 {
338 balance, found := led.balances.Get(addr.String())
339 if !found {
340 return 0
341 }
342 return balance.(int64)
343}
344
345// allowance returns the allowance of the specified owner and spender.
346func (led PrivateLedger) allowance(owner, spender address) int64 {
347 allowance, found := led.allowances.Get(allowanceKey(owner, spender))
348 if !found {
349 return 0
350 }
351 return allowance.(int64)
352}
353
354// allowanceKey returns the key for the allowance of the specified owner and spender.
355func allowanceKey(owner, spender address) string {
356 return owner.String() + ":" + spender.String()
357}