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