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

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}