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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

wrapdemo.gno

10.24 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/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/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}