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] }
590Raw Package Data
Raw JSON data