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