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.

budgets_test.gno

47.50 Kb · 1275 lines
   1package budgets
   2
   3import (
   4	"strings"
   5	"testing"
   6
   7	"chain"
   8
   9	"gno.land/p/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/coinio"
  10	"gno.land/p/nt/testutils/v0"
  11	"gno.land/p/nt/uassert/v0"
  12)
  13
  14var (
  15	owner1    = testutils.TestAddress("bgowner1")
  16	owner2    = testutils.TestAddress("bgowner2")
  17	delegate1 = testutils.TestAddress("bgdeleg1")
  18	delegate2 = testutils.TestAddress("bgdeleg2")
  19	mallory   = testutils.TestAddress("bgmallory")
  20)
  21
  22// HARNESS NOTES, carried from the crowdfund, treasury_board and vesting
  23// suites (measured there, relied on here):
  24//
  25//   - testing.SetRealm records the override per FRAME INDEX: stage the
  26//     caller INLINE in this test function's own frame before a uassert
  27//     call, or inside a helper that stages and calls in one frame of its
  28//     own. Never inside a uassert closure — that staging does not take.
  29//
  30//   - testing.SkipHeights REPLACES the whole context (OriginCaller,
  31//     CurrentRealm, OriginSend included). Re-stage after every skip.
  32//
  33//   - Realm globals PERSIST across tests while banker state resets per
  34//     test. begin() disarms any leftover envelope and re-seeds the
  35//     realm's bank to its carried liabilities, so solvency is asserted in
  36//     DELTA form and every test tidies up after itself.
  37//
  38//   - A panic caught by uassert does NOT roll back realm globals here,
  39//     whereas on-chain the whole transaction reverts. Every abort
  40//     asserted below happens strictly BEFORE this realm writes state, and
  41//     the one case where that is the POINT of the test
  42//     (TestDrawSolvencyRefusalConsumesNothing) asserts the untouched
  43//     state directly rather than relying on a rollback that does not
  44//     happen in this harness.
  45//
  46//   - The SEND envelope is process-global: a staged envelope must be
  47//     cleared (SetOriginSend(nil)) before the next non-payable call, or
  48//     rejectStraySend fires and the test asserts the harness leak instead
  49//     of the guard.
  50//
  51// unsafe.CurrentRealm() is not available to a unit test, so `self` is
  52// seeded from the package path the same way the chain derives it.
  53func init() {
  54	self = chain.PackageAddress(
  55		"gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/budgets")
  56}
  57
  58type baseline struct {
  59	held   int64
  60	pooled int64
  61	credit int64
  62	fees   int64
  63}
  64
  65func begin() baseline {
  66	testing.SetOriginSend(nil)
  67	if l := Liabilities(); l > 0 {
  68		testing.IssueCoins(Address(), ugnot(l))
  69	}
  70	return baseline{
  71		held:   Held(),
  72		pooled: TotalPooled(),
  73		credit: UnclaimedCredit(),
  74		fees:   FeesAccrued(),
  75	}
  76}
  77
  78func ugnot(n int64) chain.Coins {
  79	return chain.NewCoins(chain.NewCoin(Denom, n))
  80}
  81
  82// assertSolvent asserts the two halves of H == P + L + S that can actually
  83// FAIL. The identity itself is definitional here, because Surplus() is
  84// defined as Held() - Liabilities(), so asserting it would be a tautology
  85// dressed as a check. What can fail is that the realm owes more than it
  86// holds, and that L decomposes exactly into delegate credit plus the fee
  87// pot — neither of which follows from any definition in this file.
  88// It also re-derives the P term the slow way. totalPooled is maintained
  89// O(1) for Render's sake, so it is a CACHE of the pools tree, and nothing
  90// in the realm ever recomputes it — exactly the shape that drifts silently.
  91// Sabotage found this gap rather than review: disabling the per-owner debit
  92// in Draw while leaving totalPooled correct was detected only by a later
  93// native-Go banker panic, never by an assertion.
  94func assertSolvent(t *testing.T) {
  95	uassert.True(t, Held() >= Liabilities())
  96	uassert.Equal(t, TotalPooled()+UnclaimedCredit()+FeesAccrued(), Liabilities())
  97
  98	sum := int64(0)
  99	pools.Iterate("", "", func(_ string, v any) bool {
 100		n := v.(int64)
 101		// setPool removes a zeroed entry rather than storing it, so every
 102		// surviving entry must be strictly positive.
 103		uassert.True(t, n > 0)
 104		sum += n
 105		return false
 106	})
 107	uassert.Equal(t, sum, TotalPooled())
 108}
 109
 110// fundAs stages a direct EOA call with -send and mirrors the envelope into
 111// the realm's bank, as the chain would.
 112func fundAs(cur realm, from address, amount int64) int64 {
 113	testing.SetRealm(testing.NewUserRealm(from))
 114	testing.SetOriginSend(ugnot(amount))
 115	testing.IssueCoins(Address(), ugnot(amount))
 116	n := Fund(cross(cur))
 117	testing.SetOriginSend(nil)
 118	return n
 119}
 120
 121func grantAs(cur realm, from, spender address,
 122	perPeriod, periodLength, total, expiresAt int64) int64 {
 123	testing.SetRealm(testing.NewUserRealm(from))
 124	return Grant(cross(cur), spender, "", perPeriod, periodLength, total, expiresAt)
 125}
 126
 127func drawAs(cur realm, from address, id, amount int64) int64 {
 128	testing.SetRealm(testing.NewUserRealm(from))
 129	return Draw(cross(cur), id, amount)
 130}
 131
 132func claimAs(cur realm, from address) int64 {
 133	testing.SetRealm(testing.NewUserRealm(from))
 134	return Claim(cross(cur))
 135}
 136
 137func revokeAs(cur realm, from address, id int64) {
 138	testing.SetRealm(testing.NewUserRealm(from))
 139	Revoke(cross(cur), id)
 140}
 141
 142func withdrawAllAs(cur realm, from address) int64 {
 143	testing.SetRealm(testing.NewUserRealm(from))
 144	return WithdrawPoolAll(cross(cur))
 145}
 146
 147func balanceOf(addr address) int64 {
 148	return coinio.HeldAt(addr, Denom)
 149}
 150
 151// --- the pool ---
 152
 153func TestFundMakesAnOwner(cur realm, t *testing.T) {
 154	b := begin()
 155	uassert.Equal(t, int64(0), PoolOf(owner1))
 156
 157	pool := fundAs(cur, owner1, MinFunding)
 158	uassert.Equal(t, MinFunding, pool)
 159	uassert.Equal(t, MinFunding, PoolOf(owner1))
 160	uassert.Equal(t, b.pooled+MinFunding, TotalPooled())
 161	uassert.Equal(t, b.held+MinFunding, Held())
 162	uassert.Equal(t, b.credit, UnclaimedCredit())
 163	uassert.Equal(t, b.fees, FeesAccrued())
 164	assertSolvent(t)
 165
 166	// Funding again accumulates into the same pool.
 167	pool = fundAs(cur, owner1, 2*MinFunding)
 168	uassert.Equal(t, 3*MinFunding, pool)
 169	uassert.Equal(t, b.pooled+3*MinFunding, TotalPooled())
 170
 171	// A second owner's pool is independent.
 172	fundAs(cur, owner2, MinFunding)
 173	uassert.Equal(t, 3*MinFunding, PoolOf(owner1))
 174	uassert.Equal(t, MinFunding, PoolOf(owner2))
 175	assertSolvent(t)
 176
 177	uassert.Equal(t, 3*MinFunding, withdrawAllAs(cur, owner1))
 178	uassert.Equal(t, MinFunding, withdrawAllAs(cur, owner2))
 179	uassert.Equal(t, b.pooled, TotalPooled())
 180	// setPool removes a zeroed entry rather than storing 0.
 181	uassert.Equal(t, int64(0), PoolOf(owner1))
 182}
 183
 184func TestFundGuards(cur realm, t *testing.T) {
 185	begin()
 186
 187	// coinio's receipt shape: a realm-routed payment is refused by the
 188	// primitive, not by anything this file could forget to write.
 189	testing.SetRealm(testing.NewCodeRealm("gno.land/r/demo/attacker"))
 190	testing.SetOriginSend(ugnot(MinFunding))
 191	uassert.AbortsWithMessage(t, cur,
 192		"coinio: payment must be a direct EOA call with -send (realms and maketx-run are rejected)",
 193		func() { Fund(cross(cur)) })
 194	testing.SetOriginSend(nil)
 195
 196	// Wrong denomination.
 197	testing.SetRealm(testing.NewUserRealm(owner1))
 198	testing.SetOriginSend(chain.NewCoins(chain.NewCoin("foocoin", MinFunding)))
 199	uassert.AbortsWithMessage(t, cur, "coinio: send exactly one coin type: ugnot",
 200		func() { Fund(cross(cur)) })
 201	testing.SetOriginSend(nil)
 202
 203	// No envelope at all.
 204	testing.SetRealm(testing.NewUserRealm(owner1))
 205	uassert.AbortsWithMessage(t, cur, "coinio: send exactly one coin type: ugnot",
 206		func() { Fund(cross(cur)) })
 207
 208	// Below the minimum. Not issued into the bank, so the refused call
 209	// leaves no surplus behind.
 210	testing.SetRealm(testing.NewUserRealm(owner1))
 211	testing.SetOriginSend(ugnot(MinFunding - 1))
 212	uassert.AbortsWithMessage(t, cur, "fund at least 1000000ugnot",
 213		func() { Fund(cross(cur)) })
 214	testing.SetOriginSend(nil)
 215
 216	uassert.Equal(t, int64(0), PoolOf(owner1))
 217	assertSolvent(t)
 218}
 219
 220func TestWithdrawPoolIsUnconditional(cur realm, t *testing.T) {
 221	b := begin()
 222	fundAs(cur, owner1, 3*MinFunding)
 223	before := balanceOf(owner1)
 224
 225	testing.SetRealm(testing.NewUserRealm(owner1))
 226	left := WithdrawPool(cross(cur), MinFunding)
 227	uassert.Equal(t, 2*MinFunding, left)
 228	uassert.Equal(t, before+MinFunding, balanceOf(owner1))
 229	uassert.Equal(t, b.pooled+2*MinFunding, TotalPooled())
 230	assertSolvent(t)
 231
 232	testing.SetRealm(testing.NewUserRealm(owner1))
 233	uassert.AbortsWithMessage(t, cur, "amount exceeds pool",
 234		func() { WithdrawPool(cross(cur), 2*MinFunding+1) })
 235
 236	testing.SetRealm(testing.NewUserRealm(owner1))
 237	uassert.AbortsWithMessage(t, cur, "amount must be positive",
 238		func() { WithdrawPool(cross(cur), 0) })
 239
 240	// A non-owner has no pool to take, and mallory cannot reach owner1's.
 241	testing.SetRealm(testing.NewUserRealm(mallory))
 242	uassert.AbortsWithMessage(t, cur, "no pool to withdraw",
 243		func() { WithdrawPoolAll(cross(cur)) })
 244	uassert.Equal(t, 2*MinFunding, PoolOf(owner1))
 245
 246	uassert.Equal(t, 2*MinFunding, withdrawAllAs(cur, owner1))
 247	uassert.Equal(t, b.pooled, TotalPooled())
 248	assertSolvent(t)
 249}
 250
 251// TestOwnerCanAlwaysLeave pins the documented consequence of that design:
 252// an owner may empty the pool under a LIVE grant, and the delegate's next
 253// draw then fails on solvency rather than being trapped half-way. A realm
 254// that locked the capital behind the grant would be the worse realm, and
 255// this test is here so the behaviour cannot drift silently either way.
 256func TestOwnerCanAlwaysLeave(cur realm, t *testing.T) {
 257	begin()
 258	fundAs(cur, owner1, MinFunding)
 259	id := grantAs(cur, owner1, delegate1, 500_000, MinPeriodBlocks, 0, 0)
 260
 261	uassert.Equal(t, int64(500_000), Drawable(id))
 262	withdrawAllAs(cur, owner1)
 263	uassert.Equal(t, int64(0), PoolOf(owner1))
 264
 265	// The AUTHORITY is untouched; only the honest figure moved.
 266	uassert.Equal(t, int64(500_000), Remaining(id))
 267	uassert.Equal(t, int64(0), Drawable(id))
 268
 269	testing.SetRealm(testing.NewUserRealm(delegate1))
 270	uassert.AbortsWithMessage(t, cur, "owner's pool cannot cover this draw",
 271		func() { Draw(cross(cur), id, 1_000) })
 272
 273	revokeAs(cur, owner1, id)
 274	assertSolvent(t)
 275}
 276
 277// TestSelfGrantRecoversThePoolForTheFeeAlone pins the premise that the
 278// MaxLiveGrants comment rests on, and it exists because the claim that
 279// comment USED to make rotted undetected until an adversarial audit read
 280// it (finding Y1). The lesson was not "that number was wrong" but "a
 281// mitigation asserted nowhere is prose", so the replacement reasoning is
 282// asserted here.
 283//
 284// The reasoning is that no capital bound on grant-slot occupation can be
 285// built out of the pool, because an owner may be its OWN delegate. Make
 286// an owner retain MinFunding while grants are live and this is the way
 287// around it: grant to yourself, draw the pool to zero through your own
 288// grant, Claim the credit back. You recover everything but the draw fee,
 289// the slot is still held, and a retention rule reads as satisfied the
 290// whole way through. The realm permits this deliberately — refusing a
 291// self-grant would not close it either, since a second address the same
 292// operator controls is free.
 293func TestSelfGrantRecoversThePoolForTheFeeAlone(cur realm, t *testing.T) {
 294	begin()
 295	feesBefore := FeesAccrued()
 296
 297	fundAs(cur, owner1, MinFunding)
 298	// owner1 is both parties. Nothing refuses it.
 299	id := grantAs(cur, owner1, owner1, MinFunding, MinPeriodBlocks, 0, 0)
 300
 301	// Drain the whole pool through the owner's own grant.
 302	credited := drawAs(cur, owner1, id, MinFunding)
 303	fee := MinFunding * DrawFeeBps / 10_000
 304	uassert.Equal(t, MinFunding-fee, credited)
 305	uassert.Equal(t, int64(0), PoolOf(owner1))
 306
 307	// ... and take it back out. Net cost of holding the slot: the fee.
 308	uassert.Equal(t, MinFunding-fee, claimAs(cur, owner1))
 309	uassert.Equal(t, feesBefore+fee, FeesAccrued())
 310
 311	// The slot is still occupied, which is the whole point: the grant
 312	// survived the pool reaching zero, and no third party can remove it
 313	// because it carries no expiry.
 314	uassert.Equal(t, int64(1), ownerOpen(owner1.String()))
 315	uassert.False(t, IsExpired(id))
 316	testing.SetRealm(testing.NewUserRealm(delegate1))
 317	uassert.AbortsWithMessage(t, cur, "spendbook: allowance has not expired",
 318		func() { Reap(cross(cur), id) })
 319
 320	revokeAs(cur, owner1, id)
 321	uassert.Equal(t, int64(0), ownerOpen(owner1.String()))
 322	assertSolvent(t)
 323}
 324
 325// --- grants ---
 326
 327func TestGrantRecordsTermsAndParties(cur realm, t *testing.T) {
 328	begin()
 329	fundAs(cur, owner1, MinFunding)
 330	h := Height()
 331	liveBefore := LiveGrants()
 332
 333	testing.SetRealm(testing.NewUserRealm(owner1))
 334	id := Grant(cross(cur), delegate1, "monthly ops", 50_000, MinPeriodBlocks,
 335		200_000, h+500)
 336
 337	uassert.Equal(t, id, NumGrants())
 338	uassert.Equal(t, liveBefore+1, LiveGrants())
 339	uassert.Equal(t, int64(1), GrantsOf(owner1))
 340	uassert.Equal(t, "monthly ops", GrantLabel(id))
 341
 342	o, s, perPeriod, periodLength, total, expiresAt, anchor, createdAt, feeBps :=
 343		GrantInfo(id)
 344	uassert.Equal(t, owner1, o)
 345	uassert.Equal(t, delegate1, s)
 346	uassert.Equal(t, int64(50_000), perPeriod)
 347	uassert.Equal(t, MinPeriodBlocks, periodLength)
 348	uassert.Equal(t, int64(200_000), total)
 349	uassert.Equal(t, h+500, expiresAt)
 350	// The anchor is the grant height: windows are anchored, not sliding,
 351	// so nobody resets a cap by choosing when to call.
 352	uassert.Equal(t, h, anchor)
 353	uassert.Equal(t, h, createdAt)
 354	uassert.Equal(t, DrawFeeBps, feeBps)
 355
 356	spentWindow, spentTotal, windowIndex, draws := GrantUsage(id)
 357	uassert.Equal(t, int64(0), spentWindow)
 358	uassert.Equal(t, int64(0), spentTotal)
 359	uassert.Equal(t, int64(0), windowIndex)
 360	uassert.Equal(t, int64(0), draws)
 361
 362	uassert.False(t, IsExpired(id))
 363
 364	revokeAs(cur, owner1, id)
 365	uassert.Equal(t, int64(0), GrantsOf(owner1))
 366	uassert.Equal(t, liveBefore, LiveGrants())
 367	withdrawAllAs(cur, owner1)
 368}
 369
 370func TestGrantValidation(cur realm, t *testing.T) {
 371	begin()
 372
 373	// No pool: a grant from an address with nothing behind it is refused
 374	// before any allowance is opened.
 375	testing.SetRealm(testing.NewUserRealm(owner1))
 376	uassert.AbortsWithMessage(t, cur, "fund a pool before granting",
 377		func() { Grant(cross(cur), delegate1, "", 50_000, MinPeriodBlocks, 0, 0) })
 378
 379	fundAs(cur, owner1, MinFunding)
 380
 381	testing.SetRealm(testing.NewUserRealm(owner1))
 382	uassert.AbortsWithMessage(t, cur,
 383		"spender is not a valid address in canonical (lowercase) form",
 384		func() { Grant(cross(cur), address("not-an-address"), "", 50_000, MinPeriodBlocks, 0, 0) })
 385
 386	testing.SetRealm(testing.NewUserRealm(owner1))
 387	uassert.AbortsWithMessage(t, cur, "label exceeds 64 bytes",
 388		func() {
 389			Grant(cross(cur), delegate1, strings.Repeat("x", MaxLabelLen+1),
 390				50_000, MinPeriodBlocks, 0, 0)
 391		})
 392
 393	testing.SetRealm(testing.NewUserRealm(owner1))
 394	uassert.AbortsWithMessage(t, cur, "perPeriod must be at least 1000ugnot",
 395		func() { Grant(cross(cur), delegate1, "", MinPerPeriod-1, MinPeriodBlocks, 0, 0) })
 396
 397	testing.SetRealm(testing.NewUserRealm(owner1))
 398	uassert.AbortsWithMessage(t, cur,
 399		"periodLength must be 0 (lifetime) or at least 100 blocks",
 400		func() { Grant(cross(cur), delegate1, "", 50_000, MinPeriodBlocks-1, 0, 0) })
 401
 402	// An expiry already in the past is spendbook's own refusal: an
 403	// allowance that can authorize nothing must not become a live entry.
 404	testing.SetRealm(testing.NewUserRealm(owner1))
 405	uassert.AbortsWithMessage(t, cur, "spendbook: invalid terms",
 406		func() { Grant(cross(cur), delegate1, "", 50_000, MinPeriodBlocks, 0, 1) })
 407
 408	uassert.Equal(t, int64(0), GrantsOf(owner1))
 409	withdrawAllAs(cur, owner1)
 410}
 411
 412// TestGrantRefusesUppercaseSpender measures the premise of Grant's
 413// lowercase clause rather than asserting it from the bech32 spec. If
 414// IsValid() ever stops accepting the uppercase spelling, the first
 415// assertion here fails and the clause's comment must be corrected — which
 416// is the point of measuring it.
 417func TestGrantRefusesUppercaseSpender(cur realm, t *testing.T) {
 418	begin()
 419	fundAs(cur, owner1, MinFunding)
 420
 421	upper := address(strings.ToUpper(string(delegate1)))
 422	uassert.True(t, upper != delegate1)
 423	uassert.True(t, upper.IsValid()) // the hazard, measured on this toolchain
 424
 425	testing.SetRealm(testing.NewUserRealm(owner1))
 426	uassert.AbortsWithMessage(t, cur,
 427		"spender is not a valid address in canonical (lowercase) form",
 428		func() { Grant(cross(cur), upper, "", 50_000, MinPeriodBlocks, 0, 0) })
 429
 430	uassert.Equal(t, int64(0), GrantsOf(owner1))
 431	withdrawAllAs(cur, owner1)
 432}
 433
 434// TestGrantPerOwnerCap covers the gap spendbook's own documentation names:
 435// maxLive is a single global figure, so without a per-owner cap one owner
 436// could exhaust it and lock everyone else out.
 437func TestGrantPerOwnerCap(cur realm, t *testing.T) {
 438	begin()
 439	fundAs(cur, owner1, MinFunding)
 440
 441	ids := []int64{}
 442	for i := int64(0); i < MaxGrantsPerOwner; i++ {
 443		ids = append(ids, grantAs(cur, owner1, delegate1, MinPerPeriod,
 444			MinPeriodBlocks, 0, 0))
 445	}
 446	uassert.Equal(t, MaxGrantsPerOwner, GrantsOf(owner1))
 447
 448	testing.SetRealm(testing.NewUserRealm(owner1))
 449	uassert.AbortsWithMessage(t, cur, "owner already has 16 live grants",
 450		func() { Grant(cross(cur), delegate1, "", MinPerPeriod, MinPeriodBlocks, 0, 0) })
 451
 452	// A DIFFERENT owner is unaffected — the cap is per-owner, not global.
 453	fundAs(cur, owner2, MinFunding)
 454	other := grantAs(cur, owner2, delegate2, MinPerPeriod, MinPeriodBlocks, 0, 0)
 455	uassert.Equal(t, int64(1), GrantsOf(owner2))
 456
 457	// Revoking frees a slot, and the id is not reused.
 458	revokeAs(cur, owner1, ids[0])
 459	uassert.Equal(t, MaxGrantsPerOwner-1, GrantsOf(owner1))
 460	fresh := grantAs(cur, owner1, delegate1, MinPerPeriod, MinPeriodBlocks, 0, 0)
 461	uassert.True(t, fresh > ids[len(ids)-1])
 462
 463	for _, id := range ids[1:] {
 464		revokeAs(cur, owner1, id)
 465	}
 466	revokeAs(cur, owner1, fresh)
 467	revokeAs(cur, owner2, other)
 468	uassert.Equal(t, int64(0), GrantsOf(owner1))
 469	uassert.Equal(t, int64(0), GrantsOf(owner2))
 470	withdrawAllAs(cur, owner1)
 471	withdrawAllAs(cur, owner2)
 472	assertSolvent(t)
 473}
 474
 475func TestGrantAndAmendAreOwnerOnly(cur realm, t *testing.T) {
 476	begin()
 477	fundAs(cur, owner1, MinFunding)
 478	id := grantAs(cur, owner1, delegate1, 50_000, MinPeriodBlocks, 0, 0)
 479
 480	testing.SetRealm(testing.NewUserRealm(mallory))
 481	uassert.AbortsWithMessage(t, cur, "owner only",
 482		func() { Amend(cross(cur), id, 10_000, 0, 0) })
 483
 484	testing.SetRealm(testing.NewUserRealm(mallory))
 485	uassert.AbortsWithMessage(t, cur, "owner only",
 486		func() { Revoke(cross(cur), id) })
 487
 488	// Not even the delegate may amend or revoke their own grant.
 489	testing.SetRealm(testing.NewUserRealm(delegate1))
 490	uassert.AbortsWithMessage(t, cur, "owner only",
 491		func() { Amend(cross(cur), id, 10_000, 0, 0) })
 492
 493	testing.SetRealm(testing.NewUserRealm(owner1))
 494	uassert.AbortsWithMessage(t, cur, "unknown grant id",
 495		func() { Revoke(cross(cur), 999_999) })
 496
 497	revokeAs(cur, owner1, id)
 498	withdrawAllAs(cur, owner1)
 499}
 500
 501// --- drawing ---
 502
 503func TestDrawIsDelegateOnlyAndCreditsNetOfFee(cur realm, t *testing.T) {
 504	b := begin()
 505	fundAs(cur, owner1, MinFunding)
 506	id := grantAs(cur, owner1, delegate1, 500_000, MinPeriodBlocks, 0, 0)
 507
 508	// Nobody but the named delegate — not mallory, and not the owner.
 509	testing.SetRealm(testing.NewUserRealm(mallory))
 510	uassert.AbortsWithMessage(t, cur, "delegate only",
 511		func() { Draw(cross(cur), id, 1_000) })
 512	testing.SetRealm(testing.NewUserRealm(owner1))
 513	uassert.AbortsWithMessage(t, cur, "delegate only",
 514		func() { Draw(cross(cur), id, 1_000) })
 515
 516	testing.SetRealm(testing.NewUserRealm(delegate1))
 517	uassert.AbortsWithMessage(t, cur, "amount must be positive",
 518		func() { Draw(cross(cur), id, 0) })
 519
 520	heldBefore := Held()
 521	credited := drawAs(cur, delegate1, id, 100_000)
 522
 523	// 1% of the gross, floor-rounded in the delegate's favour.
 524	uassert.Equal(t, int64(99_000), credited)
 525	uassert.Equal(t, int64(99_000), CreditOf(delegate1))
 526	uassert.Equal(t, b.fees+1_000, FeesAccrued())
 527
 528	// The owner's pool is debited the GROSS: the fee is paid out of the
 529	// budget, not out of the delegate's authority.
 530	uassert.Equal(t, MinFunding-100_000, PoolOf(owner1))
 531	uassert.Equal(t, b.pooled+MinFunding-100_000, TotalPooled())
 532
 533	// Draw moves value between P and L only. H does not move at all, which
 534	// is why this transition has no payout that could fail.
 535	uassert.Equal(t, heldBefore, Held())
 536	assertSolvent(t)
 537
 538	spentWindow, spentTotal, _, draws := GrantUsage(id)
 539	uassert.Equal(t, int64(100_000), spentWindow)
 540	uassert.Equal(t, int64(100_000), spentTotal)
 541	uassert.Equal(t, int64(1), draws)
 542	uassert.Equal(t, int64(400_000), Remaining(id))
 543
 544	// Claim is the separate transaction that moves coins.
 545	before := balanceOf(delegate1)
 546	paid := claimAs(cur, delegate1)
 547	uassert.Equal(t, int64(99_000), paid)
 548	uassert.Equal(t, before+99_000, balanceOf(delegate1))
 549	uassert.Equal(t, int64(0), CreditOf(delegate1))
 550	uassert.Equal(t, b.credit, UnclaimedCredit())
 551	assertSolvent(t)
 552
 553	testing.SetRealm(testing.NewUserRealm(delegate1))
 554	uassert.AbortsWithMessage(t, cur, "no credit to claim",
 555		func() { Claim(cross(cur)) })
 556
 557	revokeAs(cur, owner1, id)
 558	withdrawAllAs(cur, owner1)
 559	tidyFees(cur, t)
 560}
 561
 562// TestDrawSolvencyRefusalConsumesNothing is this realm's central
 563// invariant, and the ordering inside Draw is what it proves: the solvency
 564// check runs BEFORE spendbook.Spend, so a draw refused for insolvency
 565// leaves the window capacity intact rather than burning it on a draw that
 566// never happened.
 567//
 568// This is also the one abort in this file where the post-state is the
 569// assertion rather than an afterthought, because uassert does not roll
 570// back realm globals the way a reverted transaction would.
 571func TestDrawSolvencyRefusalConsumesNothing(cur realm, t *testing.T) {
 572	begin()
 573	fundAs(cur, owner1, MinFunding)
 574	// Authority deliberately EXCEEDS the pool: a budget is authority, not
 575	// a reservation, and the realm's promise is that it never credits what
 576	// it cannot pay.
 577	id := grantAs(cur, owner1, delegate1, 5*MinFunding, MinPeriodBlocks, 0, 0)
 578	uassert.Equal(t, 5*MinFunding, Remaining(id))
 579	uassert.Equal(t, MinFunding, Drawable(id)) // the honest figure
 580
 581	testing.SetRealm(testing.NewUserRealm(delegate1))
 582	uassert.AbortsWithMessage(t, cur, "owner's pool cannot cover this draw",
 583		func() { Draw(cross(cur), id, MinFunding+1) })
 584
 585	// Nothing moved, and — the part that matters — nothing was CONSUMED.
 586	spentWindow, spentTotal, _, draws := GrantUsage(id)
 587	uassert.Equal(t, int64(0), spentWindow)
 588	uassert.Equal(t, int64(0), spentTotal)
 589	uassert.Equal(t, int64(0), draws)
 590	uassert.Equal(t, 5*MinFunding, Remaining(id))
 591	uassert.Equal(t, MinFunding, PoolOf(owner1))
 592	uassert.Equal(t, int64(0), CreditOf(delegate1))
 593	assertSolvent(t)
 594
 595	// Exactly the drawable figure succeeds, which is what makes the
 596	// refusal above a boundary rather than a blanket.
 597	uassert.Equal(t, int64(990_000), drawAs(cur, delegate1, id, MinFunding))
 598	uassert.Equal(t, int64(0), PoolOf(owner1))
 599
 600	claimAs(cur, delegate1)
 601	revokeAs(cur, owner1, id)
 602	tidyFees(cur, t)
 603	assertSolvent(t)
 604}
 605
 606func TestDrawFeeRoundingFavorsTheDelegate(cur realm, t *testing.T) {
 607	b := begin()
 608	fundAs(cur, owner1, MinFunding)
 609	id := grantAs(cur, owner1, delegate1, 50_000, MinPeriodBlocks, 0, 0)
 610
 611	// 1% of 99 floors to 0, so the delegate keeps all of it. There is
 612	// deliberately no minimum draw: a floor above a grant's PerPeriod
 613	// would make the grant silently undrawable.
 614	uassert.Equal(t, int64(99), drawAs(cur, delegate1, id, 99))
 615	uassert.Equal(t, b.fees, FeesAccrued())
 616	uassert.Equal(t, int64(99), CreditOf(delegate1))
 617
 618	// 100 is the first amount that pays a fee at all.
 619	uassert.Equal(t, int64(99), drawAs(cur, delegate1, id, 100))
 620	uassert.Equal(t, b.fees+1, FeesAccrued())
 621	uassert.Equal(t, int64(198), CreditOf(delegate1))
 622	assertSolvent(t)
 623
 624	claimAs(cur, delegate1)
 625	revokeAs(cur, owner1, id)
 626	withdrawAllAs(cur, owner1)
 627	tidyFees(cur, t)
 628}
 629
 630// TestWindowRefillsAtAnchoredBoundary is spendbook consumer obligation 1
 631// observed from the consuming side: the clock is runtime.ChainHeight() and
 632// no caller supplies it, so the window cannot be rolled on demand. The
 633// refill is a RESET, not a carry.
 634func TestWindowRefillsAtAnchoredBoundary(cur realm, t *testing.T) {
 635	begin()
 636	fundAs(cur, owner1, 10*MinFunding)
 637	id := grantAs(cur, owner1, delegate1, 100_000, MinPeriodBlocks, 0, 0)
 638
 639	drawAs(cur, delegate1, id, 100_000)
 640	uassert.Equal(t, int64(0), Remaining(id))
 641
 642	testing.SetRealm(testing.NewUserRealm(delegate1))
 643	uassert.AbortsWithMessage(t, cur, "spendbook: amount exceeds the window cap",
 644		func() { Draw(cross(cur), id, 1) })
 645
 646	// One block short of the boundary: still refused.
 647	testing.SkipHeights(MinPeriodBlocks - 1)
 648	uassert.Equal(t, int64(0), Remaining(id))
 649	testing.SetRealm(testing.NewUserRealm(delegate1))
 650	uassert.AbortsWithMessage(t, cur, "spendbook: amount exceeds the window cap",
 651		func() { Draw(cross(cur), id, 1) })
 652
 653	// The boundary itself. The window index advances and the cap is full
 654	// again — not 100_000 plus a carried remainder.
 655	testing.SkipHeights(1)
 656	uassert.Equal(t, int64(100_000), Remaining(id))
 657	_, _, windowIndex, _ := GrantUsage(id)
 658	uassert.Equal(t, int64(1), windowIndex)
 659
 660	// THE STALE-PAIR CASE, and it is asserted here rather than left to
 661	// inference because this is the only moment it is observable: the
 662	// chain has crossed a boundary and NOTHING has been written since, so
 663	// Allowance.WindowIndex still names window 0 and Allowance.SpentWindow
 664	// still holds window 0's 100_000. Reading either field straight off
 665	// the record would publish "spent this window: 100000" directly above
 666	// "authorized by the terms: 100000" — a page contradicting itself.
 667	// spentInWindow is what makes the first figure 0, and this assertion
 668	// is what keeps it that way.
 669	staleWindow, _, _, _ := GrantUsage(id)
 670	uassert.Equal(t, int64(0), staleWindow)
 671	page := Render(itoa(id))
 672	uassert.True(t, strings.Contains(page, "spent this window: 0ugnot"))
 673	uassert.True(t, strings.Contains(page, "now in window 1"))
 674
 675	drawAs(cur, delegate1, id, 100_000)
 676	_, spentTotal, _, draws := GrantUsage(id)
 677	uassert.Equal(t, int64(200_000), spentTotal) // lifetime accumulates
 678	uassert.Equal(t, int64(2), draws)
 679	spentWindow, _, _, _ := GrantUsage(id)
 680	uassert.Equal(t, int64(100_000), spentWindow) // the window reset
 681	assertSolvent(t)
 682
 683	claimAs(cur, delegate1)
 684	revokeAs(cur, owner1, id)
 685	withdrawAllAs(cur, owner1)
 686	tidyFees(cur, t)
 687}
 688
 689func TestLifetimeTotalBindsAcrossARollover(cur realm, t *testing.T) {
 690	begin()
 691	fundAs(cur, owner1, 10*MinFunding)
 692	// Lifetime cap at 1.5 windows: the refill cannot outrun it.
 693	id := grantAs(cur, owner1, delegate1, 100_000, MinPeriodBlocks, 150_000, 0)
 694
 695	drawAs(cur, delegate1, id, 100_000)
 696	testing.SkipHeights(MinPeriodBlocks)
 697
 698	// The window refilled to 100_000 but the lifetime cap leaves 50_000,
 699	// and Remaining reports the LESSER of the two.
 700	uassert.Equal(t, int64(50_000), Remaining(id))
 701	testing.SetRealm(testing.NewUserRealm(delegate1))
 702	uassert.AbortsWithMessage(t, cur, "spendbook: amount exceeds the lifetime cap",
 703		func() { Draw(cross(cur), id, 50_001) })
 704
 705	drawAs(cur, delegate1, id, 50_000)
 706	uassert.Equal(t, int64(0), Remaining(id))
 707
 708	// A further window brings no further authority: the lifetime cap is
 709	// absolute.
 710	testing.SkipHeights(MinPeriodBlocks)
 711	uassert.Equal(t, int64(0), Remaining(id))
 712	testing.SetRealm(testing.NewUserRealm(delegate1))
 713	uassert.AbortsWithMessage(t, cur, "spendbook: amount exceeds the lifetime cap",
 714		func() { Draw(cross(cur), id, 1) })
 715	assertSolvent(t)
 716
 717	claimAs(cur, delegate1)
 718	revokeAs(cur, owner1, id)
 719	withdrawAllAs(cur, owner1)
 720	tidyFees(cur, t)
 721}
 722
 723// TestLifetimeWindowNeverRefills covers periodLength == 0: ONE window for
 724// the grant's whole life. It is a distinct code path in spendbook
 725// (windowIndex is pinned to 0) and a distinct product shape — a one-off
 726// authority rather than a recurring budget.
 727func TestLifetimeWindowNeverRefills(cur realm, t *testing.T) {
 728	begin()
 729	fundAs(cur, owner1, MinFunding)
 730	id := grantAs(cur, owner1, delegate1, 100_000, 0, 0, 0)
 731
 732	// GrantInfo's fourth result is periodLength; the third is perPeriod.
 733	_, _, _, periodLength, _, _, _, _, _ := GrantInfo(id)
 734	uassert.Equal(t, int64(0), periodLength)
 735
 736	drawAs(cur, delegate1, id, 100_000)
 737	uassert.Equal(t, int64(0), Remaining(id))
 738
 739	testing.SkipHeights(10 * MinPeriodBlocks)
 740	uassert.Equal(t, int64(0), Remaining(id))
 741	_, _, windowIndex, _ := GrantUsage(id)
 742	uassert.Equal(t, int64(0), windowIndex)
 743
 744	testing.SetRealm(testing.NewUserRealm(delegate1))
 745	uassert.AbortsWithMessage(t, cur, "spendbook: amount exceeds the window cap",
 746		func() { Draw(cross(cur), id, 1) })
 747
 748	// And a LOOSENING cannot be queued against a grant with no boundary to
 749	// wait for — spendbook refuses rather than accepting an amendment that
 750	// would never take effect.
 751	testing.SetRealm(testing.NewUserRealm(owner1))
 752	uassert.AbortsWithMessage(t, cur,
 753		"spendbook: no reachable next window boundary",
 754		func() { Amend(cross(cur), id, 200_000, 0, 0) })
 755	assertSolvent(t)
 756
 757	claimAs(cur, delegate1)
 758	revokeAs(cur, owner1, id)
 759	withdrawAllAs(cur, owner1)
 760	tidyFees(cur, t)
 761}
 762
 763// --- amendment ---
 764
 765func TestAmendIsOneSided(cur realm, t *testing.T) {
 766	begin()
 767	fundAs(cur, owner1, 10*MinFunding)
 768	id := grantAs(cur, owner1, delegate1, 100_000, MinPeriodBlocks, 0, 0)
 769
 770	// A TIGHTENING takes effect immediately, mid-window.
 771	testing.SetRealm(testing.NewUserRealm(owner1))
 772	uassert.True(t, Amend(cross(cur), id, 20_000, 0, 0))
 773	uassert.Equal(t, int64(20_000), Remaining(id))
 774	_, _, pending := pendingOf(id)
 775	uassert.False(t, pending)
 776
 777	testing.SetRealm(testing.NewUserRealm(delegate1))
 778	uassert.AbortsWithMessage(t, cur, "spendbook: amount exceeds the window cap",
 779		func() { Draw(cross(cur), id, 20_001) })
 780
 781	// A LOOSENING is queued for the next boundary, not applied now. That
 782	// asymmetry is the whole guarantee: the delegate's exposure in the
 783	// current window can only ever shrink.
 784	testing.SetRealm(testing.NewUserRealm(owner1))
 785	uassert.False(t, Amend(cross(cur), id, 300_000, 0, 0))
 786	uassert.Equal(t, int64(20_000), Remaining(id))
 787	perPeriod, _, pending := pendingOf(id)
 788	uassert.True(t, pending)
 789	uassert.Equal(t, int64(300_000), perPeriod)
 790
 791	testing.SetRealm(testing.NewUserRealm(delegate1))
 792	uassert.AbortsWithMessage(t, cur, "spendbook: amount exceeds the window cap",
 793		func() { Draw(cross(cur), id, 20_001) })
 794
 795	// At the boundary the pending terms become the live ones.
 796	testing.SkipHeights(MinPeriodBlocks)
 797	uassert.Equal(t, int64(300_000), Remaining(id))
 798	drawAs(cur, delegate1, id, 300_000)
 799	assertSolvent(t)
 800
 801	// The realm's own floor still applies to an amendment.
 802	testing.SetRealm(testing.NewUserRealm(owner1))
 803	uassert.AbortsWithMessage(t, cur, "perPeriod must be at least 1000ugnot",
 804		func() { Amend(cross(cur), id, MinPerPeriod-1, 0, 0) })
 805
 806	claimAs(cur, delegate1)
 807	revokeAs(cur, owner1, id)
 808	withdrawAllAs(cur, owner1)
 809	tidyFees(cur, t)
 810}
 811
 812// TestAmendCannotMovePeriodLength asserts a STRUCTURAL property, not a
 813// refusal: Amend has no periodLength parameter at all, so moving every
 814// future boundary is not merely rejected, it is unexpressible. The realm
 815// reads the grant's own periodLength and passes it back unchanged.
 816func TestAmendCannotMovePeriodLength(cur realm, t *testing.T) {
 817	begin()
 818	fundAs(cur, owner1, MinFunding)
 819	id := grantAs(cur, owner1, delegate1, 100_000, MinPeriodBlocks, 0, 0)
 820
 821	_, _, _, before, _, _, anchorBefore, _, _ := GrantInfo(id)
 822	testing.SetRealm(testing.NewUserRealm(owner1))
 823	uassert.True(t, Amend(cross(cur), id, 10_000, 0, 0))
 824	_, _, _, after, _, _, anchorAfter, _, _ := GrantInfo(id)
 825
 826	uassert.Equal(t, before, after)
 827	uassert.Equal(t, MinPeriodBlocks, after)
 828	// The anchor is immovable too, so the boundaries stay where they were.
 829	uassert.Equal(t, anchorBefore, anchorAfter)
 830
 831	revokeAs(cur, owner1, id)
 832	withdrawAllAs(cur, owner1)
 833}
 834
 835// --- ending a grant ---
 836
 837func TestRevokeEndsAuthorityButNotEarnedCredit(cur realm, t *testing.T) {
 838	begin()
 839	fundAs(cur, owner1, MinFunding)
 840	id := grantAs(cur, owner1, delegate1, 100_000, MinPeriodBlocks, 0, 0)
 841	drawAs(cur, delegate1, id, 100_000)
 842	uassert.Equal(t, int64(99_000), CreditOf(delegate1))
 843
 844	revokeAs(cur, owner1, id)
 845
 846	// The grant record is gone and the id is never reused, so a later draw
 847	// fails as unknown rather than as unauthorized.
 848	testing.SetRealm(testing.NewUserRealm(delegate1))
 849	uassert.AbortsWithMessage(t, cur, "unknown grant id",
 850		func() { Draw(cross(cur), id, 1) })
 851	testing.SetRealm(testing.NewUserRealm(owner1))
 852	uassert.AbortsWithMessage(t, cur, "unknown grant id",
 853		func() { Revoke(cross(cur), id) })
 854
 855	// Credit already drawn is the delegate's. Revoke ends FUTURE authority
 856	// and claws back nothing.
 857	uassert.Equal(t, int64(99_000), CreditOf(delegate1))
 858	before := balanceOf(delegate1)
 859	uassert.Equal(t, int64(99_000), claimAs(cur, delegate1))
 860	uassert.Equal(t, before+99_000, balanceOf(delegate1))
 861	assertSolvent(t)
 862
 863	withdrawAllAs(cur, owner1)
 864	tidyFees(cur, t)
 865}
 866
 867func TestReapIsPermissionlessButOnlyForExpired(cur realm, t *testing.T) {
 868	begin()
 869	fundAs(cur, owner1, MinFunding)
 870	h := Height()
 871	id := grantAs(cur, owner1, delegate1, 100_000, MinPeriodBlocks, 0, h+200)
 872	noExpiry := grantAs(cur, owner1, delegate2, 100_000, MinPeriodBlocks, 0, 0)
 873
 874	// Live: refused, for anyone.
 875	testing.SetRealm(testing.NewUserRealm(mallory))
 876	uassert.AbortsWithMessage(t, cur, "spendbook: allowance has not expired",
 877		func() { Reap(cross(cur), id) })
 878	uassert.False(t, IsExpired(id))
 879
 880	testing.SkipHeights(200)
 881	uassert.True(t, IsExpired(id))
 882
 883	// An expired allowance can authorize nothing, so its own delegate is
 884	// refused too — and a stranger may clear it.
 885	testing.SetRealm(testing.NewUserRealm(delegate1))
 886	uassert.AbortsWithMessage(t, cur, "spendbook: allowance has expired",
 887		func() { Draw(cross(cur), id, 1) })
 888
 889	liveBefore := LiveGrants()
 890	testing.SetRealm(testing.NewUserRealm(mallory))
 891	Reap(cross(cur), id)
 892	uassert.Equal(t, liveBefore-1, LiveGrants())
 893	uassert.Equal(t, int64(1), GrantsOf(owner1))
 894
 895	// A grant with NO expiry can never be reaped: only its owner ends it.
 896	uassert.False(t, IsExpired(noExpiry))
 897	testing.SetRealm(testing.NewUserRealm(mallory))
 898	uassert.AbortsWithMessage(t, cur, "spendbook: allowance has not expired",
 899		func() { Reap(cross(cur), noExpiry) })
 900
 901	revokeAs(cur, owner1, noExpiry)
 902	withdrawAllAs(cur, owner1)
 903	assertSolvent(t)
 904}
 905
 906// --- fees, surplus, and the absence of an admin ---
 907
 908func TestWithdrawFeesGoesOnlyToTheFixedCollector(cur realm, t *testing.T) {
 909	begin()
 910	fundAs(cur, owner1, MinFunding)
 911	id := grantAs(cur, owner1, delegate1, 500_000, MinPeriodBlocks, 0, 0)
 912	drawAs(cur, delegate1, id, 500_000)
 913	uassert.Equal(t, int64(5_000), FeesAccrued())
 914
 915	// Permissionless, because the destination is a compile-time constant:
 916	// there is nothing for a caller to steer.
 917	before := balanceOf(FeeCollector)
 918	testing.SetRealm(testing.NewUserRealm(mallory))
 919	uassert.Equal(t, int64(5_000), WithdrawFees(cross(cur)))
 920	uassert.Equal(t, before+5_000, balanceOf(FeeCollector))
 921	uassert.Equal(t, int64(0), FeesAccrued())
 922
 923	testing.SetRealm(testing.NewUserRealm(mallory))
 924	uassert.AbortsWithMessage(t, cur, "no fees accrued",
 925		func() { WithdrawFees(cross(cur)) })
 926	assertSolvent(t)
 927
 928	claimAs(cur, delegate1)
 929	revokeAs(cur, owner1, id)
 930	withdrawAllAs(cur, owner1)
 931}
 932
 933func TestSweepSurplusCannotReachTrackedLiabilities(cur realm, t *testing.T) {
 934	begin()
 935	fundAs(cur, owner1, MinFunding)
 936	id := grantAs(cur, owner1, delegate1, 500_000, MinPeriodBlocks, 0, 0)
 937	drawAs(cur, delegate1, id, 200_000)
 938
 939	// Nothing out of band yet: the reserve is everything the realm holds,
 940	// and the primitive ABORTS rather than reporting a transfer of zero.
 941	uassert.Equal(t, int64(0), Surplus())
 942	testing.SetRealm(testing.NewUserRealm(mallory))
 943	uassert.AbortsWithMessage(t, cur, "coinio: no surplus to sweep for ugnot",
 944		func() { SweepSurplus(cross(cur), Denom) })
 945	uassert.Equal(t, MinFunding-200_000, PoolOf(owner1))
 946	uassert.Equal(t, int64(198_000), CreditOf(delegate1))
 947	uassert.Equal(t, int64(2_000), FeesAccrued())
 948
 949	// A foreign denom the realm has never held is the same answer: there
 950	// is nothing to move, so nothing claims to have moved.
 951	testing.SetRealm(testing.NewUserRealm(mallory))
 952	uassert.AbortsWithMessage(t, cur, "coinio: no surplus to sweep for foocoin",
 953		func() { SweepSurplus(cross(cur), "foocoin") })
 954
 955	// Coins arriving outside any entrypoint ARE sweepable, and only those.
 956	testing.IssueCoins(Address(), ugnot(777))
 957	uassert.Equal(t, int64(777), Surplus())
 958	before := balanceOf(FeeCollector)
 959	testing.SetRealm(testing.NewUserRealm(mallory))
 960	uassert.Equal(t, int64(777), SweepSurplus(cross(cur), Denom))
 961	uassert.Equal(t, before+777, balanceOf(FeeCollector))
 962	uassert.Equal(t, int64(0), Surplus())
 963
 964	// Every tracked term survived the sweep untouched.
 965	uassert.Equal(t, MinFunding-200_000, PoolOf(owner1))
 966	uassert.Equal(t, int64(198_000), CreditOf(delegate1))
 967	uassert.Equal(t, int64(2_000), FeesAccrued())
 968	assertSolvent(t)
 969
 970	claimAs(cur, delegate1)
 971	revokeAs(cur, owner1, id)
 972	withdrawAllAs(cur, owner1)
 973	tidyFees(cur, t)
 974}
 975
 976// TestStraySendIsRejectedOnEveryNonPayableEntrypoint sweeps the whole
 977// non-payable surface. A coin attached to any of these would otherwise
 978// become sweepable surplus — silently converting a user's coins into
 979// operator funds — so aborting and reverting the transfer is the only safe
 980// answer. The count assertion is deliberate: a table that silently lost
 981// its rows would otherwise report green.
 982func TestStraySendIsRejectedOnEveryNonPayableEntrypoint(cur realm, t *testing.T) {
 983	begin()
 984	fundAs(cur, owner1, MinFunding)
 985	id := grantAs(cur, owner1, delegate1, 100_000, MinPeriodBlocks, 0, 0)
 986
 987	// No IssueCoins: the envelope is staged but never mirrored into the
 988	// bank, so a refused call cannot leave surplus behind.
 989	calls := []func(){
 990		func() { WithdrawPool(cross(cur), 1) },
 991		func() { WithdrawPoolAll(cross(cur)) },
 992		func() { Grant(cross(cur), delegate2, "", 100_000, MinPeriodBlocks, 0, 0) },
 993		func() { Amend(cross(cur), id, 100_000, 0, 0) },
 994		func() { Revoke(cross(cur), id) },
 995		func() { Reap(cross(cur), id) },
 996		func() { Draw(cross(cur), id, 1_000) },
 997		func() { Claim(cross(cur)) },
 998		func() { WithdrawFees(cross(cur)) },
 999		func() { SweepSurplus(cross(cur), Denom) },
1000	}
1001	uassert.Equal(t, 10, len(calls))
1002
1003	for _, call := range calls {
1004		testing.SetRealm(testing.NewUserRealm(owner1))
1005		testing.SetOriginSend(ugnot(1))
1006		uassert.AbortsWithMessage(t, cur,
1007			"this entrypoint does not accept coins", call)
1008		testing.SetOriginSend(nil)
1009	}
1010
1011	// The guard runs FIRST in every one of them, which is why even calls
1012	// that would have failed on their own arguments abort with this
1013	// message — and why nothing above changed any state.
1014	uassert.Equal(t, MinFunding, PoolOf(owner1))
1015	uassert.Equal(t, int64(1), GrantsOf(owner1))
1016	uassert.Equal(t, int64(100_000), Remaining(id))
1017	assertSolvent(t)
1018
1019	revokeAs(cur, owner1, id)
1020	withdrawAllAs(cur, owner1)
1021}
1022
1023// TestDrawableIsTheLesserOfAuthorityAndPool pins the realm's own
1024// contribution to the view surface. spendbook bounds PERMISSION and says
1025// so; it never speaks about solvency. Remaining is the authority figure
1026// and Drawable is the honest one, and conflating them is what would make
1027// the realm promise what it does not hold.
1028func TestDrawableIsTheLesserOfAuthorityAndPool(cur realm, t *testing.T) {
1029	begin()
1030	fundAs(cur, owner1, MinFunding)
1031
1032	// Authority below the pool: authority binds.
1033	small := grantAs(cur, owner1, delegate1, 100_000, MinPeriodBlocks, 0, 0)
1034	uassert.Equal(t, int64(100_000), Remaining(small))
1035	uassert.Equal(t, int64(100_000), Drawable(small))
1036
1037	// Authority above the pool: the pool binds, and Remaining does NOT
1038	// move — the two views answer different questions.
1039	big := grantAs(cur, owner1, delegate2, 5*MinFunding, MinPeriodBlocks, 0, 0)
1040	uassert.Equal(t, 5*MinFunding, Remaining(big))
1041	uassert.Equal(t, MinFunding, Drawable(big))
1042
1043	// Two grants, one pool: three grants of 100 against a pool of 100 are
1044	// all three valid and the first delegate to draw gets the money. The
1045	// sum of what is drawable deliberately EXCEEDS the pool, because a
1046	// budget is authority and not a reservation.
1047	uassert.True(t, Drawable(small)+Drawable(big) > PoolOf(owner1))
1048
1049	drawAs(cur, delegate1, small, 100_000)
1050	uassert.Equal(t, MinFunding-100_000, Drawable(big)) // the other shrank
1051
1052	claimAs(cur, delegate1)
1053	revokeAs(cur, owner1, small)
1054	revokeAs(cur, owner1, big)
1055	withdrawAllAs(cur, owner1)
1056	tidyFees(cur, t)
1057}
1058
1059func TestViewsRefuseUnknownIds(cur realm, t *testing.T) {
1060	begin()
1061	// PanicsWithMessage, not AbortsWithMessage, and the distinction is the
1062	// view/entrypoint one rather than a style choice. revive() catches an
1063	// ABORT, which is what a panic becomes when it crosses a realm
1064	// boundary; these six are non-crossing view functions called straight
1065	// from the test's own frame, so their panic never crosses anything and
1066	// stays a plain panic. Every other refusal asserted in this file goes
1067	// through a crossing helper (drawAs, grantAs, …) and is correctly an
1068	// abort. Getting this backwards does not weaken the assertion — it
1069	// fails loudly, which is how it was found.
1070	uassert.PanicsWithMessage(t, cur, "unknown grant id", func() { GrantInfo(999_999) })
1071	uassert.PanicsWithMessage(t, cur, "unknown grant id", func() { GrantUsage(999_999) })
1072	uassert.PanicsWithMessage(t, cur, "unknown grant id", func() { GrantLabel(999_999) })
1073	uassert.PanicsWithMessage(t, cur, "unknown grant id", func() { Remaining(999_999) })
1074	uassert.PanicsWithMessage(t, cur, "unknown grant id", func() { Drawable(999_999) })
1075	uassert.PanicsWithMessage(t, cur, "unknown grant id", func() { IsExpired(999_999) })
1076
1077	// The zero values of the non-panicking views are "nothing", not an
1078	// error, which is correct for an address that simply has no state.
1079	uassert.Equal(t, int64(0), PoolOf(mallory))
1080	uassert.Equal(t, int64(0), CreditOf(mallory))
1081	uassert.Equal(t, int64(0), GrantsOf(mallory))
1082}
1083
1084// --- render ---
1085
1086func TestRenderPages(cur realm, t *testing.T) {
1087	begin()
1088	fundAs(cur, owner1, MinFunding)
1089	h := Height()
1090	testing.SetRealm(testing.NewUserRealm(owner1))
1091	id := Grant(cross(cur), delegate1, "ops <script>alert(1)</script>",
1092		100_000, MinPeriodBlocks, 500_000, h+1_000)
1093	drawAs(cur, delegate1, id, 40_000)
1094
1095	index := Render("")
1096	uassert.True(t, strings.Contains(index, "# Budgets"))
1097	uassert.True(t, strings.Contains(index, "conservation: OK"))
1098	uassert.True(t, strings.Contains(index, "[live]"))
1099	uassert.True(t, strings.Contains(index, "#"+itoa(id)))
1100	// The free-text label reaches markdown only through the ecosystem
1101	// sanitizer, so the raw tag must not survive into the page.
1102	uassert.False(t, strings.Contains(index, "<script>"))
1103
1104	page := Render(itoa(id))
1105	uassert.True(t, strings.Contains(page, "# Grant #"+itoa(id)))
1106	uassert.True(t, strings.Contains(page, owner1.String()))
1107	uassert.True(t, strings.Contains(page, delegate1.String()))
1108	uassert.True(t, strings.Contains(page, "spent this window: 40000ugnot"))
1109	uassert.True(t, strings.Contains(page, "authorized by the terms: 60000ugnot"))
1110	uassert.True(t, strings.Contains(page, "drawable: 60000ugnot"))
1111	uassert.True(t, strings.Contains(page, "draw fee: 1%"))
1112	uassert.False(t, strings.Contains(page, "<script>"))
1113
1114	// The not-funded case is called by its name rather than shown as a
1115	// bare zero, because "authorized but unfunded" and "nothing
1116	// authorized" are different situations for a delegate.
1117	withdrawAllAs(cur, owner1)
1118	page = Render(itoa(id))
1119	uassert.True(t, strings.Contains(page, "authorized by the terms: 60000ugnot"))
1120	uassert.True(t, strings.Contains(page, "drawable: 0ugnot"))
1121	uassert.True(t, strings.Contains(page, "Authorized but **not funded**"))
1122
1123	// A queued loosening is disclosed on the page, with the terms that
1124	// actually govern until the boundary.
1125	testing.SetRealm(testing.NewUserRealm(owner1))
1126	uassert.False(t, Amend(cross(cur), id, 900_000, 0, 0))
1127	page = Render(itoa(id))
1128	uassert.True(t, strings.Contains(page, "A **looser** amendment is queued"))
1129
1130	// Expiry, and the keeper prompt that comes with it.
1131	testing.SkipHeights(1_000)
1132	page = Render(itoa(id))
1133	uassert.True(t, strings.Contains(page, "has **expired**"))
1134	uassert.True(t, strings.Contains(page, "Reap("+itoa(id)+")"))
1135	index = Render("")
1136	uassert.True(t, strings.Contains(index, "expired (reapable)"))
1137	uassert.True(t, strings.Contains(index, "## Keeper work"))
1138
1139	// Bad paths render a warning rather than panicking a page.
1140	uassert.True(t, strings.Contains(Render("not-a-number"), "invalid grant id"))
1141	uassert.True(t, strings.Contains(Render("999999"), "unknown grant id"))
1142
1143	testing.SetRealm(testing.NewUserRealm(mallory))
1144	Reap(cross(cur), id)
1145	claimAs(cur, delegate1)
1146	tidyFees(cur, t)
1147	uassert.True(t, strings.Contains(Render(""), "None yet"))
1148}
1149
1150func TestRenderIndexIsBounded(cur realm, t *testing.T) {
1151	begin()
1152	fundAs(cur, owner1, MinFunding)
1153	fundAs(cur, owner2, MinFunding)
1154
1155	ids := []int64{}
1156	for i := int64(0); i < int64(MaxRenderRows+2); i++ {
1157		from, to := owner1, delegate1
1158		if i >= MaxGrantsPerOwner {
1159			from, to = owner2, delegate2
1160		}
1161		ids = append(ids, grantAs(cur, from, to, MinPerPeriod, MinPeriodBlocks, 0, 0))
1162	}
1163	uassert.Equal(t, MaxRenderRows+2, len(ids))
1164
1165	index := Render("")
1166	uassert.True(t, strings.Contains(index, "Showing the 20 most recent of 22"))
1167	// Newest first, and the two oldest are off the page.
1168	uassert.True(t, strings.Contains(index, "#"+itoa(ids[len(ids)-1])))
1169	uassert.False(t, strings.Contains(index, "#"+itoa(ids[0])+"]"))
1170
1171	for i, id := range ids {
1172		from := owner1
1173		if int64(i) >= MaxGrantsPerOwner {
1174			from = owner2
1175		}
1176		revokeAs(cur, from, id)
1177	}
1178	withdrawAllAs(cur, owner1)
1179	withdrawAllAs(cur, owner2)
1180	assertSolvent(t)
1181}
1182
1183// --- the realm's own arithmetic ---
1184
1185func TestBpsRendersExactly(t *testing.T) {
1186	uassert.Equal(t, "1%", bps(100))
1187	uassert.Equal(t, "5%", bps(500))
1188	uassert.Equal(t, "0%", bps(0))
1189	uassert.Equal(t, "0.5%", bps(50))
1190	uassert.Equal(t, "0.05%", bps(5))
1191	uassert.Equal(t, "2.25%", bps(225))
1192	uassert.Equal(t, "100%", bps(10_000))
1193}
1194
1195func TestPadIDOrdersNumerically(t *testing.T) {
1196	uassert.True(t, padID(2) > padID(1))
1197	uassert.True(t, padID(10) > padID(9))
1198	uassert.True(t, padID(100) > padID(99))
1199	uassert.Equal(t, 20, len(padID(1)))
1200	uassert.Equal(t, 20, len(padID(1_000_000)))
1201}
1202
1203func TestCheckedAddRefusesOverflow(t *testing.T) {
1204	const max = int64(1<<63 - 1)
1205	sum, ok := checkedAdd(2, 3)
1206	uassert.True(t, ok)
1207	uassert.Equal(t, int64(5), sum)
1208
1209	_, ok = checkedAdd(max, 1)
1210	uassert.False(t, ok)
1211	_, ok = checkedAdd(max-1, 2)
1212	uassert.False(t, ok)
1213	sum, ok = checkedAdd(max-1, 1)
1214	uassert.True(t, ok)
1215	uassert.Equal(t, max, sum)
1216}
1217
1218// --- helpers used by the cases above ---
1219
1220// pendingOf unpacks PendingTerms without the four-value call site noise.
1221func pendingOf(id int64) (perPeriod, total int64, pending bool) {
1222	p, tot, _, has := PendingTerms(id)
1223	return p, tot, has
1224}
1225
1226// tidyFees drains the fee pot between cases so each test's fee
1227// assertions are absolute rather than cumulative. It is a test fixture,
1228// not a privileged path: WithdrawFees is permissionless and its
1229// destination is a compile-time constant, so there is nothing here a real
1230// caller could not also do.
1231func tidyFees(cur realm, t *testing.T) {
1232	if FeesAccrued() == 0 {
1233		return
1234	}
1235	testing.SetRealm(testing.NewUserRealm(mallory))
1236	WithdrawFees(cross(cur))
1237	uassert.Equal(t, int64(0), FeesAccrued())
1238}
1239
1240// TestRenderShowsGoverningTermsPastAnUnobservedBoundary pins the audit
1241// 2026-11-04 N1 repair. A queued loosening GOVERNS enforcement from the
1242// first boundary after it was queued -- spendbook's at() applies Pending
1243// whenever the window has rolled -- but the stored record still holds
1244// the old Terms until the next write. The grant page used to print the
1245// old per-window cap with a note claiming "the terms above are what
1246// govern": risk-understatement to the owner, on the same page whose
1247// "authorized by the terms" figure already used the pending cap. The
1248// page must derive its terms exactly as the gate does.
1249func TestRenderShowsGoverningTermsPastAnUnobservedBoundary(cur realm, t *testing.T) {
1250	ownr := testutils.TestAddress("n1-owner")
1251	spndr := testutils.TestAddress("n1-spender")
1252	fundAs(cur, ownr, 2_000_000)
1253	id := grantAs(cur, ownr, spndr, 1_000, MinPeriodBlocks, 0, 0)
1254
1255	// Queue a loosening: raised per-window cap. It must NOT govern yet.
1256	testing.SetRealm(testing.NewUserRealm(ownr))
1257	applied := Amend(cross(cur), id, 2_000, 0, 0)
1258	uassert.False(t, applied) // queued, not applied
1259	page := Render(itoa(id))
1260	if !strings.Contains(page, "per-window cap: 1000ugnot") ||
1261		!strings.Contains(page, "Until then the terms above are what govern") {
1262		t.Fatalf("pre-boundary page wrong:\n%s", page)
1263	}
1264
1265	// Cross the boundary with NO intervening write. The pending terms
1266	// now govern enforcement (at() applies them); the page must say so.
1267	testing.SkipHeights(MinPeriodBlocks)
1268	page = Render(itoa(id))
1269	if !strings.Contains(page, "per-window cap: 2000ugnot") {
1270		t.Fatalf("page still prints the superseded cap past the boundary:\n%s", page)
1271	}
1272	if strings.Contains(page, "Until then the terms above are what govern") {
1273		t.Fatalf("page still claims the stored terms govern past the boundary")
1274	}
1275}