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}