Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}