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

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}