Transaction
CB7290455E0591…7B6A67ED4D20
Block 328,007 · index 0 · indexed
Summary
- Hash
- CB7290455E0591DFABA424AF40B7FF721F584624FAFB87E05DB67B6A67ED4D20
- Block
- 328,007
- Size
- 29284 bytes
- Gas used
- 39,192,540 / 54,000,000
- Fee
- 54000 ugnot
- Status
- success
Messages
- Attached funds
- 15000000 ugnot
Arguments · 17
- #1bubble
- #2admin.gno
- #3package bubble // The settings, and who may change them. // // admin is whoever deployed the realm (Jae's hotkey signs the addpkg) and is handed to the box's keeper key in the // same minute ("a token that you control, but admin can be transferred and even destroyed"). An empty admin is // nobody, permanently: RenounceAdmin is the "destroyed" Jae asked for. After it, every one of these settings moves // only by a holders' vote (kinds.gno), and the mint goes on under the minter until the holders stop it. var ( // admin is Jae's cold key (jarvis room 201, 2026-09-25: "(b) g1ecsu … make sure that even if the hotkey is // hacked the g1ecsu can restore"), named here rather than read from OriginCaller at init: the deployer is the // hotkey, which holds no power once the realm is up, and a test package or a filetest loads with no deployer // at all. The box holds the minter, whose damage the day cap bounds; if the box or the hotkey is ever taken, // this key pauses, names another minter, and goes on. admin = address("g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6") // minter is the box's keeper key, the only address that mints referrals; "" is nobody. minter = address("g18dd5mstjqlcg9ngqpg26azeya6wqg7upd9t8xa") paused bool // the admin's brake: stops Mint and MintMany, nothing else dayCap = 250_000 * one ) func init() { ledger.SetID(tokenID) ledger.Mint(premineTo, premine) premined = premine } func Admin() address { return admin } func Minter() address { return minter } func Paused() bool { return paused } func DayCap() int64 { return dayCap } func mustBeAdmin(cur realm) { who := caller(cur) if admin == "" { panic("bubble: the admin was given up; the holders decide now") } if who != admin { panic("bubble: only the admin") } } // SetAdmin hands the settings to another address. func SetAdmin(cur realm, to address) { mustBeAdmin(cur) if !to.IsValid() { panic("bubble: not an address") } admin = to } // RenounceAdmin gives the settings up for good: from here they move only by vote. func RenounceAdmin(cur realm) { mustBeAdmin(cur) admin = "" } // SetMinter names who mints; "" is nobody (the referral mint stops until a vote names one). func SetMinter(cur realm, to address) { mustBeAdmin(cur) if to != "" && !to.IsValid() { panic("bubble: not an address") } minter = to } // SetPaused stops the mint, or lets it go on. func SetPaused(cur realm, on bool) { mustBeAdmin(cur) paused = on } // SetDayCap lowers the daily ceiling. Raising it is the holders' (govern:daycap): the cap is what bounds a stolen // key, so the key it bounds may not raise it. func SetDayCap(cur realm, n int64) { mustBeAdmin(cur) if n < 0 || n > dayCap { panic("bubble: the admin can only lower the day cap; raising it is a vote") } dayCap = n }
- #4gnomod.toml
- #5module = "gno.land/r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/bubble" gno = "0.9"
- #6govern.gno
- #7package bubble import ( governor "gno.land/p/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/governor/v0" ) // The governor: kourt's engine on this ledger, with this realm's own powers installed under the reserved prefix. // Everything about proposals — kinds, tally, rules, sweep, pages — is the engine's; what is here is what only a // realm can do: hold a live cur, check it, read who is calling, dispatch a decided kind with a sub-realm. // // "Usable for governance flexibly as we build it out": a new power is a Kind any realm can Offer (published, // immutable code plus the bar it would pass at) and the holders adopt by govern:adopt. The token is never // redeployed to gain a power. type ( Kind = governor.Kind Rules = governor.Rules ) func NewRules(quorumBps, thresholdBps, votingBlocks, delayBlocks, graceBlocks, proposeBps int64) Rules { return governor.NewRules(quorumBps, thresholdBps, votingBlocks, delayBlocks, graceBlocks, proposeBps) } func TextOnly(s string) error { return governor.TextOnly(s) } // engine is this realm's governor. Unexported: whoever holds it can adopt a kind without a vote. var engine = governor.New(ledger, ledger) func init() { installKinds() } // dispatch runs a decided kind with a SUB-realm, never this realm's own capability (the engine's note on // dispatch: a kind holding the live cur could cross into anything that trusts this realm). func dispatch(cur realm) governor.Dispatch { return func(k Kind, subpath, payload string) error { return k.Do(0, cur.Sub(subpath), payload) } } // Offer publishes a kind. It grants nothing: the holders adopt it by vote. func Offer(cur realm, k Kind, r Rules) { engine.Offer(caller(cur), k, r) } // Propose asks a question under an adopted kind. A money kind carries its absolute turnout floor (kinds.gno). func Propose(cur realm, kind, payload, title string) int64 { who := caller(cur) var id int64 if floor := quorumFloor(kind); floor > 0 { id = engine.ProposeWithQuorum(who, kind, payload, title, floor) } else { id = engine.Propose(who, kind, payload, title) } if len(askedKinds) > 512 { // a guard, not a record: only decided-but-unnoted money votes need it, and those are few askedKinds = map[int64]string{} } askedKinds[id] = kind return id } // Vote casts the caller's weight as of the proposal's sealed epoch: "yes", "no" or "abstain". func Vote(cur realm, id int64, choice string) { engine.Vote(caller(cur), id, choice) } func VoteWithReason(cur realm, id int64, choice, reason string) { engine.VoteWithReason(caller(cur), id, choice, reason) } // Cancel withdraws the proposer's own open proposal. func Cancel(cur realm, id int64) { engine.Cancel(caller(cur), id) } // Settle writes down a conclusion the rules already reached. Permissionless. A decided money vote's turnout goes // into the bar's memory (kinds.gno). func Settle(cur realm, id int64) { engine.Settle(caller(cur), id) if isMoneyKind(kindOf(id)) { noteTurnout(id) } } // Execute runs a decided proposal after its delay. Permissionless. func Execute(cur realm, id int64) { engine.Execute(caller(cur), id, dispatch(cur)) if isMoneyKind(kindOf(id)) { noteTurnout(id) } } // kindOf is the kind a proposal was asked under, as this realm noted it at Propose (the engine describes a // proposal by its payload alone); an id it never saw is "". func kindOf(id int64) string { return askedKinds[id] } // askedKinds is id -> kind for the proposals asked here, the last few hundred. var askedKinds = map[int64]string{} func ReleaseRoll(cur realm, id int64) { engine.ReleaseRoll(caller(cur), id) } func Sweep(cur realm) { caller(cur) engine.Sweep() } // ------------------------------------------------------------------ reads -- func State(id int64) string { return engine.State(id) } func Describe(id int64) string { return engine.Describe(id) } func Preview(kind, payload string) string { return engine.Preview(kind, payload) } func HasVoted(id int64, who address) bool { return engine.HasVoted(id, who) } func Tally(id int64) (yes, no, abstain, total int64) { return engine.Tally(id) } func KindRules(name string) (Rules, bool) { return engine.KindRules(name) } func Timings(id int64) (opened, closes, ready, expires int64) { return engine.Timings(id) } func VoteOf(id int64, who address) (choice string, weight int64, ok bool) { return engine.VoteOf(id, who) }
- #8kinds.gno
- #9package bubble import ( "strconv" "strings" governor "gno.land/p/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/governor/v0" ) // This realm's own powers, installed at init under the engine's reserved prefix (Offer refuses it, so nothing // can impersonate one). Each is a setting the admin holds (admin.gno) put under the holders too, so the admin's // every power can be taken away by vote — and one that is nobody's but theirs: the reserve. // // govern:admin "g1…" | "none" hand the settings to that address, or destroy the admin for good // govern:minter "g1…" | "none" name who mints referrals, or nobody // govern:pause "on" | "off" stop or resume the referral mint // govern:daycap N the daily ceiling, in whole BUBBLE // govern:reserve "g1… N" mint N whole BUBBLE from the reserve to that address (the only way it moves) // govern:signal text a question with no effect: the sense of the holders // // Describe reads the payload alone (two voters at different heights must see the same question); Check reads // the world; Do changes it. A Do that returns an error finishes the proposal as failed; one that panics aborts // the transaction and leaves it executable again. const none = "none" // The money kinds (everything but the signal) pass on kourt's own terms (Jae, room 201: "use the system from // kourt.xyz … it should auto adjust"): a fifth of the supply turning out, two thirds of yes+no, a week to vote, two // days' delay, a fortnight's grace — and the turnout bar ADJUSTS: it is the greater of that fifth and the mean // turnout of the last few money votes (never above a third of the supply), kourt's min(X̄, votable/3) with the // bootstrap fifth as the base. Nobody sets it; the holders' own habit does. A signal decides nothing and asks less. const ( ReserveMaxPer = 2_000_000 * one // per proposal ReserveGap = int64(90 * 24 * 60 * 60) // seconds between two reserve mints: a full drain is 25 votes over 6 years turnoutKeep = 8 // money votes the mean remembers ) var ( reserveLast int64 // block time of the last reserve mint turnouts []int64 // the last turnoutKeep money votes' turnout (yes+no+abstain), newest last noted = map[int64]bool{} ) func installKinds() { money := governor.BootstrapRules() engine.InstallBuiltin(adminKind{}, money) engine.InstallBuiltin(minterKind{}, money) engine.InstallBuiltin(pauseKind{}, money) engine.InstallBuiltin(daycapKind{}, money) engine.InstallBuiltin(reserveKind{}, money) // A signal decides nothing: a lighter bar, so the holders can be asked often. engine.InstallBuiltin(signalKind{}, governor.NewRules(500, 5000, 3*24*60*60/5, 0, 14*24*60*60/5, 0)) } func isMoneyKind(kind string) bool { switch kind { case governor.Reserved + "reserve", governor.Reserved + "admin", governor.Reserved + "minter", governor.Reserved + "pause", governor.Reserved + "daycap": return true } return false } // quorumFloor is the absolute turnout a money kind needs at the last sealed epoch: max(supply/5, min(X̄, supply/3)), // X̄ the mean turnout of the last money votes. Zero for the rest (the rules' fraction alone). func quorumFloor(kind string) int64 { if !isMoneyKind(kind) { return 0 } at := ledger.Epoch() - 1 if at == 0 { return 0 // the engine refuses the proposal itself: no sealed epoch yet } supply := ledger.PastTotal(at) floor := supply / 5 if x := meanTurnout(); x > floor { if third := supply / 3; x > third { x = third } if x > floor { floor = x } } return floor } func meanTurnout() int64 { if len(turnouts) == 0 { return 0 } var sum int64 for _, t := range turnouts { sum += t } return sum / int64(len(turnouts)) } // noteTurnout remembers a decided money vote's turnout, once, when it is settled or executed. func noteTurnout(id int64) { if noted[id] { return } switch engine.State(id) { case "active", "canceled": return } yes, no, abstain, _ := engine.Tally(id) noted[id] = true turnouts = append(turnouts, yes+no+abstain) if len(turnouts) > turnoutKeep { turnouts = turnouts[len(turnouts)-turnoutKeep:] } if len(noted) > 256 { // the map is a guard, not a record: a fresh one loses nothing that matters noted = map[int64]bool{} } } // MeanTurnout is what the bar follows, for the page and the box. func MeanTurnout() int64 { return meanTurnout() } type bubbleErr struct{ msg string } func (e *bubbleErr) Error() string { return "bubble: " + e.msg } func errf(s string) error { return &bubbleErr{s} } func addrOrNone(payload, what string) error { if payload == none { return nil } if !address(payload).IsValid() { return errf("not an address, and not \"" + none + "\"") } return nil } // ---- govern:admin type adminKind struct{} func (adminKind) Name() string { return governor.Reserved + "admin" } func (adminKind) Describe(p string) string { if p == none { return "destroy the admin — every setting moves only by vote from then on" } return "hand the settings (minter, pause, day cap) to " + p } func (adminKind) Check(p string) error { if err := addrOrNone(p, "admin"); err != nil { return err } if p != none && address(p) == admin { return errf("that is already the admin") } if p == none && admin == "" { return errf("the admin is already gone") } return nil } func (adminKind) Do(_ int, rlm realm, p string) error { if p == none { admin = "" return nil } admin = address(p) return nil } // ---- govern:minter type minterKind struct{} func (minterKind) Name() string { return governor.Reserved + "minter" } func (minterKind) Describe(p string) string { if p == none { return "name nobody as minter — the referral mint stops until a vote names one" } return "name " + p + " as the minter of referral rewards" } func (minterKind) Check(p string) error { if err := addrOrNone(p, "minter"); err != nil { return err } if p != none && address(p) == minter { return errf("that is already the minter") } return nil } func (minterKind) Do(_ int, rlm realm, p string) error { if p == none { minter = "" return nil } minter = address(p) return nil } // ---- govern:pause type pauseKind struct{} func (pauseKind) Name() string { return governor.Reserved + "pause" } func (pauseKind) Describe(p string) string { if p == "on" { return "pause the referral mint" } return "resume the referral mint" } func (pauseKind) Check(p string) error { if p != "on" && p != "off" { return errf("expected \"on\" or \"off\"") } if (p == "on") == paused { return errf("the mint is already " + p) } return nil } func (pauseKind) Do(_ int, rlm realm, p string) error { paused = p == "on" return nil } // ---- govern:daycap type daycapKind struct{} func (daycapKind) Name() string { return governor.Reserved + "daycap" } func (daycapKind) Describe(p string) string { return "set the daily mint ceiling to " + p + " BUBBLE" } func (daycapKind) Check(p string) error { n, err := strconv.ParseInt(p, 10, 64) if err != nil || n < 0 || n > ReferralPool/one { return errf("expected a number of whole BUBBLE, up to the referral pool") } return nil } func (daycapKind) Do(_ int, rlm realm, p string) error { n, err := strconv.ParseInt(p, 10, 64) if err != nil { return errf("not a number") } dayCap = n * one return nil } // ---- govern:reserve type reserveKind struct{} func (reserveKind) Name() string { return governor.Reserved + "reserve" } func (reserveKind) Describe(p string) string { to, n, err := parseReserve(p) if err != nil { return "mint from the reserve: " + p } return "mint " + strconv.FormatInt(n, 10) + " BUBBLE from the reserve to " + to.String() } func (reserveKind) Check(p string) error { _, n, err := parseReserve(p) if err != nil { return err } if reserveMinted+n*one > Reserve { return errf("only " + whole(Reserve-reserveMinted) + " BUBBLE are left in the reserve") } if reserveLast != 0 && ledger.Now() < reserveLast+ReserveGap { return errf("the reserve moved less than ninety days ago; the next mint can run after " + strconv.FormatInt(reserveLast+ReserveGap, 10)) } return nil } func (reserveKind) Do(_ int, rlm realm, p string) error { to, n, err := parseReserve(p) if err != nil { return err } if reserveMinted+n*one > Reserve || (reserveLast != 0 && ledger.Now() < reserveLast+ReserveGap) { return errf("the reserve is not open to this mint") // Check ran just before; a batch member could have changed the world } reserveMinted += n * one reserveLast = ledger.Now() ledger.Mint(to, n*one) return nil } func parseReserve(p string) (address, int64, error) { parts := strings.Fields(p) if len(parts) != 2 { return "", 0, errf("expected \"g1… N\": an address and whole BUBBLE") } to := address(parts[0]) if !to.IsValid() { return "", 0, errf("not an address") } n, err := strconv.ParseInt(parts[1], 10, 64) if err != nil || n <= 0 || n > ReserveMaxPer/one { return "", 0, errf("expected whole BUBBLE, between 1 and " + whole(ReserveMaxPer) + " in one proposal") } return to, n, nil } // ---- govern:signal type signalKind struct{} func (signalKind) Name() string { return governor.Reserved + "signal" } func (signalKind) Describe(p string) string { return "signal (no effect): " + p } func (signalKind) Check(p string) error { if p == "" { return errf("a signal needs words") } if len(p) > 1000 { return errf("a signal is under a thousand characters") } return governor.TextOnly(p) } func (signalKind) Do(_ int, rlm realm, p string) error { return nil }
- #10mint.gno
- #11package bubble import ( "strconv" "strings" "time" "chain" ) // The supply, in three pools that add up to the maximum and never mix. const ( MaxSupply = 100_000_000 * one premine = 10_000_000 * one // Jae's, at init ReferralPool = 40_000_000 * one // the minter's, for referrals, under the day cap Reserve = 50_000_000 * one // the holders', by govern:reserve only MaxBatch = 100 // entries in one MintMany MaxEntry = 50_000 * one // per address per batch: room for a big inviter's day, never a whole day's cap ) // premineTo is the address the premine goes to (Jae, jarvis room 99/102/105, 2026-09-25). const premineTo = address("g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6") var ( premined int64 // what init minted (the premine) referralMinted int64 // out of ReferralPool reserveMinted int64 // out of Reserve mintedDay int64 // minted by the minter on dayKey's day dayKey int64 // unix day of mintedDay lastBatch string batches int64 ) func Premined() int64 { return premined } func ReferralMinted() int64 { return referralMinted } func ReserveMinted() int64 { return reserveMinted } func Batches() int64 { return batches } func LastBatch() string { return lastBatch } // MintedToday is what the minter has minted on the current UTC day, and the day's cap. func MintedToday() (minted, ceiling int64) { if dayOf() != dayKey { return 0, dayCap } return mintedDay, dayCap } func dayOf() int64 { return time.Now().Unix() / 86400 } func mustBeMinter(cur realm) { who := caller(cur) if minter == "" { panic("bubble: nobody may mint; the holders can name a minter") } if who != minter && (admin == "" || who != admin) { panic("bubble: only the minter") } if paused { panic("bubble: minting is paused") } } // referralMint is one credit out of the referral pool, under every wall. amount is base units. func referralMint(to address, amount int64) { if !to.IsValid() { panic("bubble: not an address: " + to.String()) } if amount <= 0 || amount > MaxEntry { panic("bubble: an entry is between 1 and " + whole(MaxEntry) + " BUBBLE") } if referralMinted+amount > ReferralPool { panic("bubble: the referral pool is spent; what is left is the holders' reserve") } if d := dayOf(); d != dayKey { dayKey, mintedDay = d, 0 } if mintedDay+amount > dayCap { panic("bubble: over today's cap of " + whole(dayCap) + " BUBBLE") } mintedDay += amount referralMinted += amount ledger.Mint(to, amount) } // Mint credits one address. The minter's hand tool; the box mints through MintMany. func Mint(cur realm, to address, amount int64) { mustBeMinter(cur) referralMint(to, amount) chain.Emit("Credit", "to", to.String(), "amount", strconv.FormatInt(amount, 10)) } // MintMany credits a batch: "g1…:amount,g1…:amount" (base units), at most MaxBatch entries, no address twice, under // the pool and the day cap. All or nothing — a bad entry panics, which aborts the whole transaction, and names the // entry ("entry 3 g1…: over today's cap") so the box can drop it and send the rest. batchID is eight ASCII digits // that must exceed the last one landed, so a batch the box re-sends after a lost receipt cannot land twice. func MintMany(cur realm, batchID string, list string) { mustBeMinter(cur) if len(batchID) != 8 { panic("bubble: a batch id is eight digits") } for i := 0; i < len(batchID); i++ { if batchID[i] < '0' || batchID[i] > '9' { panic("bubble: a batch id is eight digits") } } if batchID <= lastBatch { panic("bubble: batch " + batchID + " is not after " + lastBatch) } parts := strings.Split(list, ",") if list == "" || len(parts) > MaxBatch { panic("bubble: a batch is 1 to " + strconv.Itoa(MaxBatch) + " entries") } seen := map[string]bool{} total := int64(0) for i, p := range parts { c := strings.Index(p, ":") if c <= 0 { panic("bubble: entry " + strconv.Itoa(i+1) + ": expected g1…:amount") } to, raw := p[:c], p[c+1:] n, err := strconv.ParseInt(raw, 10, 64) if err != nil { panic("bubble: entry " + strconv.Itoa(i+1) + " " + to + ": bad amount") } if seen[to] { panic("bubble: entry " + strconv.Itoa(i+1) + " " + to + ": twice in one batch") } seen[to] = true func() { defer func() { if r := recover(); r != nil { panic("bubble: entry " + strconv.Itoa(i+1) + " " + to + ": " + strings.TrimPrefix(toString(r), "bubble: ")) } }() referralMint(address(to), n) }() total += n } lastBatch = batchID batches++ chain.Emit("Batch", "id", batchID, "entries", strconv.Itoa(len(parts)), "total", strconv.FormatInt(total, 10)) } func toString(r any) string { switch e := r.(type) { case error: return e.Error() case string: return e } return "failed" } // whole renders base units as whole BUBBLE for a message. func whole(n int64) string { return strconv.FormatInt(n/one, 10) }
- #12render.gno
- #13package bubble import ( "strconv" "strings" ) // Render: "" the front page (the token's figures, then the governor's page); "gov/…" the governor's pages alone; // "json" the figures for the box; "me/<addr>" one holder's balance and votes. func Render(path string) string { path = strings.Trim(path, "/") switch { case path == "json": return jsonAll() case strings.HasPrefix(path, "me/"): a := address(path[3:]) return "{\"addr\":\"" + a.String() + "\",\"balance\":" + itoa(ledger.BalanceOf(a)) + ",\"votes\":" + itoa(ledger.VotesOf(a)) + ",\"delegate\":\"" + ledger.DelegateOf(a).String() + "\"}" case strings.HasPrefix(path, "gov"): return engine.Render(strings.TrimPrefix(strings.TrimPrefix(path, "gov"), "/"), notes()) default: return engine.Render(path, notes()) } } // notes are this realm's lines on the governor's front page: the figures the engine cannot know. func notes() string { var b strings.Builder b.WriteString("- supply: " + whole(ledger.TotalSupply()) + " of " + whole(MaxSupply) + " BUBBLE (premine " + whole(premined) + ", referrals " + whole(referralMinted) + " of " + whole(ReferralPool) + ", reserve " + whole(reserveMinted) + " of " + whole(Reserve) + ")\n") minted, ceiling := MintedToday() b.WriteString("- today: " + whole(minted) + " of " + whole(ceiling) + " BUBBLE minted; batches " + itoa(batches) + "\n") if admin == "" { b.WriteString("- **the admin is gone** — every setting moves by vote\n") } else { b.WriteString("- admin: " + admin.String() + "\n") } if minter == "" { b.WriteString("- **nobody mints** — the holders can name a minter\n") } else { b.WriteString("- minter: " + minter.String() + (map[bool]string{true: " (paused)", false: ""})[paused] + "\n") } b.WriteString("- a money vote's turnout bar follows the holders: a fifth of the supply, or the mean turnout of the last money votes when that is higher (mean now " + whole(meanTurnout()) + "), never above a third; the reserve moves at most " + whole(ReserveMaxPer) + " a proposal, ninety days apart\n") return b.String() } func jsonAll() string { minted, ceiling := MintedToday() return "{\"name\":\"" + tokenName + "\",\"symbol\":\"" + tokenSymbol + "\",\"decimals\":" + strconv.Itoa(tokenDecimals) + ",\"supply\":" + itoa(ledger.TotalSupply()) + ",\"max\":" + itoa(MaxSupply) + ",\"premine\":" + itoa(premined) + ",\"referralMinted\":" + itoa(referralMinted) + ",\"referralPool\":" + itoa(ReferralPool) + ",\"reserveMinted\":" + itoa(reserveMinted) + ",\"reserve\":" + itoa(Reserve) + ",\"dayCap\":" + itoa(ceiling) + ",\"mintedToday\":" + itoa(minted) + ",\"day\":" + itoa(dayOf()) + ",\"paused\":" + strconv.FormatBool(paused) + ",\"admin\":\"" + admin.String() + "\",\"minter\":\"" + minter.String() + "\",\"lastBatch\":\"" + lastBatch + "\",\"batches\":" + itoa(batches) + ",\"epoch\":" + strconv.FormatUint(uint64(ledger.Epoch()), 10) + ",\"height\":" + itoa(ledger.Height()) + ",\"now\":" + itoa(ledger.Now()) + "}" } func itoa(n int64) string { return strconv.FormatInt(n, 10) }
- #14token.gno
- #15// Package bubble is BUBBLE, the Bubble Rumble token: a GRC20 that remembers voting power (kourt's grc20votes), // minted by the box for referrals under hard caps, and governed by kourt's governor on the same ledger. // // What is fixed here and what is not. The identity, the decimals, the max supply, the premine and the split // between the referral pool and the reserve are constants: a realm cannot be redeployed at its path, so a number // that must never move is a constant, not a setting. Who administers, who mints, whether minting is paused and // how much may be minted a day are settings — the admin's until the holders vote them away (kinds.gno). // // The box (presence/bubble.go) decides which referral earns what; this realm only enforces the walls: the // supply, the pools, the daily ceiling, the batch shape. See growth/BUBBLE.md. package bubble import ( grc20votes "gno.land/p/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/grc20votes/v0" ) const ( tokenName = "Bubble" tokenSymbol = "BUBBLE" tokenDecimals = 6 pkgPath = "gno.land/r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/bubble" // must match gnomod.toml tokenID = pkgPath + "." + tokenSymbol // One BUBBLE in base units. one = int64(1_000_000) // epochBlocks quantises the voting snapshots: about 55 minutes at gnoland-1's 3.3 s blocks (kourt uses 720 at // the 5 s it was measured at). A vote is weighed at the last sealed epoch, so weight bought after a question is // asked does not count. epochBlocks = int64(1000) ) // ledger is the token. Unexported and never handed out: whoever holds a *Ledger can mint. var ledger = grc20votes.NewLedger(tokenName, tokenSymbol, tokenDecimals, epochBlocks) // ---------------------------------------------------------------- reading -- func GetName() string { return ledger.Name() } func GetSymbol() string { return ledger.Symbol() } func GetDecimals() int { return ledger.Decimals() } func TotalSupply() int64 { return ledger.TotalSupply() } func BalanceOf(owner address) int64 { return ledger.BalanceOf(owner) } func Allowance(owner, spender address) int64 { return ledger.Allowance(owner, spender) } func VotesOf(who address) int64 { return ledger.VotesOf(who) } func DelegateOf(who address) address { return ledger.DelegateOf(who) } func PastVotes(who address, at uint32) int64 { return ledger.PastVotes(who, at) } func PastTotalVotes(at uint32) int64 { return ledger.PastTotal(at) } func Epoch() uint32 { return ledger.Epoch() } // ---------------------------------------------------------------- writing -- // // Refuse a stale realm value, read who is calling, hand the ledger an address: kourt's three lines, because the // ledger (a /p/ package) has no caller to read. const errStaleRealm = "bubble: that realm value is not live; pass your own cur" func caller(cur realm) address { if !cur.IsCurrent() { panic(errStaleRealm) } return cur.Previous().Address() } // Transfer moves the caller's own tokens. func Transfer(cur realm, to address, amount int64) { ledger.Transfer(caller(cur), to, amount) } // Approve lets a spender move some of the caller's balance. func Approve(cur realm, spender address, amount int64) { ledger.Approve(caller(cur), spender, amount) } // TransferFrom spends an allowance. func TransferFrom(cur realm, from, to address, amount int64) { ledger.TransferFrom(caller(cur), from, to, amount) } // Burn destroys the caller's own tokens. What is burned is gone from the supply and never re-minted: the pools // count what was minted, not what exists. func Burn(cur realm, amount int64) { ledger.Burn(caller(cur), amount) } // Delegate points the caller's voting power at someone else (one hop; self is the default). func Delegate(cur realm, to address) { ledger.Delegate(caller(cur), to) }
- #16/gno.MemPackageType
- #17 MPUserAll
Result log
msg:0,success:true,log:,events:[]