Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}