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

basket.gno

5.63 Kb · 207 lines
  1package grc20wrap
  2
  3import (
  4	"math/overflow"
  5
  6	"gno.land/p/nt/grc20/v0"
  7	"gno.land/p/nt/seqid/v0"
  8	"gno.land/p/nt/ufmt/v0"
  9)
 10
 11// leg is one component of a Basket.
 12type leg struct {
 13	tok    *grc20.Token
 14	teller grc20.Teller
 15	per    int64 // units escrowed per meta unit
 16	held   int64 // units escrowed so far
 17}
 18
 19// Basket issues ONE meta-token over several underlying GRC20s at fixed
 20// proportions. Minting a meta unit escrows every leg's `per` amount; redeeming
 21// hands all of them back.
 22//
 23// A two-leg basket is the interesting one: it makes a pair tradable, quotable
 24// and transferable as a single object, without a price oracle, an AMM or any
 25// notion of what the legs are worth. The basket never values anything - it only
 26// ever swaps a fixed bundle for a receipt and back, so it cannot be arbitraged
 27// or drained. What it CAN do is let the market price the bundle, which is
 28// exactly how an index token works.
 29//
 30// Proportions are fixed at creation. Rebalancing would mean revaluing the legs,
 31// which needs a price, which is a different contract.
 32type Basket struct {
 33	legs []*leg
 34	home address
 35	tok  *grc20.Token
 36	led  *grc20.PrivateLedger
 37}
 38
 39// NewBasket issues a meta-token over tokens[i], escrowing parts[i] units of
 40// each per meta unit.
 41//
 42// Both slices must be the same length, hold at least two entries, name no token
 43// twice, and every part must be positive. As with NewVault, pass the owning
 44// realm's own cur: escrow lands at that realm's address.
 45func NewBasket(tokens []*grc20.Token, parts []int64, name, symbol string, decimals int, id seqid.ID, rlm realm) *Basket {
 46	if len(tokens) < 2 || len(tokens) != len(parts) {
 47		panic(ErrBadLegs)
 48	}
 49	legs := make([]*leg, 0, len(tokens))
 50	for i, t := range tokens {
 51		if t == nil {
 52			panic(ErrNilToken)
 53		}
 54		if parts[i] <= 0 {
 55			panic(ErrBadLegs)
 56		}
 57		for _, seen := range legs {
 58			if seen.tok.ID() == t.ID() {
 59				panic(ErrBadLegs)
 60			}
 61		}
 62		legs = append(legs, &leg{
 63			tok:    t,
 64			teller: t.RealmTeller(0, rlm),
 65			per:    parts[i],
 66		})
 67	}
 68	tok, led := grc20.NewToken(name, symbol, decimals, id, rlm)
 69	return &Basket{
 70		legs: legs,
 71		home: rlm.Address(),
 72		tok:  tok,
 73		led:  led,
 74	}
 75}
 76
 77// Token is the meta-token this basket issues.
 78func (b *Basket) Token() *grc20.Token { return b.tok }
 79
 80// Home is the escrow account: the host realm's address.
 81func (b *Basket) Home() address { return b.home }
 82
 83// Legs is the number of components.
 84func (b *Basket) Legs() int { return len(b.legs) }
 85
 86// Leg returns the i-th component: its token, the units escrowed per meta unit,
 87// and the units escrowed so far.
 88func (b *Basket) Leg(i int) (*grc20.Token, int64, int64) {
 89	l := b.legs[i]
 90	return l.tok, l.per, l.held
 91}
 92
 93// Solvent reports whether every leg's escrow account still covers what the
 94// basket recorded taking in.
 95func (b *Basket) Solvent() bool {
 96	for _, l := range b.legs {
 97		if l.tok.BalanceOf(b.home) < l.held {
 98			return false
 99		}
100	}
101	return true
102}
103
104// Fuse escrows per*units of every leg from `from` and mints `units` of the meta
105// token to it.
106//
107// `from` must have approved b.Home() on EVERY leg first. The whole basket is
108// priced and checked before the first transfer, so a caller short on leg two
109// gets an error with leg one untouched.
110func (b *Basket) Fuse(_ int, rlm realm, from address, units int64) error {
111	if units <= 0 {
112		return ErrInvalidAmount
113	}
114	need := make([]int64, len(b.legs))
115	for i, l := range b.legs {
116		n, ok := overflow.Mul64(l.per, units)
117		if !ok {
118			return ErrOverflow
119		}
120		if l.tok.BalanceOf(from) < n {
121			return ErrShortBalance
122		}
123		if l.tok.Allowance(from, b.home) < n {
124			return ErrShortAllowance
125		}
126		need[i] = n
127	}
128	// Past here every leg was checked, so a failure is an invariant
129	// violation and the transaction must not stand.
130	for i, l := range b.legs {
131		if err := l.teller.TransferFrom(0, rlm, from, b.home, need[i]); err != nil {
132			panic(err)
133		}
134		l.held += need[i]
135	}
136	if err := b.led.Mint(from, units); err != nil {
137		panic(err)
138	}
139	return nil
140}
141
142// Defuse burns `units` of the meta token held by `to` and returns every leg's
143// share of the escrow to it.
144func (b *Basket) Defuse(_ int, rlm realm, to address, units int64) error {
145	if units <= 0 {
146		return ErrInvalidAmount
147	}
148	give := make([]int64, len(b.legs))
149	for i, l := range b.legs {
150		n, ok := overflow.Mul64(l.per, units)
151		if !ok {
152			return ErrOverflow
153		}
154		if n > l.held {
155			return ErrUnbacked
156		}
157		give[i] = n
158	}
159	if err := b.led.Burn(to, units); err != nil {
160		return err
161	}
162	for i, l := range b.legs {
163		l.held -= give[i]
164		if err := l.teller.Transfer(0, rlm, to, give[i]); err != nil {
165			panic(err)
166		}
167	}
168	return nil
169}
170
171// Move transfers meta units between two accounts.
172func (b *Basket) Move(from, to address, amount int64) error {
173	if amount <= 0 {
174		return ErrInvalidAmount
175	}
176	return b.led.Transfer(from, to, amount)
177}
178
179// Allow sets `spender`'s allowance over `owner`'s meta balance.
180func (b *Basket) Allow(owner, spender address, amount int64) error {
181	return b.led.Approve(owner, spender, amount)
182}
183
184// MoveFrom spends `spender`'s allowance over `owner`'s meta balance.
185func (b *Basket) MoveFrom(spender, owner, to address, amount int64) error {
186	if amount <= 0 {
187		return ErrInvalidAmount
188	}
189	return b.led.TransferFrom(owner, spender, to, amount)
190}
191
192// Summary renders the basket as a markdown block.
193func (b *Basket) Summary() string {
194	s := ufmt.Sprintf("**%s** (%s) - meta-token over %d legs\n\n",
195		b.tok.GetName(), b.tok.GetSymbol(), len(b.legs))
196	s += ufmt.Sprintf("- meta supply: %d\n", b.tok.TotalSupply())
197	for _, l := range b.legs {
198		s += ufmt.Sprintf("- leg %s: %d per unit, %d escrowed\n",
199			l.tok.GetSymbol(), l.per, l.held)
200	}
201	if b.Solvent() {
202		s += "- solvent: yes\n"
203	} else {
204		s += "- solvent: **NO**\n"
205	}
206	return s
207}