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

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}