Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}