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

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}