1// Package wbubble is wrapped BUBBLE: the Bubble Rumble token as a registered GRC20, backed one to one by BUBBLE2// held in this realm. It exists for gnoswap, which pools only tokens registered in gno.land/r/nt/grc20reg/v0 —3// and the registry takes only a *grc20.Token made by grc20.NewToken inside the registering realm. BUBBLE4// (gno.land/r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/bubble) keeps its ledger in kourt's grc20votes, not a5// grc20.Token, and a realm cannot be redeployed; so this realm holds BUBBLE and issues a registered token for6// it, the way gno.land/r/gnoland/wugnot wraps ugnot (ref/ holds the mainnet sources this mirrors).7//8// Wrap pulls BUBBLE from the caller — an allowance the caller granted THIS realm on the bubble realm, spent by9// this realm as itself — and mints the same amount of wrapped BUBBLE to the caller. Unwrap burns the caller's10// wrapped BUBBLE and sends the same amount of BUBBLE back. Nothing else mints or burns, and nothing here is a11// setting: no admin, no pause, no minter. The reserve (this realm's BUBBLE balance) covers the wrapped supply12// after every call, and a call that would leave it short aborts.13//14// Votes. BUBBLE carries a vote in its own realm; wrapped BUBBLE carries none. The BUBBLE held here is15// self-delegated, inside the bubble ledger, to this realm's own address — the ledger's default for every holder —16// and this realm never delegates it or votes with it, so that weight is never cast. Unwrap to vote.17package wbubble1819import (20 "chain"21 "strconv"22 "strings"2324 "gno.land/p/nt/grc20/v0"25 "gno.land/r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/bubble"26 "gno.land/r/nt/grc20reg/v0"27)2829// Token is the registered token, exported as wugnot exports its own: a *grc20.Token carries no spend30// authority (its tellers act only for the realm that builds them, and only with an allowance), and the registry31// hands the same pointer to anyone. ledger is the mint and burn, and it never leaves this package.32var (33 Token *grc20.Token34 ledger *grc20.PrivateLedger35)3637const (38 tokenName = "Bubble Rumble BUBBLE (wrapped)"39 tokenSymbol = "BUBBLE"40 tokenDecimals = 641 pkgPath = "gno.land/r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/wbubble" // must match gnomod.toml42 // RegistryKey is what gno.land/r/nt/grc20reg/v0 knows this token as: the realm path and the symbol.43 RegistryKey = pkgPath + "." + tokenSymbol4445 one = int64(1_000_000) // one BUBBLE in base units46)4748func init(cur realm) {49 // wbubble only ever creates this one token, so id 0 can't collide.50 Token, ledger = grc20.NewToken(tokenName, tokenSymbol, tokenDecimals, 0, cur)51 if key := grc20reg.Register(cross(cur), Token, ""); key != RegistryKey {52 panic("wbubble: registered as " + key + ", not " + RegistryKey + ": the package path is not the one in the source")53 }54}5556// Address is this realm's own address: the spender to approve on the bubble realm before Wrap, and the holder of57// the reserve.58func Address() address { return chain.PackageAddress(pkgPath) }5960// Reserve is the BUBBLE this realm holds. It covers TotalSupply after every call (checkReserve).61func Reserve() int64 { return bubble.BalanceOf(Address()) }6263// Wrap takes amount BUBBLE from the caller and mints as much wrapped BUBBLE to them. First, on the bubble realm,64// the caller approves THIS realm for the amount (bubble.Approve with spender Address()): bubble reads its own65// caller as the spender, and its caller here is this realm, not the user. A failure inside bubble panics across66// the realm boundary and aborts the whole transaction, so nothing is minted for BUBBLE that did not arrive.67func Wrap(cur realm, amount int64) {68 require(amount > 0, "wbubble: the amount must be positive")69 // The security guide's rule (gno-security-guide.md §5.6, §8): read the caller from cur only after IsCurrent.70 // Under interrealm v2 (gno-interrealm-v2.md §5.2) a crossing function's own cur is always current, so this71 // refuses nobody; it is kept so the realm meets both documents.72 if !cur.IsCurrent() {73 panic("wbubble: spoofed realm")74 }75 caller := cur.Previous().Address()76 self := cur.Address()77 before := bubble.BalanceOf(self)78 bubble.TransferFrom(cross(cur), caller, self, amount)79 checkErr(ledger.Mint(caller, amount))80 checkReserve(self, before+amount)81}8283// Unwrap burns amount of the caller's wrapped BUBBLE and sends them as much BUBBLE. The burn goes first: a caller84// without the wrapped tokens is refused before anything moves.85func Unwrap(cur realm, amount int64) {86 require(amount > 0, "wbubble: the amount must be positive")87 if !cur.IsCurrent() { // see Wrap88 panic("wbubble: spoofed realm")89 }90 caller := cur.Previous().Address()91 self := cur.Address()92 before := bubble.BalanceOf(self)93 checkErr(ledger.Burn(caller, amount))94 bubble.Transfer(cross(cur), caller, amount)95 checkReserve(self, before-amount)96}9798// checkReserve is the invariant after every move: the reserve moved by exactly the amount, and it covers the99// supply. Not `reserve == supply`: anyone can send BUBBLE straight to this realm's address, and a wrapper that100// demanded equality would then refuse every Wrap and Unwrap for good. Such a gift only puts the reserve over the101// supply; it belongs to nobody and stays.102func checkReserve(self address, want int64) {103 have := bubble.BalanceOf(self)104 if have != want {105 panic("wbubble: the reserve moved by the wrong amount")106 }107 if have < Token.TotalSupply() {108 panic("wbubble: the reserve is short of the wrapped supply")109 }110}111112// ------------------------------------------------------------ the GRC20 --113//114// wugnot's surface, line for line: the reads on the Token, the writes through the ledger's caller teller, which115// acts for whoever crossed into this realm and works nowhere but here.116117func TotalSupply() int64 {118 return Token.TotalSupply()119}120121func BalanceOf(owner address) int64 {122 return Token.BalanceOf(owner)123}124125func Allowance(owner, spender address) int64 {126 return Token.Allowance(owner, spender)127}128129func Transfer(cur realm, to address, amount int64) {130 userTeller := ledger.CallerTeller()131 checkErr(userTeller.Transfer(0, cur, to, amount))132}133134func Approve(cur realm, spender address, amount int64) {135 userTeller := ledger.CallerTeller()136 checkErr(userTeller.Approve(0, cur, spender, amount))137}138139func TransferFrom(cur realm, from, to address, amount int64) {140 userTeller := ledger.CallerTeller()141 checkErr(userTeller.TransferFrom(0, cur, from, to, amount))142}143144// ----------------------------------------------------------------- pages --145146// Render: "" the token's page; "balance/<addr>" one wrapped balance as a number (wugnot's path); "json" the147// figures for the box.148func Render(path string) string {149 parts := strings.Split(path, "/")150 switch {151 case path == "":152 return home()153 case path == "json":154 return json()155 case len(parts) == 2 && parts[0] == "balance":156 return strconv.FormatInt(Token.BalanceOf(address(parts[1])), 10)157 default:158 return "404"159 }160}161162func home() string {163 self := Address()164 supply, reserve := Token.TotalSupply(), bubble.BalanceOf(self)165 var b strings.Builder166 b.WriteString("Wrapped BUBBLE, symbol " + tokenSymbol + ", " + strconv.Itoa(tokenDecimals) + " decimals: the Bubble Rumble token as a registered GRC20, for gnoswap and every realm that reads the token registry.\n\n")167 b.WriteString("Wrapped in circulation: " + amount(supply) + " BUBBLE. Reserve held by this realm at " + self.String() + ": " + amount(reserve) + " BUBBLE")168 switch {169 case reserve == supply:170 b.WriteString(", exactly the supply.\n\n")171 case reserve > supply:172 b.WriteString(", " + amount(reserve-supply) + " over the supply (BUBBLE sent here directly; it belongs to nobody).\n\n")173 default:174 b.WriteString(" — SHORT of the supply by " + amount(supply-reserve) + "; this must never print.\n\n")175 }176 b.WriteString("Registry key: " + RegistryKey + " (gno.land/r/nt/grc20reg/v0).\n\n")177 b.WriteString("To wrap: on the bubble realm call Approve with spender " + self.String() + " and the amount in base units, then call Wrap here with the same amount.\n")178 b.WriteString("To unwrap: call Unwrap here with the amount; the BUBBLE comes back to your address.\n\n")179 b.WriteString("Votes: BUBBLE carries a vote in its own realm and wrapped BUBBLE carries none. What this realm holds is self-delegated to the realm's own address inside the bubble ledger and is never cast. Unwrap to vote.\n")180 return b.String()181}182183func json() string {184 return `{"name":"` + tokenName + `","symbol":"` + tokenSymbol + `","decimals":` + strconv.Itoa(tokenDecimals) +185 `,"supply":` + strconv.FormatInt(Token.TotalSupply(), 10) + `,"reserve":` + strconv.FormatInt(Reserve(), 10) +186 `,"address":"` + Address().String() + `","key":"` + RegistryKey + `"}`187}188189// amount prints base units as BUBBLE with up to six decimals, trailing zeros dropped.190func amount(n int64) string {191 whole, frac := n/one, n%one192 if frac == 0 {193 return strconv.FormatInt(whole, 10)194 }195 f := strconv.FormatInt(frac, 10)196 for len(f) < tokenDecimals {197 f = "0" + f198 }199 return strconv.FormatInt(whole, 10) + "." + strings.TrimRight(f, "0")200}201202func require(condition bool, msg string) {203 if !condition {204 panic(msg)205 }206}207208func checkErr(err error) {209 if err != nil {210 panic("wbubble: " + err.Error())211 }212}213Address() string
Allowance(owner string, spender string) int64
Approve(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}, spender string, amount int64)
BalanceOf(owner string) int64
Render(path string) string
Reserve() int64
TotalSupply() int64
Transfer(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}, to string, amount int64)
TransferFrom(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}, from string, to string, amount int64)
Unwrap(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}, amount int64)
Wrap(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}, amount int64)
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
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.