loans
gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/loans on onyx-1 testnet
realmapplicationlendinglive on onyx-1
Lend a fixed sum and collect it back in scheduled installments.
The loan disburses and repays against a published schedule, and a missed-payment default is a recorded fact anyone can mark.
In detail curated
UNSECURED TERM LENDING -- the one financial primitive the portfolio never had: CREDIT. A lender Opens a loan to a named borrower by ATTACHING the principal, which the realm disburses to the borrower IN THE SAME TRANSACTION; the borrower Repays over a fixed installment schedule; each repayment credits the lender's feeledger claimable with the protocol fee split; the lender Claims. A loan unpaid past a grace window is permissionlessly Default-able -- a recorded state with the shortfall, NOT a seizure, because v1 is unsecured. It is the asymmetric opposite of the disbursement realms (vesting/payroll/budgets pay a known total OUT on a schedule) and of the obligation realms (invoices/receivables record a one-shot debt): principal OUT once, a LARGER total back over time, plus the finance charge and the default state. CUSTODY -- the first custodial realm since the publishers, so the monetary invariant governs it as it does subscriptions/meters: Held == lender-claimable + fee pot. The PRINCIPAL IS NEVER HELD ACROSS A TRANSACTION (Open receives and disburses it in one call), so the only coins the realm holds are repayments awaiting Claim, and a stuck or abandoned loan strands NO principal -- it was disbursed -- the only value at risk being the lender's expected return, the real nature of unsecured credit. total = principal + floor(principal*rateBps/10000), a FLAT finance charge over the whole term (not compounding, not per-block, not p/tiers in v1); per-installment ceil(total/N) with the remainder on the last so the N installments sum to exactly total. Default is a state not a seizure; collateral, compounding and tiered interest are named v2 boundaries. AUTHORIZATION: lender derived via coinio's EOA guard, borrower a parameter (they only receive the principal -- a realm borrower who could never Repay is the lender's own footgun, not a hazard), Repay checked against the stored borrower, Claim ledger-scoped, Default permissionless, admin two-step. Audit led with fund safety: 0 RED / 0 YELLOW, both candidates refuted; the two GREEN notes (a tautological cross-foot test; an imprecise EOA caveat) were fixed.
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/loans.FeeBps()' \
--remote https://rpc.onyx.testnets.gno.land:443FeeBps 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 11 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/loans"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/loans |
|---|---|
| Namespace | g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3 |
| Deployed at height | 484,253 |
| Deploy transaction | caf1c5a301ab59c34ae31be6080a58ed85f583528c5122eeace37a7730fd1e2f look it up on the RPC |
| Deployer | g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3 |
| Gas used | 29,469,179 |
| Storage | not attested per package on this chain — see the limitations below |
| Files on chain | gnomod.toml loans.gno |
| Deployed bytes | loans.gno — 21,094 bytes |
| sha256 | dbad142fe119f00fe9acfac66a8b1bbd3ffa7250476a0009378cf5b14cf877de |
| Realm address | g1s39q788nff5apm24gyr4xgtjt9sl87gaft0lmp |
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/loans$download&file=loans.gno' | shasum -a 256Expected: dbad142fe119f00fe9acfac66a8b1bbd3ffa7250476a0009378cf5b14cf877de — 21,094 bytes. Use curl: the gno.land edges answer Python's default user-agent with HTTP 403.
API
chain-derived 27 exported functions.
Every function below deep-links to gnoweb's call builder, which generates a ready-to-run gnokey maketx call for it:
AcceptAdminAmountDueNowClaimClaimAllClaimableOfDefaultDefaultableAtFeeBpsFeesAccruedHasLoanHeightHeldInstallmentAmountIsDefaultableLiabilitiesLoansOfNumLoansOpenOutstandingRenderRepayScheduledDueSetFeeBpsSetFeeRecipientSweepDenomTransferAdminWithdrawFees
Overview
Realm loans is an unsecured term loan with a fixed installment schedule and a flat finance charge, holding real ugnot. A LENDER opens a loan to a named BORROWER by attaching the principal, which the realm disburses to the borrower in the same transaction; the borrower REPAYS over a fixed number of installments; each repayment credits the lender's pull-claimable balance, the protocol fee splitting off at the ledger. A loan whose installments go unpaid past a grace window is permissionlessly DEFAULT-able — a recorded state, not a seizure, because v1 is unsecured.
It is the one financial primitive this portfolio never had: CREDIT. The accounting realms RECORD obligations (r/invoices, r/receivables — p/aging's own doc lists "invoices, loans, dues") and the disbursement realms pay a KNOWN TOTAL out on a schedule (r/vesting, r/payroll, r/budgets). A loan is the asymmetric opposite: principal OUT once, a LARGER total back over time, plus the finance charge and the default state a disbursement realm has no use for. See [an unpublished record].
Custody — the conservation invariant is the product
This is the first custodial realm the factory has selected since the publishers (r/latefees, r/cashflow), so the monetary invariant governs it exactly as it does r/subscriptions and r/meters:
Held == Σ lender-claimable + fee pot (== feeledger.Liabilities())
The principal is NEVER held across a transaction: Open receives it and pays it out to the borrower in the same call, so the only coins this realm ever holds are repayments awaiting the lender's Claim. A stuck or abandoned loan therefore strands no principal — it was disbursed — and the only value at risk is the lender's expected return, which is the real nature of unsecured credit and is disclosed, not engineered away.
The finance charge is FLAT in v1, and that is a scope decision
The total repayable is `principal + floor(principal × rateBps / 10000)` — a flat charge over the whole term, NOT compounding and NOT accruing per block. Per-installment is `ceil(total / installments)` with the rounding remainder on the FINAL installment, so the installments sum to exactly the total (the cross-foot). Compounding interest and a tiered-by-duration rate (which p/tiers could price) are a v2: pricing them in v1 would add an accrual surface whose bugs are the ones lending audits most often find.
Default is a state, not a seizure — there is no collateral in v1
A borrower past the grace window who has not repaid in full is Default-able by anyone (like the sibling realms' valves). It freezes the loan and records the shortfall; no coins move, because there is nothing to seize. Secured lending is a v2.
REALM-CALLER CAVEAT, inherited from the siblings: coinio's receipt guard admits only EOA payers, so the LENDER (who funds Open) and any REPAYER (who funds Repay) are EOAs. The BORROWER is a parameter — coinio.Payout delivers the principal to any valid address, including a realm — but a realm borrower could never Repay (Repay is payable, EOA-only), so naming one is the lender's own footgun (an un-repayable gift), not a realm or third-party hazard. assertNoSend reads the ORIGIN envelope, so every non-payable function refuses any transaction that attached coins anywhere.
Imports
chainchain/runtimechain/runtime/unsafegno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/coiniogno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/feeledgergno.land/p/nt/avl/v0gno.land/p/nt/markdown/sanitize/v0strconv
Constants and variables
Loan status values. Repaid and Defaulted are terminal.
const (
LoanActive = "active"
LoanRepaid = "repaid"
LoanDefaulted = "defaulted"
)
Input bounds.
const (
// MinPrincipal keeps a loan from being dust; MaxPrincipal keeps
// principal × rateBps and the total far from int64 overflow (the
// checked arithmetic still guards it). The lender must ATTACH the
// principal, so the real ceiling is their balance.
MinPrincipal = int64(1_000)
MaxPrincipal = int64(1_000_000_000_000) // 1,000,000 GNOT
// MaxRateBps caps the flat finance charge at 100% of principal over the
// whole term. A 0 rate is a legitimate interest-free loan.
MaxRateBps = int64(10_000)
// Installment-count and period bounds. The period bounds match the
// sibling realms: the floor keeps repayment off a per-block treadmill,
// the ceiling keeps schedule arithmetic far from overflow.
MinInstallments = int64(1)
MaxInstallments = int64(60)
MinPeriodBlocks = int64(10)
MaxPeriodBlocks = int64(7_500_000)
// MaxLoansPerLender bounds one lender's stored loans; terminal loans do
// NOT free the slot, because a Repaid/Defaulted loan is a permanent
// record a borrower and auditor may need to read.
MaxLoansPerLender = 50
// RenderLimit bounds every rendered list — an unbounded Render was
// YELLOW in three prior audits.
RenderLimit = 20
)
Denom is the only asset this realm accepts.
const Denom = "ugnot"
MaxFeeBps is the hard protocol-fee cap: 1000 bps = 10%.
const MaxFeeBps = int64(1000)
Functions
AcceptAdmin
func AcceptAdmin(cur realm)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asAcceptAdmin(cross(cur), ...).
AmountDueNow
func AmountDueNow(loanID int64) int64
AmountDueNow is how far behind schedule the borrower is right now: ScheduledDue − repaid, floored at zero (a borrower who has prepaid is not behind).
Claim
func Claim(cur realm, amount int64)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asClaim(cross(cur), ...).
Claim sends amount ugnot of the caller's claimable balance (repayments collected on their loans, net of the protocol fee) back to the caller.
ClaimAll
func ClaimAll(cur realm)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asClaimAll(cross(cur), ...).
ClaimAll sends the caller's entire claimable balance back to the caller.
ClaimableOf
func ClaimableOf(a address) int64
Default
func Default(cur realm, loanID int64)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asDefault(cross(cur), ...).
Default marks an active loan defaulted once it is past its grace window — the final installment's due height plus one more period — and not repaid in full. Permissionless by design: no loan's terminal state may depend on either party acting. No coins move; the shortfall is recorded.
DefaultableAt
func DefaultableAt(loanID int64) int64
DefaultableAt is the height at or after which an unpaid loan may be Defaulted: the final installment's due height plus one grace period.
FeeBps
func FeeBps() int64
FeesAccrued
func FeesAccrued() int64
HasLoan
func HasLoan(loanID int64) bool
Height
func Height() int64
Held
func Held() int64
InstallmentAmount
func InstallmentAmount(loanID int64) int64
InstallmentAmount is the per-installment amount: ceil(total/installments), with the rounding remainder carried on the FINAL installment so the installments sum to exactly the total.
IsDefaultable
func IsDefaultable(loanID int64) bool
IsDefaultable reports whether the loan may be Defaulted right now.
Liabilities
func Liabilities() int64
LoansOf
func LoansOf(lender address) int64
NumLoans
func NumLoans() int64
Open
func Open(cur realm, borrower address, rateBps, installments, periodBlocks int64) int64
Crossing function. Callable from a transaction via
MsgCall, and from another realm asOpen(cross(cur), ...).
Open funds a loan: the caller (the lender) attaches the principal, which is disbursed to borrower in this same transaction, and the loan is recorded with a flat finance charge of rateBps of the principal, repayable over `installments` installments of `periodBlocks` blocks each. The current protocol fee is snapshotted in. Returns the loan id.
Outstanding
func Outstanding(loanID int64) int64
Outstanding is total − repaid.
Render
func Render(path string) string
Repay
func Repay(cur realm, loanID int64)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asRepay(cross(cur), ...).
Repay pays toward the caller's own loan. The caller must be the loan's borrower; the attached amount must be positive and must not exceed the outstanding balance. The payment credits the lender's claimable balance (the protocol fee splitting off at the ledger); when the cumulative repaid equals the total, the loan is Repaid. Repaying ahead of schedule is allowed.
ScheduledDue
func ScheduledDue(loanID int64) int64
ScheduledDue is the cumulative amount that should have been repaid by the current height: the sum of every installment whose due height has passed, capped at the total. Installment k (1-based) is due at start + k·period.
SetFeeBps
func SetFeeBps(cur realm, bps int64)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asSetFeeBps(cross(cur), ...).
SetFeeRecipient
func SetFeeRecipient(cur realm, recipient address)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asSetFeeRecipient(cross(cur), ...).
SweepDenom
func SweepDenom(cur realm, denom string)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asSweepDenom(cross(cur), ...).
SweepDenom recovers out-of-band coins to the fee recipient. For the ledger denom the reserve is Liabilities() — lender balances and the fee pot are structurally unreachable.
TransferAdmin
func TransferAdmin(cur realm, successor address)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asTransferAdmin(cross(cur), ...).
WithdrawFees
func WithdrawFees(cur realm)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asWithdrawFees(cross(cur), ...).
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 |
|---|---|
| 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- UNSECURED. Default is a recorded state, not a seizure -- no collateral, no liquidation in v1. An unsecured loan's default is the lender's loss. Collateralized lending is a v2.
- FLAT finance charge, not compounding and not a tiered-by-duration rate (p/tiers could price one) -- a v2. A one-time percentage of principal over the whole term.
- THE BORROWER IS A PARAMETER. Only the lender and repayers are EOA-enforced; Open will disburse to a realm borrower who could then never Repay -- the lender's own un-repayable-gift footgun, not a realm or third-party hazard.
- ScheduledDue/AmountDueNow/InstallmentAmount are VIEWS -- no fund path or default decision reads them; Repay/Default use total/repaid/outstanding/defaultableAt directly.
- LOANS ARE PERMANENT -- terminal loans keep their per-lender slot (a record an auditor may need), bounded per lender by MaxLoansPerLender=50 (monotone, no decrement by design).
- THE LIVE FEE SPLIT WAS 0 bps: loan 0 snapshotted the deploy-time default feeBps=0 before any SetFeeBps, so its repayment credited the lender the gross. The fee split is proven by the committed suite at 100 and 250 bps, not live.
- storage_bytes and storage_deposit are null for the AL56 reason in storage_measurement_note; the figures ARE chain-attested (47,481 B / 4,748,100ugnot at 484256). Do not read the nulls as an unmeasured deploy.
Source
| Source file | onyx/r/loans/loans.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 |