invoices
gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/invoices on onyx-1 testnet
realmapplicationreceivableslive on onyx-1
Issue an invoice and let the payer settle it, in part or in full.
Both sides read the same record of what is owed and what has been paid, so there is nothing to reconcile afterwards.
In detail curated
On-chain accounts receivable with REAL ugnot custody: an issuer records an obligation that a NAMED payer owes them, the payer settles it in coins, and the realm holds the settled value until the issuer pulls it. The design axis that separates it from every other custodial realm in this portfolio is that ISSUING MOVES NO COINS AND RESERVES NOTHING -- Issue writes a record and nothing else, so an invoice is a CLAIM and never a deposit, and an issuer can therefore invoice an address that has no intention of paying without the realm's books being affected at all. Issue(payer, amount, dueHeight, memo) refuses issuer == payer, a non-canonical (uppercase) payer address, an amount below MinInvoiceAmount = 1_000, a memo over MaxMemoLen = 256 and a BACKDATED dueHeight -- the last because an invoice overdue in the transaction that issued it is a claim about a deadline that was never offered. Pay is the NAMED PAYER ONLY, direct-EOA-with-send only (coinio's receipt-guaranteed envelope), accepts a partial payment at or above MinPayment = 1_000 OR any amount that settles the invoice exactly, and REFUSES AN OVERPAYMENT rather than keeping it -- accepting one would turn a payment into a donation the payer did not choose and this realm has no path to return. SettlementFeeBps = 100 (1%) is charged per payment on the gross, floor-rounded, into the feeledger fee pot; the remainder credits the ISSUER's ledger balance, which Claim then pulls. Void is ISSUER-ONLY (an issuer forgiving a debt, and nobody else may forgive it for them), does NOT reverse payments already credited, and KEEPS the paid figure so the record stays readable. Release is PERMISSIONLESS and refuses an open invoice -- which is exactly what makes it safe to let a stranger call, because a Release can never destroy an outstanding obligation -- and the chain's storage refund to the caller is the standing incentive to tidy a record that is not your own. Releasing is the ONLY way an issuer's slot against MaxRecordsPerIssuer = 64 is ever freed: settling and voiding do not free one, because the record is still stored and still walked by every issuer-scoped getter. Keys are issuer|padID(id) zero-padded to 19 digits, so each issuer's records are a contiguous avl prefix range and one issuer's iteration can never be charged for another's. The monetary invariant is Held() == Liabilities() + Surplus() WITH NO POOL TERM -- unlike r/payroll and r/budgets, nothing is ever pre-funded here, so every coin the realm holds is either a credited settlement, an accrued fee, or an unaccounted surplus that only SweepSurplus can remove. Guards: the PINNED rejectStraySend variant (!cur.IsCurrent() first) on every mutating entrypoint, checkedAdd on the issuer lifetime total, MaxFeeBps = 500, and mustDecOpen PANICS on an impossible open-count rather than masking it. FeeCollector is ADDRESS-AS-DATA -- a hard-coded g1ut6... constant that any mainnet port must classify before namespace_tree.py's blind str.replace rewrites the fee destination, documented in-source for exactly that reason.
Use it
A realm holds state. Reading it is a free query; changing it is a transaction you sign yourself.
Read it — no key, no account, no GNOT
gnokey query vm/qeval \
--data 'gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/invoices.FeesAccrued()' \
--remote https://rpc.onyx.testnets.gno.land:443FeesAccrued is read-only, so this is a query rather than a transaction: it costs nothing, signs nothing and needs no funded account. Every exported function is listed under the API below; swap the name and the arguments to read any of the others.
Change its state
Every state change is a transaction you sign with your own key — this site never asks for one and holds none. You do not have to hand-write the transaction: gnoweb generates the exact gnokey maketx call command for any of this realm's 7 crossing functions, with the argument names filled in.
Read the known limitations further down this page first. They record what was not verified on chain, which is usually the part that matters.
Import it from another realm
import "gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/invoices"The path is the address: there is no package manager, nothing to add to a manifest and no version to pin, so the package you compile against is the one the chain runs. It differs per chain because the namespace in it is the deploying account's.
Called from real code
Not an example written for this page. These lines are quoted from latefees, a deployed package in this portfolio that imports this one as invoices, out of the committed file whose sha256 onyx-1 testnet attested. They compile because they are compiled.
if !invoices.HasInvoice(issuer, invoiceID) {if !invoices.IsOverdue(issuer, invoiceID) {payer, amount, paid, dueHeight, _, _ := invoices.InvoiceInfo(issuer, invoiceID)Deployments
onyx-1 testnet onyx-1
RenderedSourceCall builderState
chain-attested| Import path | gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/invoices |
|---|---|
| Namespace | g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3 |
| Deployed at height | 378,326 |
| Deploy transaction | 37a702620558b03754fbc153722852d1ff01fa3c62df859e55f37e5ff0f02a3e look it up on the RPC |
| Deployer | g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3 |
| Gas used | 49,616,630 |
| Storage | not attested per package on this chain — see the limitations below |
| Files on chain | invoices.gno gnomod.toml |
| Deployed bytes | invoices.gno — 36,868 bytes |
| sha256 | bbc4d71846197bdfa2bd8813fe406f07759d1ebd5f1ee9f5e7cefd5af5a1edce |
| Realm address | g1t5nnvlet40cs353n3gth9fvxkqm2qjazptns4z confirmed against the realm itself |
Do not take the hash above on trust. $download returns the bytes onyx-1 is actually running; this command fetches them and prints their digest, which should equal the one in the table:
curl -sS 'https://onyx.testnets.gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/invoices$download&file=invoices.gno' | shasum -a 256Expected: bbc4d71846197bdfa2bd8813fe406f07759d1ebd5f1ee9f5e7cefd5af5a1edce — 36,868 bytes. Use curl: the gno.land edges answer Python's default user-agent with HTTP 403.
API
chain-derived 29 exported functions.
Every function below deep-links to gnoweb's call builder, which generates a ready-to-run gnokey maketx call for it:
BalanceOfClaimFeesAccruedHasInvoiceHasOverdueFromHeightHeldIsOverdueIssueIssuedOfIssuersLiabilitiesMemoNextIDOpenOfOutstandingOutstandingFromOverdueFromPayRecordsRecordsOfReleaseRenderSettledTotalStatusOfSurplusSweepSurplusVoidWithdrawFees
Overview
Package invoices is on-chain accounts receivable: an issuer records an obligation, and the named payer discharges it later, in parts.
The one idea
Every payment realm in this portfolio, and every escrow and vesting package the discovery gate found on either live chain, is PREFUNDED. service_market escrows the exact price at purchase. subscriptions prepays a whole period. payroll, vesting, fee_split and revshare push money that already arrived. In all of them the coins exist before the claim on them does.
An invoice inverts that. It is UNFUNDED AT CREATION: the obligation is recorded first, and the money may never arrive at all.
That one inversion is the whole realm, and it is visible in the arithmetic rather than only in the prose. payroll's invariant carries a pool term because an employer funds first:
payroll: Held() == TotalPooled() + Liabilities() + Surplus() invoices: Held() == Liabilities() + Surplus()
There is no pool here, and no reserve, because at the moment an invoice is issued this realm holds nothing on its behalf and promises nothing. Issuing costs the issuer a storage deposit and nothing else.
What being unfunded forces
Two features that would be decoration on a prefunded rail are load- bearing here, and they are the reason this is a realm rather than a field on an existing one:
- PARTIAL SETTLEMENT. When the money is already in escrow, "pay" is a release of a known amount and partiality is meaningless. When it is not, a payer who can send 60% today is the ordinary case. Pay accepts any amount up to the outstanding balance, repeatedly.
- AN OVERDUE STATE. A prefunded claim cannot be late. An unfunded one can, so lateness is a state other realms can read and act on. That readable state is the integration surface this realm exists to provide, in the same way subscriptions exists to provide Entitled.
No escrow, no acceptance, no arbitration
An invoice is billed for work already done, so there is nothing for this realm to hold back. Money paid is credited to the issuer's ledger balance in the same transaction. There is no acceptance window and no release step — those belong to service_market, whose model is the opposite one.
Consequently this realm DOES NOT arbitrate. A payer who disputes a bill simply does not pay it, and the invoice goes overdue. That is the mechanism off chain too, and inventing a dispute path here would make this a worse copy of service_market rather than a complement to it.
Payment is pull, in the direction that cannot fail
Pay credits the issuer's ledger balance; the issuer later calls Claim. The push alternative — paying the issuer inside the payer's own transaction — would let the issuer's address decide whether the PAYER's settlement succeeds. An issuer that is a realm which panics on receipt, or is simply unable to accept, would make its own invoices unpayable and the failure would land on the payer. Pull puts every failure on the party who owns it.
Anyone may issue an invoice naming anyone. Read this before trusting a total.
There is no permission to bill someone, because requiring one would mean a registry of who may invoice whom, which is a different application. The honest consequence is stated here rather than discovered later:
AN UNPAID INVOICE IS AN UNVERIFIED CLAIM BY ITS ISSUER.
So this realm deliberately exposes NO global "what does this address owe" aggregate. Such a number would be the sum of every stranger's self-reported assertion, and reading it as a credit signal is the error the getter's existence would invite. What it exposes instead is ISSUER-SCOPED: OutstandingFrom and OverdueFrom answer "what does this payer owe THIS issuer", which is the question a consumer realm can actually act on, because a consumer gating its own service on its own unpaid bills is trusting only itself.
Two bounds back that up mechanically. Issuing costs a storage deposit paid by the ISSUER, so volume is not free to the party who benefits from it; and MaxRecordsPerIssuer caps one issuer's STORED records, which is an availability bound on that issuer's own namespace and no one else's. Neither bound makes an unpaid claim true, and they are not presented as doing so.
Both bounds are PER ISSUER, and the global total is not bounded. Sybil issuers multiply it: N funded addresses yield N*MaxRecordsPerIssuer records, and the per-record deposit paid by each is the only thing making that expensive. Two consequences follow, stated here rather than discovered:
- A payer named on an invoice CANNOT remove it. Void is issuer-only, because forgiving a debt is the issuer's to do, and Release refuses an open record; so a stranger's open invoice naming you persists until that stranger voids it. What absorbs this is the design above rather than a bound: no global aggregate reads such a record, and the issuer-scoped getters a consumer realm actually calls stay gas-bounded at MaxRecordsPerIssuer, so a victim's reads are never charged for an attacker's volume. The residual harm is an off-chain reader's, and the unverified-claim warning above is what it is for.
- An issuer record is never reclaimed. Release decrements its count but does not delete the issuers entry, because `issued` and `invoiced` are LIFETIME figures that IssuedOf reports, and deleting the entry would reset them to zero and make that getter lie. Global state therefore grows monotonically. Keeping an honest counter is worth more than reclaiming one AVL node.
The cap counts STORED records rather than OPEN ones, and the difference is load-bearing rather than a detail. OutstandingFrom, OverdueFrom and HasOverdueFrom — the getters a consumer realm calls, and therefore the ones whose gas cost is someone else's problem — iterate an issuer's whole prefix, settled and voided records included, because a settled record is not removed until Release. A cap on open invoices alone would bound the obligations and leave that iteration unbounded: an issuer could issue and void repeatedly, keeping `open` at zero while their prefix grew without limit, until a consumer's read ran out of gas. Capping what is stored bounds both, since open can never exceed stored.
Finding "every invoice naming me" is therefore an off-chain indexer's job: on chain it would be an unbounded iteration over every issuer that has ever billed anyone, and the answer would be untrustworthy anyway.
STORAGE
An issuer record is paid for by that issuer's first Issue; an invoice record by the Issue that creates it. Release removes a finished invoice and the chain refunds the freed deposit to the transaction that frees it, which is the standing incentive to tidy up a record that is no longer anyone's business.
What this realm does NOT do
- It does not verify that work happened, value it, or arbitrate.
- It holds ugnot and only ugnot.
- It has no admin. The deploying namespace receives protocol fees and swept surplus and has no other privilege: no setter, no pause, no ability to void an invoice or touch a balance.
Imports
chainchain/runtimechain/runtime/unsafegno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/coiniogno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/feeledgergno.land/p/nt/avl/v0gno.land/p/nt/markdown/sanitize/v0strconvstrings
Constants and variables
Invoice statuses.
const (
StatusOpen = "open"
StatusSettled = "settled"
StatusVoided = "voided"
)
Denom is the only asset this realm holds.
const Denom = "ugnot"
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.
NOTE FOR ANY RE-NAMESPACED PORT: this is the address-as-data hazard that a namespace substitution rewrites silently while every byte-comparison still reports the tree faithful, because the rewrite IS the intended transformation. Re-state it deliberately per chain; never let it be derived.
const FeeCollector = address("g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3")
MaxFeeBps is the hard ceiling on any 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 MaxFeeBps = int64(500)
MaxMemoLen bounds the free-text memo, in bytes. The memo is rendered, so it is sanitized at the render boundary, never at the write boundary — storing what the issuer actually wrote keeps the record honest.
const MaxMemoLen = 256
MaxRecordsPerIssuer caps the invoice records one issuer may have STORED at once — open, settled and voided alike. It is what makes the issuer-scoped getters gas-bounded; see the package doc for why the cap counts stored records and not open ones.
Issue is permissionless, so this is an AVAILABILITY BOUND and it is documented rather than hidden: an issuer can exhaust their OWN capacity and no one else's, because the bound is per-issuer. The cost of filling it is gas plus a storage deposit on every record.
Hitting it is self-service to clear: Release is permissionless and the chain refunds the freed deposit to whoever calls it, so an issuer at the cap can always release their own finished records, and a stranger has a standing incentive to do it for them.
const MaxRecordsPerIssuer = 64
MinInvoiceAmount is the smallest invoice that may be issued. Below the fee denominator a 1% fee floors to zero, so a smaller invoice would be fee-free by accident rather than by design.
const MinInvoiceAmount = int64(1_000)
MinPayment keeps dust settlements out. It is NOT a floor on the final payment: a payment is accepted when it is at least MinPayment OR when it settles the invoice exactly. Without that second clause an invoice with 300 ugnot outstanding could never be closed, and "unpayable remainder" is a worse failure than dust.
const MinPayment = int64(1_000)
RealmPath is this realm's own path, used to build Render links.
const RealmPath = "/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/invoices"
SettlementFeeBps is charged on each payment, on the gross, floor-rounded in the ISSUER's favour: 100 bps = 1%. It matches the most conservative live fee in this portfolio (fee_split, crowdfund, budgets, payroll).
The ISSUER bears it, not the payer: a payment of N reduces the outstanding balance by exactly N, and the issuer is credited N minus the fee. A payer's debt must be discharged by what the payer sent, or the arithmetic of "paid in full" would depend on this realm's fee schedule.
It is a compile-time constant with no setter, so an invoice's economics cannot be changed after it is issued.
The fee is floored PER PAYMENT, so splitting a payment can lower the total fee. Each payment boundary forfeits at most 1 ugnot, and MinPayment bounds how many boundaries an invoice admits. MEASURED on onyx-1, not reasoned: 10,990 paid at once charges 109, while ten payments of 1,099 charge 100. The beneficiary is the ISSUER, who is credited the difference and does not control the payment schedule; the payer gains nothing, because the outstanding balance falls by the gross either way. Gas for one extra payment costs more than the 1 ugnot it saves, so this is priced out rather than prevented, and a colluding issuer/payer pair would spend more to exploit it than they recovered.
A final settlement below the fee denominator pays zero fee for that same reason and is NOT a separate case: floor(99/100) is 0 whether or not MinPayment's settle-exactly clause admitted it, so that clause forfeits nothing on its own.
const SettlementFeeBps = int64(100)
Functions
BalanceOf
func BalanceOf(issuer address) int64
BalanceOf is an issuer's settled, unclaimed balance.
Claim
func Claim(cur realm) int64
Crossing function. Callable from a transaction via
MsgCall, and from another realm asClaim(cross(cur), ...).
Claim pays the caller their whole settled balance. The issuer only — there is no "claim on behalf of", because that would let a third party choose the moment someone else's coins move.
FeesAccrued
func FeesAccrued() int64
FeesAccrued is the fee pot awaiting WithdrawFees.
HasInvoice
func HasInvoice(issuer address, id int64) bool
HasInvoice reports whether a record exists, without panicking on absence — every other invoice accessor panics, which makes them unusable as an existence probe.
HasOverdueFrom
func HasOverdueFrom(issuer, payer address) bool
HasOverdueFrom is OverdueFrom as a predicate, for a consumer that only needs the boolean. It stops at the first hit rather than summing.
Height
func Height() int64
Height is the current chain height, exposed so a caller can compute due heights against the same clock this realm uses.
Held
func Held() int64
Held is the realm's actual ugnot balance, read from the chain.
IsOverdue
func IsOverdue(issuer address, id int64) bool
IsOverdue reports whether one invoice is overdue right now.
Issue
func Issue(cur realm, payer address, amount, dueHeight int64, memo string) int64
Crossing function. Callable from a transaction via
MsgCall, and from another realm asIssue(cross(cur), ...).
Issue records an obligation of `amount` owed by `payer` to the caller, to be settled by `dueHeight`. It moves no coins and reserves nothing. Returns the invoice id.
dueHeight must not be in the past. A backdated due height would create an invoice that is overdue in the transaction that issued it, which is a claim about a deadline that was never offered.
IssuedOf
func IssuedOf(issuer address) (count, invoiced int64)
IssuedOf is how many invoices an address has ever issued, and their total face value. Note the tense: both are lifetime figures and neither falls when an invoice is settled, voided or released.
Issuers
func Issuers() int
Issuers is the number of addresses that have ever issued an invoice.
Liabilities
func Liabilities() int64
Liabilities is every coin this realm owes to anyone: the ledger's own total, which is issuer balances plus the fee pot.
There is no pool term, and its ABSENCE is the unfunded property this realm exists to model — see the package doc. Do not "fix" this to match payroll's shape.
Memo
func Memo(issuer address, id int64) string
Memo is an invoice's free-text memo, exactly as the issuer wrote it. It is UNSANITIZED and caller-supplied: a consumer that renders it owes it the same treatment Render gives it here.
NextID
func NextID() int64
NextID is the id the next Issue will allocate. Exposed so a caller can tell a never-issued id from a released one.
OpenOf
func OpenOf(issuer address) int
OpenOf is the number of open invoices an issuer holds.
Outstanding
func Outstanding(issuer address, id int64) int64
Outstanding is what is still owed on one invoice. Zero for a settled or voided one — a voided invoice owes nothing by definition, which is the point of voiding it.
OutstandingFrom
func OutstandingFrom(issuer, payer address) int64
OutstandingFrom is the total a payer still owes THIS issuer across the issuer's open invoices.
The iteration is bounded by MaxRecordsPerIssuer, NOT by the number of open invoices: it walks the issuer's whole stored prefix and skips the records that are not open. That is why the cap counts stored records — see the package doc.
This is the issuer-scoped shape the package doc argues for: a consumer realm gating its own service on its own unpaid bills is trusting only itself. There is deliberately no global per-payer total — see the package doc, "Anyone may issue an invoice naming anyone".
OverdueFrom
func OverdueFrom(issuer, payer address) int64
OverdueFrom is OutstandingFrom restricted to invoices whose due height has passed. A consumer realm that wants to withhold service from a late payer reads this.
Pay
func Pay(cur realm, issuer address, id int64) int64
Crossing function. Callable from a transaction via
MsgCall, and from another realm asPay(cross(cur), ...).
Pay settles part or all of an invoice with the attached coins. The named payer only. Direct EOA calls with -send only (the receipt-guaranteed shape); exactly one positive ugnot coin.
The attached amount must not exceed the outstanding balance. Accepting an overpayment would turn a payment into a donation the payer did not choose to make and this realm has no path to return, so it is refused and the payer keeps their coins.
Returns the outstanding balance remaining after this payment.
Records
func Records() int
Records is the number of invoice records currently stored, open or not. It falls on Release, so it is a storage figure rather than a count of invoices that have existed.
RecordsOf
func RecordsOf(issuer address) int
RecordsOf is the number of invoice records an issuer currently has stored, of any status. This — not OpenOf — is the figure capped by MaxRecordsPerIssuer, so it is the one an issuer near the cap needs.
Release
func Release(cur realm, issuer address, id int64)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asRelease(cross(cur), ...).
Release removes a finished invoice and frees its storage. Permissionless: the chain refunds the freed storage deposit to the caller, which is the standing incentive to tidy up a record that is not your own.
It refuses an open invoice, which is what makes it safe to let a stranger call: a Release can never destroy an outstanding obligation. An issuer who wants a record gone must Void it first, and that leaves an event.
Releasing frees the issuer's slot against MaxRecordsPerIssuer. That is the only way a slot is ever freed — settling and voiding do not free one, because the record is still stored and still walked by every issuer-scoped getter.
Render
func Render(path string) string
SettledTotal
func SettledTotal() int64
SettledTotal is the sum of every issuer's unclaimed balance.
StatusOf
func StatusOf(issuer address, id int64) string
StatusOf is an invoice's status.
Surplus
func Surplus() int64
Surplus is coins held beyond every obligation — what SweepSurplus would move. It is zero when every coin arrived through an entrypoint.
SweepSurplus
func SweepSurplus(cur realm) int64
Crossing function. Callable from a transaction via
MsgCall, and from another realm asSweepSurplus(cross(cur), ...).
SweepSurplus sends coins held beyond every obligation to FeeCollector. The reserve passed to coinio is the realm's full liability, so a sweep can never reach a coin an issuer is owed.
Void
func Void(cur realm, id int64)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asVoid(cross(cur), ...).
Void cancels the unpaid remainder of an invoice. The issuer only — this is an issuer forgiving a debt, and nobody else may forgive it for them.
It does NOT reverse payments already made. Coins the payer sent are already credited to the issuer's ledger balance and may already have been claimed; there is no mechanism on chain to take them back, and pretending otherwise in the status would be worse than saying so. A voided invoice therefore keeps its `paid` figure, which is what makes the record readable afterwards.
Voiding does not remove the record. Release does that.
WithdrawFees
func WithdrawFees(cur realm) int64
Crossing function. Callable from a transaction via
MsgCall, and from another realm asWithdrawFees(cross(cur), ...).
WithdrawFees moves the accrued fee pot to FeeCollector. Anyone may call it; the destination is a compile-time constant, so the caller cannot influence where the coins go.
Doc text is reproduced as vm/qdoc returns it. The node markdown-escapes doc comments, so a bracket or angle bracket may carry a backslash the committed source does not have. The source itself is at source and in this repository.
Dependencies
chain-attested| Imports | chain, chain/runtime, chain/runtime/unsafe, gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/coinio, gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/feeledger, gno.land/p/nt/avl/v0, gno.land/p/nt/markdown/sanitize/v0, strconv, strings |
|---|---|
| First-party dependencies | gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/coinio, gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/feeledger |
Known limitations
Recorded by the people who built and deployed it. This list is deliberately not empty where honesty costs something.
curated- NO DEPLOY-DAY PREFLIGHT VERDICT MAY BE QUOTED FOR THIS DEPLOY, AND THAT ABSENCE IS THE FINDING RATHER THAN A WAIVER. CLAUDE.md section 7b requires tools/chain_probe.py preflight immediately before any deploy and requires tools/test_chain_probe.sh green before any verdict from chain_probe.py is quoted anywhere. The suite exits 3 PARTIAL here -- 134 passed, 0 failed, 0 blocked, 1 skipped, with PRE1b NAMED in the verdict -- because catalog/chain_baseline.json lags the chain: namespace_package_count reads CHANGED live 20 != baseline 19 and .../payroll reads NEW package. BOTH CHANGED FACTS ARE THE AUTHORIZED GROUP 22 PAYROLL DEPLOY AND NOT DRIFT, and every GovDAO-mutable gating fact (storage price, CLA enforcement, names.IsEnabled) read match. Re-emitting the baseline is operator queue item 16: --emit-baseline catalog/chain_baseline.json is denied by the harness permission classifier while /tmp destinations are permitted, and generating via --json then installing with Write is NOT the workaround -- it is the prohibited substitution of mechanism under section 5. So this deploy proceeded on the Group 22 precedent: addpkg's exact-path collision guard is independently enforced on chain (a duplicate path cannot be written whatever a preflight said), and section 8's hard boundary is 'an unexpected deployment target collision', not 'an unquotable preflight'. WHAT IS LOST is that whether the preflight ran cannot be established from any artifact. Note the standing cost named in section 7b: every onyx deploy re-stales the baseline and re-opens this skip, so THIS DEPLOY HAS ITSELF RE-OPENED IT for the next one. DISCHARGED BY: the operator running the re-emit (queue 16), and structurally by making the preflight emit a JSON artifact the way the closure gates do.
- NO COIN EVER ENTERED THE REALM ON CHAIN, SO THE FEE SPLIT AND THE PAYMENT PATHS ARE PROVEN BY TESTS AND BY NOTHING ON THIS CHAIN. gno_key_list is denied by the harness permission classifier, so a second funded onyx key could not be enumerated or used, and Issue refuses issuer == payer. The workaround used the realm's OWN chain-derived address g1t5nnvlet40cs353n3gth9fvxkqm2qjazptns4z as the payer -- provably valid and canonical because the chain derived it rather than the issuer asserting it -- which made Issue, Void and Release fully exercisable. But a realm address structurally cannot originate the direct-EOA-with-send call that Pay demands, so Pay, Claim, WithdrawFees and SweepSurplus were reachable ONLY AS REFUSALS. Consequently SettlementFeeBps's 1% split, the partial-payment accumulation, the overpayment refusal, the settle-exactly MinPayment waiver, and the named-payer authorization boundary rest on the committed suite and the 40-case sabotage sweep under the pinned onyx toolchain -- gno test . exit 0, 40 sabotages / 0 uncaught / FULLY COVERED -- and on the feeledger arithmetic EXECUTED on onyx-1 with chain_id_bound true. Skipped, not passed. The monetary invariant Held() - Liabilities() - Surplus() == 0 did hold live, but it held at 0 == 0 - 0 - 0, which is the weakest point on its own curve. DISCHARGED BY: a second funded onyx key, which this harness has not provisioned.
- THE EIGHT REFUSAL PROBES ARE THE RECORD'S BEST BEHAVIOURAL EVIDENCE AND NOT ONE OF THEM IS A SECTION 9a CITATION. Every one was reached live against the deployed bytes via simulate=true -- gas-free, and simulate DOES execute the VM for MsgCall even under the inert policy -- and every one returned panic text specific to the audited source: 'an issuer cannot invoice themselves', 'payer is not a valid address in canonical (lowercase) form', 'amount is below MinInvoiceAmount', 'dueHeight is in the past', 'invoice is still open', 'no balance to claim', 'no fees accrued', and 'coinio: no surplus to sweep for ugnot'. The last is the strongest single artifact in this record because it ORIGINATES IN THE ON-CHAIN coinio DEPENDENCY, which is live execution-path evidence that the deployed realm calls the chain's coinio rather than a digest comparison asserting it could. And all eight arrive as a TOOL ERROR with no structuredContent, hence NO chain_id_bound key at all, and section 9a gates on == true with absence explicitly not a third state. So the probes whose output is most specific to the audited source are exactly the ones that cannot be cited, while the value-returning views that CAN be cited are the ones a wrong implementation would most easily reproduce. Structural -- the stamp rides on the success payload -- not a gap in this deploy. DISCHARGED BY: nothing available here; the swap is named in the memory note panic-probe-carries-no-9a-stamp.
- THE SIMULATE DID NOT TYPE-CHECK THE ADDPKG. onyx-1 runs the inert code-submission policy, so the addpkg dry run reports gas without compiling, and a green simulate=true is therefore not evidence about correctness for a DEPLOY. (It is different for MsgCall, which is why the eight refusal probes above are real execution.) Type-correctness rests entirely on the local gno test . under the onyx pin, exit 0, which is the control actually relied on. DISCHARGED BY: a chain running a non-inert policy.
- FeeCollector IS A HARD-CODED ADDRESS CONSTANT AND A MAINNET PORT MUST CLASSIFY IT BEFORE namespace_tree.py TOUCHES THIS FILE. The constant is address("g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3") -- the same string as the namespace segment of every first-party import path in the file -- and namespace substitution is a BARE str.replace, so a mainnet compose rewrites the fee destination silently while every obligation still reports FAITHFUL. Third instance of the r/crowdfund address-as-data hazard in a custodial realm, after r/payroll. Documented in-source for exactly this reason. DISCHARGED BY: an explicit classification in the mainnet port, as was done for mainnet/r/budgets (task #22).
- GATE B PRECEDED THE BROADCAST BY 22m03s, AND THAT IS STILL A REMEMBERED STEP RATHER THAN AN INSTALLED CONTROL. CLOSURE_GATE_B.json was written 2026-10-13T16:28:53Z and the deploy block's header time is 2026-10-13T16:50:56Z; invoices.gno's mtime is 16:18:47Z, BEFORE Gate B, unchanged since, and its digest bbc4d718...a1edce agrees with Gate B's record and with the chain, so the ordering holds on content and not merely on timestamps. The margin is better than Group 22's 8m58s and is the opposite of Group 21 (p/cashflow), whose Gate B ran AFTER the broadcast -- a section 7a violation. THIS IS NOT EVIDENCE THAT THE CONTROL IS FIXED: two compliant deploys following one non-compliant deploy is the signature of a remembered step. The related structural gap -- composing the addpkg body from the git blob rather than the worktree -- is task #107 and is NOT installed; what stood in for it here was COMMIT ORDER (audit, commit dd5a56b, Gate B, deploy), which is why this deploy's committed bytes matched the chain by construction instead of by a post-broadcast catch. DISCHARGED BY: the mechanical pre-broadcast gate of task #107.
- THE Issue/Void STORAGE SPLIT WAS NOT SEPARATED, AND THE DEPOSIT FIGURE IS THE WEAKER BALANCE-DERIVED KIND. The retained-storage measurement is sound in aggregate: balance 17,850,942 -> 17,367,542 (sequence 94) -> 17,589,142 (sequence 95), so Issue+Void together cost 483,400 ugnot of which 40,000 is fees leaving 443,400 of storage, and Release NETTED +221,600 despite its own 20,000 fee, implying a 241,600 storage refund -- net -261,800, so 201,800 ugnot is PERMANENTLY RETAINED for an invoice taken all the way through release. That number is the live price of the declared Y1 (the issuers AVL entry is never reclaimed), and Y1 itself was proven live: after Release, Records() == 0 with Issuers() == 1. But the balance was read after Void rather than between Issue and Void, so how the 443,400 divides between writing a record and marking it voided is unrecoverable. Separately, the DEPLOY deposit of 4,459,300 ugnot (22,459,092 -> 17,850,942 at sequence 91 -> 92, an exact multiple of 100 implying 44,593 stored bytes against 36,952 source bytes = 1.2068x) is derived from a BALANCE DELTA, not from an event, and CLAUDE.md warns that storage refunds invert balance deltas -- it is trustworthy here only because this transaction frees nothing. The FEE half by contrast is self-authenticating: /block?height=378326 holds exactly one transaction, sha256 of its 37,337 raw amino bytes reproduces the tx hash, and those bytes carry 148850ugnot and 10000000ugnot max-deposit verbatim. DISCHARGED BY: reading /tm.StorageUnlockEvent and the deposit events per transaction instead of differencing balances, which is the stronger class already named in the deploy-payload-custody note.
Source
| Source file | onyx/r/invoices/invoices.gno at commit c2e10d17ffbf in the project repository (not public — the digest command above is the check that needs no repository) |
|---|---|
| Matches the deployed bytes | yes — byte-identical |