← Back to Realms

Realm

Gno.land Mainnet

gno.land/r/samcrew/launchpad/config/v1

Overview

Network
gnoland-1
Realm Path
gno.land/r/samcrew/launchpad/config/v1
Exported Functions
41
State Entries
27
Source Files
3
Total Package Entries
82

Exported Functions

41 exported functions

State

27 state entries

Source Code

FILES
config.gno
go
1// Package config is the versioned, governed configuration for Memba Launchpad.
2// Its admin is the account that published it until a two-step handoff moves
3// the role. Every lane starts paused and without terms.
4package config
5
6import (
7	"strconv"
8	"strings"
9
10	"chain"
11	"chain/runtime/unsafe"
12	"gno.land/p/samcrew/launchpad/meta/v1"
13	sm "gno.land/p/samcrew/launchpad/safemath/v1"
14	"gno.land/r/nt/grc20reg/v0"
15)
16
17const (
18	LaneDirect     = "direct"
19	LaneFairSale   = "fairsale"
20	LaneAirdrop    = "airdrop"
21	LaneCollection = "collection"
22	LaneNFTDrops   = "nft_drops"
23	LaneNFTMarket  = "nft_market"
24	NativeCurrency = "ugnot"
25	LedgerTokens   = "tokens"
26	LedgerNFT      = "nft"
27	maxCurrencies  = 16
28	maxIssuers     = 32
29	issuerPrefix   = "gno.land/r/samcrew/"
30	maxCreationFee = sm.MaxInt64 / 12
31)
32
33// CurrencyParams holds the terms of one quote currency. A fee of -1 closes
34// the lane that charges it.
35type CurrencyParams struct {
36	Key                 string
37	DirectCreationFee   int64
38	FairSaleCreationFee int64 // never below DirectCreationFee: cancelling a sale returns its allocation
39	FairSaleRaiseCap    int64 // most quote one fair sale may hold in escrow; 0 closes fair sales
40	CollectionFee       int64
41}
42
43// Params is one immutable snapshot of launch terms. A rate of -1 closes the
44// lane that charges it.
45type Params struct {
46	PrimaryFeeBPS      int64 // share of settled fair-sale proceeds
47	NFTPrimaryFeeBPS   int64
48	NFTSecondaryFeeBPS int64
49	Treasury           address
50	Currencies         []CurrencyParams
51}
52
53var (
54	lanes = []string{LaneDirect, LaneFairSale, LaneAirdrop, LaneCollection, LaneNFTDrops, LaneNFTMarket}
55	// No function of these realms can pay out a balance sent to their address.
56	unspendable = []string{
57		"gno.land/r/samcrew/launchpad/config/v1",
58		"gno.land/r/samcrew/launchpad/tokens/v1",
59		"gno.land/r/samcrew/launchpad/sales/v1",
60		"gno.land/r/samcrew/launchpad/nft/v1",
61		"gno.land/r/samcrew/launchpad/drops/v1",
62		"gno.land/r/samcrew/launchpad/market/v1",
63		"gno.land/r/samcrew/launchpad/curation/v1",
64		"gno.land/r/samcrew/memba_dao",
65		// The governance core and its bridge to the legacy apps hold no
66		// treasury and no generic call: nothing there could pay a fee out.
67		"gno.land/r/samcrew/memba_gov",
68		"gno.land/r/samcrew/memba_bridge_v1",
69	}
70
71	admin        address
72	pendingAdmin address
73	pauser       address
74	issuers      = map[string]bool{} // "<ledger>:<realm address>"
75	versions     []Params
76	allowed      = map[string]bool{}
77	paused       = map[string]bool{}
78	verified     = map[string]bool{}
79	reserved     = map[string]bool{}
80	owedCeilings = map[string]int64{} // absent: no ceiling
81)
82
83func init() {
84	admin = unsafe.OriginCaller()
85	versions = []Params{{PrimaryFeeBPS: -1, NFTPrimaryFeeBPS: -1, NFTSecondaryFeeBPS: -1}}
86	for _, lane := range lanes {
87		paused[lane] = true
88	}
89	for _, ticker := range []string{"MEMBA", "GNOT", "UGNOT", "WUGNOT", "GNS", "USDC", "USDT", "ATOM", "BTC", "ETH", "SAMCREW"} {
90		reserved[ticker] = true
91	}
92}
93
94// caller is the immediate user or realm. Coins attached to a direct call
95// would land in this realm, which has no way to return them.
96func caller(cur realm) address {
97	// The pinned VM already refuses a realm context that is not the live one
98	// before this line runs. Published code outlives a VM release, so the
99	// check stays.
100	if !cur.IsCurrent() {
101		panic("launchpad/config: stale realm context")
102	}
103	if cur.Previous().IsUserCall() && len(unsafe.OriginSend()) != 0 {
104		panic("launchpad/config: this call does not accept coins")
105	}
106	return cur.Previous().Address()
107}
108
109func requireAdmin(cur realm) {
110	if caller(cur) != admin {
111		panic("launchpad/config: admin only")
112	}
113}
114
115func unspendableRealm(who address) bool {
116	for _, path := range unspendable {
117		if who == chain.PackageAddress(path) {
118			return true
119		}
120	}
121	return false
122}
123
124// IsUnspendable reports whether a fee or a royalty paid to who could never
125// be paid out again: who is one of the realms listed above, which have no
126// function for that, or it is not an address as the chain writes one, so no
127// caller can ever present it. It is a check for such receivers only, not for
128// the recipient of a transfer: the sales realm is on the list and rightly
129// holds and moves ledger balances of its own.
130func IsUnspendable(who address) bool {
131	return !meta.ValidAddress(who) || unspendableRealm(who)
132}
133
134func validLane(lane string) bool {
135	for _, l := range lanes {
136		if l == lane {
137			return true
138		}
139	}
140	return false
141}
142
143func versionString() string { return strconv.FormatInt(GetCurrentVersion(), 10) }
144
145func validAssetID(id string) bool {
146	if len(id) < 2 || len(id) > 20 || (id[0] != 'T' && id[0] != 'C') || id[1] < '1' || id[1] > '9' {
147		return false
148	}
149	for i := 2; i < len(id); i++ {
150		if id[i] < '0' || id[i] > '9' {
151			return false
152		}
153	}
154	n, err := strconv.ParseInt(id[1:], 10, 64)
155	return err == nil && n > 0
156}
157
158// ProposeAdmin stages a handoff to a user account or a realm. Nothing changes
159// until that exact address accepts, so a target that cannot call is harmless.
160func ProposeAdmin(cur realm, next address) {
161	requireAdmin(cur)
162	if !meta.ValidAddress(next) || next == admin {
163		panic("launchpad/config: invalid next admin")
164	}
165	pendingAdmin = next
166	chain.Emit("AdminProposed", "admin", admin.String(), "pending", next.String())
167}
168
169func CancelAdminProposal(cur realm) {
170	requireAdmin(cur)
171	if pendingAdmin == "" {
172		panic("launchpad/config: no pending admin")
173	}
174	pendingAdmin = ""
175	chain.Emit("AdminProposalCancelled", "admin", admin.String())
176}
177
178func AcceptAdmin(cur realm) {
179	who := caller(cur)
180	if pendingAdmin == "" || who != pendingAdmin {
181		panic("launchpad/config: pending admin only")
182	}
183	previous := admin
184	admin = who
185	pendingAdmin = ""
186	chain.Emit("AdminAccepted", "admin", who.String(), "previous", previous.String())
187}
188
189// validIssuerPath accepts a realm path below the samcrew namespace whose
190// segments are plain lowercase names: nothing that could resolve elsewhere.
191func validIssuerPath(path string) bool {
192	if len(path) > 200 || !strings.HasPrefix(path, issuerPrefix) {
193		return false
194	}
195	for _, segment := range strings.Split(path[len(issuerPrefix):], "/") {
196		if segment == "" {
197			return false
198		}
199		for i := 0; i < len(segment); i++ {
200			c := segment[i]
201			if (c < 'a' || c > 'z') && (c < '0' || c > '9') && c != '_' {
202				return false
203			}
204		}
205	}
206	return true
207}
208
209// AllowIssuer lists a realm under gno.land/r/samcrew/ as an issuer for one
210// ledger to read. The list only grows, so a ledger can bind what an issuer
211// created to that issuer for good.
212func AllowIssuer(cur realm, ledger, pkgPath string) {
213	requireAdmin(cur)
214	if (ledger != LedgerTokens && ledger != LedgerNFT) || !validIssuerPath(pkgPath) ||
215		len(issuers) >= maxIssuers {
216		panic("launchpad/config: invalid issuer")
217	}
218	issuer := chain.PackageAddress(pkgPath)
219	if issuer == cur.Address() {
220		panic("launchpad/config: invalid issuer")
221	}
222	key := ledger + ":" + issuer.String()
223	if issuers[key] {
224		panic("launchpad/config: issuer already allowed")
225	}
226	issuers[key] = true
227	chain.Emit("IssuerAllowed", "ledger", ledger, "path", pkgPath, "issuer", issuer.String(), "actor", admin.String())
228}
229
230func IsIssuer(ledger string, who address) bool { return issuers[ledger+":"+who.String()] }
231
232func GetAdmin() address        { return admin }
233func GetPendingAdmin() address { return pendingAdmin }
234func GetPauser() address       { return pauser }
235
236func copyParams(p Params) Params {
237	copyP := p
238	copyP.Currencies = make([]CurrencyParams, len(p.Currencies))
239	copy(copyP.Currencies, p.Currencies)
240	return copyP
241}
242
243func GetCurrentVersion() int64 { return int64(len(versions)) }
244
245func GetVersion(version int64) Params {
246	if version < 1 || version > int64(len(versions)) {
247		panic("launchpad/config: unknown version")
248	}
249	return copyParams(versions[version-1])
250}
251
252// One value of the current terms each, so a check can compare its text: the
253// publisher's manual flow reads one before and after each setter. A rate or a
254// fee of -1 is not set; so is every fee of a currency without terms.
255func GetTreasury() address         { return versions[len(versions)-1].Treasury }
256func GetPrimaryFeeBPS() int64      { return versions[len(versions)-1].PrimaryFeeBPS }
257func GetNFTPrimaryFeeBPS() int64   { return versions[len(versions)-1].NFTPrimaryFeeBPS }
258func GetNFTSecondaryFeeBPS() int64 { return versions[len(versions)-1].NFTSecondaryFeeBPS }
259
260// GetTokenTerms is "<direct creation fee>,<fair-sale creation fee>,<raise cap>"
261// in a currency.
262func GetTokenTerms(key string) string {
263	c, ok := findCurrency(versions[len(versions)-1], key)
264	if !ok {
265		return "-1,-1,0"
266	}
267	return strconv.FormatInt(c.DirectCreationFee, 10) + "," + strconv.FormatInt(c.FairSaleCreationFee, 10) + "," + strconv.FormatInt(c.FairSaleRaiseCap, 10)
268}
269
270func GetCollectionFee(key string) int64 {
271	c, ok := findCurrency(versions[len(versions)-1], key)
272	if !ok {
273		return -1
274	}
275	return c.CollectionFee
276}
277
278func AssertExpectedVersion(expected int64) {
279	if expected != GetCurrentVersion() {
280		panic("config changed")
281	}
282}
283
284func validateParams(p Params) {
285	if p.PrimaryFeeBPS < -1 || p.PrimaryFeeBPS > 500 ||
286		p.NFTPrimaryFeeBPS < -1 || p.NFTPrimaryFeeBPS > 500 ||
287		p.NFTSecondaryFeeBPS < -1 || p.NFTSecondaryFeeBPS > 200 {
288		panic("launchpad/config: parameter out of range")
289	}
290	if p.Treasury != "" {
291		if !meta.ValidAddress(p.Treasury) {
292			panic("launchpad/config: invalid treasury")
293		}
294		if unspendableRealm(p.Treasury) {
295			panic("launchpad/config: treasury cannot spend")
296		}
297	}
298	if len(p.Currencies) > maxCurrencies {
299		panic("launchpad/config: too many currencies")
300	}
301	seen := map[string]bool{}
302	for _, c := range p.Currencies {
303		if c.Key == "" || len(c.Key) > 200 || seen[c.Key] ||
304			(c.Key != NativeCurrency && grc20reg.Get(c.Key) == nil) ||
305			c.DirectCreationFee < -1 || c.DirectCreationFee > maxCreationFee ||
306			c.FairSaleCreationFee < -1 || c.FairSaleCreationFee > maxCreationFee ||
307			c.CollectionFee < -1 || c.CollectionFee > maxCreationFee ||
308			c.FairSaleRaiseCap < 0 {
309			panic("launchpad/config: invalid currency terms")
310		}
311		if c.FairSaleCreationFee >= 0 &&
312			(c.DirectCreationFee < 0 || c.FairSaleCreationFee < c.DirectCreationFee) {
313			panic("launchpad/config: fair-sale fee below direct fee")
314		}
315		// With direct <= fair < cap, and no cap while fair sales are closed,
316		// no two of the three can be swapped unnoticed.
317		if c.FairSaleRaiseCap != 0 &&
318			(c.FairSaleCreationFee < 0 || c.FairSaleRaiseCap <= c.FairSaleCreationFee) {
319			panic("launchpad/config: raise cap not above fair-sale fee")
320		}
321		seen[c.Key] = true
322	}
323}
324
325// Terms change only through the setters below. Each takes scalar arguments,
326// so a user account can call it, and appends one snapshot that passed the
327// whole validation; earlier snapshots never change. A launch pins the version
328// it was created under, and an action signed against an older version fails.
329
330func latest() Params { return copyParams(versions[len(versions)-1]) }
331
332func appendVersion(next Params, change string) int64 {
333	validateParams(next)
334	versions = append(versions, next)
335	chain.Emit("ConfigVersionAdded", "version", versionString(), "change", change, "actor", admin.String())
336	return GetCurrentVersion()
337}
338
339func findCurrency(p Params, key string) (CurrencyParams, bool) {
340	for _, c := range p.Currencies {
341		if c.Key == key {
342			return c, true
343		}
344	}
345	return CurrencyParams{}, false
346}
347
348// currencyTerms returns the index of key's terms in p, adding them with every
349// lane closed when the currency is new.
350func currencyTerms(p *Params, key string) int {
351	for i, c := range p.Currencies {
352		if c.Key == key {
353			return i
354		}
355	}
356	p.Currencies = append(p.Currencies, CurrencyParams{
357		Key: key, DirectCreationFee: -1, FairSaleCreationFee: -1, CollectionFee: -1,
358	})
359	return len(p.Currencies) - 1
360}
361
362// SetTreasury names the receiver of protocol fees. Once named it can be
363// replaced, not removed.
364func SetTreasury(cur realm, treasury address) int64 {
365	requireAdmin(cur)
366	if treasury == "" {
367		panic("launchpad/config: invalid treasury")
368	}
369	next := latest()
370	next.Treasury = treasury
371	return appendVersion(next, "treasury")
372}
373
374func SetPrimaryFeeBPS(cur realm, bps int64) int64 {
375	requireAdmin(cur)
376	next := latest()
377	next.PrimaryFeeBPS = bps
378	return appendVersion(next, "primary_fee")
379}
380
381func SetNFTPrimaryFeeBPS(cur realm, bps int64) int64 {
382	requireAdmin(cur)
383	next := latest()
384	next.NFTPrimaryFeeBPS = bps
385	return appendVersion(next, "nft_primary_fee")
386}
387
388func SetNFTSecondaryFeeBPS(cur realm, bps int64) int64 {
389	requireAdmin(cur)
390	next := latest()
391	next.NFTSecondaryFeeBPS = bps
392	return appendVersion(next, "nft_secondary_fee")
393}
394
395// SetTokenTerms prices token creation in one currency and bounds what a
396// single fair sale may raise in it.
397func SetTokenTerms(cur realm, key string, directCreationFee, fairSaleCreationFee, fairSaleRaiseCap int64) int64 {
398	requireAdmin(cur)
399	next := latest()
400	i := currencyTerms(&next, key)
401	next.Currencies[i].DirectCreationFee = directCreationFee
402	next.Currencies[i].FairSaleCreationFee = fairSaleCreationFee
403	next.Currencies[i].FairSaleRaiseCap = fairSaleRaiseCap
404	return appendVersion(next, "token_terms")
405}
406
407func SetCollectionFee(cur realm, key string, fee int64) int64 {
408	requireAdmin(cur)
409	next := latest()
410	i := currencyTerms(&next, key)
411	next.Currencies[i].CollectionFee = fee
412	return appendVersion(next, "collection_fee")
413}
414
415// RemoveCurrency drops a currency's terms, its allowlist entry and its owed
416// ceiling, so adding terms again later neither reopens it nor brings back an
417// old ceiling by itself.
418func RemoveCurrency(cur realm, key string) int64 {
419	requireAdmin(cur)
420	current := latest()
421	next := current
422	next.Currencies = nil
423	for _, c := range current.Currencies {
424		if c.Key != key {
425			next.Currencies = append(next.Currencies, c)
426		}
427	}
428	if len(next.Currencies) == len(current.Currencies) {
429		panic("launchpad/config: unknown currency")
430	}
431	delete(allowed, key)
432	delete(owedCeilings, key)
433	return appendVersion(next, "currency_removed")
434}
435
436// IsLaneReady checks only immutable terms. The immediate allowlist is checked
437// at action time so delisting cannot prevent an old launch's exits.
438func IsLaneReady(lane, currency string) bool {
439	p := versions[len(versions)-1]
440	c, ok := findCurrency(p, currency)
441	if !ok || p.Treasury == "" {
442		return false
443	}
444	switch lane {
445	// A direct fee is what opens a currency to the token lanes: a fair sale
446	// cannot be priced without one, and an airdrop, which is free, needs it too.
447	case LaneDirect, LaneAirdrop:
448		return c.DirectCreationFee >= 0
449	case LaneFairSale:
450		return c.FairSaleCreationFee >= 0 && c.FairSaleRaiseCap > 0 && p.PrimaryFeeBPS >= 0
451	// A collection fee is what opens a currency to the NFT lanes.
452	case LaneCollection:
453		return c.CollectionFee >= 0
454	case LaneNFTDrops:
455		return c.CollectionFee >= 0 && p.NFTPrimaryFeeBPS >= 0
456	case LaneNFTMarket:
457		return c.CollectionFee >= 0 && p.NFTSecondaryFeeBPS >= 0
458	}
459	return false
460}
461
462// Pause closes a lane to new launches and contributions. Claims, refunds and
463// settlements never read the gate, so nothing here can hold funds back.
464func Pause(cur realm, lane string) {
465	who := caller(cur)
466	if !validLane(lane) {
467		panic("launchpad/config: invalid lane")
468	}
469	if who != admin && who != pauser {
470		panic("launchpad/config: pauser or admin only")
471	}
472	paused[lane] = true
473	chain.Emit("LanePaused", "lane", lane, "actor", who.String())
474}
475
476// Unpause opens a lane. It needs a pauser in place and one listed currency
477// whose terms for the lane are complete.
478func Unpause(cur realm, lane, currency string) {
479	requireAdmin(cur)
480	if !validLane(lane) {
481		panic("launchpad/config: invalid lane")
482	}
483	if pauser == "" || !allowed[currency] || !IsLaneReady(lane, currency) {
484		panic("launchpad/config: lane is incomplete")
485	}
486	paused[lane] = false
487	chain.Emit("LaneUnpaused", "lane", lane, "currency", currency, "version", versionString(), "actor", admin.String())
488}
489
490func IsPaused(lane string) bool {
491	if !validLane(lane) {
492		return true
493	}
494	return paused[lane]
495}
496
497// AssertLaneOpen gates a new inflow: the lane is open and the currency is
498// listed with complete terms. A caller that charges the terms in force reads
499// them in the same transaction, as the NFT realms do; one that charges at the
500// version its user signed for checks that version with AssertNewAction, as
501// token creations and fair-sale launches do. The token lanes' exits use the
502// terms recorded at launch and pass no gate.
503func AssertLaneOpen(lane, currency string) {
504	if IsPaused(lane) || !allowed[currency] || !IsLaneReady(lane, currency) {
505		panic("launchpad/config: new action unavailable")
506	}
507}
508
509// AssertNewAction also pins the version whose prices the caller signed for.
510func AssertNewAction(lane, currency string, expectedVersion int64) {
511	AssertExpectedVersion(expectedVersion)
512	AssertLaneOpen(lane, currency)
513}
514
515// SetPauser names the one address, besides the admin, that may close lanes.
516func SetPauser(cur realm, next address) {
517	requireAdmin(cur)
518	if !meta.ValidAddress(next) {
519		panic("launchpad/config: invalid pauser")
520	}
521	pauser = next
522	chain.Emit("PauserChanged", "pauser", next.String(), "actor", admin.String())
523}
524
525func SetCurrencyAllowed(cur realm, key string, value bool) {
526	requireAdmin(cur)
527	if !value {
528		delete(allowed, key)
529	} else if _, ok := findCurrency(versions[len(versions)-1], key); ok {
530		allowed[key] = true
531	} else {
532		panic("launchpad/config: currency terms missing")
533	}
534	chain.Emit("CurrencyAllowed", "currency", key, "allowed", strconv.FormatBool(value), "version", versionString(), "actor", admin.String())
535}
536
537func IsCurrencyAllowed(key string) bool { return allowed[key] }
538
539// SetSalesOwedCeiling bounds what the sales realm may owe in one currency,
540// its fees, escrow, refunds and proceeds together, for a fair-sale
541// contribution to be accepted. Nothing else reads it: settlements, claims,
542// releases and sweeps go through at any ceiling, and a ceiling below what is
543// already owed only stops new contributions. Unlike a pause, it lets running
544// sales take orders while there is room. MaxInt64, the default, is no
545// ceiling. It is not a term a launch signs for, so it adds no version.
546func SetSalesOwedCeiling(cur realm, key string, ceiling int64) {
547	requireAdmin(cur)
548	if _, ok := findCurrency(versions[len(versions)-1], key); !ok {
549		panic("launchpad/config: currency terms missing")
550	}
551	if ceiling < 0 {
552		panic("launchpad/config: invalid ceiling")
553	}
554	if ceiling == sm.MaxInt64 {
555		delete(owedCeilings, key)
556	} else {
557		owedCeilings[key] = ceiling
558	}
559	chain.Emit("SalesOwedCeiling", "currency", key, "ceiling", strconv.FormatInt(ceiling, 10), "actor", admin.String())
560}
561
562func GetSalesOwedCeiling(key string) int64 {
563	if ceiling, ok := owedCeilings[key]; ok {
564		return ceiling
565	}
566	return sm.MaxInt64
567}
568
569func SetVerified(cur realm, assetID string, value bool) {
570	requireAdmin(cur)
571	if !validAssetID(assetID) {
572		panic("launchpad/config: invalid asset ID")
573	}
574	verified[assetID] = value
575	chain.Emit("Verified", "asset", assetID, "verified", strconv.FormatBool(value), "actor", admin.String())
576}
577
578func IsVerified(assetID string) bool { return verified[assetID] }
579
580func SetReserved(cur realm, ticker string, value bool) {
581	requireAdmin(cur)
582	if meta.Ticker(ticker) != nil {
583		panic("launchpad/config: invalid ticker")
584	}
585	reserved[ticker] = value
586	chain.Emit("Reserved", "ticker", ticker, "reserved", strconv.FormatBool(value), "actor", admin.String())
587}
588
589func IsReserved(ticker string) bool { return reserved[ticker] }
590

Raw Package Data

Raw JSON data