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 source realm

Realm budgets is a delegated-budget treasury in GNOT: an owner funds a pool, grants named delegates a standing, rate-...

Overview

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.

What a budget is, and what it is not

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:

Example
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.

Credit, then pull

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.

LIFECYCLE

Example
 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.

The owner can always leave, and that is the design

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.

THE FEE, precisely

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.

THERE IS NO ADMIN

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.

MONETARY INVARIANT (conservation)

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:

Example
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.

ONLY GNOT IS ACCEPTED

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.

AUTHORIZATION

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.

STORAGE

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.

Constants 12

const Denom

1const Denom = "ugnot"
source

Denom is the only asset this realm holds.

const DrawFeeBps

1const DrawFeeBps = int64(100)
source

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%.

const FeeCollector

1const FeeCollector = address("g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3")
source

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.

const MaxFeeBps

1const MaxFeeBps = int64(500)
source

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%.

const MaxGrantsPerOwner

1const MaxGrantsPerOwner = int64(16)
source

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.)

const MaxLabelLen

1const MaxLabelLen = 64
source

MaxLabelLen bounds the grant's free-text label, which is sanitized before reaching markdown.

const MaxLiveGrants

1const MaxLiveGrants = 4096
source

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:

  • MinFunding is NOT a capital bound. The funded-pool requirement is checked at Grant time only, and WithdrawPool has no lock, so one address funds a pool, opens MaxGrantsPerOwner grants, withdraws the pool in full, and the grants survive holding their slots. TestOwnerCanAlwaysLeave pins exactly that sequence, as intended behavior — the attack is built out of a documented feature.
  • Reap is inert against it. Reap requires spendbook's IsExpired, which is never true for expiresAt == 0, so an attacker omits the expiry and no third party can remove the grant.

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.

const MaxRenderRows

1const MaxRenderRows = 20
source

MaxRenderRows bounds the grant list on the index page.

const MinFunding

1const MinFunding = int64(1_000_000)
source

MinFunding keeps dust pools out: 1 GNOT per Fund call.

const MinPerPeriod

1const MinPerPeriod = int64(1_000)
source

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.

const MinPeriodBlocks

1const MinPeriodBlocks = int64(100)
source

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).

const RealmPath

1const RealmPath = "/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/budgets"
source

RealmPath is this realm's own path, used to build Render links.

Functions 30

func Amend

crossing Action
1func Amend(cur realm, id int64, perPeriod, total, expiresAt int64) bool
source

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.

func Claim

crossing Action
1func Claim(cur realm) int64
source

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.

func CreditOf

Action
1func CreditOf(addr address) int64
source

CreditOf returns a delegate's drawn-but-unclaimed credit.

func Draw

crossing Action
1func Draw(cur realm, id int64, amount int64) int64
source

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.

func Drawable

Action
1func Drawable(id int64) int64
source

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.

func Fund

crossing Action
1func Fund(cur realm) int64
source

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.

func Grant

crossing Action
1func Grant(cur realm, spender address, label string,
2	perPeriod, periodLength, total, expiresAt int64) int64
source

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:

Example
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.

func GrantLabel

Action
1func GrantLabel(id int64) string
source

GrantLabel returns a grant's raw label (unsanitized: callers rendering it into markdown must sanitize, as this realm's Render does).

func GrantUsage

Action
1func GrantUsage(id int64) (spentWindow, spentTotal, windowIndex, draws int64)
source

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.

func GrantsOf

Action
1func GrantsOf(addr address) int64
source

GrantsOf returns how many live grants an owner currently holds.

func Height

Action
1func Height() int64
source

Height returns the current chain height: the clock every window and expiry is measured against, and the one no caller can choose.

func Held

Action
1func Held() int64
source

Held returns H: the ugnot actually at this realm's address.

func IsExpired

Action
1func IsExpired(id int64) bool
source

IsExpired reports whether a grant's authority has lapsed, which is also the condition under which anyone may Reap it.

func LiveGrants

Action
1func LiveGrants() int64
source

LiveGrants returns how many grants are currently live, from the primitive's own count.

func NumGrants

Action
1func NumGrants() int64
source

NumGrants returns how many grants have ever been created. Ids are never reused, so this only grows.

func PendingTerms

Action
1func PendingTerms(id int64) (perPeriod, total, expiresAt int64, pending bool)
source

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.

func PoolOf

Action
1func PoolOf(addr address) int64
source

PoolOf returns an owner's unspent pool.

func Reap

crossing Action
1func Reap(cur realm, id int64)
source

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.

func Remaining

Action
1func Remaining(id int64) int64
source

Remaining is the AUTHORITY figure: what the grant's terms permit right now, ignoring whether the owner's pool can cover it.

func Render

1func Render(path string) string
source

Render shows the realm at "" and a grant page at "<id>". Labels are free text and pass through the ecosystem sanitizer before hitting markdown.

func Revoke

crossing Action
1func Revoke(cur realm, id int64)
source

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.

func Surplus

Action
1func Surplus() int64
source

Surplus returns H - (P + L) — out-of-band coins, sweepable.

func SweepSurplus

crossing Action
1func SweepSurplus(cur realm, denom string) int64
source

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.

func WithdrawFees

crossing Action
1func WithdrawFees(cur realm) int64
source

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.

func WithdrawPool

crossing Action
1func WithdrawPool(cur realm, amount int64) int64
source

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.

func WithdrawPoolAll

crossing Action
1func WithdrawPoolAll(cur realm) int64
source

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.

Dependencies

Source Files 2