wrap_test.gno
11.42 Kb · 324 lines
1package grc20wrap
2
3import (
4 "testing"
5
6 "gno.land/p/nt/grc20/v0"
7 "gno.land/p/nt/testutils/v0"
8 "gno.land/p/nt/uassert/v0"
9 "gno.land/p/nt/urequire/v0"
10)
11
12// The two realms every test needs: one that issues the underlying token, one
13// that wraps it. They MUST be distinct, because wrapping a token issued by your
14// own realm exercises none of the interesting machinery: the allowance, the
15// eagerly-bound teller, the foreign escrow.
16const (
17 underPath = "gno.land/r/test/under"
18 wrapPath = "gno.land/r/test/wrapper"
19)
20
21var (
22 alice = testutils.TestAddress("alice")
23 bob = testutils.TestAddress("bob")
24 carl = testutils.TestAddress("carl")
25)
26
27// mintUnder issues the underlying token from underPath and funds holders.
28func mintUnder(cur realm, t *testing.T, funded ...address) (*grc20.Token, *grc20.PrivateLedger) {
29 t.Helper()
30 testing.SetRealm(testing.NewCodeRealm(underPath))
31 tok, led := grc20.NewToken("Under", "UND", 4, 0, cur)
32 for _, a := range funded {
33 urequire.NoError(t, led.Mint(a, 1_000))
34 }
35 return tok, led
36}
37
38// approve stands in for the underlying realm's own Approve entry point: on
39// chain the holder calls it, here the ledger does it on their behalf.
40func approve(cur realm, t *testing.T, led *grc20.PrivateLedger, owner, spender address, amount int64) {
41 t.Helper()
42 urequire.NoError(t, led.ImpersonateTeller(owner).Approve(0, cur, spender, amount))
43}
44
45func TestOneToOneRoundTrip(cur realm, t *testing.T) {
46 under, led := mintUnder(cur, t, alice)
47
48 testing.SetRealm(testing.NewCodeRealm(wrapPath))
49 v := NewVault(under, OneToOne{}, "Wrapped Under", "wUND", 4, 0, cur)
50 approve(cur, t, led, alice, v.Home(), 1_000)
51
52 out, err := v.Wrap(0, cur, alice, 400)
53 urequire.NoError(t, err)
54 uassert.Equal(t, int64(400), out)
55 uassert.Equal(t, int64(400), v.Token().BalanceOf(alice))
56 uassert.Equal(t, int64(600), under.BalanceOf(alice))
57 uassert.Equal(t, int64(400), v.Held())
58 uassert.Equal(t, int64(400), v.Custody())
59 uassert.True(t, v.Solvent())
60
61 back, err := v.Unwrap(0, cur, alice, 150)
62 urequire.NoError(t, err)
63 uassert.Equal(t, int64(150), back)
64 uassert.Equal(t, int64(250), v.Token().BalanceOf(alice))
65 uassert.Equal(t, int64(250), v.Supply())
66 uassert.Equal(t, int64(750), under.BalanceOf(alice))
67 uassert.Equal(t, int64(250), v.Held())
68 uassert.True(t, v.Solvent())
69}
70
71// Escrow is pulled with an allowance, so a wrapper can never take more than a
72// holder granted it. This is the property that makes a wrapper safe to publish
73// as a permissionless factory: it has no authority nobody handed it.
74func TestWrapStopsAtTheAllowance(cur realm, t *testing.T) {
75 under, led := mintUnder(cur, t, alice)
76
77 testing.SetRealm(testing.NewCodeRealm(wrapPath))
78 v := NewVault(under, OneToOne{}, "Wrapped Under", "wUND", 4, 0, cur)
79 approve(cur, t, led, alice, v.Home(), 100)
80
81 _, err := v.Wrap(0, cur, alice, 101)
82 uassert.ErrorContains(t, err, "insufficient allowance")
83 uassert.Equal(t, int64(0), v.Supply())
84 uassert.Equal(t, int64(1_000), under.BalanceOf(alice))
85
86 _, err = v.Wrap(0, cur, alice, 0)
87 uassert.ErrorIs(t, err, ErrInvalidAmount)
88}
89
90// Unwrapping is bounded by the wrapped balance, not by the escrow: a holder
91// cannot redeem somebody else's deposit.
92func TestUnwrapStopsAtTheBalance(cur realm, t *testing.T) {
93 under, led := mintUnder(cur, t, alice, bob)
94
95 testing.SetRealm(testing.NewCodeRealm(wrapPath))
96 v := NewVault(under, OneToOne{}, "Wrapped Under", "wUND", 4, 0, cur)
97 approve(cur, t, led, alice, v.Home(), 1_000)
98 approve(cur, t, led, bob, v.Home(), 1_000)
99
100 _, err := v.Wrap(0, cur, alice, 500)
101 urequire.NoError(t, err)
102 _, err = v.Wrap(0, cur, bob, 500)
103 urequire.NoError(t, err)
104 uassert.Equal(t, int64(1_000), v.Held())
105
106 _, err = v.Unwrap(0, cur, bob, 501)
107 uassert.ErrorContains(t, err, "insufficient balance")
108 uassert.Equal(t, int64(1_000), v.Held())
109}
110
111// Ratio gives a token more decimals than the realm that issued it ever offered.
112func TestRatioRedenominates(cur realm, t *testing.T) {
113 under, led := mintUnder(cur, t, alice)
114
115 testing.SetRealm(testing.NewCodeRealm(wrapPath))
116 v := NewVault(under, Ratio{Num: 1_000, Den: 1}, "Kilo Under", "kUND", 7, 0, cur)
117 approve(cur, t, led, alice, v.Home(), 1_000)
118
119 out, err := v.Wrap(0, cur, alice, 5)
120 urequire.NoError(t, err)
121 uassert.Equal(t, int64(5_000), out)
122 uassert.Equal(t, int64(5), v.Held())
123
124 back, err := v.Unwrap(0, cur, alice, 3_000)
125 urequire.NoError(t, err)
126 uassert.Equal(t, int64(3), back)
127 uassert.Equal(t, int64(2), v.Held())
128
129 // A redemption too small to buy one underlying unit is refused rather
130 // than silently burning the wrapped units for nothing.
131 _, err = v.Unwrap(0, cur, alice, 999)
132 uassert.ErrorIs(t, err, ErrDust)
133 uassert.Equal(t, int64(2_000), v.Supply())
134}
135
136// Soulbound: wrap in, unwrap out, but never hand the receipt to anybody.
137func TestSoulboundReceipt(cur realm, t *testing.T) {
138 under, led := mintUnder(cur, t, alice)
139
140 testing.SetRealm(testing.NewCodeRealm(wrapPath))
141 v := NewVault(under, Soulbound{}, "Bound Under", "sUND", 4, 0, cur)
142 approve(cur, t, led, alice, v.Home(), 1_000)
143
144 _, err := v.Wrap(0, cur, alice, 300)
145 urequire.NoError(t, err)
146
147 uassert.ErrorIs(t, v.Move(alice, bob, 1), ErrSoulbound)
148 uassert.Equal(t, int64(0), v.Token().BalanceOf(bob))
149
150 // An allowance may be granted, and is still worthless when spent.
151 urequire.NoError(t, v.Allow(alice, bob, 100))
152 uassert.ErrorIs(t, v.MoveFrom(bob, alice, carl, 100), ErrSoulbound)
153
154 // The exit is always open: unwrap, then move the underlying.
155 back, err := v.Unwrap(0, cur, alice, 300)
156 urequire.NoError(t, err)
157 uassert.Equal(t, int64(300), back)
158 uassert.Equal(t, int64(0), v.Supply())
159}
160
161// Pool is the yield-bearing pattern: a donation nobody can withdraw raises what
162// every share redeems for, and later depositors buy in at the new price.
163func TestPoolSharesAppreciate(cur realm, t *testing.T) {
164 under, led := mintUnder(cur, t, alice, bob, carl)
165
166 testing.SetRealm(testing.NewCodeRealm(wrapPath))
167 v := NewVault(under, Pool{}, "Pooled Under", "pUND", 4, 0, cur)
168 for _, a := range []address{alice, bob, carl} {
169 approve(cur, t, led, a, v.Home(), 1_000)
170 }
171
172 // First in, priced 1:1 because the pool is empty.
173 out, err := v.Wrap(0, cur, alice, 100)
174 urequire.NoError(t, err)
175 uassert.Equal(t, int64(100), out)
176
177 // bob donates: escrow doubles, share count does not move.
178 urequire.NoError(t, v.Donate(0, cur, bob, 100))
179 uassert.Equal(t, int64(200), v.Held())
180 uassert.Equal(t, int64(100), v.Supply())
181
182 // carl now pays twice as much per share.
183 out, err = v.Wrap(0, cur, carl, 100)
184 urequire.NoError(t, err)
185 uassert.Equal(t, int64(50), out)
186 uassert.Equal(t, int64(300), v.Held())
187 uassert.Equal(t, int64(150), v.Supply())
188
189 // alice redeems 100 shares of 150 against 300 escrowed: 200 back, on a
190 // 100 deposit. bob's donation went to whoever was holding.
191 back, err := v.Unwrap(0, cur, alice, 100)
192 urequire.NoError(t, err)
193 uassert.Equal(t, int64(200), back)
194 uassert.Equal(t, int64(1_100), under.BalanceOf(alice))
195
196 // carl is made whole, and the pool closes empty.
197 back, err = v.Unwrap(0, cur, carl, 50)
198 urequire.NoError(t, err)
199 uassert.Equal(t, int64(100), back)
200 uassert.Equal(t, int64(0), v.Held())
201 uassert.Equal(t, int64(0), v.Supply())
202 uassert.True(t, v.Solvent())
203}
204
205// Fee over Pool: the haircut is not paid out, it is left behind, so the people
206// still holding are the ones who collect it.
207func TestFeeOverPoolPaysTheHolders(cur realm, t *testing.T) {
208 under, led := mintUnder(cur, t, alice, bob)
209
210 testing.SetRealm(testing.NewCodeRealm(wrapPath))
211 v := NewVault(under, Fee{Base: Pool{}, WrapBPS: 0, UnwrapBPS: 1_000}, "Sticky Under", "fUND", 4, 0, cur)
212 approve(cur, t, led, alice, v.Home(), 1_000)
213 approve(cur, t, led, bob, v.Home(), 1_000)
214
215 _, err := v.Wrap(0, cur, alice, 500)
216 urequire.NoError(t, err)
217 _, err = v.Wrap(0, cur, bob, 500)
218 urequire.NoError(t, err)
219 uassert.Equal(t, int64(1_000), v.Supply())
220
221 // alice leaves and pays 10% on the way out: 500 - 50.
222 back, err := v.Unwrap(0, cur, alice, 500)
223 urequire.NoError(t, err)
224 uassert.Equal(t, int64(450), back)
225
226 // bob stayed, and the 50 alice left behind is now his.
227 uassert.Equal(t, int64(550), v.Held())
228 uassert.Equal(t, int64(500), v.Supply())
229 back, err = v.Unwrap(0, cur, bob, 500)
230 urequire.NoError(t, err)
231 uassert.Equal(t, int64(495), back) // 550 pro rata, minus the same 10%
232 uassert.Equal(t, int64(55), v.Held())
233}
234
235// Two vaults over the same underlying share one escrow account, so Held() is
236// the only thing that keeps them apart. Pin that they do.
237func TestTwoVaultsShareOneEscrowAccount(cur realm, t *testing.T) {
238 under, led := mintUnder(cur, t, alice)
239
240 testing.SetRealm(testing.NewCodeRealm(wrapPath))
241 a := NewVault(under, OneToOne{}, "Vault A", "vA", 4, 0, cur)
242 b := NewVault(under, OneToOne{}, "Vault B", "vB", 4, 1, cur)
243 uassert.Equal(t, a.Home().String(), b.Home().String())
244 approve(cur, t, led, alice, a.Home(), 1_000)
245
246 _, err := a.Wrap(0, cur, alice, 300)
247 urequire.NoError(t, err)
248 _, err = b.Wrap(0, cur, alice, 200)
249 urequire.NoError(t, err)
250
251 uassert.Equal(t, int64(300), a.Held())
252 uassert.Equal(t, int64(200), b.Held())
253 // Custody is realm-wide: it sees both.
254 uassert.Equal(t, int64(500), a.Custody())
255 uassert.Equal(t, int64(500), b.Custody())
256
257 // Vault B refuses to release more than it took in, even though the
258 // account it draws on holds A's escrow too. Without the per-vault
259 // counter this is exactly where one vault would eat the other's.
260 _, err = b.Unwrap(0, cur, alice, 201)
261 uassert.ErrorIs(t, err, ErrUnbacked)
262 uassert.Equal(t, int64(200), b.Held())
263 uassert.Equal(t, int64(500), b.Custody())
264
265 // Both still unwind cleanly and independently.
266 _, err = b.Unwrap(0, cur, alice, 200)
267 urequire.NoError(t, err)
268 _, err = a.Unwrap(0, cur, alice, 300)
269 urequire.NoError(t, err)
270 uassert.Equal(t, int64(0), a.Custody())
271 uassert.Equal(t, int64(1_000), under.BalanceOf(alice))
272}
273
274func TestMoveAndAllowance(cur realm, t *testing.T) {
275 under, led := mintUnder(cur, t, alice)
276
277 testing.SetRealm(testing.NewCodeRealm(wrapPath))
278 v := NewVault(under, OneToOne{}, "Wrapped Under", "wUND", 4, 0, cur)
279 approve(cur, t, led, alice, v.Home(), 1_000)
280 _, err := v.Wrap(0, cur, alice, 600)
281 urequire.NoError(t, err)
282
283 urequire.NoError(t, v.Move(alice, bob, 100))
284 uassert.Equal(t, int64(100), v.Token().BalanceOf(bob))
285 uassert.ErrorIs(t, v.Move(alice, bob, 0), ErrInvalidAmount)
286
287 urequire.NoError(t, v.Allow(alice, bob, 250))
288 uassert.Equal(t, int64(250), v.Token().Allowance(alice, bob))
289 urequire.NoError(t, v.MoveFrom(bob, alice, carl, 250))
290 uassert.Equal(t, int64(250), v.Token().BalanceOf(carl))
291 uassert.ErrorContains(t, v.MoveFrom(bob, alice, carl, 1), "insufficient allowance")
292}
293
294func TestNewVaultRejectsNilUnderlying(cur realm, t *testing.T) {
295 testing.SetRealm(testing.NewCodeRealm(wrapPath))
296 uassert.PanicsContains(t, cur, "nil underlying token", func() {
297 NewVault(nil, OneToOne{}, "Broken", "BRK", 4, 0, cur)
298 })
299}
300
301func TestNilPolicyMeansOneToOne(cur realm, t *testing.T) {
302 under, _ := mintUnder(cur, t)
303
304 testing.SetRealm(testing.NewCodeRealm(wrapPath))
305 v := NewVault(under, nil, "Wrapped Under", "wUND", 4, 0, cur)
306 uassert.Equal(t, "1:1", v.Policy().Name())
307}
308
309func TestSummaryReportsSolvency(cur realm, t *testing.T) {
310 under, led := mintUnder(cur, t, alice)
311
312 testing.SetRealm(testing.NewCodeRealm(wrapPath))
313 v := NewVault(under, OneToOne{}, "Wrapped Under", "wUND", 4, 0, cur)
314 approve(cur, t, led, alice, v.Home(), 1_000)
315 _, err := v.Wrap(0, cur, alice, 250)
316 urequire.NoError(t, err)
317
318 want := "**Wrapped Under** (wUND) - 1:1 wrapper over UND\n\n" +
319 "- wrapped supply: 250\n" +
320 "- escrowed underlying: 250\n" +
321 "- realm custody: 250\n" +
322 "- solvent: yes\n"
323 uassert.Equal(t, want, v.Summary())
324}