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.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}