const Denom
Denom is the only asset this realm holds.
Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.
Realm budgets is a delegated-budget treasury in GNOT: an owner funds a pool, grants named delegates a standing, rate-...
Realm budgets is a delegated-budget treasury in GNOT: an owner funds a pool, grants named delegates a standing, rate-limited, revocable authority to spend from it, and each delegate then draws within that authority with no further approval from anyone.
WHY THIS EXISTS (see DISCOVERY.md): every disbursement path on either live chain is one of three shapes — a fresh group decision per spend (this portfolio's treasury_board, gno.land's r/gov/dao/treasury, the mainnet bubbletreasury), a fixed schedule (vesting), or a pull against an already-settled entitlement (fee_split, revshare, crowdfund, subscriptions). None of them lets one party spend another's money at its own discretion within a bound. That is the gap, and bubbletreasury — a real mainnet treasury that hand-rolled a sliding spend window inside the realm, with every spend still a token-holder proposal — is the evidence someone wanted it.
It is also, deliberately, the FIRST CONSUMER of spendbook on any chain. A primitive with no consumer has two properties unproven no matter how many unit tests pass: that its API is usable from a realm, and that its state survives across transactions. Only a realm can answer either, and this one is the answer.
COMPOSITION: spending authority is delegated entirely to spendbook (anchored windows, lifetime caps, expiry, one-sided amendments, revocation), coin movement to coinio, per-delegate credit and the fee pot to feeledger, ordered storage to p/nt/avl/v0, and free-text render output to p/nt/markdown/sanitize/v0. This realm owns only the pool accounting, the grant records, and the solvency rule.
A budget is AUTHORITY, not a reservation. Three grants of 100 against a pool of 100 are all three valid, and the first delegate to draw gets the money. This is not a defect to be fixed by reserving funds: a reservation would make an owner unable to grant a standing allowance without pre-committing the capital, which is the whole convenience being bought. What the realm guarantees instead is that it never PROMISES what it does not HOLD:
1Draw refuses unless amount <= the owner's own unspent pool.
So a delegate is told "no" at draw time rather than credited with something unclaimable. The pool is the solvency bound and it is per-owner, which is strictly stronger than a realm-wide check: one owner's grants can never reach another owner's funds.
Draw moves nothing. It consumes the allowance, debits the owner's pool, and credits the delegate in the ledger; the delegate then signs a separate Claim to receive the coins. This is not ritual. If Draw paid out directly, a failing payout inside the same transaction would leave a consumed allowance with no money delivered, or force the allowance consumption to happen after an effect — and spendbook's own consumer contract item 3 says state first, effect second, for exactly that reason. Splitting it also means a delegate can draw repeatedly and claim once, which is cheaper than claiming per draw.
1Fund (owner, +send) : adds real coins to the caller's own pool.
2 Anyone may fund; funding makes you an
3 owner. Minimum MinFunding.
4WithdrawPool (owner) : pulls unspent pool back out, any amount,
5 any time. See "the owner can always
6 leave" below.
7WithdrawPoolAll (owner) : the whole pool.
8Grant (owner) : opens one spendbook allowance for one
9 delegate and returns the grant id. The
10 draw fee is snapshotted here.
11Amend (owner) : changes the grant's terms. Tighter-or-
12 equal applies now; looser waits for the
13 next window boundary. PeriodLength is
14 NOT a parameter — spendbook makes it
15 immutable and this realm makes that
16 structural.
17Revoke (owner) : immediate, and the id is never reused.
18Draw (DELEGATE) : consumes authority, debits the pool,
19 credits the delegate net of the fee.
20Claim (DELEGATE) : pays the delegate's whole credit out.
21Reap (ANYONE) : removes an EXPIRED grant, freeing its
22 slot. Permissionless because an expired
23 allowance can authorize nothing, so
24 removing it takes nothing from anyone.
25WithdrawFees (ANYONE) : pays the fee pot to the compile-time
26 FeeCollector. Fixed destination, so
27 there is nothing to steer.
28SweepSurplus (ANYONE) : out-of-band coins to the FeeCollector,
29 never touching tracked liabilities.
WithdrawPool has no lock, no notice period, and no check against outstanding grants. An owner can empty the pool under a live grant and the delegate's next Draw will fail on solvency. This is stated plainly rather than buried: the money is the owner's, Revoke is the clean way to end a relationship, and a realm that could trap an owner's capital behind a grant they regret would be a worse realm. A delegate who needs a guarantee of funds does not want a budget — they want escrow, which is a different artifact.
What the owner CANNOT do is undo work already authorized: credit already drawn into the ledger is the delegate's, and Revoke does not claw it back. Revoking a grant ends future authority only.
feeBps is snapshotted into the grant at Grant time from the compile-time DrawFeeBps, so a delegate's terms are fixed when the grant is created and visible in GrantInfo from that moment. The fee is charged ONCE per draw, on the gross amount, with feeledger's floor rounding (rounding favors the delegate). The owner's pool is debited the GROSS amount and the delegate is credited the net — the fee is paid by the owner out of the budget, not taken out of the delegate's authority, so a grant of PerPeriod means PerPeriod of the owner's money leaves the pool per window, which is the figure an owner actually cares about bounding. The ledger is constructed with the compile-time MaxFeeBps cap, so a fee above the cap is refused by the primitive, not by a check in this file. There is no fee on funding, on withdrawing an unspent pool, or on claiming.
The fee rate, the fee collector, every bound and every cap are compile-time constants. No address can change terms, pause the realm, touch a pool it did not fund, or move a grant it did not create. The deployer has no privilege of any kind after deployment.
Let H be the ugnot held at this realm's address, P the sum of all owner pools (tracked O(1) as totalPooled), L the ledger's liabilities (unclaimed delegate credit plus the fee pot), and S >= 0 the out-of-band surplus:
1H == P + L + S
Every transition moves value between exactly two terms inside one transaction. Fund raises H and P together (coinio.Receive is the receipt-guaranteed shape). WithdrawPool debits P before the identical amount leaves H. Draw moves gross out of P and into L, split into the delegate's credit and the fee — H does not move at all, which is why Draw has no payout to fail. Claim and WithdrawFees debit L before their payout. Any panic aborts the whole transaction; this realm never issues or removes coins.
Fund rejects any envelope that is not exactly one positive ugnot coin (coinio.Receive). Every other entrypoint rejects attached coins outright rather than converting them into sweepable surplus.
Every identity is derived from the crossing entrypoint's cur.Previous().Address(). No function takes a caller identity as a parameter, so funding another owner's pool, drawing another delegate's budget, claiming another delegate's credit, or amending a grant you do not own is impossible by construction. This is spendbook consumer contract item 2, closed structurally.
The clock is runtime.ChainHeight(), never an argument. That is spendbook consumer contract item 1: a realm that let a caller choose `now` would have handed the delegate the power to roll the window at will, defeating every cap in the primitive.
A pool entry is paid for by the owner's own funding transaction; a grant record and its allowance by the owner's Grant. Revoke and Reap both remove the grant record and the allowance in the same call, and the chain refunds the freed storage deposit to the transaction that frees it — which is the standing incentive to call Reap on someone else's expired grant.
Denom is the only asset this realm holds.
DrawFeeBps is the fee actually snapshotted into every grant: 100 bps = 1%, charged once per draw on the gross amount, floor-rounded in the delegate's favor. The most conservative live fee in this portfolio (fee_split, crowdfund) is also 1%.
FeeCollector receives withdrawn fees and swept surplus. Compile-time constant: there is no setter and no transfer path. It is the deploying namespace's address — stated plainly: the operator of this realm.
MaxFeeBps is the hard ceiling on any draw fee this realm could ever charge, enforced by the feeledger the realm is constructed with — a feeBps above it is refused by the primitive's own validation, not by a check in this file. 500 bps = 5%.
MaxGrantsPerOwner bounds how much of the global cap one owner can occupy. spendbook's own package documentation names this as the gap a consuming realm must close — "maxLive is a single global figure, so any owner permitted to Open can exhaust it and lock out the rest … the consuming realm decides who may Open, and a Book shared across mutually untrusting owners needs its own per-owner cap on top of this one." This is that cap. (permission_registry finding R1 is the same lesson from this portfolio's own history.)
MaxLabelLen bounds the grant's free-text label, which is sanitized before reaching markdown.
MaxLiveGrants caps simultaneously live grants, via spendbook's own construction bound. Grant is permissionless to anyone with a funded pool, so the cap is exhaustible in principle, and that AVAILABILITY BOUND is documented rather than hidden.
It is documented here in its CORRECTED form. An earlier version of this comment claimed the mitigation was "MaxGrantsPerOwner plus the cost of becoming an owner at all (MinFunding per pool), and Reap is permissionless so expired grants cannot accumulate". An adversarial audit (finding Y1, DISCOVERY.md § 9) showed BOTH HALVES were false. A wrong mitigation is worse than an acknowledged bound, because a reader sizes the risk from it and stops looking:
What the cost ACTUALLY is: gas for one Fund, MaxGrantsPerOwner Grants and one WithdrawPoolAll per address — about MaxLiveGrants*17/16 transactions to fill the cap — plus the storage deposit on every grant record, which the chain holds for as long as the grant does and refunds only when it is removed. That is real and unrecoverable while the grief lasts. It is not the locked pool capital the old comment promised, and it is priced in gas and storage rather than in capital.
No capital bound was added, because the obvious one does not work. Requiring an owner with live grants to retain MinFunding is defeated for the price of the draw fee: nothing forbids granting to YOURSELF, so the attacker draws its own pool to zero through its own grant and Claims the credit back, recovering all but DrawFeeBps of it while the retention rule still reads as satisfied. Closing THAT would mean bounding Draw below MinFunding — the realm refusing to let a delegate spend the budget, which is the feature. The remaining alternative, a non-refundable per-grant fee, taxes every honest owner to price a grief, and at a fee high enough to deter one it is a tenth of MinFunding per grant.
The blast radius is narrow and that is the reason this is disclosed rather than redesigned: a full book blocks NEW grants only. Every existing grant, pool, draw, credit and claim keeps working, no value is at risk, and any owner can free their own slots with Revoke.
The figure was 1024 until that audit, and 1024 was conservative rather than measured: nothing in this realm iterates the book (Render is bounded by MaxRenderRows, every write path is a single avl lookup) and a grant record's storage is paid for by whoever creates it. Quadrupling it quadruples the cost of filling the cap and costs an honest owner nothing. spendbook's own MaxLiveLimit is 10000.
MaxRenderRows bounds the grant list on the index page.
MinFunding keeps dust pools out: 1 GNOT per Fund call.
MinPerPeriod is the smallest per-window cap a grant may carry. It exists so a grant cannot be created that no draw could ever satisfy, and it is the ONLY lower bound in the draw path — there is deliberately no minimum Draw amount, because a minimum draw above a grant's PerPeriod would make the grant silently undrawable.
MinPeriodBlocks is the shortest rolling window a grant may use. A window of a handful of blocks is a rate, not a budget, and it makes PerPeriod meaningless as a bound on worst-case loss. A periodLength of 0 is still accepted and means ONE LIFETIME WINDOW that never refills (spendbook's own semantics).
RealmPath is this realm's own path, used to build Render links.
Amend changes a grant's terms. Owner only. The returned bool reports whether the new terms are in force NOW (a tighter-or-equal amendment) or queued for the next window boundary (a looser one) — spendbook's one-sided rule, and the distinction worth knowing.
periodLength is NOT a parameter. spendbook makes it immutable because moving it would move every future boundary, which is an enlargement vector wearing a different hat; this realm reads the grant's own periodLength and passes it unchanged, so changing it is not merely refused, it is unexpressible.
Claim pays the caller's entire accrued credit out. DELEGATE ONLY: the account is the calling address. Credit from a grant that has since been revoked, expired or reaped is still claimable — it was earned under a live authority and this realm does not revisit that.
CreditOf returns a delegate's drawn-but-unclaimed credit.
Draw spends amount from the grant's budget. DELEGATE ONLY: the spender is the calling address, never an argument. Nothing is paid out here — the delegate is credited net of the fee and claims separately.
Three things must all hold and they fail in this order, which matters: the caller must be the grant's delegate and the amount must be within every spendbook bound (window cap, lifetime cap, expiry, start); and the owner's pool must cover the GROSS amount. The solvency check runs BEFORE the allowance is consumed, so a draw refused for insolvency leaves the budget untouched rather than burning window capacity on a draw that did not happen.
Drawable is the HONEST figure, and the one a delegate should read: the lesser of what the grant authorizes and what the owner's pool holds. This distinction is the realm's own — spendbook bounds permission and says so, never solvency.
FeesAccrued returns the fee pot: charged, not yet withdrawn.
Fund adds the attached coins to the caller's own pool. Direct EOA calls with -send only (the receipt-guaranteed shape); exactly one positive ugnot coin, at least MinFunding. Anyone may fund; funding is what makes an address an owner.
Grant opens a standing spending authority for one delegate and returns the grant id. The caller becomes the grant's owner and must already have a funded pool — not because the grant reserves anything (it does not), but because a grant from an address that has never funded anything is a promise with nothing behind it, and this realm declines to record one.
Terms, all in the chain's own units:
1perPeriod the cap per window, at least MinPerPeriod. Required.
2periodLength blocks per window. 0 means ONE LIFETIME WINDOW that
3 never refills; otherwise at least MinPeriodBlocks.
4total lifetime cap across every window. 0 means unlimited.
5expiresAt absolute block height at which authority ends. 0 means
6 no expiry.
Every bound above except the two realm minimums is validated and enforced by spendbook, and an invalid combination surfaces as that package's own sentinel error.
GrantLabel returns a grant's raw label (unsanitized: callers rendering it into markdown must sanitize, as this realm's Render does).
GrantUsage returns a grant's consumption AT THE CURRENT HEIGHT: what the window the chain is in right now has spent, the lifetime gross, that window's index, and how many draws there have been.
The two window figures are computed rather than read straight off the record, and that is the whole point of windowAt below: a window that has rolled with no draw since leaves Allowance.WindowIndex naming a window the chain has already left, and Allowance.SpentWindow holding the previous window's spend. spentTotal and draws are cumulative and never go stale.
GrantsOf returns how many live grants an owner currently holds.
Height returns the current chain height: the clock every window and expiry is measured against, and the one no caller can choose.
Held returns H: the ugnot actually at this realm's address.
IsExpired reports whether a grant's authority has lapsed, which is also the condition under which anyone may Reap it.
Liabilities returns P + L — everything this realm owes.
LiveGrants returns how many grants are currently live, from the primitive's own count.
NumGrants returns how many grants have ever been created. Ids are never reused, so this only grows.
PendingTerms returns a queued loosening amendment and whether there is one. An amendment is queued only when it would widen the live window, and it takes effect at the next window boundary.
PoolOf returns an owner's unspent pool.
Reap removes a grant whose authority has EXPIRED, freeing its slot and its storage. Anyone may call it, and the reasoning is spendbook's own: an expired allowance can never authorize another draw, so removing it takes nothing from the owner and grants nothing to the caller. The chain refunds the freed storage deposit to the reaping transaction, which is the standing incentive.
A grant with no expiry can never be reaped; only its owner can end it.
Remaining is the AUTHORITY figure: what the grant's terms permit right now, ignoring whether the owner's pool can cover it.
Render shows the realm at "" and a grant page at "<id>". Labels are free text and pass through the ecosystem sanitizer before hitting markdown.
Revoke ends a grant immediately. Owner only. The id is never reused, so a revoked grant cannot be resurrected and a later Draw against it fails as unknown.
Revoking does NOT claw back credit the delegate has already drawn — that is theirs and stays claimable. Revoke ends future authority.
Surplus returns H - (P + L) — out-of-band coins, sweepable.
SweepSurplus sends out-of-band coins to the FeeCollector: for the pool denom, everything above tracked liabilities; for any foreign denom, the full balance. Owner pools, delegate credit and the fee pot are untouchable by construction (coinio.Sweep refuses to dip below the reserve). Anyone may call it.
With nothing out of band this ABORTS ("coinio: no surplus to sweep for <denom>") rather than returning 0. That is the primitive's choice and the right one: a sweep is a transfer, and a transfer of nothing that reports success is how a caller comes to believe a balance was moved.
TotalPooled returns the P term: unspent owner funds.
UnclaimedCredit returns the delegates' share of the L term.
WithdrawFees pays the entire accrued fee pot to the compile-time FeeCollector. Anyone may call it: the destination is fixed, so there is nothing for a caller to steer.
WithdrawPool returns part of the caller's unspent pool and reports the pool REMAINING afterwards. There is no lock and no check against outstanding grants: the money is the owner's, and a delegate's authority is not a claim on it (see the package doc, "the owner can always leave"). Revoke is the clean way to end a grant.
WithdrawPoolAll returns the caller's entire unspent pool and reports the amount WITHDRAWN.
The return values of the two withdraw entrypoints are deliberately asymmetric, which is worth naming rather than letting a caller discover: each returns the figure its caller does not already have. WithdrawPool was told the amount and wants to know what is left; WithdrawPoolAll knows what is left (nothing) and wants to know what it got. Returning the remainder here would hand every caller a constant 0.