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.

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}