Cosmic Bull

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:443

FeeBps 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 pathgno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/loans
Namespaceg1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3
Deployed at height484,253
Deploy transactioncaf1c5a301ab59c34ae31be6080a58ed85f583528c5122eeace37a7730fd1e2f look it up on the RPC
Deployerg1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3
Gas used29,469,179
Storagenot attested per package on this chain — see the limitations below
Files on chaingnomod.toml loans.gno
Deployed bytesloans.gno — 21,094 bytes
sha256dbad142fe119f00fe9acfac66a8b1bbd3ffa7250476a0009378cf5b14cf877de
Realm addressg1s39q788nff5apm24gyr4xgtjt9sl87gaft0lmp

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 256

Expected: 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

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 as AcceptAdmin(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 as Claim(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 as ClaimAll(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 as Default(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 as Open(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 as Repay(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 as SetFeeBps(cross(cur), ...).

SetFeeRecipient

func SetFeeRecipient(cur realm, recipient address)

Crossing function. Callable from a transaction via MsgCall, and from another realm as SetFeeRecipient(cross(cur), ...).

SweepDenom

func SweepDenom(cur realm, denom string)

Crossing function. Callable from a transaction via MsgCall, and from another realm as SweepDenom(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 as TransferAdmin(cross(cur), ...).

WithdrawFees

func WithdrawFees(cur realm)

Crossing function. Callable from a transaction via MsgCall, and from another realm as WithdrawFees(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
Importschain, 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 dependenciesgno.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

Source

Source fileonyx/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 bytesyes — byte-identical