Skip to main content
Orma measures credit risk in XLS-65 vaults and XLS-66 lending on the XRP Ledger. This page explains the on-chain mechanics the rest of the documentation assumes. It is written for a reader who understands credit funds and is new to XRPL. Everything described here was measured on Devnet against rippled 3.4.0-rc5 at wss://s.devnet.rippletest.net:51233 (network id 2).

The vault is a ledger object, not a contract

A vault is a native object on the XRP Ledger. There is no contract code to read or audit. Depositors send VaultDeposit, receive units of a share token, and the object carries the fund’s balance sheet in a handful of fields: Two of those deserve immediate attention. AssetsTotal does not net LossUnrealized, and units outstanding live on a separate ledger object — the share token’s MPTokenIssuance. A tool that reads AssetsTotal and divides by units is reading the fund before provisions.
rippled omits any field whose value equals the type default. A vault with no recognised loss has no LossUnrealized key at all — not "0". “Absent” means zero everywhere in Orma’s arithmetic, and confusing “absent because zero” with “absent because the field doesn’t exist” is a real source of wrong numbers.

Open-ended and closed-ended vaults

An open-ended vault has no calendar. Deposits and withdrawals are legal at any time, subject to liquidity. It cannot be used for lending — attaching a LoanBroker returns tecNO_PERMISSION. A closed-ended vault is created with VaultKind: 1 plus a SubscriptionDate and a RedemptionDate. It is a fixed-term fund: capital is raised during Subscription, deployed during Investment, then returned during Redemption.
VaultKind is immutable. A vault created open-ended can never become a lending vault. If you plan to lend, you must decide that at VaultCreate — you cannot change it afterwards.

The three phases

Phase is derived, not stored. It is a comparison between the validated ledger’s close_time and the two dates on the vault object. The clock is the validated ledger close time, not the wall clock — Devnet close time can lag wall time by up to ten seconds, and a countdown computed against the wrong clock goes negative on stage. Subscription runs until SubscriptionDate. Capital comes in and may leave freely. No lending is possible yet — LoanSet returns tecTOO_SOON. Investment runs from SubscriptionDate to RedemptionDate. The book is locked. Loans are originated, impaired, and defaulted here. Investors cannot deposit and cannot withdraw for the entire phase — VaultDeposit returns tecEXPIRED and VaultWithdraw returns tecTOO_SOON. This is a phase predicate, not a liquidity check: Orma measured it with 80 XRP of AssetsAvailable sitting idle and the exit was still closed. Redemption begins at RedemptionDate. Withdrawals reopen, deposits stay closed, and no new loan can be written. Under the default WithdrawalPolicy: 1, redemption is first-come first-served, so the order investors queue matters when liquidity is short.

Phase rejection matrix

The table below is measured, not inferred. Each row was verified against a live Devnet facility with a passing control alongside it (a control that succeeds where the action is legal proves the rejection is about phase, not about a malformed transaction). * The Investment LoanSet returned tecNO_PERMISSION in this particular matrix run because the loan’s scheduled maturity overran the vault’s RedemptionDate. The ledger rule is StartDate + PaymentInterval × PaymentTotal + 60 ≤ RedemptionDate. Three of Orma’s four live facilities have loans written successfully in Investment — this was a window-sizing issue in the matrix script, not a phase rule.
tecNO_PERMISSION on LoanSet is overloaded. It means both “this vault is not closed-ended” and “this loan matures after the vault’s RedemptionDate”. A 180-second loan on the same vault returned tesSUCCESS; a 1200-second loan returned tecNO_PERMISSION. Read the code in LoanSet.cpp — the rule involves RedemptionDate and GracePeriod is not part of it.
Because the exit is closed for the entire Investment phase, a credit signal published during Investment cannot be acted on by selling. That is why the redemption cliff anchors the composite grade rather than sitting as one factor among five, and why Orma publishes the signal where a third party can act on it — not just display it. See Mathematics — The Five Measured Factors.

The LoanBroker and first-loss capital

Loans are not made directly by the vault. They flow through a LoanBroker, a separate ledger object that points at the vault and holds its own fields: Both rates are expressed in units of 10510^{-5}. A stated 10000 means 0.100.10 — ten percent — divided by 10510^5. The cover consumed on a single default is: T(p)=min(Dcmincliq,  p,  C)T(p) = \min\Big(\big\lceil D \cdot c_{\min} \cdot c_{\text{liq}} \big\rceil,\; p,\; C\Big) where DD is the broker’s total drawn debt, pp is the principal of the defaulting loan, and CC is CoverAvailable. At cmin=cliq=10000c_{\min} = c_{\text{liq}} = 10000 the effective coefficient is 0.10×0.10=0.010.10 \times 0.10 = 0.01 — one percent of the book, not ten. Note what the base is. The formula uses the broker’s total book DD, not the principal of the loan that failed. This means the same loan defaulting inside a larger book liquidates more cover, and a loan that represents most of the book liquidates cover keyed to that entire book and leaves little behind it. Orma reports strandedCoverFraction to surface this. On Calder Structured Credit III — 10 XRP of debt, 5 XRP of cover posted — 98 percent of the posted cover cannot be reached by any single default at the current book size.

The broker owner is the vault owner

This is a protocol fact, not a configuration choice. LoanBrokerSet requires the submitting account to be the vault’s Owner. So the party who posts first-loss capital and the party who manages the fund are necessarily the same account. Only that owner may impair, unimpair, or default a loan. There is no independent servicer, no trustee, and no rating committee. Credit events are declared by the party whose own capital those declarations consume.
Delegation does not fix this. XLS-75 permission delegation returns temMALFORMED for the entire lending transaction suite, so credit decisions cannot be handed to a separate account even if a manager wanted to.

Cash-basis accounting and LossUnrealized

Under LEVersion: 1 the vault keeps cash-basis books. The practical consequences:
  • Origination moves DebtTotal, not AssetsTotal. Lending out cash is not a gain or a loss, so the vault’s asset total does not change when a loan is written.
  • Impairment moves only LossUnrealized. Nothing else on the Vault object changes.
  • Default moves assets. The loss is realised: the asset is written off, the provision is released, cover is liquidated, and the book reads clean afterwards.

Impairment vs. default

Test loan flags by bit, never by equality. A loan can carry more than one flag, and Flags === 131072 silently stops matching the moment a second flag is set. Use a bitwise check: (flags & 0x20000) !== 0 for impaired, (flags & 0x10000) !== 0 for defaulted.
An impairment is a disclosure — it says “this exposure is in trouble” without moving a single asset. A default is a write-off. Both are the vault owner’s unilateral decision, and neither has a deadline attached. Realisation is also why a clean-looking book is not evidence of a clean history. Kestrel Bridge Financing II took 50 XRP of subscriptions, defaulted on 40 XRP, and now holds 10.5 XRP with lossUnrealized of "0" and a NAV divergence of zero basis points. The naive and correct unit values agree at 0.210000 — and only the 79% gap between units outstanding and assets held tells you what happened.

Lever one: when to recognise a loss

The manager chooses the moment LossUnrealized moves. Until they act, AssetsTotal carries the troubled loan at book and a naive reader sees a fund at par: NAVnaive=AS,NAVheld=ALS\text{NAV}_{\text{naive}} = \frac{A}{S}, \qquad \text{NAV}_{\text{held}} = \frac{A - L}{S} On Meridian Trade Finance I: A=51,000,000A = 51{,}000{,}000 drops, L=10,000,000L = 10{,}000{,}000, S=51,000,000S = 51{,}000{,}000. Naive reads 1.000000 and held reads 0.803922 — a divergence of 1961 basis points on the same object at the same instant. The delay is not merely invisible to careless tools. It is invisible to careful ones that watch transaction metadata. On the first impairment of a healthy vault, LossUnrealized moves from zero and under cash-basis accounting nothing else changes, so PreviousFields is emitted empty. An indexer that diffs metadata sees no change at all on the most important credit event the protocol has. Delay also flatters the manager directly: CoverRateMinimum is keyed to DebtTotal, so the cover they must hold, and the cover liquidated when they eventually act, both shrink as the book ages.
This is why Orma is a four-second poller that re-reads full object state rather than a metadata differ. Re-reading cannot miss a field that was never diffed. See src/poll.mjs.

Lever two: in what order to realise losses

Because TT is keyed to DebtTotal at the instant of each default, and DebtTotal is decremented as each default lands, declaring losses in a different order changes the total cover consumed. With c=cmincliqc = c_{\min} c_{\text{liq}}, D0D_0 the book at the first default, and kk defaults declared in order σ\sigma: T(σ,k)=c[kD0i=1k(ki)pσ(i)]T(\sigma, k) = c \left[ k D_0 - \sum_{i=1}^{k} (k - i)\, p_{\sigma(i)} \right] Only the sum depends on σ\sigma, and its coefficient (ki)(k-i) decreases in ii. By the rearrangement inequality the sum is maximised — and TT minimised — when principals are declared in decreasing order.
Total first-loss capital consumed is minimised by declaring the largest exposure first, and maximised by declaring the smallest first.
Orma places the observed sequence between the two extremes as a sequence score: ϕ=TobservedTminTmaxTmin,ϕ[0,1]\phi = \frac{T_{\text{observed}} - T_{\min}}{T_{\max} - T_{\min}}, \qquad \phi \in [0, 1] ϕ=0\phi = 0 is the sequence worst for investors; ϕ=1\phi = 1 is the best. Kestrel Bridge Financing II scored ϕ=0.0000\phi = 0.0000 — the manager chose the investor-worst order on live Devnet. See Mathematics for the full derivation and three independent cross-checks.

One asymmetry, two levers, then enforcement

The vault owner and the broker owner are the same account — that is the single asymmetry. It gives one party two levers over investor money: when a loss is recognised, and in what order losses are realised. Both are discretionary, both move money, and neither leaves a signal a metadata-diffing tool would catch. Orma’s response is three steps, each on its own page:
  • Measure both levers from public ledger state, with no privileged access. See Mathematics.
  • Publish the result where it can be acted on: a native XLS-47 Oracle object and a pointer inside the share token’s own metadata. See Mathematics — Publication and NAV Pointer.
  • Enforce with XLS-70 credentials and a permissioned domain, so an independent rater decides who may subscribe. See Gate.