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.

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}