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}