latefees
gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/latefees on onyx-1 testnet
realmapplicationreceivableslive on onyx-1
Charge a late fee that grows the longer an invoice stays unpaid.
The creditor publishes the schedule once, so the penalty on an overdue invoice can be read off instead of argued over.
In detail curated
A GRADUATED LATE-FEE PUBLISHER over the live r/invoices, and the portfolio's first realm to IMPORT ANOTHER REALM through the deploy path (invoices, read by scalar view). A creditor who issued invoices declares a p/tiers schedule over OVERDUE BLOCKS; the realm prices a penalty as a function of overdue DURATION, escalating by tier, readable live (PenaltyNow) or frozen into a sequence-numbered assessment (Assess). It is the fourth consumer of p/tiers, exercising its progressive-fee reading over TIME where r/meters prices usage and r/payslips withholding; and the second consumer of r/invoices after r/receivables, reading the integration surface that realm's header advertises -- InvoiceInfo, IsOverdue, HasInvoice, all non-crossing, so no struct slice crosses a realm boundary. IT IS A PUBLISHER, NOT A COLLECTOR: r/invoices is immutable so this realm cannot add a charge there, and it holds NO coins -- no banker, no deposit, no payout -- so the threat model is accounting integrity and authorization, not custody, and the monetary-conservation invariant does not apply. A published penalty is a verifiable CLAIM backed by the invoice's own facts and the creditor's declared schedule, exactly as receivables publishes a loss provision; it compels nothing. THE PRICING AXIS is overdue duration, not balance: p/tiers prices one amount against breakpoints (one dimension), and the base is now - dueHeight with each tier's rate a ugnot accrual per 10000 blocks, so penalty <= overdueBlocks by the rate cap -- a time-escalating absolute accrual, NOT a percentage of the outstanding (two dimensions p/tiers cannot express in one call), with the outstanding recorded beside the penalty so a reader can derive an effective rate. AUTHORIZATION is the invoices key scoping: Assess derives the issuer from cur.Previous().Address() and reads InvoiceInfo under invKey(issuer,id), which returns only invoices that address issued, so a creditor may assess only debts owed TO them -- proven live, a foreign caller reads 'no invoice ... issued by the caller'. THE TRUST BOUNDARY is a self-report of a self-report of a self-report: the issuer set the invoice amount, its due height, and this schedule; the realm computes faithfully and validates none of them for fairness. PenaltyNow reads 0 once the invoice is no longer overdue (paid or voided), because r/invoices does not expose the pay height, so accrual is computable only while open and late -- a frozen Assess is how a claim survives payment.
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/latefees.Height()' \
--remote https://rpc.onyx.testnets.gno.land:443Height 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 2 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/latefees"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.
Deployments
onyx-1 testnet onyx-1
RenderedSourceCall builderState
chain-attested| Import path | gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/latefees |
|---|---|
| Namespace | g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3 |
| Deployed at height | 483,001 |
| Deploy transaction | c56bd70be9a0eee15443af38f1c0206cde58da4bed8044b297f85cbbf28823b3 look it up on the RPC |
| Deployer | g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3 |
| Gas used | 24,254,154 |
| Storage | not attested per package on this chain — see the limitations below |
| Files on chain | gnomod.toml latefees.gno |
| Deployed bytes | latefees.gno — 17,007 bytes |
| sha256 | 4ea5efcd1546366cf1408b8ac4b6f5b6f6882d76572fdfb51fb1b791ad35d5ce |
| Realm address | g1mwkdz0e3ay3s3syuwvngcmn5q6rm8tgpsz48de |
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/latefees$download&file=latefees.gno' | shasum -a 256Expected: 4ea5efcd1546366cf1408b8ac4b6f5b6f6882d76572fdfb51fb1b791ad35d5ce — 17,007 bytes. Use curl: the gno.land edges answer Python's default user-agent with HTTP 403.
API
chain-derived 13 exported functions.
Every function below deep-links to gnoweb's call builder, which generates a ready-to-run gnokey maketx call for it:
AssessAssessmentScheduleAssessmentsOfHasAssessmentHasScheduleHeightInvoicesNumAssessmentsPenaltyNowQuotePenaltyRenderScheduleOfSetSchedule
Overview
Realm latefees publishes a graduated late-fee penalty on an overdue invoice held in the live r/invoices. A creditor (an invoice issuer) declares a late-fee SCHEDULE once — a p/tiers schedule over how many blocks an invoice is overdue — and the realm prices the penalty as a function of that overdue duration, escalating by tier. The penalty can be read live (PenaltyNow) or frozen into a sequence-numbered ASSESSMENT the creditor can point a payer to.
It is the fourth consumer of p/tiers and the one that exercises its "progressive fee" reading over TIME rather than quantity: r/meters prices usage, r/payslips prices withholding, and this prices lateness. It is the second consumer of r/invoices after r/receivables, and reads the exact integration surface that realm's header advertises — invoices.InvoiceInfo, invoices.IsOverdue, invoices.HasInvoice — all non-crossing views, no struct slice crossing a realm boundary.
It is a PUBLISHER, not a collector
r/invoices is live and immutable, so this realm CANNOT add a charge to an invoice there, and it holds no coins of its own — no banker, no deposit, no payout, no withdrawal path. A published penalty is a verifiable CLAIM backed by the invoice's own facts and the creditor's declared schedule, exactly as r/receivables publishes a loss provision and r/cashflow a statement. It compels nothing; a debtor who ignores a late fee is in the same position as one who ignores the invoice. A custodial variant that collects the fee is a separate, larger product and is deliberately out of scope.
The pricing axis, and the one it is not
The base handed to p/tiers is the OVERDUE DURATION in blocks (`now - dueHeight`), and each tier's rate is a ugnot accrual per RateScale blocks. So the penalty ESCALATES WITH TIME: the longer an invoice is overdue, the higher the marginal accrual. p/tiers' rate cap makes `penalty <= overdueBlocks` an arithmetic fact — the fee can never exceed one ugnot per overdue block, a natural ceiling.
It is NOT a percentage of the outstanding amount. p/tiers prices one amount against breakpoints — a single dimension — and a fee that is both time-escalating AND balance-scaled is two dimensions it cannot express in one call. The creditor scales the ugnot accrual to their debt magnitudes through the schedule; the assessment records the outstanding beside the penalty so a reader can compute an effective rate. Composing a balance- scaled fee as a single Charge call would be a plausible, wrong number.
The trust boundary, inherited twice over
A penalty is a self-report of a self-report of a self-report: the issuer set the invoice amount in r/invoices, the due height there, AND the late-fee schedule here. This realm computes faithfully from those; it does not validate that any of them is fair. A creditor may only assess their OWN invoices — Assess derives the issuer from `cur.Previous().Address()` and reads invoices.InvoiceInfo under that key, which returns only invoices that address issued.
Imports
chainchain/runtimechain/runtime/unsafegno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/tiersgno.land/p/nt/avl/v0gno.land/p/nt/markdown/sanitize/v0gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/invoicesstrconv
Constants and variables
Denom is the asset the invoices this realm prices are denominated in, and the unit a penalty is expressed in. This realm never holds it.
const Denom = "ugnot"
InvoicesPath pins the realm this one reads, by value, so a reader of a published assessment can see which invoices it priced.
const InvoicesPath = invoices.RealmPath
MaxAssessmentsPerIssuer bounds one creditor's stored assessment records, so one address cannot grow this realm's state without bound. It counts STORED records, the invoices-realm discipline.
const MaxAssessmentsPerIssuer = 256
RenderLimit bounds every rendered list — an unbounded Render was YELLOW in three prior audits.
const RenderLimit = 20
Functions
Assess
func Assess(cur realm, invoiceID int64) int64
Crossing function. Callable from a transaction via
MsgCall, and from another realm asAssess(cross(cur), ...).
Assess freezes a late-fee penalty on one of the caller's own overdue invoices and returns its sequence number. The caller must be the invoice's issuer (the creditor), must have declared a schedule, and the invoice must be overdue right now. The invoice's facts and the current schedule are snapshotted, so the assessment is a stable claim even as the invoice keeps accruing or is later paid.
AssessmentSchedule
func AssessmentSchedule(seq int64) (edges, rates []int64)
AssessmentSchedule returns the schedule snapshot an assessment was priced under — the edges and rates frozen with it, which may differ from the issuer's current schedule.
AssessmentsOf
func AssessmentsOf(issuer address) int64
AssessmentsOf is the number of assessments an issuer has frozen.
HasAssessment
func HasAssessment(seq int64) bool
HasAssessment reports whether a sequence number names a frozen assessment.
HasSchedule
func HasSchedule(issuer address) bool
HasSchedule reports whether an issuer has declared a late-fee schedule.
Height
func Height() int64
Invoices
func Invoices() string
NumAssessments
func NumAssessments() int64
PenaltyNow
func PenaltyNow(issuer address, invoiceID int64) int64
PenaltyNow is the penalty an issuer's invoice has accrued as of the current height, under the issuer's CURRENT schedule. It is 0 when the invoice is not overdue — including once it is paid or voided, because r/invoices does not expose when a payment landed, so accrual is only computable while the invoice is still open and late. A frozen Assess is how a claim survives the invoice being paid. Panics only if the issuer has no schedule or the invoice does not exist.
QuotePenalty
func QuotePenalty(issuer address, overdueBlocks int64) int64
QuotePenalty prices a hypothetical overdue duration against an issuer's current schedule, without reading any invoice — the preview a creditor uses to tune a schedule. Panics if the issuer has no schedule or overdueBlocks is negative.
Render
func Render(path string) string
Render shows the catalog at "", an assessment at "seq/<n>", and an issuer's schedule at "issuer/<addr>". Every user-supplied value rendered between table pipes is escaped with sanitize.TableCell; addresses carry no markdown metacharacter but are passed through InlineText for the same reason the sibling realms do.
ScheduleOf
func ScheduleOf(issuer address) (edges, rates []int64)
ScheduleOf returns an issuer's current schedule as its edge and rate slices, or two empty slices if none is declared.
SetSchedule
func SetSchedule(cur realm, edges, rates string) int
Crossing function. Callable from a transaction via
MsgCall, and from another realm asSetSchedule(cross(cur), ...).
SetSchedule declares or replaces the caller's late-fee schedule: a graduated accrual over OVERDUE BLOCKS, built from comma-separated int64 lists where len(rates) == len(edges)+1 and each rate is a ugnot accrual per RateScale blocks (0..10000). Edges are block counts — e.g. "216000,648000" is roughly ten and thirty days at ~4s blocks. A single rate with no edges is a flat accrual. Replacing a schedule does not touch already-frozen assessments, which snapshot the schedule that priced them.
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/tiers, gno.land/p/nt/avl/v0, gno.land/p/nt/markdown/sanitize/v0, gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/invoices, strconv |
|---|---|
| First-party dependencies | gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/tiers, gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/invoices |
Known limitations
Recorded by the people who built and deployed it. This list is deliberately not empty where honesty costs something.
curated- IT IS A PUBLISHER, NOT A COLLECTOR. A penalty is a claim the realm computes faithfully and cannot enforce; r/invoices is immutable so no charge is added there. The realm holds no coins.
- A SELF-REPORT OF A SELF-REPORT OF A SELF-REPORT: the issuer set the invoice amount in r/invoices, its due height there, and the schedule here. The realm validates none of them for fairness.
- PenaltyNow READS 0 ONCE THE INVOICE IS NO LONGER OVERDUE (paid or voided), because r/invoices does not expose the pay height, so accrual is computable only while open and late. A frozen Assess is how a claim survives payment -- proven live (PenaltyNow before a boundary, Assess freezing it).
- THE FEE IS AN ABSOLUTE ugnot ACCRUAL OVER TIME, NOT A % OF THE BALANCE. p/tiers is one-dimensional; the creditor scales rates to debt magnitudes, and the outstanding is recorded beside the penalty for an effective-rate derivation.
- ASSESSMENTS ARE PERMANENT, never removed, bounded per issuer by MaxAssessmentsPerIssuer = 256 (a monotone lifetime cap, no decrement by design).
- THE LIVE LIFECYCLE RAN UNDER A ONE-KEY-PLUS-ONE HARNESS: default as creditor, hunter (g19tv9th...) as the foreign caller for the cross-issuer refusal. A non-trivial nonzero penalty was realized by letting the invoice age past the floor; the graduated arithmetic is otherwise proven by the committed suite and QuotePenalty(1500)=35 read live.
- storage_bytes and storage_deposit are null for the AL56 reason in storage_measurement_note; the figures ARE chain-attested (31,414 B / 3,141,400ugnot at 483005). Do not read the nulls as an unmeasured deploy.
- MaxAssessmentsPerIssuer=256 is a LIFETIME cap with no release valve -- the doc cites 'the invoices-realm discipline' whose whole point is that Release frees slots, and no analogue exists here; an active creditor's Assess bricks permanently at 256. PenaltyNow keeps working. Successor: a permissionless release path mirroring invoices.Release. Audit 2026-11-04, disclosed per the operator's disclose-and-queue-successor rule; details and severity in [an unpublished record].
Source
| Source file | onyx/r/latefees/latefees.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 |