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

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}