PathrockNetwork Gno Explorer
HomeBlocksTransactionsTokensRealmsPackagesValidatorsAnalytics

PathrockNetwork Gno Explorer — an independent explorer for Gno.land Mainnet (gnoland-1), operated by PathrockNetwork. Not an official Gno.land service.

gnowebarchive RPC

gno.land/r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/bubbletreasury

Realm
Open in gnoweb ↗

Overview

Kind
Realm (renderable)
Name
bubbletreasury
Namespace
g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr
Files
2 (gnomod.toml)
Exported functions
11
Module
gno.land/r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/bubbletreasury
gno
0.9

Files (2)

  • gnomod.tomltoml
  • treasury.gnogno
treasury.gnogno
1// The BUBBLE treasury: where the pools' royalty lands, and the one power that moves it.2//3// Bubble Rumble's pools (v5 on) send 5 % of every boop to an address that belongs to the BUBBLE token realm and to4// nobody else: the token's SUB-realm for the kind published here, `treasury:spend`. A sub-realm is a distinct5// package path (`…/bubble#treasury-spend`) with its own address; it can hold and send coins and can never issue6// tokens. The token's governor hands exactly that sub-realm to this kind's Do when the holders have decided a7// spend, and to nothing else — so the only way a coin leaves the treasury is a proposal under `treasury:spend`,8// adopted by the holders (govern:adopt) and passed on the token's own money terms: a fifth of the supply turning9// out, two thirds yes, a week to vote, two days' delay.10//11// This realm holds no coins and no power of its own. It publishes the kind (Offer, at its deploy), says where the12// treasury is (Address), how much it holds (Balance) and renders the terms. The kind's Do runs inside the token13// realm and crosses back here with the sub-realm's own token to note each spend (noteSpend: the pace lives here);14// nothing else writes here, and what was spent is also on the chain (the TreasurySpend event).15package bubbletreasury1617import (18	"chain"19	"chain/banker"20	"chain/runtime"21	"strconv"22	"strings"23	"time"2425	governor "gno.land/p/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/governor/v0"26	"gno.land/r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/bubble"27)2829const (30	bubblePath = "gno.land/r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/bubble"31	ownPath    = "gno.land/r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/bubbletreasury"3233	// KindName is what the holders adopt and propose under. The governor derives the sub-realm's path from it:34	// lower-cased, every character outside [a-z0-9] turned into '-' — so `treasury:spend` lives at `treasury-spend`.35	KindName = "treasury:spend"36	subpath  = "treasury-spend"3738	one = int64(1_000_000) // ugnot in one GNOT3940	// MaxSpend is the most one proposal may move, in whole GNOT — and never more than a QUARTER of what the treasury41	// holds (MaxShare), or SmallTreasury whole GNOT if that is more (so a small treasury can be spent whole, and a42	// treasury just over it has no cliff). The cap bounds one question; the PACE is the window: a window opens at43	// the first spend after Window seconds of none, and the spends in the Window seconds from that first spend44	// together may move at most that same quarter of what the treasury held then (spentAt/windowBase below); the45	// next spend after those seconds opens the next window. So "a quarter per two-day window" is true in code: a46	// majority that wants it all has to say so in public, and wait, several times over.47	MaxSpend      = int64(100_000)48	MaxShare      = int64(4)                // a proposal, and a window, may move at most balance/MaxShare…49	SmallTreasury = int64(40)               // …or this many whole GNOT, whichever is more (up to the balance)50	Window        = int64(2 * 24 * 60 * 60) // seconds: the pace — one quarter per window51)5253// Offer, at deploy: the kind is published in the same transaction that stores this realm, so there is no window54// in which somebody else can bind the name first (r/bubble's Offer is open to anyone and a name, once taken, is55// taken for good). If the name IS taken by then, the deploy fails as a whole and this realm is redeployed under a56// name that maps to the same sub-path (see Recovery in the plan) — nothing has been paid to the address yet.57func init(cur realm) {58	bubble.Offer(cross(cur), spendKind{}, governor.BootstrapRules())59	offered = true60}6162// poolRealms are the game's realms: a spend to one of their addresses would strand the coins (a realm's balance is63// not a bubble's pot), and "seed a bubble" is exactly the spend the holders will think of first.64var poolRealms = []string{65	"gno.land/r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/bubblerumble",66	"gno.land/r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/bubblerumble2",67	"gno.land/r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/bubblerumble3",68	"gno.land/r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/bubblerumble4",69	"gno.land/r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/bubblerumble5",70}7172var (73	// treasury is the address the pools pay: the token realm's sub-realm for the kind. Pure arithmetic on the two74	// paths (chain.DerivePkgSubAddr), the same the VM does when the token realm runs the kind.75	treasury = chain.DerivePkgSubAddr(bubblePath, subpath)76	offered  bool77	// spentAt is the height of the last spend: ONE spend per block. The governor's own govern:batch runs every78	// member of a decided batch in one transaction, and each member's Check sees the balance before any of them79	// ran — so a ladder of quarter-sized lines (25, 18, 14, 10 … of 100) would pass every cap and drain the80	// treasury in one vote. A second spend in the block a spend already landed in aborts instead (Do panics: the81	// transaction unwinds, nothing moves, the proposal stays decided and runs in a later block); a batch of two82	// spends therefore never runs at all. So "a quarter at a time" means a quarter per vote, as it should.83	spentAt int6484	spends  int64 // spends executed, ever85	spent   int64 // ugnot moved out, ever86	// the window: opened by the first spend after Window seconds of none; what it may move is the cap for what the87	// treasury held then (windowBase), and windowSpent is what it has moved so far88	windowAt    int64 // block time the window opened89	windowBase  int64 // ugnot held when it opened, before its first spend90	windowSpent int64 // ugnot moved in it91)9293// WindowLeft is what may still move before the window closes, in ugnot — the whole cap for a window not open.94func WindowLeft() int64 {95	now := time.Now().Unix()96	if windowAt == 0 || now-windowAt >= Window {97		return maxFor(Balance()) * one98	}99	left := maxFor(windowBase)*one - windowSpent100	if left < 0 {101		return 0102	}103	return left104}105106// SpentAt is the height of the last spend; Spends and Spent, the count and the sum — the realm's own log.107func SpentAt() int64 { return spentAt }108func Spends() int64  { return spends }109func Spent() int64   { return spent }110111// noteSpend is the kind's way to write here: Do runs inside the token realm, which cannot touch this realm's112// variables, so it crosses back with the sub-realm token it was handed — and only that token is let in113// (its address is the treasury). Called BEFORE the coins move; a second call in one block panics so the114// transaction, and with it any batch, unwinds with nothing moved.115func noteSpend(cur realm, amt int64) {116	if cur.Previous().Address() != treasury {117		panic("treasury: only the treasury's own kind may note a spend")118	}119	h := runtime.ChainHeight()120	if h == spentAt {121		panic("treasury: one spend per block — this block already has one; run it again in the next")122	}123	now := time.Now().Unix()124	if windowAt == 0 || now-windowAt >= Window {125		windowAt, windowBase, windowSpent = now, Balance(), 0 // Balance() is read before this spend's send126	}127	if windowSpent+amt > maxFor(windowBase)*one {128		panic("treasury: this window (" + strconv.FormatInt(Window/86400, 10) + " days) has moved its quarter already; run it again when the window has passed")129	}130	windowSpent += amt131	spentAt, spends, spent = h, spends+1, spent+amt132}133134// Address is where the royalty goes. Every pool version from v5 pays here; anyone may send here.135func Address() address { return treasury }136137// Balance is what the treasury holds right now, in ugnot.138func Balance() int64 { return banker.NewReadonlyBanker().GetCoins(treasury).AmountOf("ugnot") }139140// Offered is whether the kind has been published to the token's governor.141func Offered() bool { return offered }142143// Offer is the same publication for a realm loaded without its init having run it (a test harness): it does144// nothing once the token's governor knows the name. The holders still have to adopt the kind by vote before a spend145// can be proposed.146func Offer(cur realm) {147	if _, known := bubble.KindRules(KindName); known {148		offered = true149		return150	}151	bubble.Offer(cross(cur), spendKind{}, governor.BootstrapRules())152	offered = true153}154155// MaxNow is the most one proposal may move right now, in whole GNOT: the cap for a balance.156func MaxNow() int64 { return maxFor(Balance()) }157158func maxFor(balance int64) int64 {159	whole := balance / one160	m := whole / MaxShare161	if m < SmallTreasury {162		m = minInt(whole, SmallTreasury) // a small treasury may go whole; just over it, the same forty — no cliff163	}164	return minInt(m, MaxSpend)165}166167func minInt(a, b int64) int64 {168	if a < b {169		return a170	}171	return b172}173174// Preview is what a proposal with this payload would say, and whether it could open now.175func Preview(payload string) string {176	k := spendKind{}177	s := k.Describe(payload)178	if err := k.Check(payload); err != nil {179		return s + " — refused: " + err.Error()180	}181	return s182}183184// ---- the kind185186type spendKind struct{}187188func (spendKind) Name() string { return KindName }189190// Describe reads the payload alone: two voters at different heights must see the same question.191func (spendKind) Describe(p string) string {192	to, n, err := parse(p)193	if err != nil {194		return "spend from the BUBBLE treasury: " + p195	}196	return "send " + strconv.FormatInt(n, 10) + " GNOT from the BUBBLE treasury to " + to.String()197}198199// Check runs when the proposal opens and again right before Do: the amount must be within the cap both times.200func (spendKind) Check(p string) error {201	_, n, err := parse(p)202	if err != nil {203		return err204	}205	return within(n)206}207208// within is the cap for the balance as it stands: the whole of a small treasury, a quarter of a large one.209func within(n int64) error {210	have := Balance()211	if n*one > have {212		return errf("the treasury holds " + gnot(have) + " GNOT, not " + strconv.FormatInt(n, 10))213	}214	if m := maxFor(have); n > m {215		return errf("one proposal may move at most " + strconv.FormatInt(m, 10) + " GNOT of the " + gnot(have) + " held (a quarter, or " + strconv.FormatInt(SmallTreasury, 10) + " whole GNOT if that is more)")216	}217	return nil218}219220// Do moves the coins. rlm is the token realm's sub-realm for this kind, handed over by its governor for this one221// decided proposal: its address is the treasury, and RealmSend from it is the only send there is. An error ends222// the proposal as failed with nothing moved; nothing here can half-finish.223func (spendKind) Do(_ int, rlm realm, p string) error {224	to, n, err := parse(p)225	if err != nil {226		return err227	}228	if rlm.Address() != treasury {229		return errf("that is not the treasury's realm") // a governor other than the token's ran this kind230	}231	if err := within(n); err != nil {232		return err233	}234	amt, have := n*one, Balance()235	noteSpend(cross(rlm), amt) // one spend per block: a batch of spends aborts here (see spentAt)236	banker.NewBanker(banker.BankerTypeRealmSend, rlm).SendCoins(treasury, to, chain.Coins{{"ugnot", amt}})237	chain.Emit("TreasurySpend", "to", to.String(), "ugnot", strconv.FormatInt(amt, 10), "left", strconv.FormatInt(have-amt, 10))238	return nil239}240241// parse reads "g1… N": an address and whole GNOT, 1 ≤ N ≤ MaxSpend.242func parse(p string) (address, int64, error) {243	parts := strings.Fields(p)244	if len(parts) != 2 {245		return "", 0, errf("expected \"g1… N\": an address and whole GNOT")246	}247	to := address(parts[0])248	if !to.IsValid() {249		return "", 0, errf("not an address")250	}251	if to == treasury || to == chain.PackageAddress(bubblePath) || to == chain.PackageAddress(ownPath) {252		return "", 0, errf("that is a realm of the token's own: coins sent there could never come back")253	}254	for _, r := range poolRealms { // a pool realm's address takes coins into no pool: they would sit there for good255		if to == chain.PackageAddress(r) {256			return "", 0, errf("that is a pool realm's own address: coins sent there join no bubble and never come back (seed a bubble with CreatePool, from a wallet)")257		}258	}259	n, err := strconv.ParseInt(parts[1], 10, 64)260	if err != nil || n <= 0 || n > MaxSpend {261		return "", 0, errf("expected whole GNOT, between 1 and " + strconv.FormatInt(MaxSpend, 10) + " in one proposal")262	}263	return to, n, nil264}265266type treasuryErr struct{ msg string }267268func (e *treasuryErr) Error() string { return "treasury: " + e.msg }269func errf(s string) error            { return &treasuryErr{s} }270271// gnot prints ugnot as GNOT with up to six decimals, trailing zeros dropped.272func gnot(u int64) string {273	whole, frac := u/one, u%one274	if frac == 0 {275		return strconv.FormatInt(whole, 10)276	}277	f := strconv.FormatInt(frac, 10)278	for len(f) < 6 {279		f = "0" + f280	}281	return strconv.FormatInt(whole, 10) + "." + strings.TrimRight(f, "0")282}283284// Render: the treasury for a reader — where, how much, and how it moves.285func Render(path string) string {286	if path == "json" {287		return `{"address":"` + treasury.String() + `","ugnot":` + strconv.FormatInt(Balance(), 10) + `,"kind":"` + KindName + `","offered":` + strconv.FormatBool(offered) + `,"maxSpend":` + strconv.FormatInt(MaxSpend, 10) + `,"maxNow":` + strconv.FormatInt(MaxNow(), 10) + `,"windowLeft":` + strconv.FormatInt(WindowLeft(), 10) + `,"spends":` + strconv.FormatInt(spends, 10) + `,"spent":` + strconv.FormatInt(spent, 10) + `}`288	}289	var b strings.Builder290	b.WriteString("# The BUBBLE treasury\n\n")291	b.WriteString("**" + gnot(Balance()) + " GNOT** at `" + treasury.String() + "` — the token realm's own sub-realm `" + subpath + "`.\n\n")292	b.WriteString("Every Bubble Rumble pool from v5 on sends **5% of every boop's bid** here (not of a bubble's seed, not of shots). Nothing leaves except by a decided vote of the BUBBLE holders under `" + KindName + "`")293	if offered {294		b.WriteString(" (published to the token's governor; the holders adopt it with `govern:adopt`, then anyone may propose `\"g1… N\"` — an address and whole GNOT)")295	} else {296		b.WriteString(" (not yet published: call `Offer` once)")297	}298	b.WriteString(". A proposal may move at most a quarter of what the treasury holds, or " + strconv.FormatInt(SmallTreasury, 10) + " GNOT if that is more, never over " + strconv.FormatInt(MaxSpend, 10) + " — right now " + strconv.FormatInt(MaxNow(), 10) + " GNOT; and the spends in a window — the " + strconv.FormatInt(Window/86400, 10) + " days from its first spend — together may move that same quarter (or " + strconv.FormatInt(SmallTreasury, 10) + ") of what it held then (" + gnot(WindowLeft()) + " GNOT left in the window now); the next spend after those days opens the next window; one spend a block, so a batch of spends cannot run. A quarter per two-day window, whatever is proposed. Only the holders' votes move money, on the token's money terms: a fifth of the supply turning out (this kind takes the plain fifth; the token's own settings raise their bar with past turnout), two thirds yes, a week to vote (or sooner, once the outcome cannot change), two days' delay, a fortnight to run it. Votes count what a wallet held at the last sealed epoch before the question opened. Today the founding share (10,000,000 of the BUBBLE in issue) carries every vote; that thins as referrals mint.\n\n")299	b.WriteString("BUBBLE carries a vote, not a claim: nothing here promises holders a payment from the treasury, and the holders' custom is prizes, work on the game and the token's market. Spends so far: " + strconv.FormatInt(spends, 10) + ", " + gnot(spent) + " GNOT.\n\n")300	b.WriteString("Replacing this kind: a kind under another name that the governor maps to the same sub-path `" + subpath + "` (for example `treasury/spend` or `treasury.spend`) spends from the same address once adopted; a name that maps elsewhere is a new, empty treasury. Anyone may offer a look-alike name: before adopting one, read who offered it (the KindOffered event) and its code.\n")301	return b.String()302}303

Functions

  • Address() string

  • Balance() int64

  • MaxNow() int64

  • Offer(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string})

  • Offered() bool

  • Preview(payload string) string

  • Render(path string) string

  • Spends() int64

  • Spent() int64

  • SpentAt() int64

  • WindowLeft() int64

Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.

Rendered

RenderedRawgnoweb ↗

vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.