nativeify.gno
9.59 Kb · 255 lines
1// Package nativeify turns a GRC20 into a native coin, which is wugnot run
2// backwards.
3//
4// wugnot takes GNOT, a coin the bank holds, and issues a GRC20 receipt for it,
5// because a GRC20 can be pulled by a contract and a native coin cannot. This
6// realm takes a GRC20, escrows it, and issues a native denom against it 1:1,
7// because a native coin can do four things a GRC20 cannot:
8//
9// - move with no realm call at all (`gnokey maketx send`, from any wallet)
10// - ride the `-send` envelope of a call, so it can PAY for something
11// - sit beside GNOT on an account page, with nothing registered anywhere
12// - be named as the gas denom by a chain whose genesis says so
13//
14// # What you give up, and it is not small
15//
16// A native coin cannot be pulled. `banker.SendCoins` refuses any `from` that is
17// not the banker's own realm address, so there is no allowance, no
18// `TransferFrom`, and no way for an AMM, an escrow or a subscription to take
19// what you approved. Everything downstream of `Approve` stops working the moment
20// a coin becomes native. That is not a gap in this realm; it is the property
21// that makes the wrapping worth doing in the other direction.
22//
23// And the issuer of a native coin can always delete a balance: `RemoveCoin`
24// takes an arbitrary address. This realm never calls it on an address other than
25// its own, [Unwrap] explains why in detail, and the gnomod.toml explains why this
26// path is public rather than redeployable. Those three together are the entire
27// guarantee. There is no on-chain way to prove a negative about code.
28//
29// # The exchange rate is 1, always
30//
31// One smallest unit of the underlying is one unit of the coin. Not 10^decimals:
32// the bank has no notion of divisibility, so a token with 6 decimals wrapped
33// here produces a coin counted in the token's own smallest units, and the 6 is
34// carried to `r/moul/x/nativereg` as a display hint and nowhere else.
35//
36// Solvency is therefore a plain equality that anybody can check without trusting
37// this realm's bookkeeping: the GRC20 balance at this realm's address equals the
38// total supply of the denom. [Solvency] computes it from both sides.
39//
40// Play money to try it on: `r/moul/x/grc20faucet/v0`.
41package nativeify
42
43import (
44 "chain"
45 "chain/banker"
46 "chain/runtime"
47 "strings"
48
49 "gno.land/p/moul/x/envelope/v0"
50 "gno.land/p/nt/avl/v0"
51 "gno.land/p/nt/grc20/v0"
52 "gno.land/p/nt/ufmt/v0"
53 "gno.land/r/nt/grc20reg/v0"
54 "gno.land/r/moul/x/nativereg/v0"
55)
56
57// Path is this realm's package path, which every denom it issues embeds.
58const Path = "gno.land/r/moul/x/nativeify/v0"
59
60// MinBase and MaxBase bound a base denom name. The chain enforces exactly this
61// (`isValidBaseDenom` in the banker stdlib): a lowercase letter, then 2 to 15
62// more lowercase letters or digits. Validating here rather than letting the
63// banker abort is the difference between a usable error and a stack trace.
64const (
65 MinBase = 3
66 MaxBase = 16
67)
68
69type pair struct {
70 base string // base denom name, unique in this realm
71 denom string // the full chain denom
72 key string // grc20reg key of the underlying
73 creator address
74 height int64
75}
76
77var (
78 byBase = avl.NewTree() // base name -> *pair
79 byKey = avl.NewTree() // grc20reg key -> *pair
80 bases []string // creation order, for a stable Render
81 home address // this realm's address: where every escrow sits
82)
83
84func init(cur realm) {
85 home = cur.Address()
86}
87
88// Home is the address to approve on the underlying token before wrapping. Every
89// escrow this realm holds sits there.
90func Home() address { return home }
91
92// Nativeify creates the native counterpart of a GRC20 registered in
93// `r/nt/grc20reg`, and returns its denom. It issues nothing: [Wrap] does that.
94//
95// `base` is the part of the denom after the colon, yours to choose, unique in
96// this realm and in the chain's charset. One native denom per underlying token:
97// a second one would double the chain's per-denom storage for no new capability,
98// and realm-denom balances live under their own store keys, which are not
99// gas-metered.
100func Nativeify(cur realm, tokenKey, base string) string {
101 under := grc20reg.MustGet(tokenKey)
102 assertBase(base)
103 if byBase.Get(base) != nil {
104 panic("this realm already issues " + base)
105 }
106 if v := byKey.Get(tokenKey); v != nil {
107 panic(ufmt.Sprintf("%s is already native here as %s", under.GetSymbol(), v.(*pair).denom))
108 }
109
110 p := &pair{
111 base: base,
112 denom: chain.CoinDenom(Path, base),
113 key: tokenKey,
114 creator: cur.Previous().Address(),
115 height: runtime.ChainHeight(),
116 }
117 byBase.Set(base, p)
118 byKey.Set(tokenKey, p)
119 bases = append(bases, base)
120
121 // This realm is the issuer named inside the denom, so it is the only thing
122 // on the chain that can describe it. The decimals travel as a display hint;
123 // the bank still counts whole units.
124 nativereg.Register(cross(cur), p.denom,
125 under.GetName()+" (native)", "n"+under.GetSymbol(), under.GetDecimals(),
126 "1:1 native wrapper of "+tokenKey+", redeemable at "+Path)
127 return p.denom
128}
129
130// Wrap escrows `amount` of the underlying and issues the same number of native
131// coins to the caller.
132//
133// You must `Approve` this realm's address on the underlying token first, through
134// that token's own realm. Until you do, this realm has no authority over your
135// balance at all, and Wrap fails with "insufficient allowance", which is the
136// system working.
137func Wrap(cur realm, base string, amount int64) int64 {
138 p := mustPair(base)
139 if amount <= 0 {
140 panic("wrap a positive amount")
141 }
142 who := cur.Previous().Address()
143
144 // Last point at which nothing has moved.
145 under := grc20reg.MustGet(p.key)
146 if err := under.RealmTeller(0, cur).TransferFrom(0, cur, who, home, amount); err != nil {
147 panic(err)
148 }
149 banker.NewBanker(banker.BankerTypeRealmIssue, cur).IssueCoin(who, p.denom, amount)
150 return amount
151}
152
153// Unwrap destroys the native coins attached to this call and releases the same
154// number of underlying GRC20 units to the caller.
155//
156// # Why you have to send the coins rather than let this realm take them
157//
158// This realm holds a `BankerTypeRealmIssue` banker, and `RemoveCoin` takes an
159// arbitrary address, so it could delete the caller's coins directly and save
160// them a flag. It does not, and the reason is worth stating where somebody
161// copying this will read it: a realm whose normal operation is to remove coins
162// from addresses that did not hand them over has no way left to demonstrate that
163// it only ever does so consensually. Every `RemoveCoin` here names this realm's
164// own address, which anybody can check by reading the source, and the coins get
165// to that address the only way a native coin ever moves, by their holder pushing
166// them.
167//
168// So: attach them.
169//
170// -send <amount>/gno.land/r/moul/x/nativeify/v0:<base>
171func Unwrap(cur realm, base string) int64 {
172 p := mustPair(base)
173 // Reading the envelope is also what makes this call payable at all: MsgCall
174 // rejects a message whose coins no code looked at.
175 amount := envelope.Require(p.denom)
176 who := cur.Previous().Address()
177
178 // Destroy the receipt before releasing the backing, so a failure between the
179 // two can only ever leave the pair over-collateralized.
180 banker.NewBanker(banker.BankerTypeRealmIssue, cur).RemoveCoin(home, p.denom, amount)
181 under := grc20reg.MustGet(p.key)
182 if err := under.RealmTeller(0, cur).Transfer(0, cur, who, amount); err != nil {
183 panic(err)
184 }
185 return amount
186}
187
188// Denom returns the denom this realm issues for a base name.
189func Denom(base string) string { return mustPair(base).denom }
190
191// TokenKey returns the grc20reg key of the underlying behind a base name.
192func TokenKey(base string) string { return mustPair(base).key }
193
194// Bases returns every base name this realm issues, in creation order.
195func Bases() []string {
196 out := make([]string, len(bases))
197 copy(out, bases)
198 return out
199}
200
201// Solvency returns what is escrowed and what is issued for a base name. They are
202// read from two different places on the chain, the GRC20 ledger and the bank,
203// and this realm's own bookkeeping is not consulted for either: if they ever
204// disagree, the disagreement is the answer.
205func Solvency(base string) (escrowed, issued int64) {
206 p := mustPair(base)
207 escrowed = grc20reg.MustGet(p.key).BalanceOf(home)
208 issued = banker.NewReadonlyBanker().TotalCoin(p.denom)
209 return escrowed, issued
210}
211
212// IsSolvent reports whether every coin issued for a base name is backed.
213//
214// It allows escrowed > issued, which is what a donation to this realm's address
215// looks like and cannot hurt anybody, and refuses the reverse.
216func IsSolvent(base string) bool {
217 escrowed, issued := Solvency(base)
218 return escrowed >= issued
219}
220
221// Underlying returns the underlying token behind a base name, for a caller that
222// wants its name, symbol or decimals.
223func Underlying(base string) *grc20.Token {
224 return grc20reg.MustGet(mustPair(base).key)
225}
226
227func mustPair(base string) *pair {
228 v := byBase.Get(base)
229 if v == nil {
230 panic("this realm does not issue " + base)
231 }
232 return v.(*pair)
233}
234
235// assertBase applies the chain's own base-denom rule before the banker does, so
236// the caller reads a sentence instead of a stack trace.
237func assertBase(base string) {
238 if len(base) < MinBase || len(base) > MaxBase {
239 panic(ufmt.Sprintf("base %q is %d characters, must be %d to %d",
240 base, len(base), MinBase, MaxBase))
241 }
242 for i := 0; i < len(base); i++ {
243 c := base[i]
244 lower := c >= 'a' && c <= 'z'
245 digit := c >= '0' && c <= '9'
246 if lower || (i > 0 && digit) {
247 continue
248 }
249 panic(ufmt.Sprintf("base %q: a base denom is a lowercase letter followed by "+
250 "lowercase letters or digits, and %q is not", base, string(c)))
251 }
252 if strings.Contains(base, ":") { // unreachable past the charset loop; belt and braces
253 panic("a base denom cannot contain a colon")
254 }
255}