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
1. LoanBroker requires a closed-ended vault — and no standard states it (P0)
1. LoanBroker requires a closed-ended vault — and no standard states it (P0)
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: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.2. The first impairment is invisible to metadata diffs (P1)
2. The first impairment is invisible to metadata diffs (P1)
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):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.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:3. LoanManage carries no LoanBrokerID — every impairment drops from naive filters (P1)
3. LoanManage carries no LoanBrokerID — every impairment drops from naive filters (P1)
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:Loan node in the same metadata carries FinalFields.LoanBrokerID: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.4. Pin xrpl@5.2.0 — VaultCreate fails on earlier versions (P1)
4. Pin xrpl@5.2.0 — VaultCreate fails on earlier versions (P1)
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.5. LoanSet fee must be doubled by hand — autofill doesn't pay for the counterparty signature (P1)
5. LoanSet fee must be doubled by hand — autofill doesn't pay for the counterparty signature (P1)
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:6. OracleSet must bypass validate() — it rejects legal scale values (P1)
6. OracleSet must bypass validate() — it rejects legal scale values (P1)
validate() accepts a well-formed OracleSet transaction.What actually happens. xrpl.js validate() rejects OracleSet transactions carrying valid Scale values for non-price dimensions. The six Orma dimensions use AssetClass: "risk" and Scale values of 6 for NAV and 4 for the other five. validate() refuses them.Pass the transaction directly to the signing layer without calling validate():OracleSet traps that corrupt data silently instead of erroring:- Not a merge. A
(BaseAsset, QuoteAsset)pair already on the object but omitted from the transaction is kept with itsAssetPricestripped. Publishing one dimension blanks the other five. Always send the full six-dimension series. LastUpdateTimemust strictly increase. Equal or lower burns a fee and a sequence number. One update per second per object, maximum.AssetPriceis hexadecimal. Writing"100"means 256. UseBigInt(n).toString(16).toUpperCase()to encode andBigInt("0x" + hex)to decode.
OracleDocumentID for a different set of assets accumulates stale pairs until the series exceeds the ledger ceiling and OracleSet returns tecARRAY_TOO_LARGE. At that point nothing can be published to that document and the only recovery is delete and recreate.7. DomainID lives on MPTokenIssuance, not on Vault (P2)
7. DomainID lives on MPTokenIssuance, not on Vault (P2)
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.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.Scaleis 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 bothDefaultAmountandCoverAvailable, so only the first term ever bound. LoanDelete,LoanBrokerDelete,VaultDelete,LoanBrokerCoverClawback,tfLoanFullPayment, and the fee and rate fields attached to loan closure.OracleSetunder a loaded ledger or a regular-key account. Devnetload_factorwas 1 throughout.