| 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. |
| 4 | package config |
| 5 | |
| 6 | import ( |
| 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 | |
| 17 | const ( |
| 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. |
| 35 | type 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. |
| 45 | type 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 | |
| 53 | var ( |
| 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 | |
| 83 | func 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. |
| 96 | func 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 | |
| 109 | func requireAdmin(cur realm) { |
| 110 | if caller(cur) != admin { |
| 111 | panic("launchpad/config: admin only") |
| 112 | } |
| 113 | } |
| 114 | |
| 115 | func 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. |
| 130 | func IsUnspendable(who address) bool { |
| 131 | return !meta.ValidAddress(who) || unspendableRealm(who) |
| 132 | } |
| 133 | |
| 134 | func validLane(lane string) bool { |
| 135 | for _, l := range lanes { |
| 136 | if l == lane { |
| 137 | return true |
| 138 | } |
| 139 | } |
| 140 | return false |
| 141 | } |
| 142 | |
| 143 | func versionString() string { return strconv.FormatInt(GetCurrentVersion(), 10) } |
| 144 | |
| 145 | func 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. |
| 160 | func 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 | |
| 169 | func 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 | |
| 178 | func 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. |
| 191 | func 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. |
| 212 | func 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 | |
| 230 | func IsIssuer(ledger string, who address) bool { return issuers[ledger+":"+who.String()] } |
| 231 | |
| 232 | func GetAdmin() address { return admin } |
| 233 | func GetPendingAdmin() address { return pendingAdmin } |
| 234 | func GetPauser() address { return pauser } |
| 235 | |
| 236 | func 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 | |
| 243 | func GetCurrentVersion() int64 { return int64(len(versions)) } |
| 244 | |
| 245 | func 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. |
| 255 | func GetTreasury() address { return versions[len(versions)-1].Treasury } |
| 256 | func GetPrimaryFeeBPS() int64 { return versions[len(versions)-1].PrimaryFeeBPS } |
| 257 | func GetNFTPrimaryFeeBPS() int64 { return versions[len(versions)-1].NFTPrimaryFeeBPS } |
| 258 | func 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. |
| 262 | func 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 | |
| 270 | func 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 | |
| 278 | func AssertExpectedVersion(expected int64) { |
| 279 | if expected != GetCurrentVersion() { |
| 280 | panic("config changed") |
| 281 | } |
| 282 | } |
| 283 | |
| 284 | func 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 | |
| 330 | func latest() Params { return copyParams(versions[len(versions)-1]) } |
| 331 | |
| 332 | func 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 | |
| 339 | func 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. |
| 350 | func 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. |
| 364 | func 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 | |
| 374 | func SetPrimaryFeeBPS(cur realm, bps int64) int64 { |
| 375 | requireAdmin(cur) |
| 376 | next := latest() |
| 377 | next.PrimaryFeeBPS = bps |
| 378 | return appendVersion(next, "primary_fee") |
| 379 | } |
| 380 | |
| 381 | func 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 | |
| 388 | func 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. |
| 397 | func 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 | |
| 407 | func 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. |
| 418 | func 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. |
| 438 | func 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. |
| 464 | func 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. |
| 478 | func 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 | |
| 490 | func 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. |
| 503 | func 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. |
| 510 | func 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. |
| 516 | func 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 | |
| 525 | func 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 | |
| 537 | func 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. |
| 546 | func 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 | |
| 562 | func GetSalesOwedCeiling(key string) int64 { |
| 563 | if ceiling, ok := owedCeilings[key]; ok { |
| 564 | return ceiling |
| 565 | } |
| 566 | return sm.MaxInt64 |
| 567 | } |
| 568 | |
| 569 | func 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 | |
| 578 | func IsVerified(assetID string) bool { return verified[assetID] } |
| 579 | |
| 580 | func 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 | |
| 589 | func IsReserved(ticker string) bool { return reserved[ticker] } |