Skip to main content
Orma exists because a number that should be easy to read turned out not to be readable at all by the obvious method. Getting from there to a working measurement, publication, and enforcement layer meant originating real loans on Devnet, servicing them, impairing them, defaulting them, liquidating first-loss cover, and reading every result back. This page is what that cost — seven findings in the order you meet them as a developer, each verified against the live protocol with a transaction hash.
This runs on Devnet because it has to. SingleAssetVault, LendingProtocol, and LendingProtocolV1_1 are not on Mainnet. XLS-47 Price Oracle is live on Mainnet, so the publication rail is the one component with a production path today. The measurement and the gate both read or bind a vault object, so neither is deployable until the lending amendments ship.

Severity rubric


Findings

What you expect. LoanBrokerSet requires the submitter to be the vault owner. You create a vault, you own it, you attach a broker.What actually happens. tecNO_PERMISSION, on a vault you own. The missing rule: under LendingProtocolV1_1, a loan broker may only be attached to a closed-ended vault. The proof by exhaustion, holding the owner constant and varying only the vault kind:The rule is in the C++ source. LoanBrokerSet.cpp lines 149–161 carry both the check and a comment explaining exactly the confusion it causes, ending in JLOG(ctx.j.warn()) << "LoanBroker requires a closed-ended Vault.". rippled computes that sentence and then discards it.Across the published XLS-0065, XLS-0065/65.1, and XLS-0066 documents on master, the strings VaultKind, SubscriptionDate, RedemptionDate, LEVersion, and “closed-ended” each appear zero times. The closed-ended vault is specified in XRPL-Standards PR #587 (open since 2026-07-21), cash-basis accounting in PR #582 (2026-07-16). Library maintainers can read them; application developers cannot.What it costs you. VaultKind is immutable. VaultSet rejects it at deserialisation with "Field 'VaultKind' found in disallowed location." There is no conversion path. If you follow the published documentation, create an open-ended vault, take deposits into it, and then discover the lending layer is unavailable — you rebuild from scratch.Cheapest fix. Add an eleventh protocol-level failure condition to XLS-66 §3.3.3.2: Vault(VaultID).VaultKind is not ClosedEnded (tecNO_PERMISSION), requires LendingProtocolV1_1.” The behaviour is already agreed (rippled#8076, merged 2026-08-26) and already documented in the open xrpl-dev-portal#3923, which targets release-3.4.0 and is not on xrpl.org yet.Your fix today. Always set VaultKind: 1, SubscriptionDate, RedemptionDate, and LEVersion: 1 on VaultCreate.
What you expect. Vault.LossUnrealized moves from 0 to 10,000,000 drops. Transaction metadata records changes, so an indexer that diffs PreviousFields against FinalFields — the standard indexer pattern — sees it.What actually happens. PreviousFields is an empty object. Verbatim, from LoanManage with tfLoanImpair, transaction 075FE6D2E0F29919AF477A2A8F581A680805A006138BB967D8434611E49229C3 (the Meridian facility, Devnet):
rippled omits from PreviousFields any field whose previous value equalled the type default. LossUnrealized defaults to 0. Under cash-basis accounting an impairment changes nothing else on the Vault — AssetsTotal and AssetsAvailable are untouched. The result is a ModifiedNode that reads as touched but unchanged.A metadata-diffing indexer reports no change across a 19.61% fall in net asset value.The failure is asymmetric, which is why it survives testing:An indexer author who tests the reversal or a second impairment sees correct metadata and concludes everything works. The only broken case is the transition from healthy to distressed.This was reported before us as rippled#6487, by the operator of the XRPLWin explorer — precisely the metadata-diffing indexer author this trap is built for. He closed it himself the same day: “Not a bug.” No maintainer replied. The behaviour is unchanged on 3.4.0-rc5.Your fix. Never diff. Re-read the full Vault SLE after every LoanManage. Coalesce absent fields to zero in one place:
What you expect. To assess a manager, you walk account_tx for the broker owner and keep transactions that concern this broker. A natural filter: keep transactions where tx.LoanBrokerID matches, or where the LoanBroker node appears in AffectedNodes.What actually happens. LoanManage carries LoanID only. It has no LoanBrokerID field on any flag. An impairment does not touch the LoanBroker object at all. The complete AffectedNodes set for an impairment on a funded vault with a broker and cover in place:A filter on either the transaction field or the broker node returns cover deposits and defaults — and silently drops every impairment. The record comes back complete, in the right order, with correct figures, and missing precisely the events that show a manager behaving well.In Orma’s original conduct assessment this inverted a reputation rule: brokers who impaired before defaulting looked like brokers who never warned anyone. No exception was thrown. No result code was wrong. The assessment returned a plausible, reviewer-acceptable history that quietly penalised disclosure and rewarded concealment.Your fix. The join is free. The Loan node in the same metadata carries FinalFields.LoanBrokerID:
Note also: on a default, the ledger deletes PrincipalOutstanding from the loan, so FinalFields carries nothing and the obvious read returns zero. Fall back to PreviousFields.PrincipalOutstanding, then to the broker’s book movement.
What you expect. npm install xrpl installs a library that can serialise a closed-ended VaultCreate with VaultKind, SubscriptionDate, and RedemptionDate.What actually happens. For 17 days before the event, installing xrpl brought a version whose codec could not serialise those fields. validate() and autofill() both passed the transaction. The failure surfaced as an opaque codec error inside Wallet.sign() — no result code, no field name, no amendment name.ripple-binary-codec@2.11.0 fixed this on 2026-09-11, and xrpl@5.2.0 followed hours later with the SIGNING_ENCODERS table fix that also enabled counterparty signing for LoanSet. Both reached every fresh npm install automatically, because the codec range floats.xrpl-py received neither fix. No published Python version can counterparty-sign a LoanSet, so there is no Python path to originate a loan on this protocol at time of writing.Your fix. Pin "xrpl": "5.2.0" and "ripple-binary-codec": "2.11.0" explicitly in package.json. Floating the range means the floor can move under you mid-session.
What you expect. autofill() computes the correct fee for LoanSet.What actually happens. LoanSet requires two signatures — the lender’s and the borrower’s. autofill() computes the fee for one. The counterparty signature is a second signing operation that consumes additional fee. autofill warns about this but does not pay for it. The transaction fails at submission with an insufficient-fee error.Your fix. Double the fee before signing:
What you expect. Reading the Vault ledger entry tells you whether a vault is gated, because DomainID is a vault property.What actually happens. DomainID is not stored on the Vault ledger entry. It lives on the share MPTokenIssuance. Reading the Vault to check whether it is gated returns nothing — which reads exactly like “open to everyone”.
tfVaultPrivate (0x00010000) is required on VaultCreate alongside DomainID. A DomainID without the flag gates nothing at all. The Orma API reports that case explicitly as "carries a domain but is not flagged private, so nothing is enforced" rather than presenting it as protected.Use GET /api/vaults/:vaultId/gate to verify a gate setup — it reads the issuance, not the Vault.

The full register covers 40 findings, 7 P0, 19 P1, 14 P2, plus three withdrawn after re-verification (listed rather than deleted). Every claim carries a transaction hash, a file and line, a verbatim error string, or a registry timestamp. Every hash resolves at https://devnet.xrpl.org/transactions/<hash>.

What was not tested

Stated so no one builds on claims that were not made.
  • Non-XRP vault assets. Everything above uses XRP, so Vault.Scale is 0 and all arithmetic is integer drops. With an IOU or MPT asset and a non-zero scale, precision and dust paths become active and several exact numbers will shift.
  • The min() clamps in the cover formula. In the four defaults tested, the double product was always far below both DefaultAmount and CoverAvailable, so only the first term ever bound.
  • LoanDelete, LoanBrokerDelete, VaultDelete, LoanBrokerCoverClawback, tfLoanFullPayment, and the fee and rate fields attached to loan closure.
  • OracleSet under a loaded ledger or a regular-key account. Devnet load_factor was 1 throughout.