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

wrap.gno

10.10 Kb · 284 lines
  1// Package grc20wrap builds derived GRC20 tokens on top of existing ones.
  2//
  3// Two shapes:
  4//
  5//   - Vault wraps ONE underlying token. It escrows underlying units at the
  6//     host realm's address and issues its own GRC20 against that escrow. A
  7//     Policy sets the exchange rate in both directions and may veto a move of
  8//     the wrapped token, which is where a wrapper stops being plumbing and
  9//     becomes a pattern: 1:1 custody, a denomination change, a
 10//     non-transferable receipt, a share in a growing pool.
 11//   - Basket wraps SEVERAL. One meta-token is minted against fixed parts of
 12//     every leg and splits back into them on redemption.
 13//
 14// Neither type holds chain state of its own beyond the GRC20 ledger it
 15// creates, so a realm keeps them wherever it likes.
 16//
 17// # How custody works
 18//
 19// Escrow lands at the HOST REALM's address. A vault moves it through
 20// grc20.RealmTeller, which binds eagerly to that address, so a vault can only
 21// ever spend what its own realm holds. Pulling a user's tokens in therefore
 22// takes the ordinary allowance route: the user approves the host realm's
 23// address on the underlying token, through that token realm's own entry
 24// point, and the vault draws on the allowance as itself.
 25//
 26// # Reverting, not returning
 27//
 28// Wrapping is two ledger writes on two different tokens, and gno has no
 29// rollback short of a panic: a returned error does not undo what already ran.
 30// Every method here therefore validates while nothing has moved and returns an
 31// error, then panics if a write fails after the point of no return. Aborting
 32// the transaction is the only atomicity available.
 33//
 34// # What a Policy can and cannot enforce
 35//
 36// A Policy veto binds end users, not realms. Moving the wrapped token through
 37// Vault.Move is the only path a signing account has, because MsgCall cannot
 38// build the realm argument grc20's tellers require. Another realm holding the
 39// wrapped token can always call grc20.RealmTeller on it and move its OWN
 40// balance. Soulbound means "no user can pass it on", not "it can never move".
 41//
 42// Live demo: gno.land/r/moul/x/grc20wrapdemo/v0.
 43package grc20wrap
 44
 45import (
 46	"errors"
 47	"math/overflow"
 48
 49	"gno.land/p/nt/grc20/v0"
 50	"gno.land/p/nt/seqid/v0"
 51	"gno.land/p/nt/ufmt/v0"
 52)
 53
 54var (
 55	ErrNilToken       = errors.New("grc20wrap: nil underlying token")
 56	ErrInvalidAmount  = errors.New("grc20wrap: amount must be positive")
 57	ErrDust           = errors.New("grc20wrap: amount rounds to zero")
 58	ErrUnbacked       = errors.New("grc20wrap: payout exceeds this vault's escrow")
 59	ErrSoulbound      = errors.New("grc20wrap: soulbound, the wrapped token cannot change hands")
 60	ErrOverflow       = errors.New("grc20wrap: arithmetic overflow")
 61	ErrBadRatio       = errors.New("grc20wrap: ratio terms must be positive")
 62	ErrBadBPS         = errors.New("grc20wrap: basis points must be within 0..10000")
 63	ErrBadLegs        = errors.New("grc20wrap: a basket needs at least two legs, each with a positive part")
 64	ErrShortBalance   = errors.New("grc20wrap: insufficient balance on a leg")
 65	ErrShortAllowance = errors.New("grc20wrap: insufficient allowance on a leg")
 66)
 67
 68// Vault escrows one underlying GRC20 and issues another against it.
 69//
 70// The escrow account is the host realm's own address, shared with every other
 71// vault that realm creates. Separation between them is the per-vault Held()
 72// counter, not separate accounts: a vault only ever releases what it recorded
 73// taking in, so the realm-wide invariant is sum(Held) <= Custody().
 74type Vault struct {
 75	under  *grc20.Token
 76	teller grc20.Teller // bound to home, eagerly, at construction
 77	home   address
 78	tok    *grc20.Token
 79	led    *grc20.PrivateLedger
 80	pol    Policy
 81	held   int64
 82}
 83
 84// NewVault issues a new GRC20 backed by units of under.
 85//
 86// Call it from the realm that will own the vault, forwarding that realm's own
 87// cur: both the teller binding and the new token's origRealm are taken from it
 88// and cannot be forged afterwards. id disambiguates several tokens minted by
 89// the same realm - allocate it from one persistent seqid.ID.
 90//
 91// A nil pol means OneToOne.
 92func NewVault(under *grc20.Token, pol Policy, name, symbol string, decimals int, id seqid.ID, rlm realm) *Vault {
 93	if under == nil {
 94		panic(ErrNilToken)
 95	}
 96	if pol == nil {
 97		pol = OneToOne{}
 98	}
 99	tok, led := grc20.NewToken(name, symbol, decimals, id, rlm)
100	return &Vault{
101		under:  under,
102		teller: under.RealmTeller(0, rlm),
103		home:   rlm.Address(),
104		tok:    tok,
105		led:    led,
106		pol:    pol,
107	}
108}
109
110// Token is the wrapped token this vault issues. It is safe to hand out: a
111// *grc20.Token carries metadata and read access, never the authority to debit
112// anybody.
113func (v *Vault) Token() *grc20.Token { return v.tok }
114
115// Underlying is the token held in escrow.
116func (v *Vault) Underlying() *grc20.Token { return v.under }
117
118// Policy is this vault's personality.
119func (v *Vault) Policy() Policy { return v.pol }
120
121// Home is the escrow account: the host realm's address.
122func (v *Vault) Home() address { return v.home }
123
124// Held is the underlying escrowed for THIS vault, by its own accounting.
125func (v *Vault) Held() int64 { return v.held }
126
127// Supply is the wrapped token in circulation.
128func (v *Vault) Supply() int64 { return v.tok.TotalSupply() }
129
130// Custody is the underlying actually sitting at the escrow account. It covers
131// every vault the host realm created over the same underlying, plus anything
132// sent there by mistake, so Custody >= Held is necessary for solvency and
133// sufficient only when this is the realm's single vault over that token.
134func (v *Vault) Custody() int64 { return v.under.BalanceOf(v.home) }
135
136// Solvent reports whether the escrow account still covers what this vault
137// recorded taking in. False means the host realm moved the underlying behind
138// the vault's back.
139func (v *Vault) Solvent() bool { return v.Custody() >= v.held }
140
141// Wrap escrows amount units of the underlying from `from` and mints the
142// wrapped token to `from`, returning how much was minted.
143//
144// `from` must already have granted v.Home() an allowance of at least amount on
145// the underlying token, set through that token realm's own entry point.
146func (v *Vault) Wrap(_ int, rlm realm, from address, amount int64) (int64, error) {
147	if amount <= 0 {
148		return 0, ErrInvalidAmount
149	}
150	out, err := v.pol.WrapRate(v, amount)
151	if err != nil {
152		return 0, err
153	}
154	if out <= 0 {
155		return 0, ErrDust
156	}
157	// Last point at which nothing has moved.
158	if err := v.teller.TransferFrom(0, rlm, from, v.home, amount); err != nil {
159		return 0, err
160	}
161	held, ok := overflow.Add64(v.held, amount)
162	if !ok {
163		panic(ErrOverflow)
164	}
165	v.held = held
166	if err := v.led.Mint(from, out); err != nil {
167		// The escrow already moved; only aborting the tx can undo it.
168		panic(err)
169	}
170	return out, nil
171}
172
173// Unwrap burns amount wrapped units held by `from` and releases the underlying
174// back to it, returning how much was released.
175func (v *Vault) Unwrap(_ int, rlm realm, from address, amount int64) (int64, error) {
176	if amount <= 0 {
177		return 0, ErrInvalidAmount
178	}
179	out, err := v.pol.UnwrapRate(v, amount)
180	if err != nil {
181		return 0, err
182	}
183	if out <= 0 {
184		return 0, ErrDust
185	}
186	if out > v.held {
187		return 0, ErrUnbacked
188	}
189	// Burn first: it is the step that can legitimately fail on a short
190	// balance, and it fails before anything has moved.
191	if err := v.led.Burn(from, amount); err != nil {
192		return 0, err
193	}
194	v.held -= out
195	if err := v.teller.Transfer(0, rlm, from, out); err != nil {
196		// The burn already happened; only aborting the tx can undo it.
197		panic(err)
198	}
199	return out, nil
200}
201
202// Donate escrows amount units of the underlying without minting anything.
203//
204// Under a pool policy this is how the wrapper gets interesting: the donation
205// raises what every outstanding wrapped unit redeems for, so anyone can make
206// every holder richer and nobody can take it back out except by holding.
207// `from` must have approved v.Home() on the underlying, as for Wrap.
208func (v *Vault) Donate(_ int, rlm realm, from address, amount int64) error {
209	if amount <= 0 {
210		return ErrInvalidAmount
211	}
212	if err := v.teller.TransferFrom(0, rlm, from, v.home, amount); err != nil {
213		return err
214	}
215	held, ok := overflow.Add64(v.held, amount)
216	if !ok {
217		panic(ErrOverflow)
218	}
219	v.held = held
220	return nil
221}
222
223// Move transfers wrapped units after the policy has had its say. It is the
224// only path a signing user has to move the wrapped token; see the package doc
225// for what that does and does not guarantee.
226func (v *Vault) Move(from, to address, amount int64) error {
227	if amount <= 0 {
228		return ErrInvalidAmount
229	}
230	if err := v.pol.CanMove(v, from, to, amount); err != nil {
231		return err
232	}
233	return v.led.Transfer(from, to, amount)
234}
235
236// Allow sets `spender`'s allowance over `owner`'s wrapped balance.
237//
238// The policy is deliberately not consulted: an allowance moves nothing. It is
239// checked when the allowance is spent, in MoveFrom, so a policy that starts
240// vetoing later still binds allowances granted before.
241func (v *Vault) Allow(owner, spender address, amount int64) error {
242	return v.led.Approve(owner, spender, amount)
243}
244
245// MoveFrom spends `spender`'s allowance over `owner`'s wrapped balance, after
246// the policy has had its say.
247func (v *Vault) MoveFrom(spender, owner, to address, amount int64) error {
248	if amount <= 0 {
249		return ErrInvalidAmount
250	}
251	if err := v.pol.CanMove(v, owner, to, amount); err != nil {
252		return err
253	}
254	return v.led.TransferFrom(owner, spender, to, amount)
255}
256
257// Summary renders the vault as a markdown block.
258func (v *Vault) Summary() string {
259	s := ufmt.Sprintf("**%s** (%s) - %s wrapper over %s\n\n",
260		v.tok.GetName(), v.tok.GetSymbol(), v.pol.Name(), v.under.GetSymbol())
261	s += ufmt.Sprintf("- wrapped supply: %d\n", v.Supply())
262	s += ufmt.Sprintf("- escrowed underlying: %d\n", v.held)
263	s += ufmt.Sprintf("- realm custody: %d\n", v.Custody())
264	if v.Solvent() {
265		s += "- solvent: yes\n"
266	} else {
267		s += "- solvent: **NO**\n"
268	}
269	return s
270}
271
272// mulDiv returns a*b/c, rejecting an overflowing product and a zero divisor.
273// The division truncates toward zero, which always rounds in the vault's
274// favour rather than the holder's.
275func mulDiv(a, b, c int64) (int64, error) {
276	if c <= 0 {
277		return 0, ErrBadRatio
278	}
279	p, ok := overflow.Mul64(a, b)
280	if !ok {
281		return 0, ErrOverflow
282	}
283	return p / c, nil
284}