Kind Pure package
Name v0
Namespace moul / x / grc20wrap
Exported functions n/a — not supported for pure packages by the node (vm/qfuncs)
Module gno.land/p/moul/x/grc20wrap/v0
gno 0.9 wrap.gno gno
⧉
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 not supported for pure packages by the node (vm/qfuncs)
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
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.
43 package grc20wrap
44
45 import (
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
54 var (
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().
74 type 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.
92 func 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.
113 func (v *Vault) Token() *grc20.Token { return v.tok }
114
115 // Underlying is the token held in escrow.
116 func (v *Vault) Underlying() *grc20.Token { return v.under }
117
118 // Policy is this vault's personality.
119 func (v *Vault) Policy() Policy { return v.pol }
120
121 // Home is the escrow account: the host realm's address.
122 func (v *Vault) Home() address { return v.home }
123
124 // Held is the underlying escrowed for THIS vault, by its own accounting.
125 func (v *Vault) Held() int64 { return v.held }
126
127 // Supply is the wrapped token in circulation.
128 func (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.
134 func (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.
139 func (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.
146 func (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.
175 func (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.
208 func (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.
226 func (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.
241 func (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.
247 func (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.
258 func (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.
275 func 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 }
285