wbubble.gno
9.04 Kb · 212 lines
1// Package wbubble is wrapped BUBBLE: the Bubble Rumble token as a registered GRC20, backed one to one by BUBBLE
2// 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. BUBBLE
4// (gno.land/r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/bubble) keeps its ledger in kourt's grc20votes, not a
5// grc20.Token, and a realm cannot be redeployed; so this realm holds BUBBLE and issues a registered token for
6// 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 by
9// this realm as itself — and mints the same amount of wrapped BUBBLE to the caller. Unwrap burns the caller's
10// wrapped BUBBLE and sends the same amount of BUBBLE back. Nothing else mints or burns, and nothing here is a
11// setting: no admin, no pause, no minter. The reserve (this realm's BUBBLE balance) covers the wrapped supply
12// 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 is
15// 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 wbubble
18
19import (
20 "chain"
21 "strconv"
22 "strings"
23
24 "gno.land/p/nt/grc20/v0"
25 "gno.land/r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/bubble"
26 "gno.land/r/nt/grc20reg/v0"
27)
28
29// Token is the registered token, exported as wugnot exports its own: a *grc20.Token carries no spend
30// authority (its tellers act only for the realm that builds them, and only with an allowance), and the registry
31// hands the same pointer to anyone. ledger is the mint and burn, and it never leaves this package.
32var (
33 Token *grc20.Token
34 ledger *grc20.PrivateLedger
35)
36
37const (
38 tokenName = "Bubble Rumble BUBBLE (wrapped)"
39 tokenSymbol = "BUBBLE"
40 tokenDecimals = 6
41 pkgPath = "gno.land/r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/wbubble" // must match gnomod.toml
42 // RegistryKey is what gno.land/r/nt/grc20reg/v0 knows this token as: the realm path and the symbol.
43 RegistryKey = pkgPath + "." + tokenSymbol
44
45 one = int64(1_000_000) // one BUBBLE in base units
46)
47
48func 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}
55
56// Address is this realm's own address: the spender to approve on the bubble realm before Wrap, and the holder of
57// the reserve.
58func Address() address { return chain.PackageAddress(pkgPath) }
59
60// Reserve is the BUBBLE this realm holds. It covers TotalSupply after every call (checkReserve).
61func Reserve() int64 { return bubble.BalanceOf(Address()) }
62
63// 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 own
65// caller as the spender, and its caller here is this realm, not the user. A failure inside bubble panics across
66// 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 this
71 // 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}
82
83// Unwrap burns amount of the caller's wrapped BUBBLE and sends them as much BUBBLE. The burn goes first: a caller
84// 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 Wrap
88 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}
97
98// checkReserve is the invariant after every move: the reserve moved by exactly the amount, and it covers the
99// supply. Not `reserve == supply`: anyone can send BUBBLE straight to this realm's address, and a wrapper that
100// demanded equality would then refuse every Wrap and Unwrap for good. Such a gift only puts the reserve over the
101// 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}
111
112// ------------------------------------------------------------ the GRC20 --
113//
114// wugnot's surface, line for line: the reads on the Token, the writes through the ledger's caller teller, which
115// acts for whoever crossed into this realm and works nowhere but here.
116
117func TotalSupply() int64 {
118 return Token.TotalSupply()
119}
120
121func BalanceOf(owner address) int64 {
122 return Token.BalanceOf(owner)
123}
124
125func Allowance(owner, spender address) int64 {
126 return Token.Allowance(owner, spender)
127}
128
129func Transfer(cur realm, to address, amount int64) {
130 userTeller := ledger.CallerTeller()
131 checkErr(userTeller.Transfer(0, cur, to, amount))
132}
133
134func Approve(cur realm, spender address, amount int64) {
135 userTeller := ledger.CallerTeller()
136 checkErr(userTeller.Approve(0, cur, spender, amount))
137}
138
139func TransferFrom(cur realm, from, to address, amount int64) {
140 userTeller := ledger.CallerTeller()
141 checkErr(userTeller.TransferFrom(0, cur, from, to, amount))
142}
143
144// ----------------------------------------------------------------- pages --
145
146// Render: "" the token's page; "balance/<addr>" one wrapped balance as a number (wugnot's path); "json" the
147// 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}
161
162func home() string {
163 self := Address()
164 supply, reserve := Token.TotalSupply(), bubble.BalanceOf(self)
165 var b strings.Builder
166 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}
182
183func 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}
188
189// 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%one
192 if frac == 0 {
193 return strconv.FormatInt(whole, 10)
194 }
195 f := strconv.FormatInt(frac, 10)
196 for len(f) < tokenDecimals {
197 f = "0" + f
198 }
199 return strconv.FormatInt(whole, 10) + "." + strings.TrimRight(f, "0")
200}
201
202func require(condition bool, msg string) {
203 if !condition {
204 panic(msg)
205 }
206}
207
208func checkErr(err error) {
209 if err != nil {
210 panic("wbubble: " + err.Error())
211 }
212}