package budgets import ( "strings" "testing" "chain" "gno.land/p/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/coinio" "gno.land/p/nt/testutils/v0" "gno.land/p/nt/uassert/v0" ) var ( owner1 = testutils.TestAddress("bgowner1") owner2 = testutils.TestAddress("bgowner2") delegate1 = testutils.TestAddress("bgdeleg1") delegate2 = testutils.TestAddress("bgdeleg2") mallory = testutils.TestAddress("bgmallory") ) // HARNESS NOTES, carried from the crowdfund, treasury_board and vesting // suites (measured there, relied on here): // // - testing.SetRealm records the override per FRAME INDEX: stage the // caller INLINE in this test function's own frame before a uassert // call, or inside a helper that stages and calls in one frame of its // own. Never inside a uassert closure — that staging does not take. // // - testing.SkipHeights REPLACES the whole context (OriginCaller, // CurrentRealm, OriginSend included). Re-stage after every skip. // // - Realm globals PERSIST across tests while banker state resets per // test. begin() disarms any leftover envelope and re-seeds the // realm's bank to its carried liabilities, so solvency is asserted in // DELTA form and every test tidies up after itself. // // - A panic caught by uassert does NOT roll back realm globals here, // whereas on-chain the whole transaction reverts. Every abort // asserted below happens strictly BEFORE this realm writes state, and // the one case where that is the POINT of the test // (TestDrawSolvencyRefusalConsumesNothing) asserts the untouched // state directly rather than relying on a rollback that does not // happen in this harness. // // - The SEND envelope is process-global: a staged envelope must be // cleared (SetOriginSend(nil)) before the next non-payable call, or // rejectStraySend fires and the test asserts the harness leak instead // of the guard. // // unsafe.CurrentRealm() is not available to a unit test, so `self` is // seeded from the package path the same way the chain derives it. func init() { self = chain.PackageAddress( "gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/budgets") } type baseline struct { held int64 pooled int64 credit int64 fees int64 } func begin() baseline { testing.SetOriginSend(nil) if l := Liabilities(); l > 0 { testing.IssueCoins(Address(), ugnot(l)) } return baseline{ held: Held(), pooled: TotalPooled(), credit: UnclaimedCredit(), fees: FeesAccrued(), } } func ugnot(n int64) chain.Coins { return chain.NewCoins(chain.NewCoin(Denom, n)) } // assertSolvent asserts the two halves of H == P + L + S that can actually // FAIL. The identity itself is definitional here, because Surplus() is // defined as Held() - Liabilities(), so asserting it would be a tautology // dressed as a check. What can fail is that the realm owes more than it // holds, and that L decomposes exactly into delegate credit plus the fee // pot — neither of which follows from any definition in this file. // It also re-derives the P term the slow way. totalPooled is maintained // O(1) for Render's sake, so it is a CACHE of the pools tree, and nothing // in the realm ever recomputes it — exactly the shape that drifts silently. // Sabotage found this gap rather than review: disabling the per-owner debit // in Draw while leaving totalPooled correct was detected only by a later // native-Go banker panic, never by an assertion. func assertSolvent(t *testing.T) { uassert.True(t, Held() >= Liabilities()) uassert.Equal(t, TotalPooled()+UnclaimedCredit()+FeesAccrued(), Liabilities()) sum := int64(0) pools.Iterate("", "", func(_ string, v any) bool { n := v.(int64) // setPool removes a zeroed entry rather than storing it, so every // surviving entry must be strictly positive. uassert.True(t, n > 0) sum += n return false }) uassert.Equal(t, sum, TotalPooled()) } // fundAs stages a direct EOA call with -send and mirrors the envelope into // the realm's bank, as the chain would. func fundAs(cur realm, from address, amount int64) int64 { testing.SetRealm(testing.NewUserRealm(from)) testing.SetOriginSend(ugnot(amount)) testing.IssueCoins(Address(), ugnot(amount)) n := Fund(cross(cur)) testing.SetOriginSend(nil) return n } func grantAs(cur realm, from, spender address, perPeriod, periodLength, total, expiresAt int64) int64 { testing.SetRealm(testing.NewUserRealm(from)) return Grant(cross(cur), spender, "", perPeriod, periodLength, total, expiresAt) } func drawAs(cur realm, from address, id, amount int64) int64 { testing.SetRealm(testing.NewUserRealm(from)) return Draw(cross(cur), id, amount) } func claimAs(cur realm, from address) int64 { testing.SetRealm(testing.NewUserRealm(from)) return Claim(cross(cur)) } func revokeAs(cur realm, from address, id int64) { testing.SetRealm(testing.NewUserRealm(from)) Revoke(cross(cur), id) } func withdrawAllAs(cur realm, from address) int64 { testing.SetRealm(testing.NewUserRealm(from)) return WithdrawPoolAll(cross(cur)) } func balanceOf(addr address) int64 { return coinio.HeldAt(addr, Denom) } // --- the pool --- func TestFundMakesAnOwner(cur realm, t *testing.T) { b := begin() uassert.Equal(t, int64(0), PoolOf(owner1)) pool := fundAs(cur, owner1, MinFunding) uassert.Equal(t, MinFunding, pool) uassert.Equal(t, MinFunding, PoolOf(owner1)) uassert.Equal(t, b.pooled+MinFunding, TotalPooled()) uassert.Equal(t, b.held+MinFunding, Held()) uassert.Equal(t, b.credit, UnclaimedCredit()) uassert.Equal(t, b.fees, FeesAccrued()) assertSolvent(t) // Funding again accumulates into the same pool. pool = fundAs(cur, owner1, 2*MinFunding) uassert.Equal(t, 3*MinFunding, pool) uassert.Equal(t, b.pooled+3*MinFunding, TotalPooled()) // A second owner's pool is independent. fundAs(cur, owner2, MinFunding) uassert.Equal(t, 3*MinFunding, PoolOf(owner1)) uassert.Equal(t, MinFunding, PoolOf(owner2)) assertSolvent(t) uassert.Equal(t, 3*MinFunding, withdrawAllAs(cur, owner1)) uassert.Equal(t, MinFunding, withdrawAllAs(cur, owner2)) uassert.Equal(t, b.pooled, TotalPooled()) // setPool removes a zeroed entry rather than storing 0. uassert.Equal(t, int64(0), PoolOf(owner1)) } func TestFundGuards(cur realm, t *testing.T) { begin() // coinio's receipt shape: a realm-routed payment is refused by the // primitive, not by anything this file could forget to write. testing.SetRealm(testing.NewCodeRealm("gno.land/r/demo/attacker")) testing.SetOriginSend(ugnot(MinFunding)) uassert.AbortsWithMessage(t, cur, "coinio: payment must be a direct EOA call with -send (realms and maketx-run are rejected)", func() { Fund(cross(cur)) }) testing.SetOriginSend(nil) // Wrong denomination. testing.SetRealm(testing.NewUserRealm(owner1)) testing.SetOriginSend(chain.NewCoins(chain.NewCoin("foocoin", MinFunding))) uassert.AbortsWithMessage(t, cur, "coinio: send exactly one coin type: ugnot", func() { Fund(cross(cur)) }) testing.SetOriginSend(nil) // No envelope at all. testing.SetRealm(testing.NewUserRealm(owner1)) uassert.AbortsWithMessage(t, cur, "coinio: send exactly one coin type: ugnot", func() { Fund(cross(cur)) }) // Below the minimum. Not issued into the bank, so the refused call // leaves no surplus behind. testing.SetRealm(testing.NewUserRealm(owner1)) testing.SetOriginSend(ugnot(MinFunding - 1)) uassert.AbortsWithMessage(t, cur, "fund at least 1000000ugnot", func() { Fund(cross(cur)) }) testing.SetOriginSend(nil) uassert.Equal(t, int64(0), PoolOf(owner1)) assertSolvent(t) } func TestWithdrawPoolIsUnconditional(cur realm, t *testing.T) { b := begin() fundAs(cur, owner1, 3*MinFunding) before := balanceOf(owner1) testing.SetRealm(testing.NewUserRealm(owner1)) left := WithdrawPool(cross(cur), MinFunding) uassert.Equal(t, 2*MinFunding, left) uassert.Equal(t, before+MinFunding, balanceOf(owner1)) uassert.Equal(t, b.pooled+2*MinFunding, TotalPooled()) assertSolvent(t) testing.SetRealm(testing.NewUserRealm(owner1)) uassert.AbortsWithMessage(t, cur, "amount exceeds pool", func() { WithdrawPool(cross(cur), 2*MinFunding+1) }) testing.SetRealm(testing.NewUserRealm(owner1)) uassert.AbortsWithMessage(t, cur, "amount must be positive", func() { WithdrawPool(cross(cur), 0) }) // A non-owner has no pool to take, and mallory cannot reach owner1's. testing.SetRealm(testing.NewUserRealm(mallory)) uassert.AbortsWithMessage(t, cur, "no pool to withdraw", func() { WithdrawPoolAll(cross(cur)) }) uassert.Equal(t, 2*MinFunding, PoolOf(owner1)) uassert.Equal(t, 2*MinFunding, withdrawAllAs(cur, owner1)) uassert.Equal(t, b.pooled, TotalPooled()) assertSolvent(t) } // TestOwnerCanAlwaysLeave pins the documented consequence of that design: // an owner may empty the pool under a LIVE grant, and the delegate's next // draw then fails on solvency rather than being trapped half-way. A realm // that locked the capital behind the grant would be the worse realm, and // this test is here so the behaviour cannot drift silently either way. func TestOwnerCanAlwaysLeave(cur realm, t *testing.T) { begin() fundAs(cur, owner1, MinFunding) id := grantAs(cur, owner1, delegate1, 500_000, MinPeriodBlocks, 0, 0) uassert.Equal(t, int64(500_000), Drawable(id)) withdrawAllAs(cur, owner1) uassert.Equal(t, int64(0), PoolOf(owner1)) // The AUTHORITY is untouched; only the honest figure moved. uassert.Equal(t, int64(500_000), Remaining(id)) uassert.Equal(t, int64(0), Drawable(id)) testing.SetRealm(testing.NewUserRealm(delegate1)) uassert.AbortsWithMessage(t, cur, "owner's pool cannot cover this draw", func() { Draw(cross(cur), id, 1_000) }) revokeAs(cur, owner1, id) assertSolvent(t) } // TestSelfGrantRecoversThePoolForTheFeeAlone pins the premise that the // MaxLiveGrants comment rests on, and it exists because the claim that // comment USED to make rotted undetected until an adversarial audit read // it (finding Y1). The lesson was not "that number was wrong" but "a // mitigation asserted nowhere is prose", so the replacement reasoning is // asserted here. // // The reasoning is that no capital bound on grant-slot occupation can be // built out of the pool, because an owner may be its OWN delegate. Make // an owner retain MinFunding while grants are live and this is the way // around it: grant to yourself, draw the pool to zero through your own // grant, Claim the credit back. You recover everything but the draw fee, // the slot is still held, and a retention rule reads as satisfied the // whole way through. The realm permits this deliberately — refusing a // self-grant would not close it either, since a second address the same // operator controls is free. func TestSelfGrantRecoversThePoolForTheFeeAlone(cur realm, t *testing.T) { begin() feesBefore := FeesAccrued() fundAs(cur, owner1, MinFunding) // owner1 is both parties. Nothing refuses it. id := grantAs(cur, owner1, owner1, MinFunding, MinPeriodBlocks, 0, 0) // Drain the whole pool through the owner's own grant. credited := drawAs(cur, owner1, id, MinFunding) fee := MinFunding * DrawFeeBps / 10_000 uassert.Equal(t, MinFunding-fee, credited) uassert.Equal(t, int64(0), PoolOf(owner1)) // ... and take it back out. Net cost of holding the slot: the fee. uassert.Equal(t, MinFunding-fee, claimAs(cur, owner1)) uassert.Equal(t, feesBefore+fee, FeesAccrued()) // The slot is still occupied, which is the whole point: the grant // survived the pool reaching zero, and no third party can remove it // because it carries no expiry. uassert.Equal(t, int64(1), ownerOpen(owner1.String())) uassert.False(t, IsExpired(id)) testing.SetRealm(testing.NewUserRealm(delegate1)) uassert.AbortsWithMessage(t, cur, "spendbook: allowance has not expired", func() { Reap(cross(cur), id) }) revokeAs(cur, owner1, id) uassert.Equal(t, int64(0), ownerOpen(owner1.String())) assertSolvent(t) } // --- grants --- func TestGrantRecordsTermsAndParties(cur realm, t *testing.T) { begin() fundAs(cur, owner1, MinFunding) h := Height() liveBefore := LiveGrants() testing.SetRealm(testing.NewUserRealm(owner1)) id := Grant(cross(cur), delegate1, "monthly ops", 50_000, MinPeriodBlocks, 200_000, h+500) uassert.Equal(t, id, NumGrants()) uassert.Equal(t, liveBefore+1, LiveGrants()) uassert.Equal(t, int64(1), GrantsOf(owner1)) uassert.Equal(t, "monthly ops", GrantLabel(id)) o, s, perPeriod, periodLength, total, expiresAt, anchor, createdAt, feeBps := GrantInfo(id) uassert.Equal(t, owner1, o) uassert.Equal(t, delegate1, s) uassert.Equal(t, int64(50_000), perPeriod) uassert.Equal(t, MinPeriodBlocks, periodLength) uassert.Equal(t, int64(200_000), total) uassert.Equal(t, h+500, expiresAt) // The anchor is the grant height: windows are anchored, not sliding, // so nobody resets a cap by choosing when to call. uassert.Equal(t, h, anchor) uassert.Equal(t, h, createdAt) uassert.Equal(t, DrawFeeBps, feeBps) spentWindow, spentTotal, windowIndex, draws := GrantUsage(id) uassert.Equal(t, int64(0), spentWindow) uassert.Equal(t, int64(0), spentTotal) uassert.Equal(t, int64(0), windowIndex) uassert.Equal(t, int64(0), draws) uassert.False(t, IsExpired(id)) revokeAs(cur, owner1, id) uassert.Equal(t, int64(0), GrantsOf(owner1)) uassert.Equal(t, liveBefore, LiveGrants()) withdrawAllAs(cur, owner1) } func TestGrantValidation(cur realm, t *testing.T) { begin() // No pool: a grant from an address with nothing behind it is refused // before any allowance is opened. testing.SetRealm(testing.NewUserRealm(owner1)) uassert.AbortsWithMessage(t, cur, "fund a pool before granting", func() { Grant(cross(cur), delegate1, "", 50_000, MinPeriodBlocks, 0, 0) }) fundAs(cur, owner1, MinFunding) testing.SetRealm(testing.NewUserRealm(owner1)) uassert.AbortsWithMessage(t, cur, "spender is not a valid address in canonical (lowercase) form", func() { Grant(cross(cur), address("not-an-address"), "", 50_000, MinPeriodBlocks, 0, 0) }) testing.SetRealm(testing.NewUserRealm(owner1)) uassert.AbortsWithMessage(t, cur, "label exceeds 64 bytes", func() { Grant(cross(cur), delegate1, strings.Repeat("x", MaxLabelLen+1), 50_000, MinPeriodBlocks, 0, 0) }) testing.SetRealm(testing.NewUserRealm(owner1)) uassert.AbortsWithMessage(t, cur, "perPeriod must be at least 1000ugnot", func() { Grant(cross(cur), delegate1, "", MinPerPeriod-1, MinPeriodBlocks, 0, 0) }) testing.SetRealm(testing.NewUserRealm(owner1)) uassert.AbortsWithMessage(t, cur, "periodLength must be 0 (lifetime) or at least 100 blocks", func() { Grant(cross(cur), delegate1, "", 50_000, MinPeriodBlocks-1, 0, 0) }) // An expiry already in the past is spendbook's own refusal: an // allowance that can authorize nothing must not become a live entry. testing.SetRealm(testing.NewUserRealm(owner1)) uassert.AbortsWithMessage(t, cur, "spendbook: invalid terms", func() { Grant(cross(cur), delegate1, "", 50_000, MinPeriodBlocks, 0, 1) }) uassert.Equal(t, int64(0), GrantsOf(owner1)) withdrawAllAs(cur, owner1) } // TestGrantRefusesUppercaseSpender measures the premise of Grant's // lowercase clause rather than asserting it from the bech32 spec. If // IsValid() ever stops accepting the uppercase spelling, the first // assertion here fails and the clause's comment must be corrected — which // is the point of measuring it. func TestGrantRefusesUppercaseSpender(cur realm, t *testing.T) { begin() fundAs(cur, owner1, MinFunding) upper := address(strings.ToUpper(string(delegate1))) uassert.True(t, upper != delegate1) uassert.True(t, upper.IsValid()) // the hazard, measured on this toolchain testing.SetRealm(testing.NewUserRealm(owner1)) uassert.AbortsWithMessage(t, cur, "spender is not a valid address in canonical (lowercase) form", func() { Grant(cross(cur), upper, "", 50_000, MinPeriodBlocks, 0, 0) }) uassert.Equal(t, int64(0), GrantsOf(owner1)) withdrawAllAs(cur, owner1) } // TestGrantPerOwnerCap covers the gap spendbook's own documentation names: // maxLive is a single global figure, so without a per-owner cap one owner // could exhaust it and lock everyone else out. func TestGrantPerOwnerCap(cur realm, t *testing.T) { begin() fundAs(cur, owner1, MinFunding) ids := []int64{} for i := int64(0); i < MaxGrantsPerOwner; i++ { ids = append(ids, grantAs(cur, owner1, delegate1, MinPerPeriod, MinPeriodBlocks, 0, 0)) } uassert.Equal(t, MaxGrantsPerOwner, GrantsOf(owner1)) testing.SetRealm(testing.NewUserRealm(owner1)) uassert.AbortsWithMessage(t, cur, "owner already has 16 live grants", func() { Grant(cross(cur), delegate1, "", MinPerPeriod, MinPeriodBlocks, 0, 0) }) // A DIFFERENT owner is unaffected — the cap is per-owner, not global. fundAs(cur, owner2, MinFunding) other := grantAs(cur, owner2, delegate2, MinPerPeriod, MinPeriodBlocks, 0, 0) uassert.Equal(t, int64(1), GrantsOf(owner2)) // Revoking frees a slot, and the id is not reused. revokeAs(cur, owner1, ids[0]) uassert.Equal(t, MaxGrantsPerOwner-1, GrantsOf(owner1)) fresh := grantAs(cur, owner1, delegate1, MinPerPeriod, MinPeriodBlocks, 0, 0) uassert.True(t, fresh > ids[len(ids)-1]) for _, id := range ids[1:] { revokeAs(cur, owner1, id) } revokeAs(cur, owner1, fresh) revokeAs(cur, owner2, other) uassert.Equal(t, int64(0), GrantsOf(owner1)) uassert.Equal(t, int64(0), GrantsOf(owner2)) withdrawAllAs(cur, owner1) withdrawAllAs(cur, owner2) assertSolvent(t) } func TestGrantAndAmendAreOwnerOnly(cur realm, t *testing.T) { begin() fundAs(cur, owner1, MinFunding) id := grantAs(cur, owner1, delegate1, 50_000, MinPeriodBlocks, 0, 0) testing.SetRealm(testing.NewUserRealm(mallory)) uassert.AbortsWithMessage(t, cur, "owner only", func() { Amend(cross(cur), id, 10_000, 0, 0) }) testing.SetRealm(testing.NewUserRealm(mallory)) uassert.AbortsWithMessage(t, cur, "owner only", func() { Revoke(cross(cur), id) }) // Not even the delegate may amend or revoke their own grant. testing.SetRealm(testing.NewUserRealm(delegate1)) uassert.AbortsWithMessage(t, cur, "owner only", func() { Amend(cross(cur), id, 10_000, 0, 0) }) testing.SetRealm(testing.NewUserRealm(owner1)) uassert.AbortsWithMessage(t, cur, "unknown grant id", func() { Revoke(cross(cur), 999_999) }) revokeAs(cur, owner1, id) withdrawAllAs(cur, owner1) } // --- drawing --- func TestDrawIsDelegateOnlyAndCreditsNetOfFee(cur realm, t *testing.T) { b := begin() fundAs(cur, owner1, MinFunding) id := grantAs(cur, owner1, delegate1, 500_000, MinPeriodBlocks, 0, 0) // Nobody but the named delegate — not mallory, and not the owner. testing.SetRealm(testing.NewUserRealm(mallory)) uassert.AbortsWithMessage(t, cur, "delegate only", func() { Draw(cross(cur), id, 1_000) }) testing.SetRealm(testing.NewUserRealm(owner1)) uassert.AbortsWithMessage(t, cur, "delegate only", func() { Draw(cross(cur), id, 1_000) }) testing.SetRealm(testing.NewUserRealm(delegate1)) uassert.AbortsWithMessage(t, cur, "amount must be positive", func() { Draw(cross(cur), id, 0) }) heldBefore := Held() credited := drawAs(cur, delegate1, id, 100_000) // 1% of the gross, floor-rounded in the delegate's favour. uassert.Equal(t, int64(99_000), credited) uassert.Equal(t, int64(99_000), CreditOf(delegate1)) uassert.Equal(t, b.fees+1_000, FeesAccrued()) // The owner's pool is debited the GROSS: the fee is paid out of the // budget, not out of the delegate's authority. uassert.Equal(t, MinFunding-100_000, PoolOf(owner1)) uassert.Equal(t, b.pooled+MinFunding-100_000, TotalPooled()) // Draw moves value between P and L only. H does not move at all, which // is why this transition has no payout that could fail. uassert.Equal(t, heldBefore, Held()) assertSolvent(t) spentWindow, spentTotal, _, draws := GrantUsage(id) uassert.Equal(t, int64(100_000), spentWindow) uassert.Equal(t, int64(100_000), spentTotal) uassert.Equal(t, int64(1), draws) uassert.Equal(t, int64(400_000), Remaining(id)) // Claim is the separate transaction that moves coins. before := balanceOf(delegate1) paid := claimAs(cur, delegate1) uassert.Equal(t, int64(99_000), paid) uassert.Equal(t, before+99_000, balanceOf(delegate1)) uassert.Equal(t, int64(0), CreditOf(delegate1)) uassert.Equal(t, b.credit, UnclaimedCredit()) assertSolvent(t) testing.SetRealm(testing.NewUserRealm(delegate1)) uassert.AbortsWithMessage(t, cur, "no credit to claim", func() { Claim(cross(cur)) }) revokeAs(cur, owner1, id) withdrawAllAs(cur, owner1) tidyFees(cur, t) } // TestDrawSolvencyRefusalConsumesNothing is this realm's central // invariant, and the ordering inside Draw is what it proves: the solvency // check runs BEFORE spendbook.Spend, so a draw refused for insolvency // leaves the window capacity intact rather than burning it on a draw that // never happened. // // This is also the one abort in this file where the post-state is the // assertion rather than an afterthought, because uassert does not roll // back realm globals the way a reverted transaction would. func TestDrawSolvencyRefusalConsumesNothing(cur realm, t *testing.T) { begin() fundAs(cur, owner1, MinFunding) // Authority deliberately EXCEEDS the pool: a budget is authority, not // a reservation, and the realm's promise is that it never credits what // it cannot pay. id := grantAs(cur, owner1, delegate1, 5*MinFunding, MinPeriodBlocks, 0, 0) uassert.Equal(t, 5*MinFunding, Remaining(id)) uassert.Equal(t, MinFunding, Drawable(id)) // the honest figure testing.SetRealm(testing.NewUserRealm(delegate1)) uassert.AbortsWithMessage(t, cur, "owner's pool cannot cover this draw", func() { Draw(cross(cur), id, MinFunding+1) }) // Nothing moved, and — the part that matters — nothing was CONSUMED. spentWindow, spentTotal, _, draws := GrantUsage(id) uassert.Equal(t, int64(0), spentWindow) uassert.Equal(t, int64(0), spentTotal) uassert.Equal(t, int64(0), draws) uassert.Equal(t, 5*MinFunding, Remaining(id)) uassert.Equal(t, MinFunding, PoolOf(owner1)) uassert.Equal(t, int64(0), CreditOf(delegate1)) assertSolvent(t) // Exactly the drawable figure succeeds, which is what makes the // refusal above a boundary rather than a blanket. uassert.Equal(t, int64(990_000), drawAs(cur, delegate1, id, MinFunding)) uassert.Equal(t, int64(0), PoolOf(owner1)) claimAs(cur, delegate1) revokeAs(cur, owner1, id) tidyFees(cur, t) assertSolvent(t) } func TestDrawFeeRoundingFavorsTheDelegate(cur realm, t *testing.T) { b := begin() fundAs(cur, owner1, MinFunding) id := grantAs(cur, owner1, delegate1, 50_000, MinPeriodBlocks, 0, 0) // 1% of 99 floors to 0, so the delegate keeps all of it. There is // deliberately no minimum draw: a floor above a grant's PerPeriod // would make the grant silently undrawable. uassert.Equal(t, int64(99), drawAs(cur, delegate1, id, 99)) uassert.Equal(t, b.fees, FeesAccrued()) uassert.Equal(t, int64(99), CreditOf(delegate1)) // 100 is the first amount that pays a fee at all. uassert.Equal(t, int64(99), drawAs(cur, delegate1, id, 100)) uassert.Equal(t, b.fees+1, FeesAccrued()) uassert.Equal(t, int64(198), CreditOf(delegate1)) assertSolvent(t) claimAs(cur, delegate1) revokeAs(cur, owner1, id) withdrawAllAs(cur, owner1) tidyFees(cur, t) } // TestWindowRefillsAtAnchoredBoundary is spendbook consumer obligation 1 // observed from the consuming side: the clock is runtime.ChainHeight() and // no caller supplies it, so the window cannot be rolled on demand. The // refill is a RESET, not a carry. func TestWindowRefillsAtAnchoredBoundary(cur realm, t *testing.T) { begin() fundAs(cur, owner1, 10*MinFunding) id := grantAs(cur, owner1, delegate1, 100_000, MinPeriodBlocks, 0, 0) drawAs(cur, delegate1, id, 100_000) uassert.Equal(t, int64(0), Remaining(id)) testing.SetRealm(testing.NewUserRealm(delegate1)) uassert.AbortsWithMessage(t, cur, "spendbook: amount exceeds the window cap", func() { Draw(cross(cur), id, 1) }) // One block short of the boundary: still refused. testing.SkipHeights(MinPeriodBlocks - 1) uassert.Equal(t, int64(0), Remaining(id)) testing.SetRealm(testing.NewUserRealm(delegate1)) uassert.AbortsWithMessage(t, cur, "spendbook: amount exceeds the window cap", func() { Draw(cross(cur), id, 1) }) // The boundary itself. The window index advances and the cap is full // again — not 100_000 plus a carried remainder. testing.SkipHeights(1) uassert.Equal(t, int64(100_000), Remaining(id)) _, _, windowIndex, _ := GrantUsage(id) uassert.Equal(t, int64(1), windowIndex) // THE STALE-PAIR CASE, and it is asserted here rather than left to // inference because this is the only moment it is observable: the // chain has crossed a boundary and NOTHING has been written since, so // Allowance.WindowIndex still names window 0 and Allowance.SpentWindow // still holds window 0's 100_000. Reading either field straight off // the record would publish "spent this window: 100000" directly above // "authorized by the terms: 100000" — a page contradicting itself. // spentInWindow is what makes the first figure 0, and this assertion // is what keeps it that way. staleWindow, _, _, _ := GrantUsage(id) uassert.Equal(t, int64(0), staleWindow) page := Render(itoa(id)) uassert.True(t, strings.Contains(page, "spent this window: 0ugnot")) uassert.True(t, strings.Contains(page, "now in window 1")) drawAs(cur, delegate1, id, 100_000) _, spentTotal, _, draws := GrantUsage(id) uassert.Equal(t, int64(200_000), spentTotal) // lifetime accumulates uassert.Equal(t, int64(2), draws) spentWindow, _, _, _ := GrantUsage(id) uassert.Equal(t, int64(100_000), spentWindow) // the window reset assertSolvent(t) claimAs(cur, delegate1) revokeAs(cur, owner1, id) withdrawAllAs(cur, owner1) tidyFees(cur, t) } func TestLifetimeTotalBindsAcrossARollover(cur realm, t *testing.T) { begin() fundAs(cur, owner1, 10*MinFunding) // Lifetime cap at 1.5 windows: the refill cannot outrun it. id := grantAs(cur, owner1, delegate1, 100_000, MinPeriodBlocks, 150_000, 0) drawAs(cur, delegate1, id, 100_000) testing.SkipHeights(MinPeriodBlocks) // The window refilled to 100_000 but the lifetime cap leaves 50_000, // and Remaining reports the LESSER of the two. uassert.Equal(t, int64(50_000), Remaining(id)) testing.SetRealm(testing.NewUserRealm(delegate1)) uassert.AbortsWithMessage(t, cur, "spendbook: amount exceeds the lifetime cap", func() { Draw(cross(cur), id, 50_001) }) drawAs(cur, delegate1, id, 50_000) uassert.Equal(t, int64(0), Remaining(id)) // A further window brings no further authority: the lifetime cap is // absolute. testing.SkipHeights(MinPeriodBlocks) uassert.Equal(t, int64(0), Remaining(id)) testing.SetRealm(testing.NewUserRealm(delegate1)) uassert.AbortsWithMessage(t, cur, "spendbook: amount exceeds the lifetime cap", func() { Draw(cross(cur), id, 1) }) assertSolvent(t) claimAs(cur, delegate1) revokeAs(cur, owner1, id) withdrawAllAs(cur, owner1) tidyFees(cur, t) } // TestLifetimeWindowNeverRefills covers periodLength == 0: ONE window for // the grant's whole life. It is a distinct code path in spendbook // (windowIndex is pinned to 0) and a distinct product shape — a one-off // authority rather than a recurring budget. func TestLifetimeWindowNeverRefills(cur realm, t *testing.T) { begin() fundAs(cur, owner1, MinFunding) id := grantAs(cur, owner1, delegate1, 100_000, 0, 0, 0) // GrantInfo's fourth result is periodLength; the third is perPeriod. _, _, _, periodLength, _, _, _, _, _ := GrantInfo(id) uassert.Equal(t, int64(0), periodLength) drawAs(cur, delegate1, id, 100_000) uassert.Equal(t, int64(0), Remaining(id)) testing.SkipHeights(10 * MinPeriodBlocks) uassert.Equal(t, int64(0), Remaining(id)) _, _, windowIndex, _ := GrantUsage(id) uassert.Equal(t, int64(0), windowIndex) testing.SetRealm(testing.NewUserRealm(delegate1)) uassert.AbortsWithMessage(t, cur, "spendbook: amount exceeds the window cap", func() { Draw(cross(cur), id, 1) }) // And a LOOSENING cannot be queued against a grant with no boundary to // wait for — spendbook refuses rather than accepting an amendment that // would never take effect. testing.SetRealm(testing.NewUserRealm(owner1)) uassert.AbortsWithMessage(t, cur, "spendbook: no reachable next window boundary", func() { Amend(cross(cur), id, 200_000, 0, 0) }) assertSolvent(t) claimAs(cur, delegate1) revokeAs(cur, owner1, id) withdrawAllAs(cur, owner1) tidyFees(cur, t) } // --- amendment --- func TestAmendIsOneSided(cur realm, t *testing.T) { begin() fundAs(cur, owner1, 10*MinFunding) id := grantAs(cur, owner1, delegate1, 100_000, MinPeriodBlocks, 0, 0) // A TIGHTENING takes effect immediately, mid-window. testing.SetRealm(testing.NewUserRealm(owner1)) uassert.True(t, Amend(cross(cur), id, 20_000, 0, 0)) uassert.Equal(t, int64(20_000), Remaining(id)) _, _, pending := pendingOf(id) uassert.False(t, pending) testing.SetRealm(testing.NewUserRealm(delegate1)) uassert.AbortsWithMessage(t, cur, "spendbook: amount exceeds the window cap", func() { Draw(cross(cur), id, 20_001) }) // A LOOSENING is queued for the next boundary, not applied now. That // asymmetry is the whole guarantee: the delegate's exposure in the // current window can only ever shrink. testing.SetRealm(testing.NewUserRealm(owner1)) uassert.False(t, Amend(cross(cur), id, 300_000, 0, 0)) uassert.Equal(t, int64(20_000), Remaining(id)) perPeriod, _, pending := pendingOf(id) uassert.True(t, pending) uassert.Equal(t, int64(300_000), perPeriod) testing.SetRealm(testing.NewUserRealm(delegate1)) uassert.AbortsWithMessage(t, cur, "spendbook: amount exceeds the window cap", func() { Draw(cross(cur), id, 20_001) }) // At the boundary the pending terms become the live ones. testing.SkipHeights(MinPeriodBlocks) uassert.Equal(t, int64(300_000), Remaining(id)) drawAs(cur, delegate1, id, 300_000) assertSolvent(t) // The realm's own floor still applies to an amendment. testing.SetRealm(testing.NewUserRealm(owner1)) uassert.AbortsWithMessage(t, cur, "perPeriod must be at least 1000ugnot", func() { Amend(cross(cur), id, MinPerPeriod-1, 0, 0) }) claimAs(cur, delegate1) revokeAs(cur, owner1, id) withdrawAllAs(cur, owner1) tidyFees(cur, t) } // TestAmendCannotMovePeriodLength asserts a STRUCTURAL property, not a // refusal: Amend has no periodLength parameter at all, so moving every // future boundary is not merely rejected, it is unexpressible. The realm // reads the grant's own periodLength and passes it back unchanged. func TestAmendCannotMovePeriodLength(cur realm, t *testing.T) { begin() fundAs(cur, owner1, MinFunding) id := grantAs(cur, owner1, delegate1, 100_000, MinPeriodBlocks, 0, 0) _, _, _, before, _, _, anchorBefore, _, _ := GrantInfo(id) testing.SetRealm(testing.NewUserRealm(owner1)) uassert.True(t, Amend(cross(cur), id, 10_000, 0, 0)) _, _, _, after, _, _, anchorAfter, _, _ := GrantInfo(id) uassert.Equal(t, before, after) uassert.Equal(t, MinPeriodBlocks, after) // The anchor is immovable too, so the boundaries stay where they were. uassert.Equal(t, anchorBefore, anchorAfter) revokeAs(cur, owner1, id) withdrawAllAs(cur, owner1) } // --- ending a grant --- func TestRevokeEndsAuthorityButNotEarnedCredit(cur realm, t *testing.T) { begin() fundAs(cur, owner1, MinFunding) id := grantAs(cur, owner1, delegate1, 100_000, MinPeriodBlocks, 0, 0) drawAs(cur, delegate1, id, 100_000) uassert.Equal(t, int64(99_000), CreditOf(delegate1)) revokeAs(cur, owner1, id) // The grant record is gone and the id is never reused, so a later draw // fails as unknown rather than as unauthorized. testing.SetRealm(testing.NewUserRealm(delegate1)) uassert.AbortsWithMessage(t, cur, "unknown grant id", func() { Draw(cross(cur), id, 1) }) testing.SetRealm(testing.NewUserRealm(owner1)) uassert.AbortsWithMessage(t, cur, "unknown grant id", func() { Revoke(cross(cur), id) }) // Credit already drawn is the delegate's. Revoke ends FUTURE authority // and claws back nothing. uassert.Equal(t, int64(99_000), CreditOf(delegate1)) before := balanceOf(delegate1) uassert.Equal(t, int64(99_000), claimAs(cur, delegate1)) uassert.Equal(t, before+99_000, balanceOf(delegate1)) assertSolvent(t) withdrawAllAs(cur, owner1) tidyFees(cur, t) } func TestReapIsPermissionlessButOnlyForExpired(cur realm, t *testing.T) { begin() fundAs(cur, owner1, MinFunding) h := Height() id := grantAs(cur, owner1, delegate1, 100_000, MinPeriodBlocks, 0, h+200) noExpiry := grantAs(cur, owner1, delegate2, 100_000, MinPeriodBlocks, 0, 0) // Live: refused, for anyone. testing.SetRealm(testing.NewUserRealm(mallory)) uassert.AbortsWithMessage(t, cur, "spendbook: allowance has not expired", func() { Reap(cross(cur), id) }) uassert.False(t, IsExpired(id)) testing.SkipHeights(200) uassert.True(t, IsExpired(id)) // An expired allowance can authorize nothing, so its own delegate is // refused too — and a stranger may clear it. testing.SetRealm(testing.NewUserRealm(delegate1)) uassert.AbortsWithMessage(t, cur, "spendbook: allowance has expired", func() { Draw(cross(cur), id, 1) }) liveBefore := LiveGrants() testing.SetRealm(testing.NewUserRealm(mallory)) Reap(cross(cur), id) uassert.Equal(t, liveBefore-1, LiveGrants()) uassert.Equal(t, int64(1), GrantsOf(owner1)) // A grant with NO expiry can never be reaped: only its owner ends it. uassert.False(t, IsExpired(noExpiry)) testing.SetRealm(testing.NewUserRealm(mallory)) uassert.AbortsWithMessage(t, cur, "spendbook: allowance has not expired", func() { Reap(cross(cur), noExpiry) }) revokeAs(cur, owner1, noExpiry) withdrawAllAs(cur, owner1) assertSolvent(t) } // --- fees, surplus, and the absence of an admin --- func TestWithdrawFeesGoesOnlyToTheFixedCollector(cur realm, t *testing.T) { begin() fundAs(cur, owner1, MinFunding) id := grantAs(cur, owner1, delegate1, 500_000, MinPeriodBlocks, 0, 0) drawAs(cur, delegate1, id, 500_000) uassert.Equal(t, int64(5_000), FeesAccrued()) // Permissionless, because the destination is a compile-time constant: // there is nothing for a caller to steer. before := balanceOf(FeeCollector) testing.SetRealm(testing.NewUserRealm(mallory)) uassert.Equal(t, int64(5_000), WithdrawFees(cross(cur))) uassert.Equal(t, before+5_000, balanceOf(FeeCollector)) uassert.Equal(t, int64(0), FeesAccrued()) testing.SetRealm(testing.NewUserRealm(mallory)) uassert.AbortsWithMessage(t, cur, "no fees accrued", func() { WithdrawFees(cross(cur)) }) assertSolvent(t) claimAs(cur, delegate1) revokeAs(cur, owner1, id) withdrawAllAs(cur, owner1) } func TestSweepSurplusCannotReachTrackedLiabilities(cur realm, t *testing.T) { begin() fundAs(cur, owner1, MinFunding) id := grantAs(cur, owner1, delegate1, 500_000, MinPeriodBlocks, 0, 0) drawAs(cur, delegate1, id, 200_000) // Nothing out of band yet: the reserve is everything the realm holds, // and the primitive ABORTS rather than reporting a transfer of zero. uassert.Equal(t, int64(0), Surplus()) testing.SetRealm(testing.NewUserRealm(mallory)) uassert.AbortsWithMessage(t, cur, "coinio: no surplus to sweep for ugnot", func() { SweepSurplus(cross(cur), Denom) }) uassert.Equal(t, MinFunding-200_000, PoolOf(owner1)) uassert.Equal(t, int64(198_000), CreditOf(delegate1)) uassert.Equal(t, int64(2_000), FeesAccrued()) // A foreign denom the realm has never held is the same answer: there // is nothing to move, so nothing claims to have moved. testing.SetRealm(testing.NewUserRealm(mallory)) uassert.AbortsWithMessage(t, cur, "coinio: no surplus to sweep for foocoin", func() { SweepSurplus(cross(cur), "foocoin") }) // Coins arriving outside any entrypoint ARE sweepable, and only those. testing.IssueCoins(Address(), ugnot(777)) uassert.Equal(t, int64(777), Surplus()) before := balanceOf(FeeCollector) testing.SetRealm(testing.NewUserRealm(mallory)) uassert.Equal(t, int64(777), SweepSurplus(cross(cur), Denom)) uassert.Equal(t, before+777, balanceOf(FeeCollector)) uassert.Equal(t, int64(0), Surplus()) // Every tracked term survived the sweep untouched. uassert.Equal(t, MinFunding-200_000, PoolOf(owner1)) uassert.Equal(t, int64(198_000), CreditOf(delegate1)) uassert.Equal(t, int64(2_000), FeesAccrued()) assertSolvent(t) claimAs(cur, delegate1) revokeAs(cur, owner1, id) withdrawAllAs(cur, owner1) tidyFees(cur, t) } // TestStraySendIsRejectedOnEveryNonPayableEntrypoint sweeps the whole // non-payable surface. A coin attached to any of these would otherwise // become sweepable surplus — silently converting a user's coins into // operator funds — so aborting and reverting the transfer is the only safe // answer. The count assertion is deliberate: a table that silently lost // its rows would otherwise report green. func TestStraySendIsRejectedOnEveryNonPayableEntrypoint(cur realm, t *testing.T) { begin() fundAs(cur, owner1, MinFunding) id := grantAs(cur, owner1, delegate1, 100_000, MinPeriodBlocks, 0, 0) // No IssueCoins: the envelope is staged but never mirrored into the // bank, so a refused call cannot leave surplus behind. calls := []func(){ func() { WithdrawPool(cross(cur), 1) }, func() { WithdrawPoolAll(cross(cur)) }, func() { Grant(cross(cur), delegate2, "", 100_000, MinPeriodBlocks, 0, 0) }, func() { Amend(cross(cur), id, 100_000, 0, 0) }, func() { Revoke(cross(cur), id) }, func() { Reap(cross(cur), id) }, func() { Draw(cross(cur), id, 1_000) }, func() { Claim(cross(cur)) }, func() { WithdrawFees(cross(cur)) }, func() { SweepSurplus(cross(cur), Denom) }, } uassert.Equal(t, 10, len(calls)) for _, call := range calls { testing.SetRealm(testing.NewUserRealm(owner1)) testing.SetOriginSend(ugnot(1)) uassert.AbortsWithMessage(t, cur, "this entrypoint does not accept coins", call) testing.SetOriginSend(nil) } // The guard runs FIRST in every one of them, which is why even calls // that would have failed on their own arguments abort with this // message — and why nothing above changed any state. uassert.Equal(t, MinFunding, PoolOf(owner1)) uassert.Equal(t, int64(1), GrantsOf(owner1)) uassert.Equal(t, int64(100_000), Remaining(id)) assertSolvent(t) revokeAs(cur, owner1, id) withdrawAllAs(cur, owner1) } // TestDrawableIsTheLesserOfAuthorityAndPool pins the realm's own // contribution to the view surface. spendbook bounds PERMISSION and says // so; it never speaks about solvency. Remaining is the authority figure // and Drawable is the honest one, and conflating them is what would make // the realm promise what it does not hold. func TestDrawableIsTheLesserOfAuthorityAndPool(cur realm, t *testing.T) { begin() fundAs(cur, owner1, MinFunding) // Authority below the pool: authority binds. small := grantAs(cur, owner1, delegate1, 100_000, MinPeriodBlocks, 0, 0) uassert.Equal(t, int64(100_000), Remaining(small)) uassert.Equal(t, int64(100_000), Drawable(small)) // Authority above the pool: the pool binds, and Remaining does NOT // move — the two views answer different questions. big := grantAs(cur, owner1, delegate2, 5*MinFunding, MinPeriodBlocks, 0, 0) uassert.Equal(t, 5*MinFunding, Remaining(big)) uassert.Equal(t, MinFunding, Drawable(big)) // Two grants, one pool: three grants of 100 against a pool of 100 are // all three valid and the first delegate to draw gets the money. The // sum of what is drawable deliberately EXCEEDS the pool, because a // budget is authority and not a reservation. uassert.True(t, Drawable(small)+Drawable(big) > PoolOf(owner1)) drawAs(cur, delegate1, small, 100_000) uassert.Equal(t, MinFunding-100_000, Drawable(big)) // the other shrank claimAs(cur, delegate1) revokeAs(cur, owner1, small) revokeAs(cur, owner1, big) withdrawAllAs(cur, owner1) tidyFees(cur, t) } func TestViewsRefuseUnknownIds(cur realm, t *testing.T) { begin() // PanicsWithMessage, not AbortsWithMessage, and the distinction is the // view/entrypoint one rather than a style choice. revive() catches an // ABORT, which is what a panic becomes when it crosses a realm // boundary; these six are non-crossing view functions called straight // from the test's own frame, so their panic never crosses anything and // stays a plain panic. Every other refusal asserted in this file goes // through a crossing helper (drawAs, grantAs, …) and is correctly an // abort. Getting this backwards does not weaken the assertion — it // fails loudly, which is how it was found. uassert.PanicsWithMessage(t, cur, "unknown grant id", func() { GrantInfo(999_999) }) uassert.PanicsWithMessage(t, cur, "unknown grant id", func() { GrantUsage(999_999) }) uassert.PanicsWithMessage(t, cur, "unknown grant id", func() { GrantLabel(999_999) }) uassert.PanicsWithMessage(t, cur, "unknown grant id", func() { Remaining(999_999) }) uassert.PanicsWithMessage(t, cur, "unknown grant id", func() { Drawable(999_999) }) uassert.PanicsWithMessage(t, cur, "unknown grant id", func() { IsExpired(999_999) }) // The zero values of the non-panicking views are "nothing", not an // error, which is correct for an address that simply has no state. uassert.Equal(t, int64(0), PoolOf(mallory)) uassert.Equal(t, int64(0), CreditOf(mallory)) uassert.Equal(t, int64(0), GrantsOf(mallory)) } // --- render --- func TestRenderPages(cur realm, t *testing.T) { begin() fundAs(cur, owner1, MinFunding) h := Height() testing.SetRealm(testing.NewUserRealm(owner1)) id := Grant(cross(cur), delegate1, "ops ", 100_000, MinPeriodBlocks, 500_000, h+1_000) drawAs(cur, delegate1, id, 40_000) index := Render("") uassert.True(t, strings.Contains(index, "# Budgets")) uassert.True(t, strings.Contains(index, "conservation: OK")) uassert.True(t, strings.Contains(index, "[live]")) uassert.True(t, strings.Contains(index, "#"+itoa(id))) // The free-text label reaches markdown only through the ecosystem // sanitizer, so the raw tag must not survive into the page. uassert.False(t, strings.Contains(index, "