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

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}