token.gno
9.93 Kb · 378 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 ledger := &PrivateLedger{}
59 token := &Token{
60 id: pkgPath + "." + symbol + "." + id.String(),
61 name: name,
62 symbol: symbol,
63 decimals: decimals,
64 ledger: ledger,
65 }
66 ledger.token = token
67
68 chain.Emit(
69 NewTokenEvent,
70 "token", token.id,
71 "name", name,
72 "symbol", symbol,
73 "decimals", strconv.Itoa(decimals),
74 )
75
76 return token, ledger
77}
78
79// validName reports whether name is a valid display name: non-empty,
80// within MaxNameLen, and contains no control characters (any rune
81// below 0x20 or 0x7f). Permits Unicode letters, digits, punctuation,
82// and spaces — name is purely a display field.
83func validName(name string) bool {
84 if name == "" || len(name) > MaxNameLen {
85 return false
86 }
87 for _, c := range name {
88 if c < 0x20 || c == 0x7f {
89 return false
90 }
91 }
92 return true
93}
94
95// validSymbol reports whether s is valid slug-compatible metadata: non-empty,
96// within MaxSymbolLen, and consists only of [A-Za-z0-9_-].
97func validSymbol(s string) bool {
98 if s == "" || len(s) > MaxSymbolLen {
99 return false
100 }
101 for _, c := range s {
102 if !isAlnum(c) && c != '_' && c != '-' {
103 return false
104 }
105 }
106 return true
107}
108
109func isAlnum(c rune) bool {
110 return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
111}
112
113// GetName returns the name of the token.
114func (tok Token) GetName() string { return tok.name }
115
116// GetSymbol returns the symbol of the token.
117func (tok Token) GetSymbol() string { return tok.symbol }
118
119// GetDecimals returns the number of decimals used to get the token's precision.
120func (tok Token) GetDecimals() int { return tok.decimals }
121
122// TotalSupply returns the total supply of the token.
123func (tok Token) TotalSupply() int64 { return tok.ledger.totalSupply }
124
125// KnownAccounts returns the number of known accounts in the bank.
126func (tok Token) KnownAccounts() int { return tok.ledger.balances.Size() }
127
128// ID returns the Identifier of the token.
129// It is composed of the original realm, the symbol, and the provided id.
130func (tok *Token) ID() string {
131 return tok.id
132}
133
134// HasAddr checks if the specified address is a known account in the bank.
135func (tok Token) HasAddr(addr address) bool {
136 return tok.ledger.hasAddr(addr)
137}
138
139// BalanceOf returns the balance of the specified address.
140func (tok Token) BalanceOf(addr address) int64 {
141 return tok.ledger.balanceOf(addr)
142}
143
144// Allowance returns the allowance of the specified owner and spender.
145func (tok Token) Allowance(owner, spender address) int64 {
146 return tok.ledger.allowance(owner, spender)
147}
148
149func (tok Token) RenderHome() string {
150 str := ""
151 str += ufmt.Sprintf("# %s ($%s)\n\n", tok.name, tok.symbol)
152 str += ufmt.Sprintf("* **Decimals**: %d\n", tok.decimals)
153 str += ufmt.Sprintf("* **Total supply**: %d\n", tok.ledger.totalSupply)
154 str += ufmt.Sprintf("* **Known accounts**: %d\n", tok.KnownAccounts())
155 return str
156}
157
158// SpendAllowance decreases the allowance of the specified owner and spender.
159func (led *PrivateLedger) SpendAllowance(owner, spender address, amount int64) error {
160 if !owner.IsValid() || !spender.IsValid() {
161 return ErrInvalidAddress
162 }
163
164 if amount < 0 {
165 return ErrInvalidAmount
166 }
167 // do nothing
168 if amount == 0 {
169 return nil
170 }
171
172 currentAllowance := led.allowance(owner, spender)
173 if currentAllowance < amount {
174 return ErrInsufficientAllowance
175 }
176
177 key := allowanceKey(owner, spender)
178 newAllowance := overflow.Sub64p(currentAllowance, amount)
179
180 if newAllowance == 0 {
181 led.allowances.Remove(key)
182 } else {
183 led.allowances.Set(key, newAllowance)
184 }
185
186 return nil
187}
188
189// Transfer transfers tokens from the specified from address to the specified to address.
190func (led *PrivateLedger) Transfer(from, to address, amount int64) error {
191 if !from.IsValid() {
192 return ErrInvalidAddress
193 }
194 if !to.IsValid() {
195 return ErrInvalidAddress
196 }
197 if from == to {
198 return ErrCannotTransferToSelf
199 }
200 if amount < 0 {
201 return ErrInvalidAmount
202 }
203
204 var (
205 toBalance = led.balanceOf(to)
206 fromBalance = led.balanceOf(from)
207 )
208
209 if fromBalance < amount {
210 return ErrInsufficientBalance
211 }
212
213 var (
214 newToBalance = overflow.Add64p(toBalance, amount)
215 newFromBalance = overflow.Sub64p(fromBalance, amount)
216 )
217
218 led.balances.Set(string(to), newToBalance)
219
220 if newFromBalance == 0 {
221 led.balances.Remove(string(from))
222 } else {
223 led.balances.Set(string(from), newFromBalance)
224 }
225
226 chain.Emit(
227 TransferEvent,
228 "token", led.token.ID(),
229 "from", from.String(),
230 "to", to.String(),
231 "value", strconv.Itoa(int(amount)),
232 )
233
234 return nil
235}
236
237// TransferFrom transfers tokens from the specified owner to the specified to address.
238// It first checks if the owner has sufficient balance and then decreases the allowance.
239func (led *PrivateLedger) TransferFrom(owner, spender, to address, amount int64) error {
240 if amount < 0 {
241 return ErrInvalidAmount
242 }
243
244 if !owner.IsValid() || !to.IsValid() {
245 return ErrInvalidAddress
246 }
247
248 if led.balanceOf(owner) < amount {
249 return ErrInsufficientBalance
250 }
251
252 // The check above guarantees that Transfer will succeed, ensuring
253 // atomicity for the subsequent operations.
254 if err := led.SpendAllowance(owner, spender, amount); err != nil {
255 return err
256 }
257
258 if err := led.Transfer(owner, to, amount); err != nil {
259 return err
260 }
261
262 return nil
263}
264
265// Approve sets the allowance of the specified owner and spender.
266func (led *PrivateLedger) Approve(owner, spender address, amount int64) error {
267 if !owner.IsValid() || !spender.IsValid() {
268 return ErrInvalidAddress
269 }
270 if amount < 0 {
271 return ErrInvalidAmount
272 }
273
274 led.allowances.Set(allowanceKey(owner, spender), amount)
275
276 chain.Emit(
277 ApprovalEvent,
278 "token", led.token.ID(),
279 "owner", string(owner),
280 "spender", string(spender),
281 "value", strconv.Itoa(int(amount)),
282 )
283
284 return nil
285}
286
287// Mint increases the total supply of the token and adds the specified amount to the specified address.
288func (led *PrivateLedger) Mint(addr address, amount int64) error {
289 if !addr.IsValid() {
290 return ErrInvalidAddress
291 }
292 if amount < 0 {
293 return ErrInvalidAmount
294 }
295
296 // limit amount to MaxInt64 - totalSupply
297 if amount > overflow.Sub64p(math.MaxInt64, led.totalSupply) {
298 return ErrMintOverflow
299 }
300
301 led.totalSupply += amount
302 currentBalance := led.balanceOf(addr)
303 newBalance := overflow.Add64p(currentBalance, amount)
304
305 led.balances.Set(string(addr), newBalance)
306
307 chain.Emit(
308 TransferEvent,
309 "token", led.token.ID(),
310 "from", "",
311 "to", string(addr),
312 "value", strconv.Itoa(int(amount)),
313 )
314
315 return nil
316}
317
318// Burn decreases the total supply of the token and subtracts the specified amount from the specified address.
319func (led *PrivateLedger) Burn(addr address, amount int64) error {
320 if !addr.IsValid() {
321 return ErrInvalidAddress
322 }
323 if amount < 0 {
324 return ErrInvalidAmount
325 }
326
327 currentBalance := led.balanceOf(addr)
328 if currentBalance < amount {
329 return ErrInsufficientBalance
330 }
331
332 led.totalSupply = overflow.Sub64p(led.totalSupply, amount)
333 newBalance := overflow.Sub64p(currentBalance, amount)
334
335 if newBalance == 0 {
336 led.balances.Remove(string(addr))
337 } else {
338 led.balances.Set(string(addr), newBalance)
339 }
340
341 chain.Emit(
342 TransferEvent,
343 "token", led.token.ID(),
344 "from", string(addr),
345 "to", "",
346 "value", strconv.Itoa(int(amount)),
347 )
348
349 return nil
350}
351
352// hasAddr checks if the specified address is a known account in the ledger.
353func (led PrivateLedger) hasAddr(addr address) bool {
354 return led.balances.Has(addr.String())
355}
356
357// balanceOf returns the balance of the specified address.
358func (led PrivateLedger) balanceOf(addr address) int64 {
359 balance := led.balances.Get(addr.String())
360 if balance == nil {
361 return 0
362 }
363 return balance.(int64)
364}
365
366// allowance returns the allowance of the specified owner and spender.
367func (led PrivateLedger) allowance(owner, spender address) int64 {
368 allowance := led.allowances.Get(allowanceKey(owner, spender))
369 if allowance == nil {
370 return 0
371 }
372 return allowance.(int64)
373}
374
375// allowanceKey returns the key for the allowance of the specified owner and spender.
376func allowanceKey(owner, spender address) string {
377 return owner.String() + ":" + spender.String()
378}