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

tokens.gno

13.05 Kb · 403 lines
  1// Package tokens creates registered Launchpad GRC20 ledgers for the issuer
  2// realms listed in config. A token belongs to the issuer that created it: no
  3// other realm, and no admin, can mint into it.
  4package tokens
  5
  6import (
  7	"chain"
  8	"strconv"
  9
 10	"gno.land/p/nt/avl/v0"
 11	"gno.land/p/nt/grc20/v0"
 12	"gno.land/p/nt/seqid/v0"
 13	"gno.land/p/samcrew/launchpad/currency/v1"
 14	"gno.land/p/samcrew/launchpad/meta/v1"
 15	sm "gno.land/p/samcrew/launchpad/safemath/v1"
 16	"gno.land/r/nt/grc20reg/v0"
 17	"gno.land/r/samcrew/launchpad/config/v1"
 18)
 19
 20const (
 21	ModeDirectFixed = "direct_fixed"
 22	ModeDirectCap   = "direct_capped"
 23	ModeFairSale    = "fairsale"
 24	maxPublicID     = 9_999_999_999 // T plus ten decimal digits: GRC20 MaxSymbolLen is 11.
 25)
 26
 27// Metadata is editable only by the original creator until frozen.
 28type Metadata struct {
 29	Description string
 30	Image       string
 31	Website     string
 32	XHandle     string
 33	Telegram    string
 34}
 35
 36// createSpec is assembled within this realm: a foreign Gno realm cannot
 37// allocate a struct declared here, so the crossing interface uses scalars.
 38type createSpec struct {
 39	Creator         address
 40	Mode            string
 41	Name            string
 42	Ticker          string
 43	Decimals        int
 44	InitialSupply   int64
 45	MaxSupply       int64
 46	CurrencyKey     string
 47	ExpectedVersion int64
 48	Metadata        Metadata
 49}
 50
 51// Info excludes the private ledger capability and can be returned safely.
 52type Info struct {
 53	ID              string
 54	RegistryKey     string
 55	GRC20ID         string
 56	Issuer          address
 57	Creator         address
 58	Mode            string
 59	Name            string
 60	Ticker          string
 61	Decimals        int
 62	InitialSupply   int64
 63	MaxSupply       int64
 64	ConfigVersion   int64
 65	CurrencyKey     string
 66	Metadata        Metadata
 67	MetadataFrozen  bool
 68	MintAuthority   address
 69	PendingMintAuth address
 70	MintRenounced   bool
 71}
 72
 73type entry struct {
 74	info   Info
 75	token  *grc20.Token
 76	ledger *grc20.PrivateLedger
 77}
 78
 79// Tokens live in an AVL tree: creating one touches its own entry and the path
 80// to it, whatever the number of tokens. A Go map, or a slice of their ids,
 81// would be rewritten whole on every creation. Ids are T1, T2, ... in creation
 82// order and no token is ever removed, so a page of the list is a range of ids.
 83var (
 84	nextID  seqid.ID
 85	entries = avl.NewTree() // token id -> *entry
 86)
 87
 88// requireIssuer returns the immediate caller, which config must list as a
 89// token issuer. Only a realm's own address is ever listed, so an account or a
 90// script never passes.
 91func requireIssuer(cur realm) address {
 92	if !cur.IsCurrent() {
 93		panic("launchpad/tokens: stale realm context")
 94	}
 95	issuer := cur.Previous().Address()
 96	if !config.IsIssuer(config.LedgerTokens, issuer) {
 97		panic("launchpad/tokens: token issuer only")
 98	}
 99	return issuer
100}
101
102func check(err error) {
103	if err != nil {
104		panic(err.Error())
105	}
106}
107
108// caller is the immediate user or realm. Coins a user attaches to a direct
109// call would land in this realm, which has no way to return them.
110func caller(cur realm) address {
111	if !cur.IsCurrent() {
112		panic("launchpad/tokens: stale realm context")
113	}
114	currency.AssertNoCoins(0, cur)
115	return cur.Previous().Address()
116}
117
118// The ledger keys balances and allowances by the address text, so an address
119// in any spelling but the one the chain gives a caller would hold something
120// no caller can reach.
121func requireAddress(a address) {
122	if !meta.ValidAddress(a) {
123		panic("launchpad/tokens: invalid address")
124	}
125}
126
127func validateMetadata(m Metadata) {
128	check(meta.Description(m.Description))
129	check(meta.Image(m.Image))
130	check(meta.Website(m.Website))
131	check(meta.Handle(m.XHandle))
132	check(meta.Handle(m.Telegram))
133}
134
135// The founding names remain unavailable even if governance globally clears a
136// config reservation. A creator-specific grant needs its own reviewed proof.
137func protectedTicker(ticker string) bool {
138	switch ticker {
139	case "MEMBA", "GNOT", "UGNOT", "WUGNOT", "GNS", "USDC", "USDT", "ATOM", "BTC", "ETH", "SAMCREW":
140		return true
141	}
142	return false
143}
144
145func validateSpec(s createSpec) {
146	requireAddress(s.Creator)
147	check(meta.Name(s.Name))
148	check(meta.Ticker(s.Ticker))
149	validateMetadata(s.Metadata)
150	if s.Decimals < 0 || s.Decimals > grc20.MaxDecimals ||
151		s.InitialSupply <= 0 || s.MaxSupply < s.InitialSupply ||
152		protectedTicker(s.Ticker) || config.IsReserved(s.Ticker) {
153		panic("launchpad/tokens: invalid launch terms")
154	}
155	switch s.Mode {
156	case ModeFairSale:
157		if s.Decimals > 12 || s.MaxSupply != s.InitialSupply {
158			panic("launchpad/tokens: invalid fair-sale terms")
159		}
160	case ModeDirectFixed:
161		if s.Decimals > 12 || s.MaxSupply != s.InitialSupply {
162			panic("launchpad/tokens: invalid direct terms")
163		}
164	case ModeDirectCap:
165		if s.Decimals > 12 || s.MaxSupply <= s.InitialSupply {
166			panic("launchpad/tokens: invalid capped terms")
167		}
168	default:
169		panic("launchpad/tokens: invalid mode")
170	}
171}
172
173func laneForMode(mode string) string {
174	switch mode {
175	case ModeDirectFixed, ModeDirectCap:
176		return config.LaneDirect
177	case ModeFairSale:
178		return config.LaneFairSale
179	}
180	panic("launchpad/tokens: invalid mode")
181}
182
183func nextPublicID() (string, seqid.ID) {
184	if uint64(nextID) >= maxPublicID {
185		panic("launchpad/tokens: token ID exhausted")
186	}
187	idNumber := nextID.Next()
188	return "T" + strconv.FormatUint(uint64(idNumber), 10), idNumber
189}
190
191// Create registers exactly one new GRC20 for a listed issuer, which must
192// authenticate the creator, reserve the launch terms and collect payment
193// before this crossing call. The currency key names the config action
194// currency: creation-fee currency for a direct token, quote currency for a
195// fair sale; the issuer binds it to the corresponding payment route. The
196// public symbol T<n> is distinct from the nonunique creator-selected ticker.
197// The whole initial mint lands in the issuer's custody for atomic allocation
198// and escrow setup, and the token stays bound to that issuer.
199func Create(cur realm, creator address, mode, name, ticker string,
200	decimals int, initialSupply, maxSupply int64,
201	currencyKey string, expectedVersion int64,
202	description, image, website, xHandle, telegram string) string {
203	issuer := requireIssuer(cur)
204	s := createSpec{
205		Creator: creator, Mode: mode, Name: name, Ticker: ticker,
206		Decimals: decimals, InitialSupply: initialSupply, MaxSupply: maxSupply,
207		CurrencyKey: currencyKey,
208		ExpectedVersion: expectedVersion,
209		Metadata: Metadata{
210			Description: description, Image: image, Website: website,
211			XHandle: xHandle, Telegram: telegram,
212		},
213	}
214	validateSpec(s)
215	config.AssertNewAction(laneForMode(s.Mode), s.CurrencyKey, s.ExpectedVersion)
216	id, idNumber := nextPublicID()
217	token, ledger := grc20.NewToken(s.Name, id, s.Decimals, idNumber, cur)
218	check(ledger.Mint(issuer, s.InitialSupply))
219	registryKey := grc20reg.Register(cross(cur), token, id)
220	info := Info{
221		ID: id, RegistryKey: registryKey, GRC20ID: token.ID(),
222		Issuer: issuer, Creator: s.Creator,
223		Mode: s.Mode, Name: s.Name, Ticker: s.Ticker, Decimals: s.Decimals,
224		InitialSupply: s.InitialSupply, MaxSupply: s.MaxSupply,
225		ConfigVersion: s.ExpectedVersion, CurrencyKey: s.CurrencyKey,
226		Metadata: s.Metadata,
227	}
228	if s.Mode == ModeDirectCap {
229		info.MintAuthority = s.Creator
230	}
231	entries.Set(id, &entry{info: info, token: token, ledger: ledger})
232	chain.Emit("LaunchpadTokenCreated", "id", id, "registry_key", registryKey,
233		"grc20_id", token.ID(), "issuer", issuer.String(),
234		"creator", s.Creator.String(),
235		"mode", s.Mode, "ticker", s.Ticker, "currency_key", s.CurrencyKey)
236	return id
237}
238
239func mustEntry(id string) *entry {
240	e, ok := entries.Get(id).(*entry)
241	if !ok || e.info.ID != id {
242		panic("launchpad/tokens: unknown token")
243	}
244	return e
245}
246
247func GetInfo(id string) Info { return mustEntry(id).info }
248
249// Count is the number of tokens created: ids are T1 to T<Count>, none removed.
250func Count() int64 { return int64(nextID) }
251
252func RegistryKeyOf(id string) string { return mustEntry(id).info.RegistryKey }
253
254func BalanceOf(id string, owner address) int64 {
255	requireAddress(owner)
256	return mustEntry(id).token.BalanceOf(owner)
257}
258
259func TotalSupply(id string) int64 { return mustEntry(id).token.TotalSupply() }
260
261// MintCapped can only be reached through the issuer that created the token.
262// That issuer authenticates the live caller as the current mint authority
263// and must partition the newly minted balance into recorded allocations in
264// this same transaction. The ledger independently rechecks the authority and
265// the immutable cap, and takes no mint while the direct lane is closed: the
266// pauser stops every issuer here, whatever each one checks by itself.
267func MintCapped(cur realm, id string, authority address, amount int64) {
268	issuer := requireIssuer(cur)
269	e := mustEntry(id)
270	config.AssertLaneOpen(config.LaneDirect, e.info.CurrencyKey)
271	if e.info.Issuer != issuer || e.info.Mode != ModeDirectCap || e.info.MintRenounced ||
272		e.info.MintAuthority != authority || amount <= 0 ||
273		sm.Add(e.token.TotalSupply(), amount) > e.info.MaxSupply {
274		panic("launchpad/tokens: capped mint denied")
275	}
276	check(e.ledger.Mint(issuer, amount))
277	chain.Emit("LaunchpadTokenCappedMinted", "id", id,
278		"authority", authority.String(), "amount", strconv.FormatInt(amount, 10))
279}
280
281func Allowance(id string, owner, spender address) int64 {
282	requireAddress(owner)
283	requireAddress(spender)
284	return mustEntry(id).token.Allowance(owner, spender)
285}
286
287// Standard wrappers operate only on the immediate caller's balance or
288// allowance, accept no coins, and take an address only as the chain writes
289// one. A realm can also move its own balance, or an allowance it holds,
290// through the token in the registry, where the GRC20 library alone checks the
291// receiver. A script an account runs is such a realm with the account's
292// address, so these wrappers are an account's only way in only while the
293// chain does not let that account submit scripts.
294func Transfer(cur realm, id string, to address, amount int64) {
295	caller(cur)
296	requireAddress(to)
297	check(mustEntry(id).ledger.CallerTeller().Transfer(0, cur, to, amount))
298}
299
300func Approve(cur realm, id string, spender address, amount int64) {
301	caller(cur)
302	requireAddress(spender)
303	check(mustEntry(id).ledger.CallerTeller().Approve(0, cur, spender, amount))
304}
305
306func TransferFrom(cur realm, id string, from, to address, amount int64) {
307	caller(cur)
308	requireAddress(from)
309	requireAddress(to)
310	check(mustEntry(id).ledger.CallerTeller().TransferFrom(0, cur, from, to, amount))
311}
312
313func Burn(cur realm, id string, amount int64) {
314	who := caller(cur)
315	if amount <= 0 {
316		panic("launchpad/tokens: invalid burn")
317	}
318	check(mustEntry(id).ledger.Burn(who, amount))
319}
320
321func SetMetadata(cur realm, id, description, image, website, xHandle, telegram string) {
322	who := caller(cur)
323	e := mustEntry(id)
324	if e.info.MetadataFrozen || who != e.info.Creator {
325		panic("launchpad/tokens: creator metadata only")
326	}
327	m := Metadata{
328		Description: description, Image: image, Website: website,
329		XHandle: xHandle, Telegram: telegram,
330	}
331	validateMetadata(m)
332	e.info.Metadata = m
333	chain.Emit("TokenMetadataChanged", "id", id, "creator", e.info.Creator.String())
334}
335
336func FreezeMetadata(cur realm, id string) {
337	who := caller(cur)
338	e := mustEntry(id)
339	if e.info.MetadataFrozen || who != e.info.Creator {
340		panic("launchpad/tokens: creator metadata only")
341	}
342	e.info.MetadataFrozen = true
343	chain.Emit("TokenMetadataFrozen", "id", id, "creator", e.info.Creator.String())
344}
345
346// A capped token's authority moves only through its own two-step handoff.
347// The issuer checks this live authority before each later mint.
348func ProposeMintAuthority(cur realm, id string, next address) {
349	who := caller(cur)
350	e := mustEntry(id)
351	if e.info.Mode != ModeDirectCap || e.info.MintRenounced ||
352		who != e.info.MintAuthority ||
353		!meta.ValidAddress(next) || next == e.info.MintAuthority {
354		panic("launchpad/tokens: mint authority only")
355	}
356	e.info.PendingMintAuth = next
357	chain.Emit("MintAuthorityProposed", "id", id, "next", next.String())
358}
359
360func CancelMintAuthority(cur realm, id string) {
361	who := caller(cur)
362	e := mustEntry(id)
363	if e.info.Mode != ModeDirectCap || e.info.MintRenounced ||
364		who != e.info.MintAuthority || e.info.PendingMintAuth == "" {
365		panic("launchpad/tokens: no mint authority proposal")
366	}
367	e.info.PendingMintAuth = ""
368	chain.Emit("MintAuthorityCancelled", "id", id)
369}
370
371func AcceptMintAuthority(cur realm, id string) {
372	who := caller(cur)
373	e := mustEntry(id)
374	if e.info.Mode != ModeDirectCap || e.info.MintRenounced ||
375		e.info.PendingMintAuth == "" || who != e.info.PendingMintAuth {
376		panic("launchpad/tokens: pending mint authority only")
377	}
378	e.info.MintAuthority = who
379	e.info.PendingMintAuth = ""
380	chain.Emit("MintAuthorityAccepted", "id", id, "authority", who.String())
381}
382
383func RenounceMintAuthority(cur realm, id string) {
384	who := caller(cur)
385	e := mustEntry(id)
386	if e.info.Mode != ModeDirectCap || e.info.MintRenounced ||
387		who != e.info.MintAuthority {
388		panic("launchpad/tokens: mint authority only")
389	}
390	e.info.MintAuthority = ""
391	e.info.PendingMintAuth = ""
392	e.info.MintRenounced = true
393	chain.Emit("MintAuthorityRenounced", "id", id)
394}
395
396func Render(path string) string {
397	if path == "" {
398		return "# Launchpad tokens\n\nRegistered GRC20 ledgers created by the issuers listed in config.\n"
399	}
400	e := mustEntry(path)
401	return "# " + meta.EscapeMarkdown(e.info.Name) + "\n\nTicker: " +
402		meta.EscapeMarkdown(e.info.Ticker) + "\n"
403}