wrapdemo_test.gno
11.22 Kb · 288 lines
1package grc20wrapdemo
2
3import (
4 "strings"
5 "testing"
6
7 "gno.land/p/nt/testutils/v0"
8 "gno.land/p/nt/uassert/v0"
9 faucet "gno.land/r/moul/x/grc20faucet/v0"
10)
11
12// Realm state carries over between test functions, but the block height does
13// not: every test starts from the same height. The faucet's per-account
14// cooldown would therefore fire on the second test to reuse an account, so each
15// test claims with its own. TestFaucetCooldown covers the cooldown itself.
16func fund(cur realm, t *testing.T, name string) address {
17 t.Helper()
18 who := testutils.TestAddress(name)
19 testing.SetRealm(testing.NewUserRealm(who))
20 faucet.Claim(cross(cur))
21 faucet.Approve(cross(cur), "RED", Home(), faucet.ClaimAmount)
22 faucet.Approve(cross(cur), "BLUE", Home(), faucet.ClaimAmount)
23 return who
24}
25
26// testing.SetRealm only governs crossing calls made from the frame that called
27// it: put it in a helper with no crossing call of its own and it is silently
28// ignored, and the caller stays whoever it was. fund() gets away with it
29// because it crosses into the faucet right after; switching accounts mid-test
30// has to be written inline, which is why there is no act() helper here.
31
32// The plain mode is the baseline: it proves custody actually crosses a realm
33// boundary. The RED never belonged to this realm, and it can only ever take
34// what the faucet's Approve handed over.
35func TestPlainWrapperRoundTrip(cur realm, t *testing.T) {
36 alice := fund(cur, t, "w-alice")
37 before := faucet.BalanceOf("RED", alice)
38
39 key := NewWrapper(cross(cur), faucet.RedKey, "plain", "wRED")
40 uassert.True(t, strings.HasSuffix(key, ".wRED"), "registered under its own symbol: "+key)
41
42 minted := Deposit(cross(cur), "wRED", 400_000)
43 uassert.Equal(t, int64(400_000), minted)
44 uassert.Equal(t, int64(400_000), BalanceOf("wRED", alice))
45 uassert.Equal(t, int64(400_000), Escrow("wRED"))
46 uassert.Equal(t, before-400_000, faucet.BalanceOf("RED", alice))
47 // The escrow really sits at this realm's address, in the faucet's ledger.
48 uassert.Equal(t, int64(400_000), faucet.BalanceOf("RED", Home()))
49
50 back := Withdraw(cross(cur), "wRED", 150_000)
51 uassert.Equal(t, int64(150_000), back)
52 uassert.Equal(t, int64(250_000), TotalSupply("wRED"))
53 uassert.Equal(t, before-250_000, faucet.BalanceOf("RED", alice))
54}
55
56// Depositing without an allowance is the default state of the world, and it
57// fails. This realm holds no standing authority over anybody's balance.
58func TestDepositNeedsAnAllowance(cur realm, t *testing.T) {
59 fund(cur, t, "noallow")
60 NewWrapper(cross(cur), faucet.RedKey, "plain", "noallow")
61
62 // Revoke what fund() granted.
63 faucet.Approve(cross(cur), "RED", Home(), 0)
64 uassert.AbortsContains(t, cur, "insufficient allowance", func() {
65 Deposit(cross(cur), "noallow", 1)
66 })
67 uassert.Equal(t, int64(0), TotalSupply("noallow"))
68}
69
70// kilo re-denominates a token whose own realm will never redeploy for it.
71func TestKiloWrapperAddsThreeDecimals(cur realm, t *testing.T) {
72 fund(cur, t, "kilo")
73 NewWrapper(cross(cur), faucet.RedKey, "kilo", "kRED")
74
75 minted := Deposit(cross(cur), "kRED", 1_000)
76 uassert.Equal(t, int64(1_000_000), minted)
77 uassert.Equal(t, int64(1_000), Escrow("kRED"))
78
79 back := Withdraw(cross(cur), "kRED", 999_000)
80 uassert.Equal(t, int64(999), back)
81 // 999 wrapped units cannot buy back a whole underlying one, so the
82 // redemption is refused rather than burning them for nothing.
83 uassert.AbortsContains(t, cur, "rounds to zero", func() {
84 Withdraw(cross(cur), "kRED", 999)
85 })
86}
87
88func TestSoulboundWrapperRefusesTransfer(cur realm, t *testing.T) {
89 fund(cur, t, "soul")
90 bob := testutils.TestAddress("soul-bob")
91 NewWrapper(cross(cur), faucet.RedKey, "soulbound", "sRED")
92 Deposit(cross(cur), "sRED", 300_000)
93
94 uassert.AbortsContains(t, cur, "soulbound", func() {
95 Transfer(cross(cur), "sRED", bob, 1)
96 })
97 uassert.Equal(t, int64(0), BalanceOf("sRED", bob))
98
99 // The exit stays open: unwrap, and the RED moves as freely as ever.
100 back := Withdraw(cross(cur), "sRED", 300_000)
101 uassert.Equal(t, int64(300_000), back)
102 uassert.Equal(t, int64(0), TotalSupply("sRED"))
103}
104
105// Pool: a donation is shared by whoever is holding, and the donor can never
106// take it back out.
107func TestPoolWrapperSharesDonations(cur realm, t *testing.T) {
108 alice := fund(cur, t, "pool-a")
109 NewWrapper(cross(cur), faucet.RedKey, "pool", "pRED")
110 uassert.Equal(t, int64(100_000), Deposit(cross(cur), "pRED", 100_000))
111
112 bob := fund(cur, t, "pool-b")
113 Donate(cross(cur), "pRED", 100_000)
114 uassert.Equal(t, int64(200_000), Escrow("pRED"))
115 uassert.Equal(t, int64(100_000), TotalSupply("pRED"))
116 uassert.Equal(t, int64(0), BalanceOf("pRED", bob)) // a donation mints nothing
117
118 // Buying in afterwards costs twice as much per share.
119 uassert.Equal(t, int64(50_000), Deposit(cross(cur), "pRED", 100_000))
120
121 // alice walks away with double what she put in.
122 testing.SetRealm(testing.NewUserRealm(alice))
123 redBefore := faucet.BalanceOf("RED", alice)
124 uassert.Equal(t, int64(200_000), Withdraw(cross(cur), "pRED", 100_000))
125 uassert.Equal(t, redBefore+200_000, faucet.BalanceOf("RED", alice))
126
127 // And bob gets back exactly the 100_000 he deposited: his own donation
128 // was to the holders, and by then that was alice.
129 testing.SetRealm(testing.NewUserRealm(bob))
130 uassert.Equal(t, int64(100_000), Withdraw(cross(cur), "pRED", 50_000))
131 uassert.Equal(t, int64(0), Escrow("pRED"))
132}
133
134// Sticky: the 1% exit fee is collected by nobody, it is left in the pool, so
135// whoever stays longest ends up with it.
136func TestStickyWrapperChargesOnTheWayOut(cur realm, t *testing.T) {
137 alice := fund(cur, t, "stick-a")
138 NewWrapper(cross(cur), faucet.RedKey, "sticky", "fRED")
139 Deposit(cross(cur), "fRED", 500_000)
140
141 bob := fund(cur, t, "stick-b")
142 Deposit(cross(cur), "fRED", 500_000)
143 uassert.Equal(t, int64(1_000_000), TotalSupply("fRED"))
144
145 testing.SetRealm(testing.NewUserRealm(alice))
146 uassert.Equal(t, int64(495_000), Withdraw(cross(cur), "fRED", 500_000))
147
148 // The 5_000 alice paid on the way out now backs bob's shares.
149 uassert.Equal(t, int64(505_000), Escrow("fRED"))
150 testing.SetRealm(testing.NewUserRealm(bob))
151 uassert.Equal(t, int64(499_950), Withdraw(cross(cur), "fRED", 500_000))
152}
153
154// Fusion: one token backed by a fixed bundle of two, and nothing else.
155func TestFusionOfRedAndBlue(cur realm, t *testing.T) {
156 alice := fund(cur, t, "fuse-a")
157 bob := testutils.TestAddress("fuse-b")
158
159 key := NewFusion(cross(cur), faucet.RedKey, 1, faucet.BlueKey, 2, "PURPLE")
160 uassert.True(t, strings.HasSuffix(key, ".PURPLE"), key)
161 uassert.Equal(t, 2, Legs("PURPLE"))
162
163 redBefore := faucet.BalanceOf("RED", alice)
164 blueBefore := faucet.BalanceOf("BLUE", alice)
165 Fuse(cross(cur), "PURPLE", 100_000)
166
167 uassert.Equal(t, int64(100_000), BalanceOf("PURPLE", alice))
168 uassert.Equal(t, redBefore-100_000, faucet.BalanceOf("RED", alice))
169 uassert.Equal(t, blueBefore-200_000, faucet.BalanceOf("BLUE", alice))
170
171 sym, per, held := Leg("PURPLE", 1)
172 uassert.Equal(t, "BLUE", sym)
173 uassert.Equal(t, int64(2), per)
174 uassert.Equal(t, int64(200_000), held)
175
176 // PURPLE moves as one object, and whoever ends up with it can split it
177 // back into both legs without ever having touched the faucet.
178 Transfer(cross(cur), "PURPLE", bob, 40_000)
179 testing.SetRealm(testing.NewUserRealm(bob))
180 Defuse(cross(cur), "PURPLE", 40_000)
181 uassert.Equal(t, int64(40_000), faucet.BalanceOf("RED", bob))
182 uassert.Equal(t, int64(80_000), faucet.BalanceOf("BLUE", bob))
183 uassert.Equal(t, int64(60_000), TotalSupply("PURPLE"))
184}
185
186// Every token issued here is registered too, so it can be wrapped again.
187func TestWrappingAWrapper(cur realm, t *testing.T) {
188 fund(cur, t, "nest")
189 NewWrapper(cross(cur), faucet.RedKey, "plain", "nRED")
190 Deposit(cross(cur), "nRED", 800_000)
191
192 // nRED lives in THIS realm, so the allowance is granted through this
193 // realm's own Approve. Same shape, one level up.
194 NewWrapper(cross(cur), Key("nRED"), "pool", "nnRED")
195 Approve(cross(cur), "nRED", Home(), 800_000)
196 uassert.Equal(t, int64(600_000), Deposit(cross(cur), "nnRED", 600_000))
197
198 // The outer wrapper escrows the inner token, not the RED.
199 uassert.Equal(t, int64(600_000), Escrow("nnRED"))
200 uassert.Equal(t, int64(800_000), Escrow("nRED"))
201 uassert.Equal(t, int64(600_000), BalanceOf("nRED", Home()))
202
203 // And it unwinds the whole way back down.
204 uassert.Equal(t, int64(600_000), Withdraw(cross(cur), "nnRED", 600_000))
205 uassert.Equal(t, int64(800_000), Withdraw(cross(cur), "nRED", 800_000))
206 uassert.Equal(t, int64(0), TotalSupply("nRED"))
207}
208
209func TestAllowanceOnAWrappedToken(cur realm, t *testing.T) {
210 alice := fund(cur, t, "allow-a")
211 bob := testutils.TestAddress("allow-b")
212
213 NewWrapper(cross(cur), faucet.RedKey, "plain", "aRED")
214 Deposit(cross(cur), "aRED", 500_000)
215 Approve(cross(cur), "aRED", bob, 120_000)
216 uassert.Equal(t, int64(120_000), Allowance("aRED", alice, bob))
217
218 testing.SetRealm(testing.NewUserRealm(bob))
219 TransferFrom(cross(cur), "aRED", alice, bob, 120_000)
220 uassert.Equal(t, int64(120_000), BalanceOf("aRED", bob))
221 uassert.AbortsContains(t, cur, "insufficient allowance", func() {
222 TransferFrom(cross(cur), "aRED", alice, bob, 1)
223 })
224}
225
226func TestFaucetCooldown(cur realm, t *testing.T) {
227 who := fund(cur, t, "cooldown")
228 uassert.AbortsContains(t, cur, "too soon", func() {
229 faucet.Claim(cross(cur))
230 })
231
232 next, ok := faucet.NextClaim(who)
233 uassert.True(t, ok, "the account has claimed once")
234 testing.SkipHeights(faucet.ClaimEvery + 1)
235 faucet.Claim(cross(cur))
236 uassert.Equal(t, 2*faucet.ClaimAmount, faucet.BalanceOf("RED", who))
237 uassert.True(t, next > 0, "a cooldown was recorded")
238
239 _, ok = faucet.NextClaim(testutils.TestAddress("never"))
240 uassert.False(t, ok, "an account that never claimed has no cooldown")
241}
242
243func TestRejectsBadInput(cur realm, t *testing.T) {
244 fund(cur, t, "bad")
245 NewWrapper(cross(cur), faucet.RedKey, "plain", "xRED")
246 NewFusion(cross(cur), faucet.RedKey, 1, faucet.BlueKey, 1, "xMIX")
247
248 uassert.AbortsContains(t, cur, "unknown mode", func() {
249 NewWrapper(cross(cur), faucet.RedKey, "moon", "xMOON")
250 })
251 uassert.AbortsContains(t, cur, "already issued by this realm", func() {
252 NewWrapper(cross(cur), faucet.RedKey, "plain", "xRED")
253 })
254 uassert.AbortsContains(t, cur, "unknown token", func() {
255 NewWrapper(cross(cur), "gno.land/r/nope/nope.NOPE", "plain", "xNOPE")
256 })
257 uassert.AbortsContains(t, cur, "is a fusion", func() {
258 Deposit(cross(cur), "xMIX", 1)
259 })
260 uassert.AbortsContains(t, cur, "is a wrapper", func() {
261 Fuse(cross(cur), "xRED", 1)
262 })
263 uassert.AbortsContains(t, cur, "has not issued", func() {
264 Deposit(cross(cur), "GHOST", 1)
265 })
266 uassert.AbortsContains(t, cur, "unknown symbol", func() {
267 faucet.Transfer(cross(cur), "GREEN", Home(), 1)
268 })
269}
270
271func TestRender(cur realm, t *testing.T) {
272 fund(cur, t, "render")
273 NewWrapper(cross(cur), faucet.RedKey, "pool", "rRED")
274 Deposit(cross(cur), "rRED", 250_000)
275
276 index := Render("")
277 uassert.True(t, strings.Contains(index, "# GRC20 wrapper factory"), "index heading")
278 uassert.True(t, strings.Contains(index, Home().String()), "index shows the escrow address")
279 uassert.True(t, strings.Contains(index, "| [rRED](/r/moul/x/grc20wrapdemo/v0:rRED) | pool | RED |"),
280 "index lists the wrapper:\n"+index)
281
282 detail := Render("rRED")
283 uassert.True(t, strings.Contains(detail, "escrowed underlying: 250000"), detail)
284 uassert.True(t, strings.Contains(detail, "- mode: `pool`"), detail)
285 uassert.True(t, strings.Contains(detail, "solvent: yes"), detail)
286
287 uassert.True(t, strings.Contains(Render("NOPE"), "404"), "unknown symbol renders a 404")
288}