Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}