wrapdemo.gno
10.21 Kb · 325 lines
1// Package grc20wrapdemo is a permissionless wrapper factory for GRC20 tokens.
2//
3// Point it at any token registered in gno.land/r/nt/grc20reg/v0 and it issues a
4// new one backed by it, with a personality you pick from a list. Point it at two
5// and it issues a meta-token backed by both. Nothing is whitelisted, nothing is
6// owned, and the realm never gains authority over anybody's balance: every
7// deposit is pulled with an allowance the holder granted to this realm's address.
8//
9// The five wrapper modes, all from gno.land/p/moul/x/grc20wrap/v0:
10//
11// plain 1:1 custody. A receipt. The boring one that proves the rest works.
12// kilo 1 underlying unit becomes 1000 wrapped ones, +3 decimals.
13// soulbound wrap and unwrap freely, but the wrapped token never changes hands.
14// pool shares in the escrow: Donate raises what every share redeems for.
15// sticky pool, plus a 1% exit fee that stays behind for whoever holds on.
16//
17// And one fusion mode: NewFusion bundles two registered tokens at a fixed
18// proportion into a single meta-token, redeemable back into both.
19//
20// # Wrapping a wrapper
21//
22// Every token this realm issues is itself registered in grc20reg, so its key can
23// be fed straight back into NewWrapper or NewFusion. A pool over a kilo over a
24// plain wrap of RED is legal, works, and is a good way to see how thin the
25// abstraction really is.
26//
27// # The one thing a caller must do first
28//
29// Approve this realm's address on the underlying token, through THAT token's own
30// realm. Home() prints the address. Without it every Deposit fails with
31// "insufficient allowance", which is the system working.
32//
33// Play money to try it on: gno.land/r/moul/x/grc20faucet/v0.
34package grc20wrapdemo
35
36import (
37 "chain/runtime"
38
39 "gno.land/p/moul/x/grc20wrap/v0"
40 "gno.land/p/nt/avl/v0"
41 "gno.land/p/nt/grc20/v0"
42 "gno.land/p/nt/seqid/v0"
43 "gno.land/p/nt/ufmt/v0"
44 "gno.land/r/nt/grc20reg/v0"
45)
46
47// entry is one issued token: either a wrapper over a single underlying, or a
48// fusion over several. Exactly one of vault/basket is set.
49type entry struct {
50 symbol string
51 mode string
52 key string // grc20reg key of the issued token
53 vault *grc20wrap.Vault
54 basket *grc20wrap.Basket
55 creator address
56 height int64
57}
58
59var (
60 ids seqid.ID
61 byName = avl.NewTree() // symbol -> *entry
62 created []string // symbols, in creation order, for a stable Render
63 home address // this realm's address: the escrow account
64)
65
66func init(cur realm) {
67 home = cur.Address()
68}
69
70// Home is the address to approve on an underlying token before depositing. It
71// is where every escrow this realm holds actually sits.
72func Home() address { return home }
73
74// NewWrapper issues a token wrapping `tokenKey`, a key in grc20reg, under the
75// given mode, and registers it. It returns the new token's own registry key.
76//
77// `symbol` is yours to choose, must be unique in this realm, and follows the
78// GRC20 rules: 1 to 11 characters of [A-Za-z0-9_-].
79func NewWrapper(cur realm, tokenKey, mode, symbol string) string {
80 who := caller(cur)
81 under := grc20reg.MustGet(tokenKey)
82 requireFree(symbol)
83
84 pol, extraDecimals := modePolicy(mode)
85 decimals := under.GetDecimals() + extraDecimals
86 if decimals > 18 {
87 panic(ufmt.Sprintf("%s already has %d decimals; mode %q would need %d, over the GRC20 limit of 18",
88 under.GetSymbol(), under.GetDecimals(), mode, decimals))
89 }
90 name := ufmt.Sprintf("%s (%s wrap of %s)", symbol, mode, under.GetSymbol())
91
92 v := grc20wrap.NewVault(under, pol, name, symbol, decimals, ids.Next(), cur)
93 key := grc20reg.Register(cross(cur), v.Token(), "")
94 record(&entry{
95 symbol: symbol,
96 mode: mode,
97 key: key,
98 vault: v,
99 creator: who,
100 height: runtime.ChainHeight(),
101 })
102 return key
103}
104
105// NewFusion issues a meta-token backed by two registered tokens at a fixed
106// proportion: one smallest unit of the meta-token is always worth `perA`
107// smallest units of A plus `perB` of B. It returns the new token's registry key.
108//
109// The proportion never changes, and the meta-token is minted and burned only
110// against the real thing, so it cannot drift from its backing or be arbitraged.
111// What it can do is make the pair one transferable object.
112func NewFusion(cur realm, keyA string, perA int64, keyB string, perB int64, symbol string) string {
113 who := caller(cur)
114 a := grc20reg.MustGet(keyA)
115 b := grc20reg.MustGet(keyB)
116 requireFree(symbol)
117
118 decimals := a.GetDecimals()
119 if b.GetDecimals() > decimals {
120 decimals = b.GetDecimals()
121 }
122 name := ufmt.Sprintf("%s (%s+%s fusion)", symbol, a.GetSymbol(), b.GetSymbol())
123
124 bk := grc20wrap.NewBasket(
125 []*grc20.Token{a, b},
126 []int64{perA, perB},
127 name, symbol, decimals, ids.Next(), cur,
128 )
129 key := grc20reg.Register(cross(cur), bk.Token(), "")
130 record(&entry{
131 symbol: symbol,
132 mode: "fusion",
133 key: key,
134 basket: bk,
135 creator: who,
136 height: runtime.ChainHeight(),
137 })
138 return key
139}
140
141// Deposit escrows `amount` of the underlying and mints `symbol` to the caller.
142// It returns how much was minted, which is not `amount` unless the mode is
143// plain. Approve Home() on the underlying first.
144func Deposit(cur realm, symbol string, amount int64) int64 {
145 who := caller(cur)
146 out, err := mustVault(symbol).Wrap(0, cur, who, amount)
147 checkErr(err)
148 return out
149}
150
151// Withdraw burns `amount` of `symbol` and returns the underlying to the caller.
152// It returns how much came back.
153func Withdraw(cur realm, symbol string, amount int64) int64 {
154 who := caller(cur)
155 out, err := mustVault(symbol).Unwrap(0, cur, who, amount)
156 checkErr(err)
157 return out
158}
159
160// Donate escrows `amount` of the underlying for `symbol` and mints nothing.
161//
162// Under a pool or sticky mode this is a gift to every current holder at once,
163// and it is irreversible: there is no share to redeem it with. Under any other
164// mode it is a gift to nobody, since the rate ignores the escrow.
165func Donate(cur realm, symbol string, amount int64) {
166 who := caller(cur)
167 checkErr(mustVault(symbol).Donate(0, cur, who, amount))
168}
169
170// Fuse escrows every leg of `symbol` and mints `units` of it to the caller.
171// Approve Home() on BOTH legs first.
172func Fuse(cur realm, symbol string, units int64) {
173 who := caller(cur)
174 checkErr(mustBasket(symbol).Fuse(0, cur, who, units))
175}
176
177// Defuse burns `units` of `symbol` and returns every leg to the caller.
178func Defuse(cur realm, symbol string, units int64) {
179 who := caller(cur)
180 checkErr(mustBasket(symbol).Defuse(0, cur, who, units))
181}
182
183// Transfer moves the caller's own units of `symbol`. A soulbound wrapper
184// refuses here, and nowhere else.
185func Transfer(cur realm, symbol string, to address, amount int64) {
186 who := caller(cur)
187 e := must(symbol)
188 if e.vault != nil {
189 checkErr(e.vault.Move(who, to, amount))
190 return
191 }
192 checkErr(e.basket.Move(who, to, amount))
193}
194
195// Approve lets `spender` draw `amount` of `symbol` from the caller's balance.
196func Approve(cur realm, symbol string, spender address, amount int64) {
197 who := caller(cur)
198 e := must(symbol)
199 if e.vault != nil {
200 checkErr(e.vault.Allow(who, spender, amount))
201 return
202 }
203 checkErr(e.basket.Allow(who, spender, amount))
204}
205
206// TransferFrom spends an allowance the caller was granted on `symbol`.
207func TransferFrom(cur realm, symbol string, from, to address, amount int64) {
208 who := caller(cur)
209 e := must(symbol)
210 if e.vault != nil {
211 checkErr(e.vault.MoveFrom(who, from, to, amount))
212 return
213 }
214 checkErr(e.basket.MoveFrom(who, from, to, amount))
215}
216
217// Key returns the grc20reg key of the token this realm issued as `symbol`.
218func Key(symbol string) string { return must(symbol).key }
219
220// TotalSupply returns how much of `symbol` is outstanding.
221func TotalSupply(symbol string) int64 { return must(symbol).token().TotalSupply() }
222
223// BalanceOf returns `owner`'s balance of `symbol`.
224func BalanceOf(symbol string, owner address) int64 {
225 return must(symbol).token().BalanceOf(owner)
226}
227
228// Allowance returns what `owner` let `spender` draw of `symbol`.
229func Allowance(symbol string, owner, spender address) int64 {
230 return must(symbol).token().Allowance(owner, spender)
231}
232
233// Escrow returns the underlying escrowed behind a wrapper.
234func Escrow(symbol string) int64 { return mustVault(symbol).Held() }
235
236// Legs returns how many components a fusion has.
237func Legs(symbol string) int { return mustBasket(symbol).Legs() }
238
239// Leg describes the i-th component of a fusion: its symbol, the units escrowed
240// per meta unit, and the units escrowed so far.
241func Leg(symbol string, i int) (string, int64, int64) {
242 tok, per, held := mustBasket(symbol).Leg(i)
243 return tok.GetSymbol(), per, held
244}
245
246// Count is how many tokens this realm has issued.
247func Count() int { return len(created) }
248
249// internals
250//
251
252// modePolicy maps a mode name to its policy and to the extra decimals the
253// wrapped token needs to represent the same value.
254func modePolicy(mode string) (grc20wrap.Policy, int) {
255 switch mode {
256 case "plain":
257 return grc20wrap.OneToOne{}, 0
258 case "kilo":
259 return grc20wrap.Ratio{Num: 1_000, Den: 1}, 3
260 case "soulbound":
261 return grc20wrap.Soulbound{}, 0
262 case "pool":
263 return grc20wrap.Pool{}, 0
264 case "sticky":
265 return grc20wrap.Fee{Base: grc20wrap.Pool{}, UnwrapBPS: 100}, 0
266 }
267 panic("unknown mode " + mode + " (plain, kilo, soulbound, pool, sticky)")
268}
269
270func (e *entry) token() *grc20.Token {
271 if e.vault != nil {
272 return e.vault.Token()
273 }
274 return e.basket.Token()
275}
276
277func record(e *entry) {
278 byName.Set(e.symbol, e)
279 created = append(created, e.symbol)
280}
281
282func requireFree(symbol string) {
283 if byName.Has(symbol) {
284 panic("symbol " + symbol + " is already issued by this realm")
285 }
286}
287
288func must(symbol string) *entry {
289 e := byName.Get(symbol)
290 if e == nil {
291 panic("this realm has not issued " + symbol)
292 }
293 return e.(*entry)
294}
295
296func mustVault(symbol string) *grc20wrap.Vault {
297 e := must(symbol)
298 if e.vault == nil {
299 panic(symbol + " is a fusion; use Fuse and Defuse")
300 }
301 return e.vault
302}
303
304func mustBasket(symbol string) *grc20wrap.Basket {
305 e := must(symbol)
306 if e.basket == nil {
307 panic(symbol + " is a wrapper; use Deposit and Withdraw")
308 }
309 return e.basket
310}
311
312// caller is the account or realm that crossed into this one. Every deposit is
313// escrowed from, and every mint credited to, exactly this address.
314func caller(cur realm) address {
315 if !cur.IsCurrent() {
316 panic("grc20wrapdemo: stale realm token")
317 }
318 return cur.Previous().Address()
319}
320
321func checkErr(err error) {
322 if err != nil {
323 panic(err)
324 }
325}