curated_test.gno
15.03 Kb · 414 lines
1package curated
2
3import (
4 "strings"
5 "testing"
6
7 "chain"
8 "chain/banker"
9 "chain/runtime"
10
11 cu "gno.land/p/moul/x/social/curated/v0"
12 "gno.land/p/nt/testutils/v0"
13 "gno.land/p/nt/uassert/v0"
14 "gno.land/p/nt/urequire/v0"
15)
16
17var realmAddr = chain.PackageAddress(realmPath)
18
19var (
20 alice = testutils.TestAddress("alice")
21 bob = testutils.TestAddress("bob")
22 carol = testutils.TestAddress("carol")
23 dave = testutils.TestAddress("dave")
24)
25
26// reset clears realm state. Realm globals live for the whole test binary, so
27// every entry, challenge and credit a later test reads starts here. The BANK
28// does not carry over the same way, which is why every balance is asserted in
29// the test that issued it.
30func reset() {
31 list = cu.New(Deposit, ChallengeBlocks)
32}
33
34// pay credits the realm the way a real transaction would: the runtime deposits
35// the envelope at the realm's address, then OriginSend reports it to the
36// crossing function.
37func pay(amount int64) {
38 testing.IssueCoins(realmAddr, chain.Coins{{Denom, amount}})
39 testing.SetOriginSend(chain.Coins{{Denom, amount}})
40}
41
42// apply lists an entry as who. It crosses immediately after SetRealm, which is
43// what makes the account switch take: a helper that sets the realm and does
44// not itself cross is silently ignored.
45func apply(cur realm, who address, key, url, description string) {
46 pay(Deposit)
47 testing.SetRealm(testing.NewUserRealm(who))
48 Apply(cross(cur), key, url, description)
49}
50
51// challenge objects to an entry as who, bonding what that entry's deposit was.
52func challenge(cur realm, who address, key string) {
53 _, _, _, bond, _, _ := Get(key)
54 pay(bond)
55 testing.SetRealm(testing.NewUserRealm(who))
56 Challenge(cross(cur), key)
57}
58
59// balance is what addr holds on chain right now.
60func balance(addr address) int64 {
61 return banker.NewReadonlyBanker().GetCoins(addr).AmountOf(Denom)
62}
63
64func TestApplyTakesExactlyTheDepositAndListsAtOnce(cur realm, t *testing.T) {
65 reset()
66
67 // Underpaying and overpaying are both refused: a realm that silently kept
68 // the excess would have invented a fee nobody agreed to.
69 pay(Deposit - 1)
70 testing.SetRealm(testing.NewUserRealm(alice))
71 uassert.AbortsContains(t, cur, "costs exactly", func() {
72 Apply(cross(cur), "gnoswap", "https://gnoswap.io", "an AMM")
73 })
74 pay(Deposit + 1)
75 testing.SetRealm(testing.NewUserRealm(alice))
76 uassert.AbortsContains(t, cur, "costs exactly", func() {
77 Apply(cross(cur), "gnoswap", "https://gnoswap.io", "an AMM")
78 })
79 uassert.Equal(t, 0, Count())
80
81 apply(cur, alice, "gnoswap", "https://gnoswap.io", "an AMM on gno.land")
82
83 url, description, owner, deposit, at, state := Get("gnoswap")
84 uassert.Equal(t, "https://gnoswap.io", url)
85 uassert.Equal(t, "an AMM on gno.land", description)
86 uassert.Equal(t, alice.String(), owner.String())
87 uassert.Equal(t, Deposit, deposit)
88 uassert.Equal(t, runtime.ChainHeight(), at)
89 uassert.Equal(t, "listed", state)
90
91 uassert.True(t, IsListed("gnoswap"))
92 uassert.Equal(t, 1, Count())
93 urequire.Equal(t, 1, len(Listed()))
94 uassert.Equal(t, "gnoswap", Listed()[0])
95
96 // A key that was never applied for reads as the empty state.
97 _, _, _, _, _, state = Get("nothing")
98 uassert.Equal(t, "", state)
99 uassert.False(t, IsListed("nothing"))
100}
101
102func TestApplyRefusesAKeyAlreadyOnTheList(cur realm, t *testing.T) {
103 reset()
104 apply(cur, alice, "gnoswap", "https://gnoswap.io", "an AMM")
105
106 pay(Deposit)
107 testing.SetRealm(testing.NewUserRealm(bob))
108 uassert.AbortsContains(t, cur, "already on the list", func() {
109 Apply(cross(cur), "gnoswap", "https://mine.io", "mine now")
110 })
111
112 pay(Deposit)
113 testing.SetRealm(testing.NewUserRealm(bob))
114 uassert.AbortsContains(t, cur, "not a key", func() {
115 Apply(cross(cur), "GnoSwap", "https://mine.io", "mine now")
116 })
117 uassert.Equal(t, 1, Count())
118}
119
120func TestChallengeMatchesTheDepositAndOpensAVote(cur realm, t *testing.T) {
121 reset()
122 apply(cur, alice, "gnoswap", "https://gnoswap.io", "an AMM")
123
124 // Not a bond, not an entry: the entry is checked before the money is.
125 pay(Deposit)
126 testing.SetRealm(testing.NewUserRealm(bob))
127 uassert.AbortsContains(t, cur, "is not an entry that can be challenged", func() {
128 Challenge(cross(cur), "nothing")
129 })
130
131 // An owner shielding their own entry would cost them nothing.
132 pay(Deposit)
133 testing.SetRealm(testing.NewUserRealm(alice))
134 uassert.AbortsContains(t, cur, "cannot challenge their own entry", func() {
135 Challenge(cross(cur), "gnoswap")
136 })
137
138 opened := runtime.ChainHeight()
139 challenge(cur, bob, "gnoswap")
140
141 challenger, bond, deadline, keep, remove, open := ChallengeOf("gnoswap")
142 urequire.True(t, open, "the challenge is readable")
143 uassert.Equal(t, bob.String(), challenger.String())
144 uassert.Equal(t, Deposit, bond)
145 uassert.Equal(t, opened+ChallengeBlocks, deadline)
146 uassert.Equal(t, int64(0), keep)
147 uassert.Equal(t, int64(0), remove)
148 uassert.True(t, IsListed("gnoswap"), "a challenge is an objection, not a verdict")
149
150 // One address, one vote, and only while the window is open.
151 testing.SetRealm(testing.NewUserRealm(carol))
152 Vote(cross(cur), "gnoswap", false)
153 testing.SetRealm(testing.NewUserRealm(carol))
154 uassert.AbortsContains(t, cur, "one address, one vote", func() {
155 Vote(cross(cur), "gnoswap", true)
156 })
157
158 testing.SkipHeights(ChallengeBlocks)
159 testing.SetRealm(testing.NewUserRealm(dave))
160 uassert.AbortsContains(t, cur, "deadline has passed", func() {
161 Vote(cross(cur), "gnoswap", true)
162 })
163}
164
165// The full round, with the coins checked against the chain rather than against
166// the realm's own books.
167func TestResolveRemovesAnEntryAndPaysTheChallenger(cur realm, t *testing.T) {
168 reset()
169 apply(cur, alice, "gnoswap", "https://gnoswap.io", "an AMM")
170 challenge(cur, bob, "gnoswap")
171 uassert.Equal(t, 2*Deposit, balance(realmAddr), "both stakes are at the realm")
172
173 testing.SetRealm(testing.NewUserRealm(carol))
174 Vote(cross(cur), "gnoswap", false)
175 testing.SetRealm(testing.NewUserRealm(dave))
176 Vote(cross(cur), "gnoswap", false)
177
178 testing.SetRealm(testing.NewUserRealm(carol))
179 uassert.AbortsContains(t, cur, "still open", func() { Resolve(cross(cur), "gnoswap") })
180
181 testing.SkipHeights(ChallengeBlocks)
182 // Anyone may settle it, including somebody with nothing on the outcome.
183 testing.SetRealm(testing.NewUserRealm(carol))
184 Resolve(cross(cur), "gnoswap")
185
186 uassert.False(t, IsListed("gnoswap"))
187 uassert.Equal(t, 0, Count())
188 uassert.Equal(t, 2*Deposit, CreditOf(bob), "the bond back, plus alice's deposit")
189 uassert.Equal(t, int64(0), CreditOf(alice))
190 uassert.Equal(t, int64(0), CreditOf(carol), "voters are paid nothing in v0")
191 uassert.Equal(t, 2*Deposit, balance(realmAddr), "credited is not sent")
192
193 before := balance(bob)
194 testing.SetRealm(testing.NewUserRealm(bob))
195 got := Withdraw(cross(cur))
196 uassert.Equal(t, 2*Deposit, got)
197 uassert.Equal(t, before+2*Deposit, balance(bob), "the coins actually arrived")
198 uassert.Equal(t, int64(0), balance(realmAddr), "and the realm kept nothing")
199
200 // A second call, which is what a reentrant one would be, finds nothing.
201 testing.SetRealm(testing.NewUserRealm(bob))
202 uassert.AbortsContains(t, cur, "nothing to withdraw", func() { Withdraw(cross(cur)) })
203}
204
205func TestATieKeepsTheEntryAndPaysTheOwner(cur realm, t *testing.T) {
206 reset()
207 apply(cur, alice, "gnoswap", "https://gnoswap.io", "an AMM")
208 challenge(cur, bob, "gnoswap")
209
210 testing.SetRealm(testing.NewUserRealm(carol))
211 Vote(cross(cur), "gnoswap", true)
212 testing.SetRealm(testing.NewUserRealm(dave))
213 Vote(cross(cur), "gnoswap", false)
214
215 testing.SkipHeights(ChallengeBlocks)
216 testing.SetRealm(testing.NewUserRealm(bob))
217 Resolve(cross(cur), "gnoswap")
218
219 uassert.True(t, IsListed("gnoswap"), "the incumbent wins ties")
220 uassert.Equal(t, Deposit, CreditOf(alice), "and takes the challenger's bond")
221 uassert.Equal(t, int64(0), CreditOf(bob))
222
223 // Her own deposit is still locked behind the entry, so only the bond is
224 // collectable until she unlists.
225 before := balance(alice)
226 testing.SetRealm(testing.NewUserRealm(alice))
227 uassert.Equal(t, Deposit, Withdraw(cross(cur)))
228 uassert.Equal(t, before+Deposit, balance(alice))
229 uassert.Equal(t, Deposit, balance(realmAddr), "the deposit stays behind the entry")
230}
231
232// Nobody voting is a tie too, which is the common case and the one that
233// decides whether a challenge is cheap. It is not: it costs the bond.
234func TestAChallengeNobodyVotedOnLoses(cur realm, t *testing.T) {
235 reset()
236 apply(cur, alice, "gnoswap", "https://gnoswap.io", "an AMM")
237 challenge(cur, bob, "gnoswap")
238
239 testing.SkipHeights(ChallengeBlocks)
240 testing.SetRealm(testing.NewUserRealm(bob))
241 Resolve(cross(cur), "gnoswap")
242
243 uassert.True(t, IsListed("gnoswap"))
244 uassert.Equal(t, Deposit, CreditOf(alice))
245 uassert.Equal(t, int64(0), CreditOf(bob), "being wrong costs the bond")
246}
247
248func TestUnlistIsTheOwnersAndNotWhileChallenged(cur realm, t *testing.T) {
249 reset()
250 apply(cur, alice, "gnoswap", "https://gnoswap.io", "an AMM")
251
252 testing.SetRealm(testing.NewUserRealm(bob))
253 uassert.AbortsContains(t, cur, "only the entry's owner", func() {
254 Unlist(cross(cur), "gnoswap")
255 })
256
257 challenge(cur, bob, "gnoswap")
258 testing.SetRealm(testing.NewUserRealm(alice))
259 uassert.AbortsContains(t, cur, "under challenge", func() {
260 Unlist(cross(cur), "gnoswap")
261 })
262 uassert.Equal(t, int64(0), CreditOf(alice), "she cannot walk away mid-challenge")
263
264 testing.SkipHeights(ChallengeBlocks)
265 testing.SetRealm(testing.NewUserRealm(alice))
266 Resolve(cross(cur), "gnoswap")
267 testing.SetRealm(testing.NewUserRealm(alice))
268 Unlist(cross(cur), "gnoswap")
269
270 uassert.False(t, IsListed("gnoswap"))
271 uassert.Equal(t, 2*Deposit, CreditOf(alice), "the bond she won, and her deposit back")
272
273 // The key is free again, and re-listing it costs a fresh deposit.
274 apply(cur, bob, "gnoswap", "https://mine.io", "mine now")
275 _, _, owner, _, _, state := Get("gnoswap")
276 uassert.Equal(t, bob.String(), owner.String())
277 uassert.Equal(t, "listed", state)
278}
279
280// The realm holds exactly what it is accountable for, at every step: a stake
281// behind something live, or a credit somebody has not collected yet.
282func TestTheRealmHoldsExactlyWhatItOwes(cur realm, t *testing.T) {
283 reset()
284 check := func(step string) {
285 t.Helper()
286 uassert.Equal(t, list.Locked()+list.Owed(), balance(realmAddr), step)
287 }
288 check("empty")
289
290 apply(cur, alice, "one", "https://1.io", "first")
291 check("after a listing")
292
293 apply(cur, bob, "two", "https://2.io", "second")
294 check("after a second listing")
295
296 challenge(cur, bob, "one")
297 check("after a challenge")
298
299 testing.SetRealm(testing.NewUserRealm(carol))
300 Vote(cross(cur), "one", false)
301 check("after a vote")
302
303 testing.SkipHeights(ChallengeBlocks)
304 testing.SetRealm(testing.NewUserRealm(carol))
305 Resolve(cross(cur), "one")
306 check("after a resolution")
307
308 testing.SetRealm(testing.NewUserRealm(bob))
309 Withdraw(cross(cur))
310 check("after the winner collected")
311
312 testing.SetRealm(testing.NewUserRealm(bob))
313 Unlist(cross(cur), "two")
314 check("after an unlisting")
315
316 testing.SetRealm(testing.NewUserRealm(bob))
317 Withdraw(cross(cur))
318 check("after the deposit came back")
319 uassert.Equal(t, int64(0), balance(realmAddr), "and the realm is empty again")
320}
321
322// A URL is caller-supplied too, and it is the one string that reaches the
323// renderer as a link target rather than as text.
324func TestAPipeInAUrlCannotOpenAColumn(cur realm, t *testing.T) {
325 reset()
326 apply(cur, alice, "weird", "https://x.io/a|b", "a url with a pipe in it")
327
328 index := Render("")
329 uassert.False(t, strings.Contains(index, "https://x.io/a|b"),
330 "the raw pipe would open a column: "+index)
331 uassert.True(t, strings.Contains(index, "https://x.io/a%7Cb"),
332 "the link sanitizer percent-encodes it: "+index)
333}
334
335// TestRender uses uassert rather than an Example because every view contains
336// consecutive blank lines, which gno collapses inside an // Output: block.
337func TestRender(cur realm, t *testing.T) {
338 reset()
339 apply(cur, alice, "gnoswap", "https://gnoswap.io/pools?a=1", "an AMM | with [a link](x)")
340
341 index := Render("")
342 uassert.True(t, strings.Contains(index, "# Curated"), index)
343 uassert.True(t, strings.Contains(index, "| 1 | 0 |"), "one listed, none challenged: "+index)
344 uassert.True(t, strings.Contains(index, "func=Apply"), "the index offers the transaction")
345 uassert.True(t, strings.Contains(index, "Voters are paid nothing"), "the index says what v0 does not do")
346 uassert.True(t, strings.Contains(index, "an AMM \\| with \\[a link\\]\\(x\\)"),
347 "a pipe in a description cannot open a column, and its markdown is inert: "+index)
348
349 entry := Render("entry/gnoswap")
350 uassert.True(t, strings.Contains(entry, "# gnoswap"), entry)
351 uassert.True(t, strings.Contains(entry, "an AMM | with \\[a link\\]\\(x\\)"),
352 "the description is escaped in prose too, where a pipe is not special: "+entry)
353 uassert.True(t, strings.Contains(entry, "func=Challenge"))
354 uassert.True(t, strings.Contains(entry, "func=Unlist"))
355
356 // Under challenge the page offers the two votes instead.
357 challenge(cur, bob, "gnoswap")
358 entry = Render("entry/gnoswap")
359 uassert.True(t, strings.Contains(entry, "## Under challenge"), entry)
360 uassert.True(t, strings.Contains(entry, "keep=true"), entry)
361 uassert.True(t, strings.Contains(entry, "keep=false"), entry)
362 uassert.False(t, strings.Contains(entry, "func=Unlist"), "an owner cannot walk away here either")
363 uassert.True(t, strings.Contains(Render(""), "## Open challenges"))
364
365 testing.SetRealm(testing.NewUserRealm(carol))
366 Vote(cross(cur), "gnoswap", false)
367 testing.SetRealm(testing.NewUserRealm(dave))
368 Vote(cross(cur), "gnoswap", false)
369
370 // Past the deadline it offers the settlement instead of the votes.
371 testing.SkipHeights(ChallengeBlocks)
372 entry = Render("entry/gnoswap")
373 uassert.True(t, strings.Contains(entry, "func=Resolve"), entry)
374 uassert.False(t, strings.Contains(entry, "keep=true"), "voting is over: "+entry)
375
376 // Removed, the key is advertised as free again.
377 testing.SetRealm(testing.NewUserRealm(bob))
378 Resolve(cross(cur), "gnoswap")
379
380 entry = Render("entry/gnoswap")
381 uassert.True(t, strings.Contains(entry, "free to apply for again"), entry)
382 uassert.True(t, strings.Contains(Render(""), "Nothing is on the list yet."))
383
384 uassert.Equal(t, "# Curated\nNo entry named nope", Render("entry/nope"))
385 uassert.True(t, strings.HasPrefix(Render("elsewhere"), "# Not found"))
386}
387
388// A realm that forwards a user's call must not be able to spend the user's
389// envelope as if it were its own. OriginSend reports what the SIGNER attached
390// to the transaction, so without the IsUserCall guard a realm that received
391// one GNOT could call in here repeatedly, each call reading the same send and
392// listing another entry for free. That is r/moul/grant Fund, and gnovet's
393// origin-send-unguarded rule caught this exact shape here in CI after the
394// local gate had passed.
395func TestOnlyAUserCanPayIn(cur realm, t *testing.T) {
396 reset()
397
398 pay(Deposit)
399 testing.SetRealm(testing.NewCodeRealm("gno.land/r/g1relay/forwarder"))
400 uassert.AbortsContains(t, cur, "must be a direct user transaction", func() {
401 Apply(cross(cur), "relayed", "/r/x", "paid with somebody else's coins")
402 })
403 uassert.Equal(t, 0, Count(), "nothing was listed")
404
405 apply(cur, alice, "real", "/r/real", "a real entry")
406
407 pay(Deposit)
408 testing.SetRealm(testing.NewCodeRealm("gno.land/r/g1relay/forwarder"))
409 uassert.AbortsContains(t, cur, "must be a direct user transaction", func() {
410 Challenge(cross(cur), "real")
411 })
412 _, _, _, _, _, open := ChallengeOf("real")
413 uassert.False(t, open, "no challenge was opened")
414}