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.

nativeify.gno

9.63 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/nym-moultest123/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/nym-moultest123/x/nativereg/v0"
 55)
 56
 57// Path is this realm's package path, which every denom it issues embeds.
 58const Path = "gno.land/r/nym-moultest123/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/nym-moultest123/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}