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