fee_split_test.gno
38.58 Kb · 1126 lines
1package fee_split
2
3import (
4 "chain"
5 "chain/banker"
6 "strconv"
7 "strings"
8 "testing"
9
10 "gno.land/p/nt/testutils/v0"
11 "gno.land/p/nt/uassert/v0"
12)
13
14var (
15 owner = testutils.TestAddress("owner")
16 alice = testutils.TestAddress("alice")
17 bob = testutils.TestAddress("bob")
18 carol = testutils.TestAddress("carol")
19 dave = testutils.TestAddress("dave")
20 attacker = testutils.TestAddress("attacker")
21)
22
23// resetState resets all package-level state between tests.
24// Required because Gno test functions share the same VM context.
25func resetState() {
26 splits = make(map[string]*Split)
27 splitIDs = []string{}
28 ownerSplits = make(map[address]int)
29 nextID = 1
30 feeBps = 0
31 feeAdmin = feeAdminAddr
32 pendingFeeAdmin = ""
33 feesAccrued = 0
34 feesClaimed = 0
35}
36
37var feeAdminAddr = testutils.TestAddress("feeadmin")
38
39func addrList(addrs ...address) string {
40 parts := make([]string, len(addrs))
41 for i, a := range addrs {
42 parts[i] = string(a)
43 }
44 return strings.Join(parts, ",")
45}
46
47// fund gives the realm's package address coins so Claim payouts succeed,
48// mirroring the deposits the accounting records.
49func fund(amount int64) {
50 pkgAddr := chain.PackageAddress("gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/fee_split/v2")
51 testing.IssueCoins(pkgAddr, chain.Coins{{Denom: Denom, Amount: amount}})
52}
53
54// deposit performs a coin-backed deposit as addr.
55func deposit(cur realm, addr address, splitID string, amount int64) {
56 testing.SetRealm(testing.NewUserRealm(addr))
57 testing.SetOriginSend(chain.Coins{{Denom: Denom, Amount: amount}})
58 fund(amount)
59 Deposit(cross(cur), splitID)
60 testing.SetOriginSend(nil)
61}
62
63// ---------- CreateSplit ----------
64
65func TestCreateSplit_Basic(cur realm, t *testing.T) {
66 resetState()
67 testing.SetRealm(testing.NewUserRealm(owner))
68
69 id := CreateSplit(cross(cur), addrList(alice, bob), "6000,4000")
70 uassert.Equal(t, "split_1", id)
71
72 s := splits["split_1"]
73 uassert.Equal(t, string(owner), string(s.Owner))
74 uassert.Equal(t, 2, len(s.Recipients))
75 uassert.Equal(t, string(alice), string(s.Recipients[0]))
76 uassert.Equal(t, string(bob), string(s.Recipients[1]))
77 uassert.Equal(t, int64(6000), s.Shares[0])
78 uassert.Equal(t, int64(4000), s.Shares[1])
79 uassert.False(t, s.Frozen, "new split must not be frozen")
80 uassert.Equal(t, int64(0), s.TotalDeposited)
81 uassert.Equal(t, int64(0), s.TotalClaimed)
82}
83
84func TestCreateSplit_IDsIncrement(cur realm, t *testing.T) {
85 resetState()
86 testing.SetRealm(testing.NewUserRealm(owner))
87
88 id1 := CreateSplit(cross(cur), addrList(alice), "10000")
89 id2 := CreateSplit(cross(cur), addrList(bob), "10000")
90 uassert.Equal(t, "split_1", id1)
91 uassert.Equal(t, "split_2", id2)
92}
93
94func TestCreateSplit_DuplicateRecipient(cur realm, t *testing.T) {
95 resetState()
96 testing.SetRealm(testing.NewUserRealm(owner))
97
98 uassert.AbortsWithMessage(t, cur,
99 "duplicate recipient: "+string(alice),
100 func() { CreateSplit(cross(cur), addrList(alice, alice), "5000,5000") },
101 )
102}
103
104func TestCreateSplit_SharesDontSum(cur realm, t *testing.T) {
105 resetState()
106 testing.SetRealm(testing.NewUserRealm(owner))
107
108 uassert.AbortsWithMessage(t, cur,
109 "shares must sum to 10000 basis points, got 5000",
110 func() { CreateSplit(cross(cur), addrList(alice), "5000") },
111 )
112}
113
114func TestCreateSplit_LengthMismatch(cur realm, t *testing.T) {
115 resetState()
116 testing.SetRealm(testing.NewUserRealm(owner))
117
118 uassert.AbortsWithMessage(t, cur,
119 "recipients and shares must have the same length",
120 func() { CreateSplit(cross(cur), addrList(alice, bob), "10000") },
121 )
122}
123
124func TestCreateSplit_InvalidAddressRejected(cur realm, t *testing.T) {
125 resetState()
126 testing.SetRealm(testing.NewUserRealm(owner))
127
128 uassert.AbortsWithMessage(t, cur,
129 "invalid recipient address: not_an_address",
130 func() { CreateSplit(cross(cur), "not_an_address", "10000") },
131 )
132}
133
134func TestCreateSplit_PerOwnerLimit(cur realm, t *testing.T) {
135 resetState()
136 testing.SetRealm(testing.NewUserRealm(owner))
137
138 for i := 0; i < MaxSplitsPerOwner; i++ {
139 CreateSplit(cross(cur), addrList(alice), "10000")
140 }
141 uassert.AbortsWithMessage(t, cur,
142 "per-owner split limit reached",
143 func() { CreateSplit(cross(cur), addrList(alice), "10000") },
144 )
145}
146
147// ---------- Deposit (coin-backed) ----------
148
149func TestDeposit_UsesSentCoins(cur realm, t *testing.T) {
150 resetState()
151 testing.SetRealm(testing.NewUserRealm(owner))
152 id := CreateSplit(cross(cur), addrList(alice, bob), "5000,5000")
153
154 deposit(cur, owner, id, 1000)
155
156 s := splits[id]
157 uassert.Equal(t, int64(1000), s.TotalDeposited)
158 uassert.Equal(t, int64(500), s.Balances[alice])
159 uassert.Equal(t, int64(500), s.Balances[bob])
160}
161
162func TestDeposit_NoCoinsPanics(cur realm, t *testing.T) {
163 resetState()
164 testing.SetRealm(testing.NewUserRealm(owner))
165 id := CreateSplit(cross(cur), addrList(alice), "10000")
166
167 // no SetOriginSend: nothing attached to the call
168 testing.SetOriginSend(nil)
169 uassert.AbortsWithMessage(t, cur,
170 "deposit must send exactly one coin of "+Denom,
171 func() { Deposit(cross(cur), id) },
172 )
173 uassert.Equal(t, int64(0), splits[id].TotalDeposited)
174}
175
176func TestDeposit_WrongDenomPanics(cur realm, t *testing.T) {
177 resetState()
178 testing.SetRealm(testing.NewUserRealm(owner))
179 id := CreateSplit(cross(cur), addrList(alice), "10000")
180
181 testing.SetOriginSend(chain.Coins{{Denom: "ufoo", Amount: 100}})
182 uassert.AbortsWithMessage(t, cur,
183 "deposit must send exactly one coin of "+Denom,
184 func() { Deposit(cross(cur), id) },
185 )
186 testing.SetOriginSend(nil)
187}
188
189func TestDeposit_RoundingDustGoesToHighestShare(cur realm, t *testing.T) {
190 resetState()
191 testing.SetRealm(testing.NewUserRealm(owner))
192 id := CreateSplit(cross(cur), addrList(alice, bob, carol), "3334,3333,3333")
193
194 deposit(cur, owner, id, 10)
195
196 s := splits[id]
197 uassert.Equal(t, int64(10), s.TotalDeposited)
198 // alice: (10*3334)/10000 = 3, +1 dust (highest share)
199 uassert.Equal(t, int64(4), s.Balances[alice])
200 uassert.Equal(t, int64(3), s.Balances[bob])
201 uassert.Equal(t, int64(3), s.Balances[carol])
202}
203
204func TestDeposit_AccumulatesAcrossMultipleDeposits(cur realm, t *testing.T) {
205 resetState()
206 testing.SetRealm(testing.NewUserRealm(owner))
207 id := CreateSplit(cross(cur), addrList(alice, bob), "5000,5000")
208
209 deposit(cur, owner, id, 100)
210 deposit(cur, owner, id, 200)
211
212 s := splits[id]
213 uassert.Equal(t, int64(300), s.TotalDeposited)
214 uassert.Equal(t, int64(150), s.Balances[alice])
215 uassert.Equal(t, int64(150), s.Balances[bob])
216}
217
218func TestDeposit_OverflowGuard(cur realm, t *testing.T) {
219 resetState()
220 testing.SetRealm(testing.NewUserRealm(owner))
221 id := CreateSplit(cross(cur), addrList(alice), "10000")
222
223 testing.SetOriginSend(chain.Coins{{Denom: Denom, Amount: MaxDepositAmount + 1}})
224 uassert.AbortsWithMessage(t, cur,
225 "deposit exceeds maximum supported amount",
226 func() { Deposit(cross(cur), id) },
227 )
228 testing.SetOriginSend(nil)
229 uassert.Equal(t, int64(0), splits[id].TotalDeposited)
230}
231
232func TestDeposit_FrozenSplitPanics(cur realm, t *testing.T) {
233 resetState()
234 testing.SetRealm(testing.NewUserRealm(owner))
235 id := CreateSplit(cross(cur), addrList(alice), "10000")
236 Freeze(cross(cur), id)
237
238 testing.SetOriginSend(chain.Coins{{Denom: Denom, Amount: 100}})
239 uassert.AbortsWithMessage(t, cur,
240 "split is frozen",
241 func() { Deposit(cross(cur), id) },
242 )
243 testing.SetOriginSend(nil)
244}
245
246func TestDeposit_UnknownSplitPanics(cur realm, t *testing.T) {
247 resetState()
248 testing.SetRealm(testing.NewUserRealm(owner))
249
250 uassert.AbortsWithMessage(t, cur,
251 "split not found: split_99",
252 func() { Deposit(cross(cur), "split_99") },
253 )
254}
255
256// ---------- Claim (pays out coins) ----------
257
258func TestClaim_SendsCoins(cur realm, t *testing.T) {
259 resetState()
260 testing.SetRealm(testing.NewUserRealm(owner))
261 id := CreateSplit(cross(cur), addrList(alice, bob), "7000,3000")
262 deposit(cur, owner, id, 10000)
263
264 before := banker.NewReadonlyBanker().GetCoins(alice).AmountOf(Denom)
265
266 testing.SetRealm(testing.NewUserRealm(alice))
267 claimed := Claim(cross(cur), id)
268 uassert.Equal(t, int64(7000), claimed)
269
270 after := banker.NewReadonlyBanker().GetCoins(alice).AmountOf(Denom)
271 uassert.Equal(t, int64(7000), after-before)
272
273 s := splits[id]
274 uassert.Equal(t, int64(0), s.Balances[alice])
275 uassert.Equal(t, int64(7000), s.TotalClaimed)
276}
277
278func TestClaim_CallerIdentityIsEnforced(cur realm, t *testing.T) {
279 resetState()
280 testing.SetRealm(testing.NewUserRealm(owner))
281 id := CreateSplit(cross(cur), addrList(alice), "10000")
282 deposit(cur, owner, id, 100)
283
284 // the attacker cannot claim alice's balance: identity comes from the
285 // chain, not from an argument
286 testing.SetRealm(testing.NewUserRealm(attacker))
287 uassert.AbortsWithMessage(t, cur,
288 "not a recipient of this split",
289 func() { Claim(cross(cur), id) },
290 )
291 uassert.Equal(t, int64(100), splits[id].Balances[alice])
292}
293
294func TestClaim_NothingToClaim(cur realm, t *testing.T) {
295 resetState()
296 testing.SetRealm(testing.NewUserRealm(owner))
297 id := CreateSplit(cross(cur), addrList(alice, bob), "5000,5000")
298
299 testing.SetRealm(testing.NewUserRealm(alice))
300 uassert.AbortsWithMessage(t, cur,
301 "nothing to claim",
302 func() { Claim(cross(cur), id) },
303 )
304}
305
306func TestClaim_CannotClaimTwice(cur realm, t *testing.T) {
307 resetState()
308 testing.SetRealm(testing.NewUserRealm(owner))
309 id := CreateSplit(cross(cur), addrList(alice), "10000")
310 deposit(cur, owner, id, 100)
311
312 testing.SetRealm(testing.NewUserRealm(alice))
313 Claim(cross(cur), id)
314 uassert.AbortsWithMessage(t, cur,
315 "nothing to claim",
316 func() { Claim(cross(cur), id) },
317 )
318}
319
320func TestClaim_ExRecipientKeepsAccruedBalance(cur realm, t *testing.T) {
321 resetState()
322 testing.SetRealm(testing.NewUserRealm(owner))
323 id := CreateSplit(cross(cur), addrList(alice, bob), "5000,5000")
324 deposit(cur, owner, id, 1000)
325
326 testing.SetRealm(testing.NewUserRealm(owner))
327 UpdateShares(cross(cur), id, addrList(carol, dave), "6000,4000")
328
329 // bob was removed but keeps — and can claim — his accrued 500
330 testing.SetRealm(testing.NewUserRealm(bob))
331 claimed := Claim(cross(cur), id)
332 uassert.Equal(t, int64(500), claimed)
333}
334
335// ---------- UpdateShares ----------
336
337func TestUpdateShares_Basic(cur realm, t *testing.T) {
338 resetState()
339 testing.SetRealm(testing.NewUserRealm(owner))
340 id := CreateSplit(cross(cur), addrList(alice, bob), "5000,5000")
341
342 UpdateShares(cross(cur), id, addrList(alice, carol), "7000,3000")
343
344 s := splits[id]
345 uassert.Equal(t, 2, len(s.Recipients))
346 uassert.Equal(t, string(alice), string(s.Recipients[0]))
347 uassert.Equal(t, string(carol), string(s.Recipients[1]))
348 uassert.Equal(t, int64(7000), s.Shares[0])
349 uassert.Equal(t, int64(3000), s.Shares[1])
350}
351
352func TestUpdateShares_PreservesUnclaimedBalances(cur realm, t *testing.T) {
353 resetState()
354 testing.SetRealm(testing.NewUserRealm(owner))
355 id := CreateSplit(cross(cur), addrList(alice, bob), "5000,5000")
356 deposit(cur, owner, id, 1000)
357
358 testing.SetRealm(testing.NewUserRealm(owner))
359 UpdateShares(cross(cur), id, addrList(carol, dave), "6000,4000")
360
361 s := splits[id]
362 uassert.Equal(t, int64(500), s.Balances[alice])
363 uassert.Equal(t, int64(500), s.Balances[bob])
364}
365
366func TestUpdateShares_WrongOwner(cur realm, t *testing.T) {
367 resetState()
368 testing.SetRealm(testing.NewUserRealm(owner))
369 id := CreateSplit(cross(cur), addrList(alice), "10000")
370
371 testing.SetRealm(testing.NewUserRealm(attacker))
372 uassert.AbortsWithMessage(t, cur,
373 "only the owner can update shares",
374 func() { UpdateShares(cross(cur), id, addrList(alice), "10000") },
375 )
376}
377
378func TestUpdateShares_FrozenSplitPanics(cur realm, t *testing.T) {
379 resetState()
380 testing.SetRealm(testing.NewUserRealm(owner))
381 id := CreateSplit(cross(cur), addrList(alice), "10000")
382 Freeze(cross(cur), id)
383
384 uassert.AbortsWithMessage(t, cur,
385 "split is frozen",
386 func() { UpdateShares(cross(cur), id, addrList(alice, bob), "5000,5000") },
387 )
388}
389
390// ---------- TransferOwnership / AcceptOwnership (two-step, v2 Y1) ----------
391
392func TestTransferOwnership_TwoStep(cur realm, t *testing.T) {
393 resetState()
394 testing.SetRealm(testing.NewUserRealm(owner))
395 id := CreateSplit(cross(cur), addrList(alice), "10000")
396
397 TransferOwnership(cross(cur), id, bob)
398 // staging moves NOTHING: owner, both quotas unchanged
399 uassert.Equal(t, string(owner), string(splits[id].Owner))
400 uassert.Equal(t, string(bob), string(splits[id].PendingOwner))
401 uassert.Equal(t, 1, ownerSplits[owner])
402 uassert.Equal(t, 0, ownerSplits[bob])
403
404 testing.SetRealm(testing.NewUserRealm(bob))
405 AcceptOwnership(cross(cur), id)
406 uassert.Equal(t, string(bob), string(splits[id].Owner))
407 uassert.Equal(t, "", string(splits[id].PendingOwner))
408}
409
410func TestTransferOwnership_WrongOwner(cur realm, t *testing.T) {
411 resetState()
412 testing.SetRealm(testing.NewUserRealm(owner))
413 id := CreateSplit(cross(cur), addrList(alice), "10000")
414
415 testing.SetRealm(testing.NewUserRealm(attacker))
416 uassert.AbortsWithMessage(t, cur,
417 "only the owner can transfer ownership",
418 func() { TransferOwnership(cross(cur), id, attacker) },
419 )
420}
421
422func TestAcceptOwnership_OnlyNominee(cur realm, t *testing.T) {
423 resetState()
424 testing.SetRealm(testing.NewUserRealm(owner))
425 id := CreateSplit(cross(cur), addrList(alice), "10000")
426 TransferOwnership(cross(cur), id, bob)
427
428 // a non-nominee cannot accept — including with no nomination staged
429 testing.SetRealm(testing.NewUserRealm(attacker))
430 uassert.AbortsWithMessage(t, cur,
431 "caller is not the pending owner",
432 func() { AcceptOwnership(cross(cur), id) },
433 )
434 // the owner clears the nomination; the ex-nominee can no longer accept
435 testing.SetRealm(testing.NewUserRealm(owner))
436 TransferOwnership(cross(cur), id, "")
437 testing.SetRealm(testing.NewUserRealm(bob))
438 uassert.AbortsWithMessage(t, cur,
439 "caller is not the pending owner",
440 func() { AcceptOwnership(cross(cur), id) },
441 )
442}
443
444func TestAcceptOwnership_MovesOwnerSlotAtConsent(cur realm, t *testing.T) {
445 resetState()
446 testing.SetRealm(testing.NewUserRealm(owner))
447 id := CreateSplit(cross(cur), addrList(alice), "10000")
448 uassert.Equal(t, 1, ownerSplits[owner])
449 TransferOwnership(cross(cur), id, bob)
450
451 testing.SetRealm(testing.NewUserRealm(bob))
452 AcceptOwnership(cross(cur), id)
453
454 // the creator's slot is freed; the acceptor's is consumed
455 uassert.Equal(t, 0, ownerSplits[owner])
456 uassert.Equal(t, 1, ownerSplits[bob])
457
458 // and the creator can fill the freed slot again
459 testing.SetRealm(testing.NewUserRealm(owner))
460 for i := 0; i < MaxSplitsPerOwner; i++ {
461 CreateSplit(cross(cur), addrList(alice), "10000")
462 }
463}
464
465func TestAcceptOwnership_AcceptorAtLimitBlocked(cur realm, t *testing.T) {
466 resetState()
467 testing.SetRealm(testing.NewUserRealm(bob))
468 for i := 0; i < MaxSplitsPerOwner; i++ {
469 CreateSplit(cross(cur), addrList(alice), "10000")
470 }
471
472 testing.SetRealm(testing.NewUserRealm(owner))
473 id := CreateSplit(cross(cur), addrList(alice), "10000")
474 TransferOwnership(cross(cur), id, bob)
475
476 // the quota gate fires at ACCEPT, against the acceptor's own count
477 testing.SetRealm(testing.NewUserRealm(bob))
478 uassert.AbortsWithMessage(t, cur,
479 "new owner is at the per-owner split limit",
480 func() { AcceptOwnership(cross(cur), id) },
481 )
482}
483
484// The v2 audit Y1 griefing scenario is dead: staging a transfer at a
485// victim consumes none of the victim's quota and changes nothing the
486// victim must act on.
487func TestTransferOwnership_UnconsentedGriefDefused(cur realm, t *testing.T) {
488 resetState()
489 testing.SetRealm(testing.NewUserRealm(attacker))
490 for i := 0; i < MaxSplitsPerOwner; i++ {
491 id := CreateSplit(cross(cur), addrList(alice), "10000")
492 TransferOwnership(cross(cur), id, bob) // staged at the victim, never accepted
493 }
494 // victim quota untouched; victim can still create a full quota of splits
495 uassert.Equal(t, 0, ownerSplits[bob])
496 testing.SetRealm(testing.NewUserRealm(bob))
497 for i := 0; i < MaxSplitsPerOwner; i++ {
498 CreateSplit(cross(cur), addrList(alice), "10000")
499 }
500 // while the attacker's own budget stays consumed — as documented
501 uassert.Equal(t, MaxSplitsPerOwner, ownerSplits[attacker])
502}
503
504// Self-transfer is refused at STAGE time. This guard is what makes
505// self-accept unreachable, and so what keeps AcceptOwnership's -1/+1
506// quota pair from collapsing onto one address (v2 audit G-7.1).
507func TestTransferOwnership_SelfRefused(cur realm, t *testing.T) {
508 resetState()
509 testing.SetRealm(testing.NewUserRealm(owner))
510 id := CreateSplit(cross(cur), addrList(alice), "10000")
511
512 uassert.AbortsWithMessage(t, cur,
513 "new owner is already the owner",
514 func() { TransferOwnership(cross(cur), id, owner) },
515 )
516 uassert.Equal(t, 1, ownerSplits[owner])
517}
518
519// Re-staging overwrites the prior nomination; only the latest nominee
520// can accept (v2 audit G-7.4).
521func TestTransferOwnership_RestageOverwrites(cur realm, t *testing.T) {
522 resetState()
523 testing.SetRealm(testing.NewUserRealm(owner))
524 id := CreateSplit(cross(cur), addrList(alice), "10000")
525 TransferOwnership(cross(cur), id, bob)
526 TransferOwnership(cross(cur), id, alice)
527 uassert.Equal(t, string(alice), GetPendingOwner(id))
528
529 testing.SetRealm(testing.NewUserRealm(bob))
530 uassert.AbortsWithMessage(t, cur,
531 "caller is not the pending owner",
532 func() { AcceptOwnership(cross(cur), id) },
533 )
534 testing.SetRealm(testing.NewUserRealm(alice))
535 AcceptOwnership(cross(cur), id)
536 uassert.Equal(t, string(alice), string(splits[id].Owner))
537}
538
539// A nomination on a split that is later archived is inert — the doc
540// comment on AcceptOwnership claims it, this pins it (v2 audit G-7.2).
541func TestAcceptOwnership_ArchivedRefused(cur realm, t *testing.T) {
542 resetState()
543 testing.SetRealm(testing.NewUserRealm(owner))
544 id := CreateSplit(cross(cur), addrList(alice), "10000")
545 TransferOwnership(cross(cur), id, bob)
546 Archive(cross(cur), id)
547
548 testing.SetRealm(testing.NewUserRealm(bob))
549 uassert.AbortsWithMessage(t, cur, "split is archived: "+id,
550 func() { AcceptOwnership(cross(cur), id) },
551 )
552 // the archived split's slot stays returned to its creator, not the nominee
553 uassert.Equal(t, 0, ownerSplits[owner])
554 uassert.Equal(t, 0, ownerSplits[bob])
555}
556
557// Freezing locks shares and deposits, not custody: a frozen split stays
558// transferable so its owner can hand off claim-servicing duty
559// (v2 audit G-7.3).
560func TestAcceptOwnership_FrozenAllowed(cur realm, t *testing.T) {
561 resetState()
562 testing.SetRealm(testing.NewUserRealm(owner))
563 id := CreateSplit(cross(cur), addrList(alice), "10000")
564 TransferOwnership(cross(cur), id, bob)
565 Freeze(cross(cur), id)
566
567 testing.SetRealm(testing.NewUserRealm(bob))
568 AcceptOwnership(cross(cur), id)
569 uassert.Equal(t, string(bob), string(splits[id].Owner))
570 uassert.True(t, splits[id].Frozen)
571}
572
573// The nominee can read the offer without paying gas to probe it, and the
574// pending line disappears once consumed or cleared (v2 audit Y1-obs).
575func TestPendingOwner_ReadSurface(cur realm, t *testing.T) {
576 resetState()
577 testing.SetRealm(testing.NewUserRealm(owner))
578 id := CreateSplit(cross(cur), addrList(alice), "10000")
579 uassert.Equal(t, "", GetPendingOwner(id))
580 uassert.False(t, strings.Contains(GetSplitInfo(id), "Pending owner:"))
581
582 TransferOwnership(cross(cur), id, bob)
583 uassert.Equal(t, string(bob), GetPendingOwner(id))
584 uassert.True(t, strings.Contains(GetSplitInfo(id), "Pending owner: "+string(bob)))
585 uassert.True(t, strings.Contains(Render(""), "Pending owner: "+string(bob)))
586
587 testing.SetRealm(testing.NewUserRealm(bob))
588 AcceptOwnership(cross(cur), id)
589 uassert.Equal(t, "", GetPendingOwner(id))
590 uassert.False(t, strings.Contains(GetSplitInfo(id), "Pending owner:"))
591}
592
593// ---------- Freeze ----------
594
595func TestFreeze_Basic(cur realm, t *testing.T) {
596 resetState()
597 testing.SetRealm(testing.NewUserRealm(owner))
598 id := CreateSplit(cross(cur), addrList(alice), "10000")
599
600 Freeze(cross(cur), id)
601 uassert.True(t, splits[id].Frozen, "split must be frozen after Freeze()")
602}
603
604func TestFreeze_WrongOwner(cur realm, t *testing.T) {
605 resetState()
606 testing.SetRealm(testing.NewUserRealm(owner))
607 id := CreateSplit(cross(cur), addrList(alice), "10000")
608
609 testing.SetRealm(testing.NewUserRealm(attacker))
610 uassert.AbortsWithMessage(t, cur,
611 "only the owner can freeze",
612 func() { Freeze(cross(cur), id) },
613 )
614}
615
616func TestFreeze_ClaimsStillWork(cur realm, t *testing.T) {
617 resetState()
618 testing.SetRealm(testing.NewUserRealm(owner))
619 id := CreateSplit(cross(cur), addrList(alice), "10000")
620 deposit(cur, owner, id, 100)
621
622 testing.SetRealm(testing.NewUserRealm(owner))
623 Freeze(cross(cur), id)
624
625 testing.SetRealm(testing.NewUserRealm(alice))
626 uassert.Equal(t, int64(100), Claim(cross(cur), id))
627}
628
629// ---------- Archive ----------
630
631func TestArchive_BlocksWithOutstandingBalance(cur realm, t *testing.T) {
632 resetState()
633 testing.SetRealm(testing.NewUserRealm(owner))
634 id := CreateSplit(cross(cur), addrList(alice), "10000")
635 deposit(cur, owner, id, 100)
636
637 testing.SetRealm(testing.NewUserRealm(owner))
638 uassert.AbortsWithMessage(t, cur,
639 "cannot archive: outstanding balance for "+string(alice),
640 func() { Archive(cross(cur), id) },
641 )
642}
643
644func TestArchive_BlocksOnExRecipientBalance(cur realm, t *testing.T) {
645 resetState()
646 testing.SetRealm(testing.NewUserRealm(owner))
647 id := CreateSplit(cross(cur), addrList(alice, bob), "5000,5000")
648 deposit(cur, owner, id, 1000)
649
650 testing.SetRealm(testing.NewUserRealm(owner))
651 // remove bob; his 500 stays claimable
652 UpdateShares(cross(cur), id, addrList(alice), "10000")
653 testing.SetRealm(testing.NewUserRealm(alice))
654 Claim(cross(cur), id)
655
656 // archive must refuse while the EX-recipient still has a balance —
657 // archiving would lock his claim forever
658 testing.SetRealm(testing.NewUserRealm(owner))
659 uassert.AbortsWithMessage(t, cur,
660 "cannot archive: outstanding balance for "+string(bob),
661 func() { Archive(cross(cur), id) },
662 )
663
664 // once bob claims, archive succeeds
665 testing.SetRealm(testing.NewUserRealm(bob))
666 Claim(cross(cur), id)
667 testing.SetRealm(testing.NewUserRealm(owner))
668 Archive(cross(cur), id)
669 uassert.True(t, splits[id].Archived, "split must be archived")
670}
671
672// ---------- queries ----------
673
674func TestGetClaimable(cur realm, t *testing.T) {
675 resetState()
676 testing.SetRealm(testing.NewUserRealm(owner))
677 id := CreateSplit(cross(cur), addrList(alice, bob), "8000,2000")
678 deposit(cur, owner, id, 5000)
679
680 uassert.Equal(t, int64(4000), GetClaimable(id, alice))
681 uassert.Equal(t, int64(1000), GetClaimable(id, bob))
682 uassert.Equal(t, int64(0), GetClaimable(id, attacker))
683}
684
685func TestGetSplitInfo_ContainsExpectedFields(cur realm, t *testing.T) {
686 resetState()
687 testing.SetRealm(testing.NewUserRealm(owner))
688 id := CreateSplit(cross(cur), addrList(alice, bob), "5000,5000")
689 deposit(cur, owner, id, 200)
690
691 info := GetSplitInfo(id)
692 uassert.True(t, strings.Contains(info, "Owner"), "GetSplitInfo output must contain 'Owner'")
693 uassert.True(t, strings.Contains(info, string(owner)), "GetSplitInfo output must contain owner address")
694 uassert.True(t, strings.Contains(info, string(alice)), "GetSplitInfo output must contain recipient")
695}
696
697// ---------- render ----------
698
699func TestRender_EmptyState(cur realm, t *testing.T) {
700 resetState()
701
702 out := Render("")
703 uassert.Equal(t, "# Fee Split\n\nNo active splits.\n", out)
704}
705
706func TestRender_WithSplits(cur realm, t *testing.T) {
707 resetState()
708 testing.SetRealm(testing.NewUserRealm(owner))
709 CreateSplit(cross(cur), addrList(alice, bob), "6000,4000")
710 deposit(cur, owner, "split_1", 1000)
711
712 out := Render("")
713 uassert.True(t, strings.Contains(out, "split_1"), "Render output must reference split_1")
714 uassert.True(t, strings.Contains(out, string(owner)), "Render output must include owner")
715}
716
717// ---------- re-audit regressions (2026-09-02) ----------
718
719func TestCreateSplit_ShareSumWraparoundRejected(cur realm, t *testing.T) {
720 resetState()
721 testing.SetRealm(testing.NewUserRealm(owner))
722
723 // re-audit P1: these shares sum past 2^64 back to exactly 10000; the
724 // per-share <=10000 bound must refuse them before the sum can wrap
725 uassert.AbortsWithMessage(t, cur,
726 "invalid share value: 1152921505606846976",
727 func() {
728 CreateSplit(cross(cur), addrList(alice, bob, carol),
729 "1152921505606846976,8646911284051357320,8646911284051357320")
730 },
731 )
732 uassert.AbortsWithMessage(t, cur,
733 "invalid share value: 10001",
734 func() { CreateSplit(cross(cur), addrList(alice, bob), "10001,10000") },
735 )
736}
737
738func TestArchiveFreesActiveListAndOwnerSlot(cur realm, t *testing.T) {
739 resetState()
740 testing.SetRealm(testing.NewUserRealm(owner))
741
742 id := CreateSplit(cross(cur), addrList(alice), "10000")
743 Archive(cross(cur), id)
744 uassert.Equal(t, 1, len(splits), "archived entry stays for audit")
745 uassert.Equal(t, 0, len(splitIDs), "archived entry frees the active list")
746 CreateSplit(cross(cur), addrList(alice), "10000") // must not abort
747}
748
749func TestNoGlobalCap_SybilsCannotBlockOthers(cur realm, t *testing.T) {
750 resetState()
751
752 // round-4 P2: with per-owner quotas only, other owners filling their
753 // own budgets never block a legitimate creator
754 for i := 0; i < 30; i++ {
755 syb := testutils.TestAddress("sybil_" + strconv.Itoa(i))
756 testing.SetRealm(testing.NewUserRealm(syb))
757 for j := 0; j < MaxSplitsPerOwner; j++ {
758 CreateSplit(cross(cur), addrList(alice), "10000")
759 }
760 }
761 uassert.Equal(t, 300, len(splitIDs))
762
763 testing.SetRealm(testing.NewUserRealm(owner))
764 CreateSplit(cross(cur), addrList(alice), "10000") // must not abort
765}
766
767func TestPreParseInputBounds(cur realm, t *testing.T) {
768 resetState()
769 testing.SetRealm(testing.NewUserRealm(owner))
770
771 // round-4 P2: oversized inputs must be refused BEFORE parsing work
772 uassert.AbortsWithMessage(t, cur,
773 "recipient list too long",
774 func() { CreateSplit(cross(cur), strings.Repeat(",", MaxRecipientListLen+1), "10000") },
775 )
776 uassert.AbortsWithMessage(t, cur,
777 "share list too long",
778 func() { CreateSplit(cross(cur), addrList(alice), strings.Repeat("1,", MaxShareListLen)) },
779 )
780}
781
782func TestStraySendRejected(cur realm, t *testing.T) {
783 resetState()
784 testing.SetRealm(testing.NewUserRealm(owner))
785 id := CreateSplit(cross(cur), addrList(alice), "10000")
786 deposit(cur, owner, id, 100)
787
788 // re-audit P3: coins attached to a non-Deposit call must abort (and
789 // therefore revert to the sender), never strand on the realm address
790 testing.SetRealm(testing.NewUserRealm(alice))
791 testing.SetOriginSend(chain.Coins{{Denom: Denom, Amount: 50}})
792 uassert.AbortsWithMessage(t, cur,
793 "this function does not accept coins; attach coins to Deposit only",
794 func() { Claim(cross(cur), id) },
795 )
796 testing.SetOriginSend(nil)
797
798 // without coins attached the same claim succeeds
799 uassert.Equal(t, int64(100), Claim(cross(cur), id))
800}
801
802func TestDeposit_CodeRealmRefused(cur realm, t *testing.T) {
803 resetState()
804 testing.SetRealm(testing.NewUserRealm(owner))
805 id := CreateSplit(cross(cur), addrList(alice), "10000")
806
807 // re-audit P1 companion: only a direct user MsgCall may deposit — a
808 // code realm as previous realm must be refused (its send would not
809 // have landed here). The MsgRun/ephemeral shape (the other half of
810 // the IsUserCall-not-IsUser rationale) cannot be constructed in this
811 // driver: SetRealm accepts only user realms and /r/ code realms
812 // (NewCodeRealm rejects /e/ paths), so the predicate choice is
813 // pinned here via the code-realm branch and verified live on-chain.
814 testing.SetRealm(testing.NewCodeRealm("gno.land/r/demo/intermediary"))
815 testing.SetOriginSend(chain.Coins{{Denom: Denom, Amount: 100}})
816 uassert.AbortsContains(t, cur,
817 "deposits must be sent by direct call",
818 func() { Deposit(cross(cur), id) },
819 )
820 testing.SetOriginSend(nil)
821}
822
823// ---------- round-3 audit regressions (2026-09-02) ----------
824
825func TestUppercaseAddressesRejected(cur realm, t *testing.T) {
826 resetState()
827 testing.SetRealm(testing.NewUserRealm(owner))
828
829 // round-3 P2: all-uppercase bech32 passes IsValid but can never
830 // match caller(): as a recipient it locks funds and blocks Archive;
831 // as upper/lower twins it bypasses the duplicate check; as an owner
832 // it orphans the split
833 upper := strings.ToUpper(string(alice))
834 uassert.AbortsWithMessage(t, cur,
835 "invalid recipient address: "+upper,
836 func() { CreateSplit(cross(cur), string(alice)+","+upper, "5000,5000") },
837 )
838
839 id := CreateSplit(cross(cur), addrList(alice), "10000")
840 uassert.AbortsWithMessage(t, cur,
841 "invalid new owner address: "+upper,
842 func() { TransferOwnership(cross(cur), id, address(upper)) },
843 )
844}
845
846func TestRender_OnlyArchivedSplits(cur realm, t *testing.T) {
847 resetState()
848 testing.SetRealm(testing.NewUserRealm(owner))
849 id := CreateSplit(cross(cur), addrList(alice), "10000")
850 Archive(cross(cur), id)
851
852 uassert.Equal(t, "# Fee Split\n\nNo active splits.\n", Render(""),
853 "archived-only state must render as no active splits")
854}
855
856// ---------- protocol fee (2026-09-03) ----------
857
858func TestFee_DefaultZeroAndCap(cur realm, t *testing.T) {
859 resetState()
860 uassert.Equal(t, int64(0), feeBps)
861
862 testing.SetRealm(testing.NewUserRealm(feeAdminAddr))
863 uassert.AbortsContains(t, cur, "fee must be between 0 and",
864 func() { SetFee(cross(cur), MaxFeeBps+1) })
865 uassert.AbortsContains(t, cur, "fee must be between 0 and",
866 func() { SetFee(cross(cur), -1) })
867 SetFee(cross(cur), MaxFeeBps) // exactly the cap is allowed
868 uassert.Equal(t, MaxFeeBps, feeBps)
869
870 testing.SetRealm(testing.NewUserRealm(attacker))
871 uassert.AbortsWithMessage(t, cur, "only the fee admin can set the fee",
872 func() { SetFee(cross(cur), 1) })
873}
874
875func TestFee_TakenFromDepositNetAccounting(cur realm, t *testing.T) {
876 resetState()
877 testing.SetRealm(testing.NewUserRealm(feeAdminAddr))
878 SetFee(cross(cur), 50) // 0.5%
879
880 testing.SetRealm(testing.NewUserRealm(owner))
881 id := CreateSplit(cross(cur), addrList(alice, bob), "5000,5000")
882 deposit(cur, owner, id, 10000)
883
884 s := splits[id]
885 // fee 50 off the top; 9950 net distributed (4975 + 4975)
886 uassert.Equal(t, int64(50), feesAccrued)
887 uassert.Equal(t, int64(9950), s.TotalDeposited)
888 uassert.Equal(t, int64(4975), s.Balances[alice])
889 uassert.Equal(t, int64(4975), s.Balances[bob])
890 // conservation on net amounts holds untouched
891 uassert.Equal(t, s.TotalDeposited, s.Balances[alice]+s.Balances[bob]+s.TotalClaimed)
892}
893
894func TestFee_NeverRetroactive(cur realm, t *testing.T) {
895 resetState()
896 testing.SetRealm(testing.NewUserRealm(owner))
897 id := CreateSplit(cross(cur), addrList(alice), "10000")
898 deposit(cur, owner, id, 10000) // fee 0 at deposit time
899
900 testing.SetRealm(testing.NewUserRealm(feeAdminAddr))
901 SetFee(cross(cur), MaxFeeBps)
902
903 // the previously credited balance is untouched by the fee change
904 uassert.Equal(t, int64(10000), splits[id].Balances[alice])
905 testing.SetRealm(testing.NewUserRealm(alice))
906 uassert.Equal(t, int64(10000), Claim(cross(cur), id))
907}
908
909func TestFee_ClaimFeesSendsCoins(cur realm, t *testing.T) {
910 resetState()
911 testing.SetRealm(testing.NewUserRealm(feeAdminAddr))
912 SetFee(cross(cur), 100) // 1%
913
914 testing.SetRealm(testing.NewUserRealm(owner))
915 id := CreateSplit(cross(cur), addrList(alice), "10000")
916 deposit(cur, owner, id, 50000) // fee 500
917
918 testing.SetRealm(testing.NewUserRealm(attacker))
919 uassert.AbortsWithMessage(t, cur, "only the fee admin can claim fees",
920 func() { ClaimFees(cross(cur)) })
921
922 before := banker.NewReadonlyBanker().GetCoins(feeAdminAddr).AmountOf(Denom)
923 testing.SetRealm(testing.NewUserRealm(feeAdminAddr))
924 uassert.Equal(t, int64(500), ClaimFees(cross(cur)))
925 after := banker.NewReadonlyBanker().GetCoins(feeAdminAddr).AmountOf(Denom)
926 uassert.Equal(t, int64(500), after-before)
927 uassert.Equal(t, int64(0), feesAccrued)
928 uassert.Equal(t, int64(500), feesClaimed)
929
930 uassert.AbortsWithMessage(t, cur, "no fees accrued", func() { ClaimFees(cross(cur)) })
931}
932
933func TestFee_TinyDepositFullyConsumedRefused(cur realm, t *testing.T) {
934 resetState()
935 testing.SetRealm(testing.NewUserRealm(feeAdminAddr))
936 SetFee(cross(cur), MaxFeeBps) // 1%
937
938 testing.SetRealm(testing.NewUserRealm(owner))
939 id := CreateSplit(cross(cur), addrList(alice), "10000")
940 // amounts < 100 at 1% round the fee to 0 — deposit passes whole
941 deposit(cur, owner, id, 99)
942 uassert.Equal(t, int64(0), feesAccrued)
943 uassert.Equal(t, int64(99), splits[id].Balances[alice])
944}
945
946func TestFee_TwoStepAdminHandover(cur realm, t *testing.T) {
947 resetState()
948 newAdmin := testutils.TestAddress("newfeeadmin")
949
950 testing.SetRealm(testing.NewUserRealm(attacker))
951 uassert.AbortsWithMessage(t, cur, "only the fee admin can nominate a successor",
952 func() { NominateFeeAdmin(cross(cur), attacker) })
953
954 testing.SetRealm(testing.NewUserRealm(feeAdminAddr))
955 uassert.AbortsContains(t, cur, "invalid nominee address",
956 func() { NominateFeeAdmin(cross(cur), address("JUNK")) })
957 NominateFeeAdmin(cross(cur), newAdmin)
958 // nomination alone changes nothing
959 uassert.Equal(t, string(feeAdminAddr), string(feeAdmin))
960
961 testing.SetRealm(testing.NewUserRealm(attacker))
962 uassert.AbortsWithMessage(t, cur, "caller is not the pending fee admin",
963 func() { AcceptFeeAdmin(cross(cur)) })
964
965 testing.SetRealm(testing.NewUserRealm(newAdmin))
966 AcceptFeeAdmin(cross(cur))
967 uassert.Equal(t, string(newAdmin), string(feeAdmin))
968 uassert.Equal(t, "", string(pendingFeeAdmin))
969
970 // clearing a nomination works
971 testing.SetRealm(testing.NewUserRealm(newAdmin))
972 NominateFeeAdmin(cross(cur), feeAdminAddr)
973 NominateFeeAdmin(cross(cur), "")
974 testing.SetRealm(testing.NewUserRealm(feeAdminAddr))
975 uassert.AbortsWithMessage(t, cur, "caller is not the pending fee admin",
976 func() { AcceptFeeAdmin(cross(cur)) })
977}
978
979func TestFee_StraySendRejectedOnFeeFunctions(cur realm, t *testing.T) {
980 resetState()
981 testing.SetRealm(testing.NewUserRealm(feeAdminAddr))
982 testing.SetOriginSend(chain.Coins{{Denom: Denom, Amount: 5}})
983 uassert.AbortsWithMessage(t, cur,
984 "this function does not accept coins; attach coins to Deposit only",
985 func() { SetFee(cross(cur), 10) },
986 )
987 testing.SetOriginSend(nil)
988}
989
990// ---------- round-5 audit regressions (fee path, 2026-09-03) ----------
991
992// Pins the benign-degradation safety property: an empty fee admin (the
993// gno-test-VM init state, and any hypothetical empty-origin deploy)
994// leaves the fee permanently disabled and zero funds at risk — SetFee
995// can never be authorized, so feeBps stays 0 and nothing accrues.
996func TestFee_EmptyAdminDegradesToDisabled(cur realm, t *testing.T) {
997 resetState()
998 feeAdmin = ""
999 feeBps = 0
1000
1001 for _, who := range []address{owner, alice, feeAdminAddr, attacker} {
1002 testing.SetRealm(testing.NewUserRealm(who))
1003 uassert.AbortsWithMessage(t, cur, "only the fee admin can set the fee",
1004 func() { SetFee(cross(cur), 100) })
1005 }
1006 uassert.Equal(t, int64(0), feeBps, "fee must remain 0 with no reachable admin")
1007
1008 // a deposit accrues nothing
1009 testing.SetRealm(testing.NewUserRealm(owner))
1010 id := CreateSplit(cross(cur), addrList(alice), "10000")
1011 deposit(cur, owner, id, 10000)
1012 uassert.Equal(t, int64(0), feesAccrued)
1013 testing.SetRealm(testing.NewUserRealm(alice))
1014 uassert.Equal(t, int64(10000), Claim(cross(cur), id)) // full amount, no fee
1015}
1016
1017// Fees accrued before a handover are claimable by the NEW admin
1018// (documented semantics: claim before transferring if that matters).
1019func TestFee_HandoverTransfersAccruedClaimRight(cur realm, t *testing.T) {
1020 resetState()
1021 newFa := testutils.TestAddress("newfa")
1022 testing.SetRealm(testing.NewUserRealm(feeAdminAddr))
1023 SetFee(cross(cur), 100)
1024 testing.SetRealm(testing.NewUserRealm(owner))
1025 id := CreateSplit(cross(cur), addrList(alice), "10000")
1026 deposit(cur, owner, id, 100000) // 1000 accrued under the original admin
1027
1028 testing.SetRealm(testing.NewUserRealm(feeAdminAddr))
1029 NominateFeeAdmin(cross(cur), newFa)
1030 testing.SetRealm(testing.NewUserRealm(newFa))
1031 AcceptFeeAdmin(cross(cur))
1032 uassert.Equal(t, int64(1000), ClaimFees(cross(cur)), "new admin claims prior-regime fees")
1033
1034 // the old admin can no longer claim — fails the authorization check
1035 testing.SetRealm(testing.NewUserRealm(feeAdminAddr))
1036 uassert.AbortsWithMessage(t, cur, "only the fee admin can claim fees", func() { ClaimFees(cross(cur)) })
1037}
1038
1039// Every fee function rejects an attached send (only SetFee was pinned).
1040func TestFee_AllFunctionsRejectStraySend(cur realm, t *testing.T) {
1041 resetState()
1042 testing.SetRealm(testing.NewUserRealm(feeAdminAddr))
1043 send := chain.Coins{{Denom: Denom, Amount: 3}}
1044 msg := "this function does not accept coins; attach coins to Deposit only"
1045
1046 testing.SetOriginSend(send)
1047 uassert.AbortsWithMessage(t, cur, msg, func() { SetFee(cross(cur), 10) })
1048 uassert.AbortsWithMessage(t, cur, msg, func() { NominateFeeAdmin(cross(cur), attacker) })
1049 uassert.AbortsWithMessage(t, cur, msg, func() { ClaimFees(cross(cur)) })
1050 testing.SetOriginSend(nil)
1051
1052 // AcceptFeeAdmin from the pending side, also with a send
1053 NominateFeeAdmin(cross(cur), attacker)
1054 testing.SetRealm(testing.NewUserRealm(attacker))
1055 testing.SetOriginSend(send)
1056 uassert.AbortsWithMessage(t, cur, msg, func() { AcceptFeeAdmin(cross(cur)) })
1057 testing.SetOriginSend(nil)
1058}
1059
1060// ---------- audit round-1 gap closures (2026-09-21) ----------
1061
1062func TestStraySendRejectedOnAllSplitFunctions(cur realm, t *testing.T) {
1063 resetState()
1064 testing.SetRealm(testing.NewUserRealm(owner))
1065 id := CreateSplit(cross(cur), addrList(alice, bob), "5000,5000")
1066
1067 // audit Y2 gap 2: only Claim and the fee functions were pinned; the
1068 // five split-management functions call the same guard — prove it
1069 msg := "this function does not accept coins; attach coins to Deposit only"
1070 testing.SetOriginSend(chain.Coins{{Denom: Denom, Amount: 7}})
1071 uassert.AbortsWithMessage(t, cur, msg, func() { CreateSplit(cross(cur), addrList(carol), "10000") })
1072 uassert.AbortsWithMessage(t, cur, msg, func() { UpdateShares(cross(cur), id, addrList(alice), "10000") })
1073 uassert.AbortsWithMessage(t, cur, msg, func() { TransferOwnership(cross(cur), id, carol) })
1074 uassert.AbortsWithMessage(t, cur, msg, func() { AcceptOwnership(cross(cur), id) })
1075 uassert.AbortsWithMessage(t, cur, msg, func() { Freeze(cross(cur), id) })
1076 uassert.AbortsWithMessage(t, cur, msg, func() { Archive(cross(cur), id) })
1077 testing.SetOriginSend(nil)
1078}
1079
1080func TestArchivedSplitRefusesDepositAndClaim(cur realm, t *testing.T) {
1081 resetState()
1082 testing.SetRealm(testing.NewUserRealm(owner))
1083 id := CreateSplit(cross(cur), addrList(alice), "10000")
1084 deposit(cur, owner, id, 100)
1085 testing.SetRealm(testing.NewUserRealm(alice))
1086 Claim(cross(cur), id)
1087 testing.SetRealm(testing.NewUserRealm(owner))
1088 Archive(cross(cur), id)
1089
1090 // audit Y2 gap 3: the terminal state must refuse both value paths
1091 testing.SetRealm(testing.NewUserRealm(owner))
1092 testing.SetOriginSend(chain.Coins{{Denom: Denom, Amount: 50}})
1093 uassert.AbortsWithMessage(t, cur, "split is archived: "+id,
1094 func() { Deposit(cross(cur), id) })
1095 testing.SetOriginSend(nil)
1096 testing.SetRealm(testing.NewUserRealm(alice))
1097 uassert.AbortsWithMessage(t, cur, "split is archived: "+id,
1098 func() { Claim(cross(cur), id) })
1099}
1100
1101func TestDeposit_TwoCoinEnvelopeRejected(cur realm, t *testing.T) {
1102 resetState()
1103 testing.SetRealm(testing.NewUserRealm(owner))
1104 id := CreateSplit(cross(cur), addrList(alice), "10000")
1105
1106 // audit Y2 gap 4: only zero-coin and wrong-denom were pinned; a
1107 // two-coin envelope must also be refused by the exact len==1 check
1108 testing.SetOriginSend(chain.Coins{{Denom: Denom, Amount: 100}, {Denom: "ufoo", Amount: 1}})
1109 uassert.AbortsWithMessage(t, cur,
1110 "deposit must send exactly one coin of "+Denom,
1111 func() { Deposit(cross(cur), id) })
1112 testing.SetOriginSend(nil)
1113 uassert.Equal(t, int64(0), splits[id].TotalDeposited)
1114}
1115
1116func TestDust_EqualShareTieGoesToFirst(cur realm, t *testing.T) {
1117 resetState()
1118 testing.SetRealm(testing.NewUserRealm(owner))
1119 id := CreateSplit(cross(cur), addrList(alice, bob), "5000,5000")
1120
1121 // audit Y2 gap 5: the dust rule at an exact share tie is
1122 // first-listed-wins (strict > never replaces dustIdx) — pin it
1123 deposit(cur, owner, id, 101)
1124 uassert.Equal(t, int64(51), splits[id].Balances[alice])
1125 uassert.Equal(t, int64(50), splits[id].Balances[bob])
1126}