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