crew_test.gno
18.17 Kb · 555 lines
1package crew
2
3import (
4 "strconv"
5 "strings"
6 "testing"
7
8 "gno.land/p/moul/kit/store/v0"
9 "gno.land/p/nt/testutils/v0"
10 "gno.land/p/nt/uassert/v0"
11 "gno.land/p/nt/urequire/v0"
12)
13
14var (
15 alice = testutils.TestAddress("alice")
16 bob = testutils.TestAddress("bob")
17 carol = testutils.TestAddress("carol")
18 dave = testutils.TestAddress("dave")
19)
20
21// join is Join with the minted share count dropped, for the cases that only
22// care that it worked.
23func join(t *testing.T, cs *Crews, id store.ID, who address, amount int64) {
24 t.Helper()
25 _, err := cs.Join(id, who, amount)
26 urequire.NoError(t, err)
27}
28
29// tenGNOT is the amount every founder in these tests puts in, chosen so the
30// share count is a round 10,000 and a halved or doubled price is still exact.
31const tenGNOT = int64(10000000)
32
33func TestValidName(t *testing.T) {
34 tests := []struct {
35 name string
36 want bool
37 }{
38 {"validators", true},
39 {"The Hackathon Team", true},
40 {"a", true},
41 {strings.Repeat("x", MaxNameLen), true},
42 {"", false},
43 {" ", false},
44 {strings.Repeat("x", MaxNameLen+1), false},
45 {"two\nlines", false},
46 {"a\tb", false},
47 {"a\x7fb", false},
48 {"a | pipe", false},
49 }
50 for _, tt := range tests {
51 uassert.Equal(t, tt.want, ValidName(tt.name), tt.name)
52 }
53}
54
55func TestValidText(t *testing.T) {
56 tests := []struct {
57 name string
58 text string
59 want bool
60 }{
61 {"plain", "ship v1 before the conference", true},
62 {"newlines and tabs are content", "a\nb\tc", true},
63 {"markdown is allowed and escaped later", "[x](y) | z", true},
64 {"empty", "", false},
65 {"blank", " \n ", false},
66 {"too long", strings.Repeat("x", MaxTextLen+1), false},
67 {"control character", "a\x01b", false},
68 {"delete character", "a\x7fb", false},
69 }
70 for _, tt := range tests {
71 uassert.Equal(t, tt.want, ValidText(tt.text), tt.name)
72 }
73}
74
75func TestCreateMintsAtTheOpeningPrice(t *testing.T) {
76 cs := New()
77 id, err := cs.Create("validators", alice, tenGNOT, 100)
78 urequire.NoError(t, err)
79
80 c, ok := cs.Get(id)
81 urequire.True(t, ok, "the crew exists")
82 uassert.Equal(t, "validators", c.Name)
83 uassert.Equal(t, int64(100), c.CreatedAt)
84 uassert.Equal(t, tenGNOT/InitialPricePerShare, c.TotalShares)
85 uassert.Equal(t, tenGNOT, c.Treasury)
86 uassert.Equal(t, c.TotalShares, c.SharesOf(alice))
87 uassert.Equal(t, 1, c.MemberCount())
88 uassert.Equal(t, InitialPricePerShare, c.ValuePerShare())
89 uassert.Equal(t, 1, cs.Len())
90}
91
92// The remainder below one whole share stays in the treasury, which is the same
93// direction every other division here rounds.
94func TestCreateKeepsTheRemainder(t *testing.T) {
95 cs := New()
96 id, err := cs.Create("crew", alice, 2999, 100)
97 urequire.NoError(t, err)
98
99 c, _ := cs.Get(id)
100 uassert.Equal(t, int64(2), c.TotalShares, "2999 ugnot buys two whole shares")
101 uassert.Equal(t, int64(2999), c.Treasury, "and the 999 left over is the crew's")
102}
103
104func TestCreateRefusals(t *testing.T) {
105 tests := []struct {
106 label string
107 name string
108 amount int64
109 want error
110 }{
111 {"empty name", "", tenGNOT, ErrBadName},
112 {"name with a newline", "two\nlines", tenGNOT, ErrBadName},
113 {"name with a pipe", "a | pipe", tenGNOT, ErrBadName},
114 {"nothing sent", "crew", 0, ErrBadAmount},
115 {"negative", "crew", -1, ErrBadAmount},
116 {"under one share", "crew", InitialPricePerShare - 1, ErrNoShares},
117 }
118 for _, tt := range tests {
119 cs := New()
120 _, err := cs.Create(tt.name, alice, tt.amount, 100)
121 uassert.ErrorIs(t, err, tt.want, tt.label)
122 uassert.Equal(t, 0, cs.Len(), tt.label+": nothing was stored")
123 }
124}
125
126// A second joiner at the same per-share value gets the same shares. This is the
127// baseline the anti-dilution rule is measured against.
128func TestASecondJoinerAtTheSamePriceGetsTheSameShares(t *testing.T) {
129 cs := New()
130 id, _ := cs.Create("crew", alice, tenGNOT, 100)
131
132 got, err := cs.Join(id, bob, tenGNOT)
133 urequire.NoError(t, err)
134
135 c, _ := cs.Get(id)
136 uassert.Equal(t, c.SharesOf(alice), got, "same money, same shares")
137 uassert.Equal(t, tenGNOT/InitialPricePerShare, got)
138 uassert.Equal(t, 2*tenGNOT, c.Treasury)
139 uassert.Equal(t, 2*(tenGNOT/InitialPricePerShare), c.TotalShares)
140 uassert.Equal(t, InitialPricePerShare, c.ValuePerShare(), "the value per share did not move")
141 uassert.Equal(t, 2, c.MemberCount())
142}
143
144// A joiner arriving after the treasury grew gets FEWER shares for the same
145// money, because they are buying at what a share is worth now. That is the
146// whole point: a late joiner cannot buy into value the existing members built.
147func TestAJoinerAfterTheTreasuryGrewGetsFewer(t *testing.T) {
148 cs := New()
149 id, _ := cs.Create("crew", alice, tenGNOT, 100)
150 urequire.NoError(t, cs.Fund(id, tenGNOT)) // the crew doubled its money
151
152 c, _ := cs.Get(id)
153 urequire.Equal(t, 2*InitialPricePerShare, c.ValuePerShare())
154
155 got, err := cs.Join(id, bob, tenGNOT)
156 urequire.NoError(t, err)
157 uassert.Equal(t, c.SharesOf(alice)/2, got, "the same money buys half as much")
158 uassert.Equal(t, 2*InitialPricePerShare, c.ValuePerShare(),
159 "and joining did not move the value for anybody else")
160}
161
162// Rounding down on the way in means a joiner never gets a share they did not
163// fully pay for, and the fractional remainder accrues to the crew.
164func TestJoinRoundsInTheCrewsFavour(t *testing.T) {
165 cs := New()
166 id, _ := cs.Create("crew", alice, 1000, 100) // 1 share, treasury 1000
167 urequire.NoError(t, cs.Fund(id, 500)) // treasury 1500, still 1 share
168
169 // 1499 ugnot is 0.999 of a share. Rounding up would hand bob a whole
170 // one and half of alice's money with it.
171 _, err := cs.Join(id, bob, 1499)
172 uassert.ErrorIs(t, err, ErrNoShares)
173
174 got, err := cs.Join(id, bob, 2999)
175 urequire.NoError(t, err)
176 uassert.Equal(t, int64(1), got, "1.999 shares' worth buys one share")
177
178 c, _ := cs.Get(id)
179 uassert.Equal(t, int64(4499), c.Treasury, "the 1499 ugnot of remainder stayed")
180 uassert.Equal(t, int64(2), c.TotalShares)
181}
182
183func TestJoinRefusals(t *testing.T) {
184 cs := New()
185 id, _ := cs.Create("crew", alice, tenGNOT, 100)
186
187 _, err := cs.Join(999, bob, tenGNOT)
188 uassert.ErrorIs(t, err, ErrNoCrew)
189
190 _, err = cs.Join(id, alice, tenGNOT)
191 uassert.ErrorIs(t, err, ErrIsMember, "a member cannot buy in twice")
192
193 _, err = cs.Join(id, bob, 0)
194 uassert.ErrorIs(t, err, ErrBadAmount)
195
196 c, _ := cs.Get(id)
197 uassert.Equal(t, 1, c.MemberCount(), "no refusal changed the roster")
198 uassert.Equal(t, tenGNOT, c.Treasury)
199}
200
201// Fifteen people is the product constraint, and the sixteenth is refused rather
202// than quietly allowed.
203func TestACrewIsFullAtMaxMembers(t *testing.T) {
204 cs := New()
205 id, _ := cs.Create("crew", alice, tenGNOT, 100)
206 for i := 1; i < MaxMembers; i++ {
207 who := testutils.TestAddress("m" + strconv.Itoa(i))
208 _, err := cs.Join(id, who, tenGNOT)
209 urequire.NoError(t, err)
210 }
211
212 c, _ := cs.Get(id)
213 urequire.Equal(t, MaxMembers, c.MemberCount())
214
215 _, err := cs.Join(id, testutils.TestAddress("one too many"), tenGNOT)
216 uassert.ErrorIs(t, err, ErrFull)
217 uassert.Equal(t, MaxMembers, c.MemberCount())
218}
219
220func TestProposeAndTallyByShareWeight(t *testing.T) {
221 cs := New()
222 id, _ := cs.Create("crew", alice, tenGNOT, 100) // alice: 10000 shares
223 cs.Join(id, bob, tenGNOT/2) // bob: 5000 shares
224
225 pid, err := cs.Propose(id, bob, "ship v1 before the conference", 200)
226 urequire.NoError(t, err)
227
228 p, ok := cs.Proposal(pid)
229 urequire.True(t, ok, "the proposal exists")
230 uassert.Equal(t, uint64(id), uint64(p.CrewID))
231 uassert.Equal(t, int64(200+VoteBlocks), p.Deadline)
232 uassert.True(t, p.Open(200))
233 uassert.False(t, p.Open(200+VoteBlocks), "the deadline is exclusive")
234
235 urequire.NoError(t, cs.Vote(pid, bob, true, 210))
236 urequire.NoError(t, cs.Vote(pid, alice, false, 211))
237 uassert.Equal(t, int64(5000), p.Yes)
238 uassert.Equal(t, int64(10000), p.No, "a bigger holder outvotes a smaller one")
239 uassert.Equal(t, 2, p.Voters())
240
241 c, _ := cs.Get(id)
242 uassert.Equal(t, 1, len(c.Proposals()))
243}
244
245// One ballot per member, replaced in place while the proposal is open, and
246// reweighed from the shares held at the moment it is recast.
247func TestAVoteIsChangeableWhileOpen(t *testing.T) {
248 cs := New()
249 id, _ := cs.Create("crew", alice, tenGNOT, 100)
250 pid, _ := cs.Propose(id, alice, "a question", 200)
251 p, _ := cs.Proposal(pid)
252
253 urequire.NoError(t, cs.Vote(pid, alice, true, 210))
254 uassert.Equal(t, int64(10000), p.Yes)
255 uassert.Equal(t, int64(0), p.No)
256
257 urequire.NoError(t, cs.Vote(pid, alice, false, 211))
258 uassert.Equal(t, int64(0), p.Yes, "the old ballot is removed, not added to")
259 uassert.Equal(t, int64(10000), p.No)
260 uassert.Equal(t, 1, p.Voters(), "still one voter")
261
262 yes, weight, voted := p.VoteOf(alice)
263 uassert.True(t, voted)
264 uassert.False(t, yes)
265 uassert.Equal(t, int64(10000), weight)
266}
267
268func TestProposeAndVoteRefusals(t *testing.T) {
269 cs := New()
270 id, _ := cs.Create("crew", alice, tenGNOT, 100)
271
272 _, err := cs.Propose(999, alice, "text", 200)
273 uassert.ErrorIs(t, err, ErrNoCrew)
274 _, err = cs.Propose(id, bob, "text", 200)
275 uassert.ErrorIs(t, err, ErrNotMember, "only a member proposes")
276 _, err = cs.Propose(id, alice, " ", 200)
277 uassert.ErrorIs(t, err, ErrBadText)
278
279 pid, _ := cs.Propose(id, alice, "a question", 200)
280 uassert.ErrorIs(t, cs.Vote(999, alice, true, 210), ErrNoProposal)
281 uassert.ErrorIs(t, cs.Vote(pid, bob, true, 210), ErrNotMember)
282 uassert.ErrorIs(t, cs.Vote(pid, alice, true, 200+VoteBlocks), ErrVoteClosed,
283 "the deadline is exclusive")
284}
285
286func TestCloseRecordsTheResult(t *testing.T) {
287 tests := []struct {
288 label string
289 aliceVotes bool
290 bobVotes bool
291 want bool
292 }{
293 {"the bigger holder carries it", true, false, true},
294 {"and blocks it", false, true, false},
295 {"unanimous yes", true, true, true},
296 {"unanimous no", false, false, false},
297 }
298 for _, tt := range tests {
299 cs := New()
300 id, _ := cs.Create("crew", alice, tenGNOT, 100) // 10000 shares
301 cs.Join(id, bob, tenGNOT/2) // 5000 shares
302 pid, _ := cs.Propose(id, alice, "a question", 200)
303
304 urequire.NoError(t, cs.Vote(pid, alice, tt.aliceVotes, 210))
305 urequire.NoError(t, cs.Vote(pid, bob, tt.bobVotes, 211))
306
307 passed, err := cs.Close(pid, 200+VoteBlocks)
308 urequire.NoError(t, err)
309 uassert.Equal(t, tt.want, passed, tt.label)
310
311 p, _ := cs.Proposal(pid)
312 uassert.True(t, p.Closed, tt.label)
313 uassert.Equal(t, tt.want, p.Passed, tt.label)
314 }
315}
316
317// A tie fails, and a proposal nobody voted on is a tie at zero.
318func TestATieFails(t *testing.T) {
319 cs := New()
320 id, _ := cs.Create("crew", alice, tenGNOT, 100)
321 cs.Join(id, bob, tenGNOT)
322 pid, _ := cs.Propose(id, alice, "a question", 200)
323
324 urequire.NoError(t, cs.Vote(pid, alice, true, 210))
325 urequire.NoError(t, cs.Vote(pid, bob, false, 211))
326
327 passed, err := cs.Close(pid, 200+VoteBlocks)
328 urequire.NoError(t, err)
329 uassert.False(t, passed, "equal weight on both sides is not agreement")
330
331 silent, _ := cs.Propose(id, alice, "nobody cares", 200)
332 passed, err = cs.Close(silent, 200+VoteBlocks)
333 urequire.NoError(t, err)
334 uassert.False(t, passed, "0 to 0 is not agreement either")
335}
336
337func TestCloseRefusals(t *testing.T) {
338 cs := New()
339 id, _ := cs.Create("crew", alice, tenGNOT, 100)
340 pid, _ := cs.Propose(id, alice, "a question", 200)
341
342 _, err := cs.Close(999, 5000)
343 uassert.ErrorIs(t, err, ErrNoProposal)
344
345 _, err = cs.Close(pid, 200+VoteBlocks-1)
346 uassert.ErrorIs(t, err, ErrVoteOpen, "one block early is early")
347
348 _, err = cs.Close(pid, 200+VoteBlocks)
349 urequire.NoError(t, err)
350 _, err = cs.Close(pid, 200+VoteBlocks)
351 uassert.ErrorIs(t, err, ErrVoteClosed, "closing twice does not re-tally")
352}
353
354// A vote keeps the weight it was cast with. Ragequitting afterwards does not
355// unwind it, which is a v0 caveat stated in the package doc rather than a bug.
356func TestAVoteKeepsTheWeightItWasCastWith(t *testing.T) {
357 cs := New()
358 id, _ := cs.Create("crew", alice, tenGNOT, 100)
359 cs.Join(id, bob, tenGNOT)
360 pid, _ := cs.Propose(id, alice, "a question", 200)
361
362 urequire.NoError(t, cs.Vote(pid, bob, true, 210))
363 _, _, err := cs.Ragequit(id, bob)
364 urequire.NoError(t, err)
365
366 p, _ := cs.Proposal(pid)
367 uassert.Equal(t, int64(10000), p.Yes, "bob's ballot stayed behind")
368 uassert.ErrorIs(t, cs.Vote(pid, bob, false, 220), ErrNotMember,
369 "but he cannot touch it again")
370}
371
372func TestRagequitTakesAProRataSlice(t *testing.T) {
373 cs := New()
374 id, _ := cs.Create("crew", alice, tenGNOT, 100)
375 cs.Join(id, bob, tenGNOT)
376 urequire.NoError(t, cs.Fund(id, 2*tenGNOT)) // the crew doubled its money
377
378 shares, amount, err := cs.Ragequit(id, bob)
379 urequire.NoError(t, err)
380 uassert.Equal(t, tenGNOT/InitialPricePerShare, shares, "every share is burned")
381 uassert.Equal(t, 2*tenGNOT, amount, "half of a treasury worth four, for half the shares")
382
383 c, _ := cs.Get(id)
384 uassert.Equal(t, 1, c.MemberCount())
385 uassert.Equal(t, int64(0), c.SharesOf(bob))
386 uassert.False(t, c.IsMember(bob))
387 uassert.Equal(t, 2*tenGNOT, c.Treasury)
388 uassert.Equal(t, tenGNOT/InitialPricePerShare, c.TotalShares)
389
390 uassert.Equal(t, 2*tenGNOT, cs.CreditOf(bob), "credited, never sent")
391 uassert.Equal(t, 2*tenGNOT, cs.TotalOwed())
392
393 _, _, err = cs.Ragequit(id, bob)
394 uassert.ErrorIs(t, err, ErrNotMember, "there is nothing left to burn")
395 _, _, err = cs.Ragequit(999, alice)
396 uassert.ErrorIs(t, err, ErrNoCrew)
397}
398
399// A one-member crew ragequitting takes everything: their shares ARE the total,
400// so the division is exact whatever the numbers are.
401func TestAOneMemberCrewTakesEverything(t *testing.T) {
402 cs := New()
403 id, _ := cs.Create("crew", alice, 5001, 100) // 5 shares, a treasury of 5001
404
405 shares, amount, err := cs.Ragequit(id, alice)
406 urequire.NoError(t, err)
407 uassert.Equal(t, int64(5), shares)
408 uassert.Equal(t, int64(5001), amount, "including the ugnot that bought no share")
409
410 c, _ := cs.Get(id)
411 uassert.Equal(t, int64(0), c.Treasury)
412 uassert.Equal(t, int64(0), c.TotalShares)
413 uassert.Equal(t, 0, c.MemberCount())
414}
415
416// Everyone ragequitting empties the treasury to the last ugnot, even when no
417// single division is exact: the dust each leaver rounds away belongs to whoever
418// is still in, and the last one out holds every share.
419func TestEveryoneRagequittingEmptiesTheTreasury(t *testing.T) {
420 cs := New()
421 id, _ := cs.Create("crew", alice, 1000, 100)
422 join(t, cs, id, bob, 1000)
423 join(t, cs, id, carol, 1000)
424 urequire.NoError(t, cs.Fund(id, 2)) // 3002 ugnot over 3 shares, divides by nothing
425
426 c, _ := cs.Get(id)
427 urequire.Equal(t, int64(3002), c.Treasury)
428 urequire.Equal(t, int64(3), c.TotalShares)
429
430 var paid int64
431 for _, who := range []address{alice, bob, carol} {
432 _, amount, err := cs.Ragequit(id, who)
433 urequire.NoError(t, err)
434 paid += amount
435 }
436
437 uassert.Equal(t, int64(3002), paid, "every ugnot left with somebody")
438 uassert.Equal(t, int64(0), c.Treasury, "to the last one")
439 uassert.Equal(t, int64(0), c.TotalShares)
440 uassert.Equal(t, 0, c.MemberCount())
441 uassert.Equal(t, int64(3002), cs.TotalOwed())
442
443 // The dust did not land evenly, which is the point: the people who
444 // stayed longest absorbed it.
445 uassert.Equal(t, int64(1000), cs.CreditOf(alice), "first out, rounded down hardest")
446 uassert.Equal(t, int64(1001), cs.CreditOf(bob))
447 uassert.Equal(t, int64(1001), cs.CreditOf(carol))
448}
449
450// An emptied crew is not a dead one. With no shares outstanding there is no
451// value to price against, so it reopens at the price it opened at.
452func TestRejoiningAnEmptiedCrewIsBackAtTheOpeningPrice(t *testing.T) {
453 cs := New()
454 id, _ := cs.Create("crew", alice, tenGNOT, 100)
455 _, _, err := cs.Ragequit(id, alice)
456 urequire.NoError(t, err)
457
458 c, _ := cs.Get(id)
459 urequire.Equal(t, int64(0), c.TotalShares)
460 uassert.Equal(t, int64(0), c.ValuePerShare(), "nothing is worth anything yet")
461 uassert.Equal(t, InitialPricePerShare, c.PricePerShare(), "but a share still has a price")
462
463 got, err := cs.Join(id, dave, tenGNOT)
464 urequire.NoError(t, err)
465 uassert.Equal(t, tenGNOT/InitialPricePerShare, got)
466 uassert.Equal(t, tenGNOT, c.Treasury)
467}
468
469func TestWithdrawZeroesTheCreditBeforeItReturns(t *testing.T) {
470 cs := New()
471 id, _ := cs.Create("crew", alice, tenGNOT, 100)
472 cs.Join(id, bob, tenGNOT)
473 _, owed, _ := cs.Ragequit(id, bob)
474 urequire.True(t, owed > 0, "bob is owed something")
475
476 got, err := cs.Withdraw(bob)
477 urequire.NoError(t, err)
478 uassert.Equal(t, owed, got)
479 uassert.Equal(t, int64(0), cs.CreditOf(bob), "zeroed, so a reentrant call finds nothing")
480 uassert.Equal(t, int64(0), cs.TotalOwed())
481
482 _, err = cs.Withdraw(bob)
483 uassert.ErrorIs(t, err, ErrNothing)
484 _, err = cs.Withdraw(carol)
485 uassert.ErrorIs(t, err, ErrNothing)
486}
487
488// Credits accumulate across crews, because one ledger serves all of them.
489func TestCreditsAccumulateAcrossCrews(t *testing.T) {
490 cs := New()
491 first, _ := cs.Create("first", alice, tenGNOT, 100)
492 second, _ := cs.Create("second", bob, tenGNOT, 100)
493 join(t, cs, second, alice, tenGNOT)
494
495 _, a, _ := cs.Ragequit(first, alice)
496 _, b, _ := cs.Ragequit(second, alice)
497 uassert.Equal(t, a+b, cs.CreditOf(alice), "one withdrawal collects both")
498 uassert.Equal(t, 2, cs.Len())
499}
500
501func TestListIsNewestFirst(t *testing.T) {
502 cs := New()
503 first, _ := cs.Create("first", alice, tenGNOT, 100)
504 second, _ := cs.Create("second", bob, tenGNOT, 101)
505 third, _ := cs.Create("third", carol, tenGNOT, 102)
506
507 got := cs.List(10)
508 urequire.Equal(t, 3, len(got))
509 uassert.Equal(t, uint64(third), uint64(got[0].ID))
510 uassert.Equal(t, uint64(second), uint64(got[1].ID))
511 uassert.Equal(t, uint64(first), uint64(got[2].ID))
512 uassert.Equal(t, "third", got[0].Crew.Name)
513
514 uassert.Equal(t, 1, len(cs.List(1)))
515 uassert.Equal(t, 0, len(cs.List(0)))
516}
517
518// Members comes back in join order and as a copy, so a Render can never
519// reshuffle the roster it is reading.
520func TestMembersAreAnOrderedCopy(t *testing.T) {
521 cs := New()
522 id, _ := cs.Create("crew", alice, tenGNOT, 100)
523 cs.Join(id, bob, tenGNOT)
524 cs.Join(id, carol, tenGNOT)
525
526 c, _ := cs.Get(id)
527 got := c.Members()
528 urequire.Equal(t, 3, len(got))
529 uassert.Equal(t, alice.String(), got[0].String())
530 uassert.Equal(t, bob.String(), got[1].String())
531 uassert.Equal(t, carol.String(), got[2].String())
532
533 got[0] = dave
534 uassert.Equal(t, alice.String(), c.Members()[0].String(), "the caller got a copy")
535
536 // Ragequitting removes the leaver and keeps the rest in order.
537 cs.Ragequit(id, bob)
538 got = c.Members()
539 urequire.Equal(t, 2, len(got))
540 uassert.Equal(t, alice.String(), got[0].String())
541 uassert.Equal(t, carol.String(), got[1].String())
542}
543
544func TestFundRefusals(t *testing.T) {
545 cs := New()
546 id, _ := cs.Create("crew", alice, tenGNOT, 100)
547
548 uassert.ErrorIs(t, cs.Fund(999, 100), ErrNoCrew)
549 uassert.ErrorIs(t, cs.Fund(id, 0), ErrBadAmount)
550 uassert.ErrorIs(t, cs.Fund(id, -1), ErrBadAmount)
551
552 c, _ := cs.Get(id)
553 uassert.Equal(t, tenGNOT, c.Treasury, "no refusal moved the treasury")
554 uassert.Equal(t, 0, cs.ProposalCount())
555}