From 5480349b5884fd1989b3a6d1f3a26e1ed882d66b Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Thu, 16 Jul 2026 18:21:55 +0200 Subject: [PATCH 01/32] adds cash-based share valuation --- XLS-0066-lending-protocol/README.md | 119 +++++++++++++++++++++++++++- 1 file changed, 118 insertions(+), 1 deletion(-) diff --git a/XLS-0066-lending-protocol/README.md b/XLS-0066-lending-protocol/README.md index 4b5a14b36..54aa3116b 100644 --- a/XLS-0066-lending-protocol/README.md +++ b/XLS-0066-lending-protocol/README.md @@ -8,7 +8,7 @@ category: Amendment requires: XLS-65, XLS-64 created: 2024-10-18 - updated: 2026-01-14 + updated: 2026-07-16 proposal-from: https://github.com/XRPLF/XRPL-Standards/discussions/190 @@ -1089,6 +1089,13 @@ The account specified in the `Account` field pays the transaction fee. 23. The `LoanBroker.Owner` is not authorized for the asset. (`tecNO_AUTH`) 24. The `LoanBroker.LoanSequence` has reached its maximum value. (`tecMAX_SEQUENCE_REACHED`) +##### 3.8.5.3 Protocol-Level Failures (`LendingProtocolV1_1`) + +Under the `LendingProtocolV1_1` amendment, `Vault.AssetsTotal` and `LoanBroker.DebtTotal` become principal-only. Checks 19, and 20 of [3.8.5.2](#3852-protocol-level-failures) are replaced by the following (all other checks are unchanged): + +19. `LoanBroker.DebtMaximum != 0` and `LoanBroker.DebtMaximum < LoanBroker.DebtTotal + PrincipalRequested` (exceeds maximum debt). (`tecLIMIT_EXCEEDED`) +20. `LoanBroker.CoverAvailable < (LoanBroker.DebtTotal + PrincipalRequested) × LoanBroker.CoverRateMinimum` (insufficient first-loss capital). (`tecINSUFFICIENT_FUNDS`) + #### 3.8.6 State Changes 1. Create the `Loan` object with computed fields (`TotalValueOutstanding`, `PeriodicPayment`, `ManagementFeeOutstanding`, `LoanScale`, etc.). @@ -1123,6 +1130,18 @@ The account specified in the `Account` field pays the transaction fee. - Add `LoanID` to the `OwnerDirectory` of the `LoanBroker` _pseudo-account_ (sets `LoanBrokerNode`). - Add `LoanID` to the `OwnerDirectory` of the `Borrower` (sets `OwnerNode`). +##### 3.8.6.1 State Changes (`LendingProtocolV1_1`) + +Under the `LendingProtocolV1_1` amendment, `Vault.AssetsTotal` and `LoanBroker.DebtTotal` become principal-only — neither tracks interest. Steps 6 and 7 of [3.8.6](#386-state-changes) are replaced by the following; all other steps are unchanged: + +6. Update `Vault` object: + - Decrease `Vault.AssetsAvailable` by `PrincipalRequested`. + - `Vault.AssetsTotal` is **not** increased by `InterestDue` (`AssetsTotal` no longer tracks interest). +7. Update `LoanBroker` object: + - Increase `LoanBroker.DebtTotal` by `PrincipalRequested` only (`InterestDue` is excluded — `DebtTotal` no longer tracks interest). + - Increment `LoanBroker.OwnerCount` by `1`. + - Increment `LoanBroker.LoanSequence` by `1`. + #### 3.8.7 Invariants **TBD** @@ -1253,6 +1272,12 @@ This transaction uses the standard transaction fee. 7. The submitter is not the `LoanBroker.Owner`. (`tecNO_PERMISSION`) 8. `tfLoanImpair` flag is specified and `Vault.LossUnrealized + (Loan.TotalValueOutstanding - Loan.ManagementFeeOutstanding) > Vault.AssetsTotal - Vault.AssetsAvailable` (impairment would exceed vault's unavailable assets). (`tecLIMIT_EXCEEDED`) +##### 3.10.4.3 Protocol-Level Failures (`LendingProtocolV1_1`) + +Under the `LendingProtocolV1_1` amendment, check 8 of [3.10.4.2](#31042-protocol-level-failures) is replaced by the following, consistent with the amended impairment amount in [3.10.5.1](#31051-state-changes-lendingprotocolv1_1) (all other checks are unchanged): + +8. `tfLoanImpair` flag is specified and `Vault.LossUnrealized + Loan.PrincipalOutstanding > Vault.AssetsTotal - Vault.AssetsAvailable` (impairment would exceed vault's unavailable assets). (`tecLIMIT_EXCEEDED`) + #### 3.10.5 State Changes 1. If the `tfLoanDefault` flag is specified: @@ -1305,6 +1330,83 @@ This transaction uses the standard transaction fee. - Otherwise: - Set `Loan.NextPaymentDueDate = currentTime + Loan.PaymentInterval`. +##### 3.10.5.1 State Changes (`LendingProtocolV1_1`) + +Under the `LendingProtocolV1_1` amendment, `Vault.AssetsTotal` and `LoanBroker.DebtTotal` become principal-only. The `DefaultAmount`, `LossUnrealized`, and `LossReversed` computations in items 1–3 of [3.10.5](#3105-state-changes) are replaced by the following, collapsing each to a single, unified principal-only amount (all other steps — including the `MinimumCover`/`DefaultCovered`/`VaultLoss` mechanics, the `CoverAvailable` drawdown, and the `Loan` object updates — are unchanged): + +1. If the `tfLoanDefault` flag is specified: + - Compute `DefaultAmount = Loan.PrincipalOutstanding` (principal only, replacing `Loan.TotalValueOutstanding - Loan.ManagementFeeOutstanding`). This single `DefaultAmount` is used, as before, for the `Vault` (`VaultLoss`/`DefaultCovered`) and `LoanBroker.DebtTotal` updates, and for `Vault.LossUnrealized` if the Loan was impaired. + - `Loan.ManagementFeeOutstanding` is unaffected by this amendment — it was already excluded from `DefaultAmount` pre-amendment (via the `- Loan.ManagementFeeOutstanding` term), and remains excluded post-amendment (the amended formula never references it). +2. If the `tfLoanImpair` flag is specified: + - Compute `LossUnrealized = Loan.PrincipalOutstanding` (principal only, replacing `Loan.TotalValueOutstanding - Loan.ManagementFeeOutstanding`), consistent with the amended `DefaultAmount` above. +3. If the `tfLoanUnimpair` flag is specified: + - Compute `LossReversed = Loan.PrincipalOutstanding` (principal only, replacing `Loan.TotalValueOutstanding - Loan.ManagementFeeOutstanding`), mirroring the amended `tfLoanImpair` calculation so the increase/decrease pair on `Vault.LossUnrealized` stays balanced. + +See [3.10.5.2](#31052-worked-example-pre--vs-post-lendingprotocolv1_1-loan-default) below for a full numeric walkthrough comparing the pre- and post-amendment trajectories. + +##### 3.10.5.2 Worked Example: Pre- vs Post-`LendingProtocolV1_1` Loan Default + +This example walks a single Loan through `LoanSet` → `LoanPay` → `LoanManage(tfLoanDefault)`, computing both the current (pre-amendment) and `LendingProtocolV1_1` (post-amendment) trajectories side by side. It uses the same figures as the [`LoanBroker` Accounting](#3110-accounting) and [First-Loss Capital](#3111-first-loss-capital) examples above, for continuity. + +**Initial State (before `LoanSet`)** + +Vault: `AssetsTotal = 100,000`, `AssetsAvailable = 100,000` + +LoanBroker: `DebtTotal = 0`, `ManagementFeeRate = 10%`, `CoverRateMinimum = 10%`, `CoverRateLiquidation = 10%`, `CoverAvailable = 1,000` + +Loan terms: `PrincipalRequested = 1,000`, gross interest over the loan's life = `100` (simplified, as in the examples above), so management fee = `10` and net interest due to the Vault = `90`. + +**Step 1 — `LoanSet`** + +| Field | Pre-amendment | Post-amendment (`LendingProtocolV1_1`) | +| ----------------------- | ------------------------------ | ------------------------------------------------------- | +| `Vault.AssetsTotal` | `100,000 + 90 =` **100,090** | unchanged: **100,000** | +| `Vault.AssetsAvailable` | `100,000 - 1,000 =` **99,000** | `100,000 - 1,000 =` **99,000** (unchanged by amendment) | +| `LoanBroker.DebtTotal` | `0 + 1,000 + 90 =` **1,090** | `0 + 1,000 =` **1,000** | + +**Step 2 — `LoanPay` (one payment: principal = 500, gross interest = 50, management fee = 5, net interest = 45)** + +`totalToVault = 500 + 45 = 545` (unchanged by the amendment — `AssetsAvailable` accounting is untouched). + +| Field | Pre-amendment | Post-amendment (`LendingProtocolV1_1`) | +| ----------------------- | ------------------------------------------ | --------------------------------------------- | +| `Vault.AssetsTotal` | unchanged (`valueChange = 0`): **100,090** | `100,000 + 45 =` **100,045** (`interestPaid`) | +| `Vault.AssetsAvailable` | `99,000 + 545 =` **99,545** | `99,000 + 545 =` **99,545** | +| `LoanBroker.DebtTotal` | `1,090 - 545 =` **545** | `1,000 - 500 =` **500** (`principalPaid`) | + +At this point `Loan.PrincipalOutstanding = 500` under both trajectories (the `Loan` object's own fields are unaffected by the amendment). Assume the remaining scheduled interest is `45` and the remaining management fee is `5`, so `Loan.TotalValueOutstanding = 500 + 45 + 5 = 550` and `Loan.ManagementFeeOutstanding = 5` (identical pre- and post-amendment, since `Loan`-level fields are out of scope for this amendment). + +**Step 3 — `LoanManage(tfLoanDefault)`** + +_Pre-amendment:_ + +- `DefaultAmount = TotalValueOutstanding - ManagementFeeOutstanding = 550 - 5 = 545` +- `MinimumCover = DebtTotal × CoverRateMinimum = 545 × 10% = 54.5` +- `DefaultCovered = min(MinimumCover × CoverRateLiquidation, DefaultAmount, CoverAvailable) = min(5.45, 545, 1000) = 5.45` +- `VaultLoss = DefaultAmount - DefaultCovered = 545 - 5.45 = 539.55` +- `Vault.AssetsTotal = 100,090 - 539.55 =` **99,550.45** +- `Vault.AssetsAvailable = 99,545 + 5.45 =` **99,550.45** +- `LoanBroker.DebtTotal = 545 - 545 =` **0** +- `LoanBroker.CoverAvailable = 1,000 - 5.45 =` **994.55** + +_Post-amendment (`LendingProtocolV1_1`):_ + +- `DefaultAmount = PrincipalOutstanding = 500` (`ManagementFeeOutstanding = 5` still excluded, as it is never referenced by the amended `DefaultAmount`) +- `MinimumCover = DebtTotal × CoverRateMinimum = 500 × 10% = 50` +- `DefaultCovered = min(MinimumCover × CoverRateLiquidation, DefaultAmount, CoverAvailable) = min(5, 500, 1000) = 5` +- `VaultLoss = DefaultAmount - DefaultCovered = 500 - 5 = 495` +- `Vault.AssetsTotal = 100,045 - 495 =` **99,550** +- `Vault.AssetsAvailable = 99,545 + 5 =` **99,550** +- `LoanBroker.DebtTotal = 500 - 500 =` **0** +- `LoanBroker.CoverAvailable = 1,000 - 5 =` **995** + +**Observations** + +- In both trajectories, `LoanBroker.DebtTotal` and `Loan.PrincipalOutstanding`/`ManagementFeeOutstanding` fully clear to (or independent of) `0` at default, and `Vault.AssetsTotal ≈ Vault.AssetsAvailable` post-default (the Vault's books balance, up to the small rounding difference below) — the default mechanics are structurally unchanged. +- The only real difference is that the pre-amendment `DefaultAmount`, `MinimumCover`, and `DefaultCovered` are all computed on figures that still include the (now-collected or now-forgiven) interest, while the post-amendment figures are computed on principal alone. This scales every downstream quantity (`MinimumCover`, `DefaultCovered`, `VaultLoss`, `CoverAvailable` drawn down) down proportionally by the amount of interest excluded — it does not change the _shape_ of the `CoverRateMinimum`/`CoverRateLiquidation` mechanics, only the base they are applied to. +- The `99,550.45` vs `99,550` difference in the final `Vault.AssetsTotal`/`AssetsAvailable` is exactly the `0.45` difference in `DefaultCovered` (`5.45` vs `5`), itself a consequence of `MinimumCover` being computed against a larger, interest-inclusive `DebtTotal` (`545`) pre-amendment vs. a principal-only `DebtTotal` (`500`) post-amendment. Since neither `AssetsTotal` nor `DebtTotal` carries interest information post-amendment, this `0.45` represents interest income the Vault would have (partially) recognized pre-amendment but never recognizes post-amendment — consistent with the loss-of-visibility tradeoff noted for a later conversation. +- `Loan.ManagementFeeOutstanding` (`5`) is unaffected by the amendment in either trajectory: it was already excluded from `DefaultAmount` before the amendment (via the `- Loan.ManagementFeeOutstanding` term), and remains excluded after the amendment (since the amended `DefaultAmount` formula never references it). + #### 3.10.6 Invariants **TBD** @@ -1449,6 +1551,19 @@ These transfers are performed according to the asset type: - The `MPTAmount` in the `Vault` pseudo-account's `MPToken` object is increased. - The `MPTAmount` in the destination account for fees' `MPToken` object is increased. +##### 3.11.5.1 State Changes (`LendingProtocolV1_1`) + +Under the `LendingProtocolV1_1` amendment, `valueChange` is removed entirely from the `LoanBroker` and `Vault` accounting — since neither `LoanBroker.DebtTotal` nor `Vault.AssetsTotal` tracks interest anymore (see [3.8.6.1](#3861-state-changes-lendingprotocolv1_1)), there is nothing left to reconcile for late payments, overpayments, or early full repayments. Steps 6 and 7 of [3.11.5](#3115-state-changes) are replaced by the following (all other steps, including the asset transfers in section 4, are unchanged): + +6. **`LoanBroker` Updates**: + - `LoanBroker.DebtTotal` is decreased by `principalPaid` only (`valueChange` does not apply) — the principal portion of the payment, from the same payment breakdown produced by [Appendix A-3](#a-3-loanpay-implementation-reference). + - If fees were directed to the cover pool, `LoanBroker.CoverAvailable` increases by `totalToBroker`. +7. **`Vault` Updates**: + - `Vault.AssetsAvailable` increases by `totalToVault`. + - `Vault.AssetsTotal` is increased by `interestPaid` only (`valueChange` does not apply) — the interest portion of the payment, net of management fee. + +`principalPaid` and `interestPaid` are computed by the same, unchanged `PeriodicPayment`/amortization logic in [Appendix A-3](#a-3-loanpay-implementation-reference) — no new machinery is introduced by this amendment; only the use of `valueChange` for `DebtTotal`/`AssetsTotal` is dropped. The `Loan` object's own `TotalValueOutstanding` is unaffected by this amendment and continues to be adjusted directly by the re-amortization recalculation described in step 5 of [3.11.5](#3115-state-changes). + #### 3.11.6 Invariants **TBD** @@ -2045,6 +2160,8 @@ The `valueChange` is a critical accounting mechanism that represents the change This `valueChange` is always split between the Vault (as a change in its net interest) and the Loan Broker (as a change in the `managementFee`). +**Under the `LendingProtocolV1_1` amendment**, `valueChange` has no remaining purpose. In the pseudo-code in [A-3.3](#a-33-pseudo-code) below, `valueChange` is never used to derive the `Loan` object's own state — `loan.totalValueOutstanding`, `loan.principalOutstanding`, `loan.managementFeeOutstanding`, and `loan.roundedState`/`loan.periodicPayment` are all computed independently, directly from the amortization/re-amortization formulas (see `compute_late_payment_interest`, `compute_full_payment`, and `try_overpayment`). `valueChange` is *only* ever returned so that [`LoanPay`](#3115-state-changes) can reconcile `Vault.AssetsTotal` and `LoanBroker.DebtTotal` against it. Since the amendment removes that reconciliation entirely (see [3.11.5.1 State Changes (`LendingProtocolV1_1`)](#31151-state-changes-lendingprotocolv1_1)), `valueChange` becomes dead output under the amendment — an implementation only supporting `LendingProtocolV1_1` need not compute it at all. `principalPaid`, `interestPaid`, and `feePaid` are unaffected and continue to be computed and used exactly as described here. + #### A-3.2.1 Regular Payment For a standard, on-time payment, the total amount due from the borrower is the sum of the calculated periodic payment and applicable service fee. From ec4816288edd6f8603a21f14641de9a6ec5107d8 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Tue, 21 Jul 2026 15:28:43 +0200 Subject: [PATCH 02/32] adds Vault version --- XLS-0065-single-asset-vault/README.md | 33 ++++++++++++++++++++++++++- XLS-0066-lending-protocol/README.md | 16 +++++++------ 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/XLS-0065-single-asset-vault/README.md b/XLS-0065-single-asset-vault/README.md index 2c381ab6a..66270ee16 100644 --- a/XLS-0065-single-asset-vault/README.md +++ b/XLS-0065-single-asset-vault/README.md @@ -8,7 +8,7 @@ category: Amendment requires: [XLS-33](../XLS-0033-multi-purpose-tokens/README.md) created: 2024-04-12 - updated: 2026-04-27 + updated: 2026-07-21 # Single Asset Vault @@ -107,6 +107,7 @@ A vault has the following fields: | `ShareMPTID` | No | Yes | `number` | `UINT192` | 0 | The identifier of the share MPTokenIssuance object. | | `WithdrawalPolicy` | No | Yes | `string` | `UINT8` | `N/A` | Indicates the withdrawal strategy used by the Vault. | | `Scale` | No | Yes | `number` | `UINT8` | 6 | The `Scale` specifies the power of 10 ($10^{\text{scale}}$) to multiply an asset's value by when converting it into an integer-based number of shares. | +| `LEVersion` | No | No | `number` | `UINT8` | 0 | Introduced by the `LendingProtocolV1_1` amendment. See [3.1.2.2](#3122-leversion-lendingprotocolv1_1). | ##### 3.1.2.1 Flags @@ -116,6 +117,19 @@ The `Vault` object supports the following flags: | ----------------- | :----------: | :---------: | :------------------------------------------: | | `lsfVaultPrivate` | `0x00010000` | No | If set, indicates that the vault is private. | +##### 3.1.2.2 `LEVersion` (`LendingProtocolV1_1`) + +`LEVersion` is a generalized ledger entry schema version field, introduced by the `LendingProtocolV1_1` amendment. Rather than encoding schema variants as ledger entry flags — which structurally allow mutually exclusive states to be set simultaneously — `LEVersion` is a dedicated field that unambiguously identifies which schema/behavior revision a ledger entry follows. While `LEVersion` is a general-purpose mechanism applicable to any ledger entry type, `Vault` is its first consumer. + +`LEVersion` is strictly protocol-driven: it is never a field of any transaction and cannot be set or changed by the transaction submitter. It is written exclusively by protocol logic, such as a Transactor's state changes at amendment activation. + +For the `Vault` object, `LEVersion` distinguishes the share-valuation and accounting behavior applied to the vault: + +- **Absent (default value `0`)**: The Vault was created before the `LendingProtocolV1_1` amendment was enabled, or otherwise predates ledger entry versioning. It uses the legacy, accrual-basis accounting described throughout this document and in [XLS-66](../XLS-0066-lending-protocol/README.md) — `AssetsTotal` includes accrued interest. +- **`2`**: The Vault was created while the `LendingProtocolV1_1` amendment was enabled. It exclusively uses the cash-basis accounting introduced by that amendment — `AssetsTotal` is principal-only (see [3.1.7.4](#3174-cash-basis-vs-accrual-basis-accounting-lendingprotocolv1_1) and the `LendingProtocolV1_1` subsections of [XLS-66](../XLS-0066-lending-protocol/README.md)). + +Any Vault created once `LendingProtocolV1_1` is enabled is always assigned `LEVersion = 2` — cash-basis accounting is not optional or user-selectable at creation time. A future revision of the `Vault` ledger entry would increment `LEVersion` to `3`, and so on. + #### 3.1.3 Pseudo-Account An AccountRoot entry holds the XRP, IOU or MPT deposited into the vault. It also acts as the issuer of the vault's shares. The _pseudo-account_ follows the XLS-64 specification for pseudo accounts. The `AccountRoot` object is created when creating the `Vault` object. @@ -309,6 +323,17 @@ Withdrawal policy controls the logic used when removing liquidity from a vault. The First Come, First Serve strategy treats all requests equally, allowing a depositor to redeem any amount of assets provided they have a sufficient number of shares. +##### 3.1.7.4 Cash-Basis vs. Accrual-Basis Accounting (`LendingProtocolV1_1`) + +The exchange rate algorithms in [3.1.7.2](#3172-exchange-rate-algorithms) above operate on $\Gamma_{assets}$ (`Vault.AssetsTotal`) without regard to how that value is accrued — that accrual behavior is defined by the connected protocol (see [XLS-66](../XLS-0066-lending-protocol/README.md)), not by the Vault's own exchange-rate math, which is unchanged by this amendment. + +Under the `LendingProtocolV1_1` amendment, `Vault.LEVersion` (see [3.1.2.2](#3122-leversion-lendingprotocolv1_1)) determines which of the two accounting models a Vault's `AssetsTotal` follows: + +- `LEVersion` absent (`0`, legacy/accrual-basis): `AssetsTotal` includes interest upfront, as accrued over the life of connected Loans. This is the pre-amendment behavior and continues unchanged for Vaults created before `LendingProtocolV1_1` was enabled. +- `LEVersion = 2` (cash-basis): `AssetsTotal` is principal-only and increases only as interest is actually collected in cash. This is the exclusive behavior for Vaults created once `LendingProtocolV1_1` is enabled. + +See the `LendingProtocolV1_1` subsections of [XLS-66 §3.8](../XLS-0066-lending-protocol/README.md#38-transaction-loanset), [§3.10](../XLS-0066-lending-protocol/README.md#310-transaction-loanmanage), and [§3.11](../XLS-0066-lending-protocol/README.md#311-transaction-loanpay) for the precise accounting differences, all of which are gated on `Vault.LEVersion == 2`. + #### 3.1.8 Frozen Assets The issuer of the Vaults asset may enact a freeze either through a [Global Freeze](https://xrpl.org/docs/concepts/tokens/fungible-tokens/freezes/#global-freeze) for IOUs or [locking MPT](../XLS-0033-multi-purpose-tokens/README.md#21122-flags). When the vaults asset is frozen, it can only be withdrawn by specifying the `Destination` account as the `Issuer` of the asset. Similarly, a frozen asset _may not_ be deposited into a vault. Furthermore, when the asset of a vault is frozen, the shares corresponding to the asset may not be transferred. @@ -400,6 +425,12 @@ _TBD_ 5. If `Vault.Asset` is an `MPT`: 1. Create `MPToken` object for the _pseudo-account_ for the `Asset.MPTokenIssuance`. +##### 3.2.6.1 State Changes (`LendingProtocolV1_1`) + +Under the `LendingProtocolV1_1` amendment, step 1 of [3.2.6](#326-state-changes) is amended; all other steps are unchanged: + +1. Create a new `Vault` ledger object, setting `Vault.LEVersion = 2` (see [3.1.2.2](#3122-leversion-lendingprotocolv1_1)). `LEVersion` is not a transaction field and is not settable by the Vault Owner — every Vault created while this amendment is enabled is unconditionally assigned cash-basis accounting. + #### 3.2.7 Invariants **TBD** diff --git a/XLS-0066-lending-protocol/README.md b/XLS-0066-lending-protocol/README.md index 54aa3116b..4380acaff 100644 --- a/XLS-0066-lending-protocol/README.md +++ b/XLS-0066-lending-protocol/README.md @@ -8,7 +8,7 @@ category: Amendment requires: XLS-65, XLS-64 created: 2024-10-18 - updated: 2026-07-16 + updated: 2026-07-21 proposal-from: https://github.com/XRPLF/XRPL-Standards/discussions/190 @@ -1091,7 +1091,7 @@ The account specified in the `Account` field pays the transaction fee. ##### 3.8.5.3 Protocol-Level Failures (`LendingProtocolV1_1`) -Under the `LendingProtocolV1_1` amendment, `Vault.AssetsTotal` and `LoanBroker.DebtTotal` become principal-only. Checks 19, and 20 of [3.8.5.2](#3852-protocol-level-failures) are replaced by the following (all other checks are unchanged): +Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 2` (cash-basis, see [XLS-65 §3.1.2.2](../XLS-0065-single-asset-vault/README.md#3122-leversion-lendingprotocolv1_1)), `Vault.AssetsTotal` and `LoanBroker.DebtTotal` become principal-only. Checks 19, and 20 of [3.8.5.2](#3852-protocol-level-failures) are replaced by the following (all other checks are unchanged); for a legacy (`LEVersion` absent) `Vault`, checks 19 and 20 of [3.8.5.2](#3852-protocol-level-failures) continue to apply unchanged: 19. `LoanBroker.DebtMaximum != 0` and `LoanBroker.DebtMaximum < LoanBroker.DebtTotal + PrincipalRequested` (exceeds maximum debt). (`tecLIMIT_EXCEEDED`) 20. `LoanBroker.CoverAvailable < (LoanBroker.DebtTotal + PrincipalRequested) × LoanBroker.CoverRateMinimum` (insufficient first-loss capital). (`tecINSUFFICIENT_FUNDS`) @@ -1132,7 +1132,7 @@ Under the `LendingProtocolV1_1` amendment, `Vault.AssetsTotal` and `LoanBroker.D ##### 3.8.6.1 State Changes (`LendingProtocolV1_1`) -Under the `LendingProtocolV1_1` amendment, `Vault.AssetsTotal` and `LoanBroker.DebtTotal` become principal-only — neither tracks interest. Steps 6 and 7 of [3.8.6](#386-state-changes) are replaced by the following; all other steps are unchanged: +Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 2` (cash-basis), `Vault.AssetsTotal` and `LoanBroker.DebtTotal` become principal-only — neither tracks interest. Steps 6 and 7 of [3.8.6](#386-state-changes) are replaced by the following; all other steps are unchanged. For a legacy (`LEVersion` absent) `Vault`, steps 6 and 7 of [3.8.6](#386-state-changes) continue to apply unchanged, even while this amendment is enabled: 6. Update `Vault` object: - Decrease `Vault.AssetsAvailable` by `PrincipalRequested`. @@ -1274,7 +1274,7 @@ This transaction uses the standard transaction fee. ##### 3.10.4.3 Protocol-Level Failures (`LendingProtocolV1_1`) -Under the `LendingProtocolV1_1` amendment, check 8 of [3.10.4.2](#31042-protocol-level-failures) is replaced by the following, consistent with the amended impairment amount in [3.10.5.1](#31051-state-changes-lendingprotocolv1_1) (all other checks are unchanged): +Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 2` (cash-basis), check 8 of [3.10.4.2](#31042-protocol-level-failures) is replaced by the following, consistent with the amended impairment amount in [3.10.5.1](#31051-state-changes-lendingprotocolv1_1) (all other checks are unchanged). For a legacy (`LEVersion` absent) `Vault`, check 8 of [3.10.4.2](#31042-protocol-level-failures) continues to apply unchanged: 8. `tfLoanImpair` flag is specified and `Vault.LossUnrealized + Loan.PrincipalOutstanding > Vault.AssetsTotal - Vault.AssetsAvailable` (impairment would exceed vault's unavailable assets). (`tecLIMIT_EXCEEDED`) @@ -1332,7 +1332,7 @@ Under the `LendingProtocolV1_1` amendment, check 8 of [3.10.4.2](#31042-protocol ##### 3.10.5.1 State Changes (`LendingProtocolV1_1`) -Under the `LendingProtocolV1_1` amendment, `Vault.AssetsTotal` and `LoanBroker.DebtTotal` become principal-only. The `DefaultAmount`, `LossUnrealized`, and `LossReversed` computations in items 1–3 of [3.10.5](#3105-state-changes) are replaced by the following, collapsing each to a single, unified principal-only amount (all other steps — including the `MinimumCover`/`DefaultCovered`/`VaultLoss` mechanics, the `CoverAvailable` drawdown, and the `Loan` object updates — are unchanged): +Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 2` (cash-basis), `Vault.AssetsTotal` and `LoanBroker.DebtTotal` become principal-only. The `DefaultAmount`, `LossUnrealized`, and `LossReversed` computations in items 1–3 of [3.10.5](#3105-state-changes) are replaced by the following, collapsing each to a single, unified principal-only amount (all other steps — including the `MinimumCover`/`DefaultCovered`/`VaultLoss` mechanics, the `CoverAvailable` drawdown, and the `Loan` object updates — are unchanged). For a legacy (`LEVersion` absent) `Vault`, items 1–3 of [3.10.5](#3105-state-changes) continue to apply unchanged, even while this amendment is enabled: 1. If the `tfLoanDefault` flag is specified: - Compute `DefaultAmount = Loan.PrincipalOutstanding` (principal only, replacing `Loan.TotalValueOutstanding - Loan.ManagementFeeOutstanding`). This single `DefaultAmount` is used, as before, for the `Vault` (`VaultLoss`/`DefaultCovered`) and `LoanBroker.DebtTotal` updates, and for `Vault.LossUnrealized` if the Loan was impaired. @@ -1348,6 +1348,8 @@ See [3.10.5.2](#31052-worked-example-pre--vs-post-lendingprotocolv1_1-loan-defau This example walks a single Loan through `LoanSet` → `LoanPay` → `LoanManage(tfLoanDefault)`, computing both the current (pre-amendment) and `LendingProtocolV1_1` (post-amendment) trajectories side by side. It uses the same figures as the [`LoanBroker` Accounting](#3110-accounting) and [First-Loss Capital](#3111-first-loss-capital) examples above, for continuity. +"Pre-amendment" here describes a legacy `Vault` (`LEVersion` absent), and "post-amendment (`LendingProtocolV1_1`)" describes a cash-basis `Vault` (`LEVersion == 2`). Both trajectories can coexist side by side once the amendment is enabled, since `LEVersion` — not amendment activation alone — determines which accounting a given `Vault` follows; a legacy `Vault` never transitions to the post-amendment column, even after the amendment activates. + **Initial State (before `LoanSet`)** Vault: `AssetsTotal = 100,000`, `AssetsAvailable = 100,000` @@ -1553,7 +1555,7 @@ These transfers are performed according to the asset type: ##### 3.11.5.1 State Changes (`LendingProtocolV1_1`) -Under the `LendingProtocolV1_1` amendment, `valueChange` is removed entirely from the `LoanBroker` and `Vault` accounting — since neither `LoanBroker.DebtTotal` nor `Vault.AssetsTotal` tracks interest anymore (see [3.8.6.1](#3861-state-changes-lendingprotocolv1_1)), there is nothing left to reconcile for late payments, overpayments, or early full repayments. Steps 6 and 7 of [3.11.5](#3115-state-changes) are replaced by the following (all other steps, including the asset transfers in section 4, are unchanged): +Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 2` (cash-basis), `valueChange` is removed entirely from the `LoanBroker` and `Vault` accounting — since neither `LoanBroker.DebtTotal` nor `Vault.AssetsTotal` tracks interest anymore (see [3.8.6.1](#3861-state-changes-lendingprotocolv1_1)), there is nothing left to reconcile for late payments, overpayments, or early full repayments. Steps 6 and 7 of [3.11.5](#3115-state-changes) are replaced by the following (all other steps, including the asset transfers in section 4, are unchanged). For a legacy (`LEVersion` absent) `Vault`, steps 6 and 7 of [3.11.5](#3115-state-changes) continue to apply unchanged, even while this amendment is enabled: 6. **`LoanBroker` Updates**: - `LoanBroker.DebtTotal` is decreased by `principalPaid` only (`valueChange` does not apply) — the principal portion of the payment, from the same payment breakdown produced by [Appendix A-3](#a-3-loanpay-implementation-reference). @@ -2160,7 +2162,7 @@ The `valueChange` is a critical accounting mechanism that represents the change This `valueChange` is always split between the Vault (as a change in its net interest) and the Loan Broker (as a change in the `managementFee`). -**Under the `LendingProtocolV1_1` amendment**, `valueChange` has no remaining purpose. In the pseudo-code in [A-3.3](#a-33-pseudo-code) below, `valueChange` is never used to derive the `Loan` object's own state — `loan.totalValueOutstanding`, `loan.principalOutstanding`, `loan.managementFeeOutstanding`, and `loan.roundedState`/`loan.periodicPayment` are all computed independently, directly from the amortization/re-amortization formulas (see `compute_late_payment_interest`, `compute_full_payment`, and `try_overpayment`). `valueChange` is *only* ever returned so that [`LoanPay`](#3115-state-changes) can reconcile `Vault.AssetsTotal` and `LoanBroker.DebtTotal` against it. Since the amendment removes that reconciliation entirely (see [3.11.5.1 State Changes (`LendingProtocolV1_1`)](#31151-state-changes-lendingprotocolv1_1)), `valueChange` becomes dead output under the amendment — an implementation only supporting `LendingProtocolV1_1` need not compute it at all. `principalPaid`, `interestPaid`, and `feePaid` are unaffected and continue to be computed and used exactly as described here. +**Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 2` (cash-basis)**, `valueChange` has no remaining purpose. For a legacy (`LEVersion` absent) `Vault`, `valueChange` continues to be computed and used exactly as described above, even while this amendment is enabled. In the pseudo-code in [A-3.3](#a-33-pseudo-code) below, `valueChange` is never used to derive the `Loan` object's own state — `loan.totalValueOutstanding`, `loan.principalOutstanding`, `loan.managementFeeOutstanding`, and `loan.roundedState`/`loan.periodicPayment` are all computed independently, directly from the amortization/re-amortization formulas (see `compute_late_payment_interest`, `compute_full_payment`, and `try_overpayment`). `valueChange` is *only* ever returned so that [`LoanPay`](#3115-state-changes) can reconcile `Vault.AssetsTotal` and `LoanBroker.DebtTotal` against it. Since the amendment removes that reconciliation entirely (see [3.11.5.1 State Changes (`LendingProtocolV1_1`)](#31151-state-changes-lendingprotocolv1_1)), `valueChange` becomes dead output under the amendment — an implementation only supporting `LendingProtocolV1_1` need not compute it at all. `principalPaid`, `interestPaid`, and `feePaid` are unaffected and continue to be computed and used exactly as described here. #### A-3.2.1 Regular Payment From 8edeab3767f9fda881cf6485c0415ae64c4b546c Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Wed, 22 Jul 2026 11:51:00 +0200 Subject: [PATCH 03/32] update the spec to not skip SLEVersion 1 --- XLS-0065-single-asset-vault/README.md | 8 ++++---- XLS-0066-lending-protocol/README.md | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/XLS-0065-single-asset-vault/README.md b/XLS-0065-single-asset-vault/README.md index 66270ee16..891a9a801 100644 --- a/XLS-0065-single-asset-vault/README.md +++ b/XLS-0065-single-asset-vault/README.md @@ -126,9 +126,9 @@ The `Vault` object supports the following flags: For the `Vault` object, `LEVersion` distinguishes the share-valuation and accounting behavior applied to the vault: - **Absent (default value `0`)**: The Vault was created before the `LendingProtocolV1_1` amendment was enabled, or otherwise predates ledger entry versioning. It uses the legacy, accrual-basis accounting described throughout this document and in [XLS-66](../XLS-0066-lending-protocol/README.md) — `AssetsTotal` includes accrued interest. -- **`2`**: The Vault was created while the `LendingProtocolV1_1` amendment was enabled. It exclusively uses the cash-basis accounting introduced by that amendment — `AssetsTotal` is principal-only (see [3.1.7.4](#3174-cash-basis-vs-accrual-basis-accounting-lendingprotocolv1_1) and the `LendingProtocolV1_1` subsections of [XLS-66](../XLS-0066-lending-protocol/README.md)). +- **`1`**: The Vault was created while the `LendingProtocolV1_1` amendment was enabled. It exclusively uses the cash-basis accounting introduced by that amendment — `AssetsTotal` is principal-only (see [3.1.7.4](#3174-cash-basis-vs-accrual-basis-accounting-lendingprotocolv1_1) and the `LendingProtocolV1_1` subsections of [XLS-66](../XLS-0066-lending-protocol/README.md)). -Any Vault created once `LendingProtocolV1_1` is enabled is always assigned `LEVersion = 2` — cash-basis accounting is not optional or user-selectable at creation time. A future revision of the `Vault` ledger entry would increment `LEVersion` to `3`, and so on. +Any Vault created once `LendingProtocolV1_1` is enabled is always assigned `LEVersion = 1` — cash-basis accounting is not optional or user-selectable at creation time. A future revision of the `Vault` ledger entry would increment `LEVersion` to `2`, and so on. #### 3.1.3 Pseudo-Account @@ -332,7 +332,7 @@ Under the `LendingProtocolV1_1` amendment, `Vault.LEVersion` (see [3.1.2.2](#312 - `LEVersion` absent (`0`, legacy/accrual-basis): `AssetsTotal` includes interest upfront, as accrued over the life of connected Loans. This is the pre-amendment behavior and continues unchanged for Vaults created before `LendingProtocolV1_1` was enabled. - `LEVersion = 2` (cash-basis): `AssetsTotal` is principal-only and increases only as interest is actually collected in cash. This is the exclusive behavior for Vaults created once `LendingProtocolV1_1` is enabled. -See the `LendingProtocolV1_1` subsections of [XLS-66 §3.8](../XLS-0066-lending-protocol/README.md#38-transaction-loanset), [§3.10](../XLS-0066-lending-protocol/README.md#310-transaction-loanmanage), and [§3.11](../XLS-0066-lending-protocol/README.md#311-transaction-loanpay) for the precise accounting differences, all of which are gated on `Vault.LEVersion == 2`. +See the `LendingProtocolV1_1` subsections of [XLS-66 §3.8](../XLS-0066-lending-protocol/README.md#38-transaction-loanset), [§3.10](../XLS-0066-lending-protocol/README.md#310-transaction-loanmanage), and [§3.11](../XLS-0066-lending-protocol/README.md#311-transaction-loanpay) for the precise accounting differences, all of which are gated on `Vault.LEVersion == 1`. #### 3.1.8 Frozen Assets @@ -429,7 +429,7 @@ _TBD_ Under the `LendingProtocolV1_1` amendment, step 1 of [3.2.6](#326-state-changes) is amended; all other steps are unchanged: -1. Create a new `Vault` ledger object, setting `Vault.LEVersion = 2` (see [3.1.2.2](#3122-leversion-lendingprotocolv1_1)). `LEVersion` is not a transaction field and is not settable by the Vault Owner — every Vault created while this amendment is enabled is unconditionally assigned cash-basis accounting. +1. Create a new `Vault` ledger object, setting `Vault.LEVersion = 1` (see [3.1.2.2](#3122-leversion-lendingprotocolv1_1)). `LEVersion` is not a transaction field and is not settable by the Vault Owner — every Vault created while this amendment is enabled is unconditionally assigned cash-basis accounting. #### 3.2.7 Invariants diff --git a/XLS-0066-lending-protocol/README.md b/XLS-0066-lending-protocol/README.md index 4380acaff..f14a4f4fc 100644 --- a/XLS-0066-lending-protocol/README.md +++ b/XLS-0066-lending-protocol/README.md @@ -1091,7 +1091,7 @@ The account specified in the `Account` field pays the transaction fee. ##### 3.8.5.3 Protocol-Level Failures (`LendingProtocolV1_1`) -Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 2` (cash-basis, see [XLS-65 §3.1.2.2](../XLS-0065-single-asset-vault/README.md#3122-leversion-lendingprotocolv1_1)), `Vault.AssetsTotal` and `LoanBroker.DebtTotal` become principal-only. Checks 19, and 20 of [3.8.5.2](#3852-protocol-level-failures) are replaced by the following (all other checks are unchanged); for a legacy (`LEVersion` absent) `Vault`, checks 19 and 20 of [3.8.5.2](#3852-protocol-level-failures) continue to apply unchanged: +Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 1` (cash-basis, see [XLS-65 §3.1.2.2](../XLS-0065-single-asset-vault/README.md#3122-leversion-lendingprotocolv1_1)), `Vault.AssetsTotal` and `LoanBroker.DebtTotal` become principal-only. Checks 19, and 20 of [3.8.5.2](#3852-protocol-level-failures) are replaced by the following (all other checks are unchanged); for a legacy (`LEVersion` absent) `Vault`, checks 19 and 20 of [3.8.5.2](#3852-protocol-level-failures) continue to apply unchanged: 19. `LoanBroker.DebtMaximum != 0` and `LoanBroker.DebtMaximum < LoanBroker.DebtTotal + PrincipalRequested` (exceeds maximum debt). (`tecLIMIT_EXCEEDED`) 20. `LoanBroker.CoverAvailable < (LoanBroker.DebtTotal + PrincipalRequested) × LoanBroker.CoverRateMinimum` (insufficient first-loss capital). (`tecINSUFFICIENT_FUNDS`) @@ -1132,7 +1132,7 @@ Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 2` ( ##### 3.8.6.1 State Changes (`LendingProtocolV1_1`) -Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 2` (cash-basis), `Vault.AssetsTotal` and `LoanBroker.DebtTotal` become principal-only — neither tracks interest. Steps 6 and 7 of [3.8.6](#386-state-changes) are replaced by the following; all other steps are unchanged. For a legacy (`LEVersion` absent) `Vault`, steps 6 and 7 of [3.8.6](#386-state-changes) continue to apply unchanged, even while this amendment is enabled: +Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 1` (cash-basis), `Vault.AssetsTotal` and `LoanBroker.DebtTotal` become principal-only — neither tracks interest. Steps 6 and 7 of [3.8.6](#386-state-changes) are replaced by the following; all other steps are unchanged. For a legacy (`LEVersion` absent) `Vault`, steps 6 and 7 of [3.8.6](#386-state-changes) continue to apply unchanged, even while this amendment is enabled: 6. Update `Vault` object: - Decrease `Vault.AssetsAvailable` by `PrincipalRequested`. @@ -1274,7 +1274,7 @@ This transaction uses the standard transaction fee. ##### 3.10.4.3 Protocol-Level Failures (`LendingProtocolV1_1`) -Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 2` (cash-basis), check 8 of [3.10.4.2](#31042-protocol-level-failures) is replaced by the following, consistent with the amended impairment amount in [3.10.5.1](#31051-state-changes-lendingprotocolv1_1) (all other checks are unchanged). For a legacy (`LEVersion` absent) `Vault`, check 8 of [3.10.4.2](#31042-protocol-level-failures) continues to apply unchanged: +Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 1` (cash-basis), check 8 of [3.10.4.2](#31042-protocol-level-failures) is replaced by the following, consistent with the amended impairment amount in [3.10.5.1](#31051-state-changes-lendingprotocolv1_1) (all other checks are unchanged). For a legacy (`LEVersion` absent) `Vault`, check 8 of [3.10.4.2](#31042-protocol-level-failures) continues to apply unchanged: 8. `tfLoanImpair` flag is specified and `Vault.LossUnrealized + Loan.PrincipalOutstanding > Vault.AssetsTotal - Vault.AssetsAvailable` (impairment would exceed vault's unavailable assets). (`tecLIMIT_EXCEEDED`) @@ -1332,7 +1332,7 @@ Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 2` ( ##### 3.10.5.1 State Changes (`LendingProtocolV1_1`) -Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 2` (cash-basis), `Vault.AssetsTotal` and `LoanBroker.DebtTotal` become principal-only. The `DefaultAmount`, `LossUnrealized`, and `LossReversed` computations in items 1–3 of [3.10.5](#3105-state-changes) are replaced by the following, collapsing each to a single, unified principal-only amount (all other steps — including the `MinimumCover`/`DefaultCovered`/`VaultLoss` mechanics, the `CoverAvailable` drawdown, and the `Loan` object updates — are unchanged). For a legacy (`LEVersion` absent) `Vault`, items 1–3 of [3.10.5](#3105-state-changes) continue to apply unchanged, even while this amendment is enabled: +Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 1` (cash-basis), `Vault.AssetsTotal` and `LoanBroker.DebtTotal` become principal-only. The `DefaultAmount`, `LossUnrealized`, and `LossReversed` computations in items 1–3 of [3.10.5](#3105-state-changes) are replaced by the following, collapsing each to a single, unified principal-only amount (all other steps — including the `MinimumCover`/`DefaultCovered`/`VaultLoss` mechanics, the `CoverAvailable` drawdown, and the `Loan` object updates — are unchanged). For a legacy (`LEVersion` absent) `Vault`, items 1–3 of [3.10.5](#3105-state-changes) continue to apply unchanged, even while this amendment is enabled: 1. If the `tfLoanDefault` flag is specified: - Compute `DefaultAmount = Loan.PrincipalOutstanding` (principal only, replacing `Loan.TotalValueOutstanding - Loan.ManagementFeeOutstanding`). This single `DefaultAmount` is used, as before, for the `Vault` (`VaultLoss`/`DefaultCovered`) and `LoanBroker.DebtTotal` updates, and for `Vault.LossUnrealized` if the Loan was impaired. @@ -1348,7 +1348,7 @@ See [3.10.5.2](#31052-worked-example-pre--vs-post-lendingprotocolv1_1-loan-defau This example walks a single Loan through `LoanSet` → `LoanPay` → `LoanManage(tfLoanDefault)`, computing both the current (pre-amendment) and `LendingProtocolV1_1` (post-amendment) trajectories side by side. It uses the same figures as the [`LoanBroker` Accounting](#3110-accounting) and [First-Loss Capital](#3111-first-loss-capital) examples above, for continuity. -"Pre-amendment" here describes a legacy `Vault` (`LEVersion` absent), and "post-amendment (`LendingProtocolV1_1`)" describes a cash-basis `Vault` (`LEVersion == 2`). Both trajectories can coexist side by side once the amendment is enabled, since `LEVersion` — not amendment activation alone — determines which accounting a given `Vault` follows; a legacy `Vault` never transitions to the post-amendment column, even after the amendment activates. +"Pre-amendment" here describes a legacy `Vault` (`LEVersion` absent), and "post-amendment (`LendingProtocolV1_1`)" describes a cash-basis `Vault` (`LEVersion == 1`). Both trajectories can coexist side by side once the amendment is enabled, since `LEVersion` — not amendment activation alone — determines which accounting a given `Vault` follows; a legacy `Vault` never transitions to the post-amendment column, even after the amendment activates. **Initial State (before `LoanSet`)** @@ -1555,7 +1555,7 @@ These transfers are performed according to the asset type: ##### 3.11.5.1 State Changes (`LendingProtocolV1_1`) -Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 2` (cash-basis), `valueChange` is removed entirely from the `LoanBroker` and `Vault` accounting — since neither `LoanBroker.DebtTotal` nor `Vault.AssetsTotal` tracks interest anymore (see [3.8.6.1](#3861-state-changes-lendingprotocolv1_1)), there is nothing left to reconcile for late payments, overpayments, or early full repayments. Steps 6 and 7 of [3.11.5](#3115-state-changes) are replaced by the following (all other steps, including the asset transfers in section 4, are unchanged). For a legacy (`LEVersion` absent) `Vault`, steps 6 and 7 of [3.11.5](#3115-state-changes) continue to apply unchanged, even while this amendment is enabled: +Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 1` (cash-basis), `valueChange` is removed entirely from the `LoanBroker` and `Vault` accounting — since neither `LoanBroker.DebtTotal` nor `Vault.AssetsTotal` tracks interest anymore (see [3.8.6.1](#3861-state-changes-lendingprotocolv1_1)), there is nothing left to reconcile for late payments, overpayments, or early full repayments. Steps 6 and 7 of [3.11.5](#3115-state-changes) are replaced by the following (all other steps, including the asset transfers in section 4, are unchanged). For a legacy (`LEVersion` absent) `Vault`, steps 6 and 7 of [3.11.5](#3115-state-changes) continue to apply unchanged, even while this amendment is enabled: 6. **`LoanBroker` Updates**: - `LoanBroker.DebtTotal` is decreased by `principalPaid` only (`valueChange` does not apply) — the principal portion of the payment, from the same payment breakdown produced by [Appendix A-3](#a-3-loanpay-implementation-reference). @@ -2162,7 +2162,7 @@ The `valueChange` is a critical accounting mechanism that represents the change This `valueChange` is always split between the Vault (as a change in its net interest) and the Loan Broker (as a change in the `managementFee`). -**Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 2` (cash-basis)**, `valueChange` has no remaining purpose. For a legacy (`LEVersion` absent) `Vault`, `valueChange` continues to be computed and used exactly as described above, even while this amendment is enabled. In the pseudo-code in [A-3.3](#a-33-pseudo-code) below, `valueChange` is never used to derive the `Loan` object's own state — `loan.totalValueOutstanding`, `loan.principalOutstanding`, `loan.managementFeeOutstanding`, and `loan.roundedState`/`loan.periodicPayment` are all computed independently, directly from the amortization/re-amortization formulas (see `compute_late_payment_interest`, `compute_full_payment`, and `try_overpayment`). `valueChange` is *only* ever returned so that [`LoanPay`](#3115-state-changes) can reconcile `Vault.AssetsTotal` and `LoanBroker.DebtTotal` against it. Since the amendment removes that reconciliation entirely (see [3.11.5.1 State Changes (`LendingProtocolV1_1`)](#31151-state-changes-lendingprotocolv1_1)), `valueChange` becomes dead output under the amendment — an implementation only supporting `LendingProtocolV1_1` need not compute it at all. `principalPaid`, `interestPaid`, and `feePaid` are unaffected and continue to be computed and used exactly as described here. +**Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 1` (cash-basis)**, `valueChange` has no remaining purpose. For a legacy (`LEVersion` absent) `Vault`, `valueChange` continues to be computed and used exactly as described above, even while this amendment is enabled. In the pseudo-code in [A-3.3](#a-33-pseudo-code) below, `valueChange` is never used to derive the `Loan` object's own state — `loan.totalValueOutstanding`, `loan.principalOutstanding`, `loan.managementFeeOutstanding`, and `loan.roundedState`/`loan.periodicPayment` are all computed independently, directly from the amortization/re-amortization formulas (see `compute_late_payment_interest`, `compute_full_payment`, and `try_overpayment`). `valueChange` is *only* ever returned so that [`LoanPay`](#3115-state-changes) can reconcile `Vault.AssetsTotal` and `LoanBroker.DebtTotal` against it. Since the amendment removes that reconciliation entirely (see [3.11.5.1 State Changes (`LendingProtocolV1_1`)](#31151-state-changes-lendingprotocolv1_1)), `valueChange` becomes dead output under the amendment — an implementation only supporting `LendingProtocolV1_1` need not compute it at all. `principalPaid`, `interestPaid`, and `feePaid` are unaffected and continue to be computed and used exactly as described here. #### A-3.2.1 Regular Payment From 50abc3b5add98be16a641f4b0e9e53b752f2165e Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Fri, 4 Sep 2026 15:07:06 +0200 Subject: [PATCH 04/32] docs: align cash-basis Vault/LoanBroker text with develop Add XLS-65.1/66.1 patch folders so this PR merges under LendingProtocolV1_1. --- XLS-0065-single-asset-vault/65.1/README.md | 20 +++++++++++++ XLS-0065-single-asset-vault/65.2/README.md | 19 ++++++++++++ XLS-0065-single-asset-vault/README.md | 13 ++++++++- XLS-0066-lending-protocol/66.1/README.md | 20 +++++++++++++ XLS-0066-lending-protocol/66.2/README.md | 19 ++++++++++++ XLS-0066-lending-protocol/README.md | 34 ++++++++++++++++------ 6 files changed, 115 insertions(+), 10 deletions(-) create mode 100644 XLS-0065-single-asset-vault/65.1/README.md create mode 100644 XLS-0065-single-asset-vault/65.2/README.md create mode 100644 XLS-0066-lending-protocol/66.1/README.md create mode 100644 XLS-0066-lending-protocol/66.2/README.md diff --git a/XLS-0065-single-asset-vault/65.1/README.md b/XLS-0065-single-asset-vault/65.1/README.md new file mode 100644 index 000000000..6c3c2897b --- /dev/null +++ b/XLS-0065-single-asset-vault/65.1/README.md @@ -0,0 +1,20 @@ +# XLS-65.1 — `LendingProtocolV1_1` + +Not yet live. When vault spec PRs merge, they land in **this** patch or in [XLS-65.2](../65.2/README.md) (`fixCleanup3_4_0`), not as a new XLS-65.N per PR. + +## Why + +`LendingProtocolV1_1` changes Vault create/delete and ledger-entry shape. Until the amendment is enabled on mainnet, the top-level README keeps pre-amendment behavior as the default and records the amended behavior in labeled subsections. This folder is the reviewable record of that patch. + +## What changed + +- New Vaults created while the amendment is enabled get `LEVersion = 1` (cash-basis `AssetsTotal`). +- Optional closed-ended lifecycle: `VaultKind`, `SubscriptionDate`, `RedemptionDate`, with `temMALFORMED` / `tecEXPIRED` checks matching `VaultCreate`. +- Those fields (and `LEVersion`) are immutable once set. +- `VaultDelete` may carry optional `MemoData` only when this amendment is enabled (`temDISABLED` otherwise). + +## Merge bucket + +PRs that fold into XLS-65.1: #582 (LEVersion), #549 (VaultCreate kind/dates), #470 (`MemoData`), #555 (immutability in §3.1.10). + +The consolidated spec is the top-level `README.md`. Commit SHA for the changelog is recorded when this patch is merged. diff --git a/XLS-0065-single-asset-vault/65.2/README.md b/XLS-0065-single-asset-vault/65.2/README.md new file mode 100644 index 000000000..410f186e8 --- /dev/null +++ b/XLS-0065-single-asset-vault/65.2/README.md @@ -0,0 +1,19 @@ +# XLS-65.2 — `fixCleanup3_4_0` + +Not yet live. When vault spec PRs merge, they land in **this** patch or in [XLS-65.1](../65.1/README.md) (`LendingProtocolV1_1`), not as a new XLS-65.N per PR. + +## Why + +`fixCleanup3_4_0` tightens VaultClawback and vault ledger invariants without waiting on `LendingProtocolV1_1`. + +## What changed + +- `VaultClawback`: reject a pseudo-account `Holder` (`tecPSEUDO_ACCOUNT`); keep ambiguous Amount and wrong-asset failures; distinguish zero-share vs dust `tecPRECISION_LOSS`; overflow → `tecPATH_DRY`. +- Invariants: `LossUnrealized` may be one asset-scale ULP over unavailable assets; `LossUnrealized >= 0`; `AssetsTotal` may exceed `AssetsMaximum` when the excess is interest, except VaultSet changing the cap. +- Wording-only VaultSet DomainID-zero clarification (#448) also folds here so it does not get its own patch number. + +## Merge bucket + +PRs that fold into XLS-65.2: #554 (VaultClawback), #555 (invariants), #448 (DomainID zero). + +The consolidated spec is the top-level `README.md`. Commit SHA for the changelog is recorded when this patch is merged. diff --git a/XLS-0065-single-asset-vault/README.md b/XLS-0065-single-asset-vault/README.md index 78cfd9fb0..6684a63f0 100644 --- a/XLS-0065-single-asset-vault/README.md +++ b/XLS-0065-single-asset-vault/README.md @@ -69,6 +69,10 @@ Shares represent the ownership of a portion of the vault's assets. On-chain shar A protocol connecting to a Vault must track its debt. Furthermore, the updates to the Vault state when funds are removed or added back must be handled in the transactors of the protocol. For an example, please refer to the [Lending Protocol](../XLS-0066-lending-protocol/README.md) specification. +## Amendments + +- `LendingProtocolV1_1` (not yet live, [XLS-65.1](./65.1/README.md)): Introduces `LEVersion = 1` for cash-basis Vault accounting. See [§3.1.2.2](#3122-leversion-lendingprotocolv1_1) and [§3.2.6.1](#3261-state-changes-lendingprotocolv1_1). This PR merges into XLS-65.1, not a new patch number. + ## 3. Specification ### 3.1 Ledger Entry: `Vault` @@ -330,7 +334,7 @@ The exchange rate algorithms in [3.1.7.2](#3172-exchange-rate-algorithms) above Under the `LendingProtocolV1_1` amendment, `Vault.LEVersion` (see [3.1.2.2](#3122-leversion-lendingprotocolv1_1)) determines which of the two accounting models a Vault's `AssetsTotal` follows: - `LEVersion` absent (`0`, legacy/accrual-basis): `AssetsTotal` includes interest upfront, as accrued over the life of connected Loans. This is the pre-amendment behavior and continues unchanged for Vaults created before `LendingProtocolV1_1` was enabled. -- `LEVersion = 2` (cash-basis): `AssetsTotal` is principal-only and increases only as interest is actually collected in cash. This is the exclusive behavior for Vaults created once `LendingProtocolV1_1` is enabled. +- `LEVersion = 1` (cash-basis): `AssetsTotal` is principal-only and increases only as interest is actually collected in cash. This is the exclusive behavior for Vaults created once `LendingProtocolV1_1` is enabled. See the `LendingProtocolV1_1` subsections of [XLS-66 §3.8](../XLS-0066-lending-protocol/README.md#38-transaction-loanset), [§3.10](../XLS-0066-lending-protocol/README.md#310-transaction-loanmanage), and [§3.11](../XLS-0066-lending-protocol/README.md#311-transaction-loanpay) for the precise accounting differences, all of which are gated on `Vault.LEVersion == 1`. @@ -1163,3 +1167,10 @@ XRP Ledger is an account based blockchain. That means that assets (XRP, IOU and ### A.5 Do `VaultDeposit` or `VaultWithdraw` transactions charge transfer fees? No, neither of the transactions charge transfer fees when depositing or withdrawing assets to and from the Vault. + +## Appendix C: Changelog + +Spec PRs merge into one of these two patches (`LendingProtocolV1_1` or `fixCleanup3_4_0`), not as their own XLS-65.N: + +- XLS-65.1: `LendingProtocolV1_1` (not yet live) — `LEVersion`, closed-ended vault lifecycle, and `VaultDelete.MemoData`. See [65.1](./65.1/README.md). Commit SHA recorded when this patch is merged. +- XLS-65.2: `fixCleanup3_4_0` (not yet live) — VaultClawback checks, vault invariants, and VaultSet DomainID-zero wording. See [65.2](./65.2/README.md). Commit SHA recorded when this patch is merged. diff --git a/XLS-0066-lending-protocol/66.1/README.md b/XLS-0066-lending-protocol/66.1/README.md new file mode 100644 index 000000000..5d2c52a9b --- /dev/null +++ b/XLS-0066-lending-protocol/66.1/README.md @@ -0,0 +1,20 @@ +# XLS-66.1 — `LendingProtocolV1_1` + +Not yet live. When lending spec PRs merge, they land in **this** patch or in [XLS-66.2](../66.2/README.md) (`fixCleanup3_4_0`), not as a new XLS-66.N per PR. + +## Why + +`LendingProtocolV1_1` changes LoanBroker attachment and principal-only vault/broker totals. Pre-amendment text stays the production default until the amendment is live. + +## What changed + +- For `LEVersion = 1` Vaults, `Vault.AssetsTotal` and `LoanBroker.DebtTotal` track principal only. +- Creating a LoanBroker while the amendment is enabled requires a closed-ended Vault (`tecNO_PERMISSION` otherwise). +- First-Loss Capital still uses `CoverRateLiquidation` (two-rate formula). Field removal does not ship under this patch (#494 alignment). +- Optional `VaultID` on `LoanBrokerSet` modify (#497) is intended to merge here if it ships; amendment vs `tem`/`tec` is still open. + +## Merge bucket + +PRs that fold into XLS-66.1: #582 (cash-basis + closed-ended broker), #494 (keep CoverRateLiquidation), #497 (optional VaultID, pending design). + +The consolidated spec is the top-level `README.md`. Commit SHA for the changelog is recorded when this patch is merged. diff --git a/XLS-0066-lending-protocol/66.2/README.md b/XLS-0066-lending-protocol/66.2/README.md new file mode 100644 index 000000000..919f56533 --- /dev/null +++ b/XLS-0066-lending-protocol/66.2/README.md @@ -0,0 +1,19 @@ +# XLS-66.2 — `fixCleanup3_4_0` + +Not yet live. When lending spec PRs merge, they land in **this** patch or in [XLS-66.1](../66.1/README.md) (`LendingProtocolV1_1`), not as a new XLS-66.N per PR. + +## Why + +`fixCleanup3_4_0` stops early impairment and due-date rewrites on `LoanManage`. That is independent of cash-basis accounting. + +## What changed + +- Impair only when the payment is already overdue (`tecTOO_SOON` otherwise). +- With the amendment enabled, impair/unimpair do not rewrite `NextPaymentDueDate`. +- Without the amendment, the previous due-date moves remain the fallback. + +## Merge bucket + +PRs that fold into XLS-66.2: #496 (impairment). + +The consolidated spec is the top-level `README.md`. Commit SHA for the changelog is recorded when this patch is merged. diff --git a/XLS-0066-lending-protocol/README.md b/XLS-0066-lending-protocol/README.md index f14a4f4fc..71beb9326 100644 --- a/XLS-0066-lending-protocol/README.md +++ b/XLS-0066-lending-protocol/README.md @@ -116,6 +116,10 @@ The lending protocol charges a number of fees that the Loan Broker can configure ![Architecture Graph](./architecture.svg) +## Amendments + +- `LendingProtocolV1_1` (not yet live, [XLS-66.1](./66.1/README.md)): Makes `Vault.AssetsTotal` and `LoanBroker.DebtTotal` principal-only for Vaults with `LEVersion = 1` and requires newly attached lending Vaults to be closed-ended. See [§3.3.3.2](#3332-protocol-level-failures), [§3.8.6.1](#3861-state-changes-lendingprotocolv1_1), [§3.10.5.1](#31051-state-changes-lendingprotocolv1_1), and [§3.11.5.1](#31151-state-changes-lendingprotocolv1_1). This PR merges into XLS-66.1, not a new patch number. + ## 3. Specification ### 3.1. Ledger Entry: `LoanBroker` @@ -587,20 +591,21 @@ This transaction uses the standard transaction fee. 1. `Vault` object with the specified `VaultID` does not exist on the ledger. (`tecNO_ENTRY`) 2. The submitter `AccountRoot.Account != Vault(VaultID).Owner`. (`tecNO_PERMISSION`) -3. Cannot add asset holding for the `Vault.Asset` (e.g., MPToken or TrustLine issues). (`tecNO_PERMISSION`) -4. The Vault _pseudo-account_ is frozen for the `Vault.Asset`. (`tecFROZEN` for IOUs, `tecLOCKED` for MPTs) -5. The submitter does not have sufficient reserve for the `LoanBroker` object and _pseudo-account_ (requires 2 owner reserves). (`tecINSUFFICIENT_RESERVE`) +3. When `LendingProtocolV1_1` is enabled, the Vault must be closed-ended (`Vault.VaultKind == ClosedEnded`, or equivalent: `VaultKind` present and equal to 1). Open-ended vaults (kind absent or 0) fail with `tecNO_PERMISSION`. (`tecNO_PERMISSION`) +4. Cannot add asset holding for the `Vault.Asset` (e.g., MPToken or TrustLine issues). (`tecNO_PERMISSION`) +5. The Vault _pseudo-account_ is frozen for the `Vault.Asset`. (`tecFROZEN` for IOUs, `tecLOCKED` for MPTs) +6. The submitter does not have sufficient reserve for the `LoanBroker` object and _pseudo-account_ (requires 2 owner reserves). (`tecINSUFFICIENT_RESERVE`) **If `LoanBrokerID` is specified (modifying existing):** -6. `LoanBroker` object with the specified `LoanBrokerID` does not exist on the ledger. (`tecNO_ENTRY`) -7. The submitter `AccountRoot.Account != LoanBroker(LoanBrokerID).Owner`. (`tecNO_PERMISSION`) -8. The transaction `VaultID` does not match `LoanBroker(LoanBrokerID).VaultID`. (`tecNO_PERMISSION`) -9. `DebtMaximum` is being reduced to a non-zero value below the current `DebtTotal`. (`tecLIMIT_EXCEEDED`) +7. `LoanBroker` object with the specified `LoanBrokerID` does not exist on the ledger. (`tecNO_ENTRY`) +8. The submitter `AccountRoot.Account != LoanBroker(LoanBrokerID).Owner`. (`tecNO_PERMISSION`) +9. The transaction `VaultID` does not match `LoanBroker(LoanBrokerID).VaultID`. (`tecNO_PERMISSION`) +10. `DebtMaximum` is being reduced to a non-zero value below the current `DebtTotal`. (`tecLIMIT_EXCEEDED`) **Precision Validation:** -10. Any value field (e.g., `DebtMaximum`) cannot be represented in the `Vault.Asset` type without precision loss (relevant for XRP and MPT). (`tecPRECISION_LOSS`) +11. Any value field (e.g., `DebtMaximum`) cannot be represented in the `Vault.Asset` type without precision loss (relevant for XRP and MPT). (`tecPRECISION_LOSS`) #### 3.3.4 State Changes @@ -1091,7 +1096,11 @@ The account specified in the `Account` field pays the transaction fee. ##### 3.8.5.3 Protocol-Level Failures (`LendingProtocolV1_1`) -Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 1` (cash-basis, see [XLS-65 §3.1.2.2](../XLS-0065-single-asset-vault/README.md#3122-leversion-lendingprotocolv1_1)), `Vault.AssetsTotal` and `LoanBroker.DebtTotal` become principal-only. Checks 19, and 20 of [3.8.5.2](#3852-protocol-level-failures) are replaced by the following (all other checks are unchanged); for a legacy (`LEVersion` absent) `Vault`, checks 19 and 20 of [3.8.5.2](#3852-protocol-level-failures) continue to apply unchanged: +Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 1` (cash-basis, see [XLS-65 §3.1.2.2](../XLS-0065-single-asset-vault/README.md#3122-leversion-lendingprotocolv1_1)), `Vault.AssetsTotal` and `LoanBroker.DebtTotal` become principal-only. Checks 14, 19, and 20 of [3.8.5.2](#3852-protocol-level-failures) are replaced by the following (all other checks are unchanged); for a legacy (`LEVersion` absent) `Vault`, checks 14, 19, and 20 of [3.8.5.2](#3852-protocol-level-failures) continue to apply unchanged: + +14. `Vault.AssetsMaximum != 0` and `Vault.AssetsTotal > Vault.AssetsMaximum` (expected interest would exceed vault assets cap). (`tecLIMIT_EXCEEDED`) + +Since `LoanBroker.DebtTotal` likewise excludes interest: 19. `LoanBroker.DebtMaximum != 0` and `LoanBroker.DebtMaximum < LoanBroker.DebtTotal + PrincipalRequested` (exceeds maximum debt). (`tecLIMIT_EXCEEDED`) 20. `LoanBroker.CoverAvailable < (LoanBroker.DebtTotal + PrincipalRequested) × LoanBroker.CoverRateMinimum` (insufficient first-loss capital). (`tecINSUFFICIENT_FUNDS`) @@ -2787,3 +2796,10 @@ function make_payment(amount, currentTime) -> (principalPaid, interestPaid, valu totalFeePaid # The total fee ) ``` + +## Appendix C: Changelog + +Spec PRs merge into one of these two patches (`LendingProtocolV1_1` or `fixCleanup3_4_0`), not as their own XLS-66.N: + +- XLS-66.1: `LendingProtocolV1_1` (not yet live) — principal-only `AssetsTotal`/`DebtTotal`, closed-ended LoanBroker attachment, and related LoanBrokerSet field rules. See [66.1](./66.1/README.md). Commit SHA recorded when this patch is merged. +- XLS-66.2: `fixCleanup3_4_0` (not yet live) — late-only impairment and no `NextPaymentDueDate` rewrite. See [66.2](./66.2/README.md). Commit SHA recorded when this patch is merged. From bf4ee3e945f332ee4bbb325e403d8ff7cb069d18 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Fri, 4 Sep 2026 15:27:00 +0200 Subject: [PATCH 05/32] docs: restate XLS-65.1 and XLS-66.1 patches in XLS template format --- XLS-0065-single-asset-vault/65.1/README.md | 89 ++++++++++++++--- XLS-0065-single-asset-vault/65.2/README.md | 19 ---- XLS-0065-single-asset-vault/README.md | 7 +- XLS-0066-lending-protocol/66.1/README.md | 109 ++++++++++++++++++--- XLS-0066-lending-protocol/66.2/README.md | 19 ---- XLS-0066-lending-protocol/README.md | 7 +- 6 files changed, 178 insertions(+), 72 deletions(-) delete mode 100644 XLS-0065-single-asset-vault/65.2/README.md delete mode 100644 XLS-0066-lending-protocol/66.2/README.md diff --git a/XLS-0065-single-asset-vault/65.1/README.md b/XLS-0065-single-asset-vault/65.1/README.md index 6c3c2897b..cd2d06cd6 100644 --- a/XLS-0065-single-asset-vault/65.1/README.md +++ b/XLS-0065-single-asset-vault/65.1/README.md @@ -1,20 +1,85 @@ -# XLS-65.1 — `LendingProtocolV1_1` +
+  xls: 65.1
+  title: Vault Cash-Basis Accounting
+  description: Adds the Vault LEVersion field, which selects cash-basis accounting for the assets of a Vault
+  author: Vytautas Vito Tumas , Aanchal Malhotra 
+  proposal-from: https://github.com/XRPLF/XRPL-Standards/discussions/192
+  status: Draft
+  category: Amendment
+  requires: [XLS-65](../README.md)
+  created: 2026-09-04
+  updated: 2026-09-04
+
-Not yet live. When vault spec PRs merge, they land in **this** patch or in [XLS-65.2](../65.2/README.md) (`fixCleanup3_4_0`), not as a new XLS-65.N per PR. +# Vault Cash-Basis Accounting -## Why +## 1. Abstract -`LendingProtocolV1_1` changes Vault create/delete and ledger-entry shape. Until the amendment is enabled on mainnet, the top-level README keeps pre-amendment behavior as the default and records the amended behavior in labeled subsections. This folder is the reviewable record of that patch. +This patch of [XLS-65](../README.md) records the changes the `LendingProtocolV1_1` amendment makes to the `Vault` ledger entry. The amendment adds a `LEVersion` field that selects the accounting model a Vault applies to `AssetsTotal`. Vaults created while the amendment is enabled use cash-basis accounting, in which `AssetsTotal` tracks principal only and grows only as interest is collected in cash. Vaults created before the amendment keep accrual-basis accounting, in which `AssetsTotal` includes interest as it accrues. -## What changed +The consolidated specification is the top-level [README.md](../README.md). -- New Vaults created while the amendment is enabled get `LEVersion = 1` (cash-basis `AssetsTotal`). -- Optional closed-ended lifecycle: `VaultKind`, `SubscriptionDate`, `RedemptionDate`, with `temMALFORMED` / `tecEXPIRED` checks matching `VaultCreate`. -- Those fields (and `LEVersion`) are immutable once set. -- `VaultDelete` may carry optional `MemoData` only when this amendment is enabled (`temDISABLED` otherwise). +## 2. Motivation -## Merge bucket +Accrual-basis accounting credits a Vault with interest that a Borrower has not paid yet, so the share exchange rate reflects income the Vault has not received. A depositor can redeem shares at a rate inflated by interest that later defaults, and the loss falls on the depositors who remain. Cash-basis accounting removes that mismatch by recognising interest only when it is paid. -PRs that fold into XLS-65.1: #582 (LEVersion), #549 (VaultCreate kind/dates), #470 (`MemoData`), #555 (immutability in §3.1.10). +Existing Vaults cannot switch accounting models without changing the value of shares already issued, so the model has to be recorded per Vault rather than derived from the amendment state at the time of the transaction. -The consolidated spec is the top-level `README.md`. Commit SHA for the changelog is recorded when this patch is merged. +## 3. Specification + +### 3.1 Ledger Entry: `Vault` + +#### 3.1.1 Fields + +| Field Name | Constant | Required | Internal Type | Default Value | Description | +| ----------- | :------: | :------: | :-----------: | :-----------: | ------------------------------------------------------------------------ | +| `LEVersion` | Yes | No | `UINT8` | absent | The accounting model the Vault applies to `AssetsTotal`. Immutable once set. | + +#### 3.1.2 `LEVersion` + +`LEVersion` selects the accounting model of the Vault: + +- `LEVersion` absent (accrual basis): `AssetsTotal` includes the interest accrued over the life of the connected Loans. This is the pre-amendment behaviour and continues to apply to every Vault created before the amendment was enabled. +- `LEVersion = 1` (cash basis): `AssetsTotal` is principal-only and increases only as interest is collected in cash. + +The field is immutable. A Vault created while the amendment is enabled is always cash-basis, and a Vault created before it is always accrual-basis; neither can be converted to the other. + +#### 3.1.3 Invariants + +- `Vault.LEVersion` is immutable once set. +- `Vault.LEVersion` is either absent or `1`. + +#### 3.1.4 Example JSON + +```json +{ + "LedgerEntryType": "Vault", + "LEVersion": 1, + "Asset": { "currency": "USD", "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn" }, + "AssetsTotal": "1000", + "AssetsAvailable": "1000", + "SharesTotal": "1000" +} +``` + +### 3.2 Transaction: `VaultCreate` + +#### 3.2.1 Failure Conditions + +No failure conditions are added or removed. `LEVersion` is not a transaction field and cannot be supplied by the submitter. + +#### 3.2.2 State Changes + +When the amendment is enabled, the created `Vault` ledger entry has `LEVersion = 1`. When it is not enabled, the field is not written and the Vault is accrual-basis. + +## 4. Rationale + +The alternative to a per-entry version field is to derive the accounting model from the amendment state at the time each Loan transaction executes. That was rejected because it would silently change the share exchange rate of every existing Vault on the ledger at the moment the amendment activates. + +`LEVersion` is a version number rather than a boolean flag so that a later accounting change can be expressed as a further version without another field. + +## 5. Security Considerations + +Cash-basis accounting reduces, but does not remove, the exposure of a depositor to an unpaid Loan: the Vault still carries the principal at face value until the Loan defaults or is impaired. `LossUnrealized` remains the mechanism for reporting an expected shortfall. + +Because `LEVersion` is immutable, an implementation must read it from the `Vault` ledger entry rather than from the amendment state when computing the interest accounting of a Loan. Reading the amendment state instead would apply cash-basis rules to accrual-basis Vaults and misprice their shares. diff --git a/XLS-0065-single-asset-vault/65.2/README.md b/XLS-0065-single-asset-vault/65.2/README.md deleted file mode 100644 index 410f186e8..000000000 --- a/XLS-0065-single-asset-vault/65.2/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# XLS-65.2 — `fixCleanup3_4_0` - -Not yet live. When vault spec PRs merge, they land in **this** patch or in [XLS-65.1](../65.1/README.md) (`LendingProtocolV1_1`), not as a new XLS-65.N per PR. - -## Why - -`fixCleanup3_4_0` tightens VaultClawback and vault ledger invariants without waiting on `LendingProtocolV1_1`. - -## What changed - -- `VaultClawback`: reject a pseudo-account `Holder` (`tecPSEUDO_ACCOUNT`); keep ambiguous Amount and wrong-asset failures; distinguish zero-share vs dust `tecPRECISION_LOSS`; overflow → `tecPATH_DRY`. -- Invariants: `LossUnrealized` may be one asset-scale ULP over unavailable assets; `LossUnrealized >= 0`; `AssetsTotal` may exceed `AssetsMaximum` when the excess is interest, except VaultSet changing the cap. -- Wording-only VaultSet DomainID-zero clarification (#448) also folds here so it does not get its own patch number. - -## Merge bucket - -PRs that fold into XLS-65.2: #554 (VaultClawback), #555 (invariants), #448 (DomainID zero). - -The consolidated spec is the top-level `README.md`. Commit SHA for the changelog is recorded when this patch is merged. diff --git a/XLS-0065-single-asset-vault/README.md b/XLS-0065-single-asset-vault/README.md index 6684a63f0..cb2db4c4e 100644 --- a/XLS-0065-single-asset-vault/README.md +++ b/XLS-0065-single-asset-vault/README.md @@ -71,7 +71,7 @@ A protocol connecting to a Vault must track its debt. Furthermore, the updates t ## Amendments -- `LendingProtocolV1_1` (not yet live, [XLS-65.1](./65.1/README.md)): Introduces `LEVersion = 1` for cash-basis Vault accounting. See [§3.1.2.2](#3122-leversion-lendingprotocolv1_1) and [§3.2.6.1](#3261-state-changes-lendingprotocolv1_1). This PR merges into XLS-65.1, not a new patch number. +- `LendingProtocolV1_1` (not yet live, [XLS-65.1](./65.1/README.md)): Introduces `LEVersion = 1` for cash-basis Vault accounting. See [§3.1.2.2](#3122-leversion-lendingprotocolv1_1) and [§3.2.6.1](#3261-state-changes-lendingprotocolv1_1). ## 3. Specification @@ -1170,7 +1170,4 @@ No, neither of the transactions charge transfer fees when depositing or withdraw ## Appendix C: Changelog -Spec PRs merge into one of these two patches (`LendingProtocolV1_1` or `fixCleanup3_4_0`), not as their own XLS-65.N: - -- XLS-65.1: `LendingProtocolV1_1` (not yet live) — `LEVersion`, closed-ended vault lifecycle, and `VaultDelete.MemoData`. See [65.1](./65.1/README.md). Commit SHA recorded when this patch is merged. -- XLS-65.2: `fixCleanup3_4_0` (not yet live) — VaultClawback checks, vault invariants, and VaultSet DomainID-zero wording. See [65.2](./65.2/README.md). Commit SHA recorded when this patch is merged. +- XLS-65.1: Cash-basis Vault accounting under `LendingProtocolV1_1`, not yet live — [XLS-65.1](./65.1/README.md) diff --git a/XLS-0066-lending-protocol/66.1/README.md b/XLS-0066-lending-protocol/66.1/README.md index 5d2c52a9b..c4bffe88d 100644 --- a/XLS-0066-lending-protocol/66.1/README.md +++ b/XLS-0066-lending-protocol/66.1/README.md @@ -1,20 +1,105 @@ -# XLS-66.1 — `LendingProtocolV1_1` +
+  xls: 66.1
+  title: Principal-Only Debt Accounting
+  description: Makes Vault.AssetsTotal and LoanBroker.DebtTotal principal-only for cash-basis Vaults and restricts lending to closed-ended Vaults
+  author: Vytautas Vito Tumas , Aanchal Malhotra 
+  implementation: https://github.com/XRPLF/rippled/pull/5270
+  proposal-from: https://github.com/XRPLF/XRPL-Standards/discussions/190
+  status: Draft
+  category: Amendment
+  requires: [XLS-66](../README.md), [XLS-65.1](../../XLS-0065-single-asset-vault/65.1/README.md)
+  created: 2026-09-04
+  updated: 2026-09-04
+
-Not yet live. When lending spec PRs merge, they land in **this** patch or in [XLS-66.2](../66.2/README.md) (`fixCleanup3_4_0`), not as a new XLS-66.N per PR. +# Principal-Only Debt Accounting -## Why +## 1. Abstract -`LendingProtocolV1_1` changes LoanBroker attachment and principal-only vault/broker totals. Pre-amendment text stays the production default until the amendment is live. +This patch of [XLS-66](../README.md) records the changes the `LendingProtocolV1_1` amendment makes to the Lending Protocol. For a Vault with `LEVersion == 1` (cash basis, see [XLS-65.1](../../XLS-0065-single-asset-vault/65.1/README.md)), `Vault.AssetsTotal` and `LoanBroker.DebtTotal` track loan principal only; interest is recognised when it is collected rather than when a Loan is issued. A `LoanBroker` may only be attached to a closed-ended Vault. -## What changed +The consolidated specification is the top-level [README.md](../README.md). -- For `LEVersion = 1` Vaults, `Vault.AssetsTotal` and `LoanBroker.DebtTotal` track principal only. -- Creating a LoanBroker while the amendment is enabled requires a closed-ended Vault (`tecNO_PERMISSION` otherwise). -- First-Loss Capital still uses `CoverRateLiquidation` (two-rate formula). Field removal does not ship under this patch (#494 alignment). -- Optional `VaultID` on `LoanBrokerSet` modify (#497) is intended to merge here if it ships; amendment vs `tem`/`tec` is still open. +## 2. Motivation -## Merge bucket +Under accrual accounting, issuing a Loan immediately increases `Vault.AssetsTotal` and `LoanBroker.DebtTotal` by the principal plus the whole of the expected interest. Two consequences follow. The `AssetsMaximum` cap and the `DebtMaximum` cap are consumed by interest that has not been paid, so a Broker reaches its debt ceiling earlier than the principal at risk implies. And the first-loss capital requirement, which is a rate applied to `DebtTotal`, is sized against expected interest as well as principal. -PRs that fold into XLS-66.1: #582 (cash-basis + closed-ended broker), #494 (keep CoverRateLiquidation), #497 (optional VaultID, pending design). +Restricting lending to closed-ended Vaults removes the second mismatch: an open-ended Vault allows redemptions at any time, so a depositor can exit at a share price that includes interest still owed by a Borrower. -The consolidated spec is the top-level `README.md`. Commit SHA for the changelog is recorded when this patch is merged. +## 3. Specification + +### 3.1 Transaction: `LoanBrokerSet` + +#### 3.1.1 Fields + +No fields are added or removed. + +#### 3.1.2 Failure Conditions + +When a new `LoanBroker` is created (no `LoanBrokerID` supplied), the following protocol-level failure applies in addition to the existing checks: + +1. The `Vault` identified by `VaultID` is not closed-ended, that is `Vault.VaultKind` is absent or not equal to `1`. (`tecNO_PERMISSION`) + +The check is evaluated before the asset-holding and reserve checks. Modifying an existing `LoanBroker` does not re-evaluate it. + +#### 3.1.3 State Changes + +Unchanged. + +#### 3.1.4 Example JSON + +```json +{ + "TransactionType": "LoanBrokerSet", + "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "Fee": "10", + "Sequence": 12345, + "VaultID": "9CD5F03A9D0F4F7C0B8B4C5F5A4D3E2B1A0F9E8D7C6B5A493827160504030201", + "DebtMaximum": "100000" +} +``` + +### 3.2 Transaction: `LoanSet` + +#### 3.2.1 Failure Conditions + +For a Vault with `LEVersion == 1`, the checks that compare expected interest against the Vault and Broker caps are evaluated against principal instead: + +1. `Vault.AssetsMaximum != 0` and `Vault.AssetsTotal > Vault.AssetsMaximum`. (`tecLIMIT_EXCEEDED`) +2. `LoanBroker.DebtMaximum != 0` and `LoanBroker.DebtMaximum < LoanBroker.DebtTotal + PrincipalRequested`. (`tecLIMIT_EXCEEDED`) +3. `LoanBroker.CoverAvailable < (LoanBroker.DebtTotal + PrincipalRequested) × LoanBroker.CoverRateMinimum`. (`tecINSUFFICIENT_FUNDS`) + +For a Vault with `LEVersion` absent, the corresponding accrual-basis checks apply unchanged. + +#### 3.2.2 State Changes + +For a Vault with `LEVersion == 1`, issuing a Loan increases `Vault.AssetsTotal` and `LoanBroker.DebtTotal` by `PrincipalRequested` only. For a Vault with `LEVersion` absent, both values increase by the principal plus the total expected interest. + +### 3.3 Transaction: `LoanPay` + +#### 3.3.1 State Changes + +For a Vault with `LEVersion == 1`, a payment splits into principal and interest: + +- The principal portion reduces `Vault.AssetsTotal` and `LoanBroker.DebtTotal` and increases `Vault.AssetsAvailable`. +- The interest portion, net of fees, increases `Vault.AssetsTotal` and `Vault.AssetsAvailable`. This is the point at which the Vault recognises the interest. + +For a Vault with `LEVersion` absent, the interest was already included in `AssetsTotal` when the Loan was issued, so a payment moves value from `AssetsTotal` to `AssetsAvailable` without changing the total. + +### 3.4 Transaction: `LoanManage` + +#### 3.4.1 State Changes + +For a Vault with `LEVersion == 1`, a default writes off principal only, because uncollected interest was never recognised. For a Vault with `LEVersion` absent, the write-off covers principal and accrued interest. + +## 4. Rationale + +Interest could have been recognised on a schedule, one payment period at a time, rather than on receipt. That was rejected because it reintroduces the original problem in a smaller form: the Vault would still credit itself with interest for a period in which the Borrower ends up not paying. + +The closed-ended restriction is enforced at `LoanBrokerSet` rather than at `LoanSet` so that the constraint is checked once, when the Vault is attached, rather than on every Loan. + +## 5. Security Considerations + +Principal-only accounting makes `DebtTotal` a smaller number for the same set of Loans, so a `CoverRateMinimum` that was calibrated under accrual accounting yields less first-loss capital. A Broker migrating to a cash-basis Vault should re-derive the rate rather than reuse it. + +Both accounting models coexist on the ledger for as long as pre-amendment Vaults exist. An implementation must branch on `Vault.LEVersion` in every place it adjusts `AssetsTotal` or `DebtTotal`; branching in some places and not others corrupts the share exchange rate of the Vault. diff --git a/XLS-0066-lending-protocol/66.2/README.md b/XLS-0066-lending-protocol/66.2/README.md deleted file mode 100644 index 919f56533..000000000 --- a/XLS-0066-lending-protocol/66.2/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# XLS-66.2 — `fixCleanup3_4_0` - -Not yet live. When lending spec PRs merge, they land in **this** patch or in [XLS-66.1](../66.1/README.md) (`LendingProtocolV1_1`), not as a new XLS-66.N per PR. - -## Why - -`fixCleanup3_4_0` stops early impairment and due-date rewrites on `LoanManage`. That is independent of cash-basis accounting. - -## What changed - -- Impair only when the payment is already overdue (`tecTOO_SOON` otherwise). -- With the amendment enabled, impair/unimpair do not rewrite `NextPaymentDueDate`. -- Without the amendment, the previous due-date moves remain the fallback. - -## Merge bucket - -PRs that fold into XLS-66.2: #496 (impairment). - -The consolidated spec is the top-level `README.md`. Commit SHA for the changelog is recorded when this patch is merged. diff --git a/XLS-0066-lending-protocol/README.md b/XLS-0066-lending-protocol/README.md index 71beb9326..8146453f2 100644 --- a/XLS-0066-lending-protocol/README.md +++ b/XLS-0066-lending-protocol/README.md @@ -118,7 +118,7 @@ The lending protocol charges a number of fees that the Loan Broker can configure ## Amendments -- `LendingProtocolV1_1` (not yet live, [XLS-66.1](./66.1/README.md)): Makes `Vault.AssetsTotal` and `LoanBroker.DebtTotal` principal-only for Vaults with `LEVersion = 1` and requires newly attached lending Vaults to be closed-ended. See [§3.3.3.2](#3332-protocol-level-failures), [§3.8.6.1](#3861-state-changes-lendingprotocolv1_1), [§3.10.5.1](#31051-state-changes-lendingprotocolv1_1), and [§3.11.5.1](#31151-state-changes-lendingprotocolv1_1). This PR merges into XLS-66.1, not a new patch number. +- `LendingProtocolV1_1` (not yet live, [XLS-66.1](./66.1/README.md)): Makes `Vault.AssetsTotal` and `LoanBroker.DebtTotal` principal-only for Vaults with `LEVersion = 1` and requires newly attached lending Vaults to be closed-ended. See [§3.3.3.2](#3332-protocol-level-failures), [§3.8.6.1](#3861-state-changes-lendingprotocolv1_1), [§3.10.5.1](#31051-state-changes-lendingprotocolv1_1), and [§3.11.5.1](#31151-state-changes-lendingprotocolv1_1). ## 3. Specification @@ -2799,7 +2799,4 @@ function make_payment(amount, currentTime) -> (principalPaid, interestPaid, valu ## Appendix C: Changelog -Spec PRs merge into one of these two patches (`LendingProtocolV1_1` or `fixCleanup3_4_0`), not as their own XLS-66.N: - -- XLS-66.1: `LendingProtocolV1_1` (not yet live) — principal-only `AssetsTotal`/`DebtTotal`, closed-ended LoanBroker attachment, and related LoanBrokerSet field rules. See [66.1](./66.1/README.md). Commit SHA recorded when this patch is merged. -- XLS-66.2: `fixCleanup3_4_0` (not yet live) — late-only impairment and no `NextPaymentDueDate` rewrite. See [66.2](./66.2/README.md). Commit SHA recorded when this patch is merged. +- XLS-66.1: Principal-only debt accounting under `LendingProtocolV1_1`, not yet live — [XLS-66.1](./66.1/README.md) From 87a5819ef2891979a63089b32afe1d76d7268d14 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Fri, 4 Sep 2026 15:45:40 +0200 Subject: [PATCH 06/32] docs: address PR 582 review comments Skip the cash-basis AssetsMaximum origination check to match develop, move the closed-ended LoanBrokerSet failure into an amendment subsection, and treat absent LEVersion as 0. --- XLS-0065-single-asset-vault/65.1/README.md | 2 +- XLS-0065-single-asset-vault/README.md | 2 +- XLS-0066-lending-protocol/66.1/README.md | 7 +++-- XLS-0066-lending-protocol/README.md | 31 +++++++++++----------- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/XLS-0065-single-asset-vault/65.1/README.md b/XLS-0065-single-asset-vault/65.1/README.md index cd2d06cd6..398c1101e 100644 --- a/XLS-0065-single-asset-vault/65.1/README.md +++ b/XLS-0065-single-asset-vault/65.1/README.md @@ -33,7 +33,7 @@ Existing Vaults cannot switch accounting models without changing the value of sh | Field Name | Constant | Required | Internal Type | Default Value | Description | | ----------- | :------: | :------: | :-----------: | :-----------: | ------------------------------------------------------------------------ | -| `LEVersion` | Yes | No | `UINT8` | absent | The accounting model the Vault applies to `AssetsTotal`. Immutable once set. | +| `LEVersion` | Yes | No | `UINT8` | absent (`0`) | The accounting model the Vault applies to `AssetsTotal`. Absent is treated as `0` (accrual basis). Immutable once set. | #### 3.1.2 `LEVersion` diff --git a/XLS-0065-single-asset-vault/README.md b/XLS-0065-single-asset-vault/README.md index cb2db4c4e..8d6176eb7 100644 --- a/XLS-0065-single-asset-vault/README.md +++ b/XLS-0065-single-asset-vault/README.md @@ -111,7 +111,7 @@ A vault has the following fields: | `ShareMPTID` | No | Yes | `number` | `UINT192` | 0 | The identifier of the share MPTokenIssuance object. | | `WithdrawalPolicy` | No | Yes | `string` | `UINT8` | `N/A` | Indicates the withdrawal strategy used by the Vault. | | `Scale` | No | Yes | `number` | `UINT8` | 6 | The `Scale` specifies the power of 10 ($10^{\text{scale}}$) to multiply an asset's value by when converting it into an integer-based number of shares. | -| `LEVersion` | No | No | `number` | `UINT8` | 0 | Introduced by the `LendingProtocolV1_1` amendment. See [3.1.2.2](#3122-leversion-lendingprotocolv1_1). | +| `LEVersion` | No | No | `number` | `UINT8` | absent (`0`) | Introduced by the `LendingProtocolV1_1` amendment. Absent is treated as `0` (legacy / accrual-basis). See [3.1.2.2](#3122-leversion-lendingprotocolv1_1). | ##### 3.1.2.1 Flags diff --git a/XLS-0066-lending-protocol/66.1/README.md b/XLS-0066-lending-protocol/66.1/README.md index c4bffe88d..241dc5ab7 100644 --- a/XLS-0066-lending-protocol/66.1/README.md +++ b/XLS-0066-lending-protocol/66.1/README.md @@ -63,11 +63,10 @@ Unchanged. #### 3.2.1 Failure Conditions -For a Vault with `LEVersion == 1`, the checks that compare expected interest against the Vault and Broker caps are evaluated against principal instead: +For a Vault with `LEVersion == 1`, origination no longer credits `InterestDue` into `Vault.AssetsTotal`, so the `AssetsMaximum` origination check does not apply. The Broker cap checks are evaluated against principal instead: -1. `Vault.AssetsMaximum != 0` and `Vault.AssetsTotal > Vault.AssetsMaximum`. (`tecLIMIT_EXCEEDED`) -2. `LoanBroker.DebtMaximum != 0` and `LoanBroker.DebtMaximum < LoanBroker.DebtTotal + PrincipalRequested`. (`tecLIMIT_EXCEEDED`) -3. `LoanBroker.CoverAvailable < (LoanBroker.DebtTotal + PrincipalRequested) × LoanBroker.CoverRateMinimum`. (`tecINSUFFICIENT_FUNDS`) +1. `LoanBroker.DebtMaximum != 0` and `LoanBroker.DebtMaximum < LoanBroker.DebtTotal + PrincipalRequested`. (`tecLIMIT_EXCEEDED`) +2. `LoanBroker.CoverAvailable < (LoanBroker.DebtTotal + PrincipalRequested) × LoanBroker.CoverRateMinimum`. (`tecINSUFFICIENT_FUNDS`) For a Vault with `LEVersion` absent, the corresponding accrual-basis checks apply unchanged. diff --git a/XLS-0066-lending-protocol/README.md b/XLS-0066-lending-protocol/README.md index 8146453f2..442aa452d 100644 --- a/XLS-0066-lending-protocol/README.md +++ b/XLS-0066-lending-protocol/README.md @@ -118,7 +118,7 @@ The lending protocol charges a number of fees that the Loan Broker can configure ## Amendments -- `LendingProtocolV1_1` (not yet live, [XLS-66.1](./66.1/README.md)): Makes `Vault.AssetsTotal` and `LoanBroker.DebtTotal` principal-only for Vaults with `LEVersion = 1` and requires newly attached lending Vaults to be closed-ended. See [§3.3.3.2](#3332-protocol-level-failures), [§3.8.6.1](#3861-state-changes-lendingprotocolv1_1), [§3.10.5.1](#31051-state-changes-lendingprotocolv1_1), and [§3.11.5.1](#31151-state-changes-lendingprotocolv1_1). +- `LendingProtocolV1_1` (not yet live, [XLS-66.1](./66.1/README.md)): Makes `Vault.AssetsTotal` and `LoanBroker.DebtTotal` principal-only for Vaults with `LEVersion = 1` and requires newly attached lending Vaults to be closed-ended. See [§3.3.3.3](#3333-protocol-level-failures-lendingprotocolv1_1), [§3.8.5.3](#3853-protocol-level-failures-lendingprotocolv1_1), [§3.8.6.1](#3861-state-changes-lendingprotocolv1_1), [§3.10.5.1](#31051-state-changes-lendingprotocolv1_1), and [§3.11.5.1](#31151-state-changes-lendingprotocolv1_1). ## 3. Specification @@ -591,21 +591,26 @@ This transaction uses the standard transaction fee. 1. `Vault` object with the specified `VaultID` does not exist on the ledger. (`tecNO_ENTRY`) 2. The submitter `AccountRoot.Account != Vault(VaultID).Owner`. (`tecNO_PERMISSION`) -3. When `LendingProtocolV1_1` is enabled, the Vault must be closed-ended (`Vault.VaultKind == ClosedEnded`, or equivalent: `VaultKind` present and equal to 1). Open-ended vaults (kind absent or 0) fail with `tecNO_PERMISSION`. (`tecNO_PERMISSION`) -4. Cannot add asset holding for the `Vault.Asset` (e.g., MPToken or TrustLine issues). (`tecNO_PERMISSION`) -5. The Vault _pseudo-account_ is frozen for the `Vault.Asset`. (`tecFROZEN` for IOUs, `tecLOCKED` for MPTs) -6. The submitter does not have sufficient reserve for the `LoanBroker` object and _pseudo-account_ (requires 2 owner reserves). (`tecINSUFFICIENT_RESERVE`) +3. Cannot add asset holding for the `Vault.Asset` (e.g., MPToken or TrustLine issues). (`tecNO_PERMISSION`) +4. The Vault _pseudo-account_ is frozen for the `Vault.Asset`. (`tecFROZEN` for IOUs, `tecLOCKED` for MPTs) +5. The submitter does not have sufficient reserve for the `LoanBroker` object and _pseudo-account_ (requires 2 owner reserves). (`tecINSUFFICIENT_RESERVE`) **If `LoanBrokerID` is specified (modifying existing):** -7. `LoanBroker` object with the specified `LoanBrokerID` does not exist on the ledger. (`tecNO_ENTRY`) -8. The submitter `AccountRoot.Account != LoanBroker(LoanBrokerID).Owner`. (`tecNO_PERMISSION`) -9. The transaction `VaultID` does not match `LoanBroker(LoanBrokerID).VaultID`. (`tecNO_PERMISSION`) -10. `DebtMaximum` is being reduced to a non-zero value below the current `DebtTotal`. (`tecLIMIT_EXCEEDED`) +6. `LoanBroker` object with the specified `LoanBrokerID` does not exist on the ledger. (`tecNO_ENTRY`) +7. The submitter `AccountRoot.Account != LoanBroker(LoanBrokerID).Owner`. (`tecNO_PERMISSION`) +8. The transaction `VaultID` does not match `LoanBroker(LoanBrokerID).VaultID`. (`tecNO_PERMISSION`) +9. `DebtMaximum` is being reduced to a non-zero value below the current `DebtTotal`. (`tecLIMIT_EXCEEDED`) **Precision Validation:** -11. Any value field (e.g., `DebtMaximum`) cannot be represented in the `Vault.Asset` type without precision loss (relevant for XRP and MPT). (`tecPRECISION_LOSS`) +10. Any value field (e.g., `DebtMaximum`) cannot be represented in the `Vault.Asset` type without precision loss (relevant for XRP and MPT). (`tecPRECISION_LOSS`) + +##### 3.3.3.3 Protocol-Level Failures (`LendingProtocolV1_1`) + +When creating a new `LoanBroker` (`LoanBrokerID` is not specified), the following check is added after [3.3.3.2](#3332-protocol-level-failures) item 2. Modifying an existing `LoanBroker` does not re-evaluate it. + +1. The `Vault` identified by `VaultID` is not closed-ended (`Vault.VaultKind` is absent or not equal to `1`). (`tecNO_PERMISSION`) #### 3.3.4 State Changes @@ -1096,11 +1101,7 @@ The account specified in the `Account` field pays the transaction fee. ##### 3.8.5.3 Protocol-Level Failures (`LendingProtocolV1_1`) -Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 1` (cash-basis, see [XLS-65 §3.1.2.2](../XLS-0065-single-asset-vault/README.md#3122-leversion-lendingprotocolv1_1)), `Vault.AssetsTotal` and `LoanBroker.DebtTotal` become principal-only. Checks 14, 19, and 20 of [3.8.5.2](#3852-protocol-level-failures) are replaced by the following (all other checks are unchanged); for a legacy (`LEVersion` absent) `Vault`, checks 14, 19, and 20 of [3.8.5.2](#3852-protocol-level-failures) continue to apply unchanged: - -14. `Vault.AssetsMaximum != 0` and `Vault.AssetsTotal > Vault.AssetsMaximum` (expected interest would exceed vault assets cap). (`tecLIMIT_EXCEEDED`) - -Since `LoanBroker.DebtTotal` likewise excludes interest: +Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 1` (cash-basis, see [XLS-65 §3.1.2.2](../XLS-0065-single-asset-vault/README.md#3122-leversion-lendingprotocolv1_1)), `Vault.AssetsTotal` and `LoanBroker.DebtTotal` become principal-only. Check 14 of [3.8.5.2](#3852-protocol-level-failures) does not apply: origination no longer credits `InterestDue` into `AssetsTotal`, so it cannot push the Vault past `AssetsMaximum`. Checks 19 and 20 are replaced by the following (all other checks are unchanged). For a legacy (`LEVersion` absent) `Vault`, checks 14, 19, and 20 of [3.8.5.2](#3852-protocol-level-failures) continue to apply unchanged: 19. `LoanBroker.DebtMaximum != 0` and `LoanBroker.DebtMaximum < LoanBroker.DebtTotal + PrincipalRequested` (exceeds maximum debt). (`tecLIMIT_EXCEEDED`) 20. `LoanBroker.CoverAvailable < (LoanBroker.DebtTotal + PrincipalRequested) × LoanBroker.CoverRateMinimum` (insufficient first-loss capital). (`tecINSUFFICIENT_FUNDS`) From ece0e375644534237f07c7264e346094a4176f87 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Fri, 4 Sep 2026 15:45:49 +0200 Subject: [PATCH 07/32] docs(XLS-65.1): treat absent LEVersion as 0 --- XLS-0065-single-asset-vault/65.1/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/XLS-0065-single-asset-vault/65.1/README.md b/XLS-0065-single-asset-vault/65.1/README.md index 398c1101e..9afe04560 100644 --- a/XLS-0065-single-asset-vault/65.1/README.md +++ b/XLS-0065-single-asset-vault/65.1/README.md @@ -39,7 +39,7 @@ Existing Vaults cannot switch accounting models without changing the value of sh `LEVersion` selects the accounting model of the Vault: -- `LEVersion` absent (accrual basis): `AssetsTotal` includes the interest accrued over the life of the connected Loans. This is the pre-amendment behaviour and continues to apply to every Vault created before the amendment was enabled. +- `LEVersion` absent (treated as `0`, accrual basis): `AssetsTotal` includes the interest accrued over the life of the connected Loans. This is the pre-amendment behaviour and continues to apply to every Vault created before the amendment was enabled. - `LEVersion = 1` (cash basis): `AssetsTotal` is principal-only and increases only as interest is collected in cash. The field is immutable. A Vault created while the amendment is enabled is always cash-basis, and a Vault created before it is always accrual-basis; neither can be converted to the other. From 118f61282f36bc41c8352c3deebda7a0a5c3865f Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Fri, 4 Sep 2026 16:47:23 +0200 Subject: [PATCH 08/32] docs: align cash-basis patches with develop accounting --- XLS-0065-single-asset-vault/65.1/README.md | 4 +- XLS-0065-single-asset-vault/README.md | 10 ++-- XLS-0066-lending-protocol/66.1/README.md | 70 ++++++---------------- XLS-0066-lending-protocol/README.md | 30 +++++----- 4 files changed, 40 insertions(+), 74 deletions(-) diff --git a/XLS-0065-single-asset-vault/65.1/README.md b/XLS-0065-single-asset-vault/65.1/README.md index 9afe04560..a509477fe 100644 --- a/XLS-0065-single-asset-vault/65.1/README.md +++ b/XLS-0065-single-asset-vault/65.1/README.md @@ -15,7 +15,7 @@ ## 1. Abstract -This patch of [XLS-65](../README.md) records the changes the `LendingProtocolV1_1` amendment makes to the `Vault` ledger entry. The amendment adds a `LEVersion` field that selects the accounting model a Vault applies to `AssetsTotal`. Vaults created while the amendment is enabled use cash-basis accounting, in which `AssetsTotal` tracks principal only and grows only as interest is collected in cash. Vaults created before the amendment keep accrual-basis accounting, in which `AssetsTotal` includes interest as it accrues. +This patch of [XLS-65](../README.md) records the changes the `LendingProtocolV1_1` amendment makes to the `Vault` ledger entry. The amendment adds a `LEVersion` field that selects the accounting model a Vault applies to `AssetsTotal`. Vaults created while the amendment is enabled use cash-basis accounting, in which `AssetsTotal` excludes uncollected interest and grows only as interest is collected in cash. Vaults created before the amendment keep accrual-basis accounting, in which `AssetsTotal` includes interest as it accrues. The consolidated specification is the top-level [README.md](../README.md). @@ -40,7 +40,7 @@ Existing Vaults cannot switch accounting models without changing the value of sh `LEVersion` selects the accounting model of the Vault: - `LEVersion` absent (treated as `0`, accrual basis): `AssetsTotal` includes the interest accrued over the life of the connected Loans. This is the pre-amendment behaviour and continues to apply to every Vault created before the amendment was enabled. -- `LEVersion = 1` (cash basis): `AssetsTotal` is principal-only and increases only as interest is collected in cash. +- `LEVersion = 1` (cash basis): `AssetsTotal` excludes uncollected interest and increases only as interest is collected in cash. The field is immutable. A Vault created while the amendment is enabled is always cash-basis, and a Vault created before it is always accrual-basis; neither can be converted to the other. diff --git a/XLS-0065-single-asset-vault/README.md b/XLS-0065-single-asset-vault/README.md index 8d6176eb7..3df2199c6 100644 --- a/XLS-0065-single-asset-vault/README.md +++ b/XLS-0065-single-asset-vault/README.md @@ -8,7 +8,7 @@ category: Amendment requires: [XLS-33](../XLS-0033-multi-purpose-tokens/README.md) created: 2024-04-12 - updated: 2026-07-21 + updated: 2026-09-04 # Single Asset Vault @@ -71,7 +71,7 @@ A protocol connecting to a Vault must track its debt. Furthermore, the updates t ## Amendments -- `LendingProtocolV1_1` (not yet live, [XLS-65.1](./65.1/README.md)): Introduces `LEVersion = 1` for cash-basis Vault accounting. See [§3.1.2.2](#3122-leversion-lendingprotocolv1_1) and [§3.2.6.1](#3261-state-changes-lendingprotocolv1_1). +- [`LendingProtocolV1_1`](./65.1/README.md) introduces `LEVersion = 1` for cash-basis Vault accounting. ## 3. Specification @@ -111,7 +111,7 @@ A vault has the following fields: | `ShareMPTID` | No | Yes | `number` | `UINT192` | 0 | The identifier of the share MPTokenIssuance object. | | `WithdrawalPolicy` | No | Yes | `string` | `UINT8` | `N/A` | Indicates the withdrawal strategy used by the Vault. | | `Scale` | No | Yes | `number` | `UINT8` | 6 | The `Scale` specifies the power of 10 ($10^{\text{scale}}$) to multiply an asset's value by when converting it into an integer-based number of shares. | -| `LEVersion` | No | No | `number` | `UINT8` | absent (`0`) | Introduced by the `LendingProtocolV1_1` amendment. Absent is treated as `0` (legacy / accrual-basis). See [3.1.2.2](#3122-leversion-lendingprotocolv1_1). | +| `LEVersion` | Yes | No | `number` | `UINT8` | absent (`0`) | Immutable protocol-selected ledger entry version. Absent is treated as `0` (legacy / accrual-basis). See [3.1.2.2](#3122-leversion-lendingprotocolv1_1). | ##### 3.1.2.1 Flags @@ -130,7 +130,7 @@ The `Vault` object supports the following flags: For the `Vault` object, `LEVersion` distinguishes the share-valuation and accounting behavior applied to the vault: - **Absent (default value `0`)**: The Vault was created before the `LendingProtocolV1_1` amendment was enabled, or otherwise predates ledger entry versioning. It uses the legacy, accrual-basis accounting described throughout this document and in [XLS-66](../XLS-0066-lending-protocol/README.md) — `AssetsTotal` includes accrued interest. -- **`1`**: The Vault was created while the `LendingProtocolV1_1` amendment was enabled. It exclusively uses the cash-basis accounting introduced by that amendment — `AssetsTotal` is principal-only (see [3.1.7.4](#3174-cash-basis-vs-accrual-basis-accounting-lendingprotocolv1_1) and the `LendingProtocolV1_1` subsections of [XLS-66](../XLS-0066-lending-protocol/README.md)). +- **`1`**: The Vault was created while the `LendingProtocolV1_1` amendment was enabled. It exclusively uses the cash-basis accounting introduced by that amendment — `AssetsTotal` excludes uncollected interest and increases when interest is paid (see [3.1.7.4](#3174-cash-basis-vs-accrual-basis-accounting-lendingprotocolv1_1) and the `LendingProtocolV1_1` subsections of [XLS-66](../XLS-0066-lending-protocol/README.md)). Any Vault created once `LendingProtocolV1_1` is enabled is always assigned `LEVersion = 1` — cash-basis accounting is not optional or user-selectable at creation time. A future revision of the `Vault` ledger entry would increment `LEVersion` to `2`, and so on. @@ -334,7 +334,7 @@ The exchange rate algorithms in [3.1.7.2](#3172-exchange-rate-algorithms) above Under the `LendingProtocolV1_1` amendment, `Vault.LEVersion` (see [3.1.2.2](#3122-leversion-lendingprotocolv1_1)) determines which of the two accounting models a Vault's `AssetsTotal` follows: - `LEVersion` absent (`0`, legacy/accrual-basis): `AssetsTotal` includes interest upfront, as accrued over the life of connected Loans. This is the pre-amendment behavior and continues unchanged for Vaults created before `LendingProtocolV1_1` was enabled. -- `LEVersion = 1` (cash-basis): `AssetsTotal` is principal-only and increases only as interest is actually collected in cash. This is the exclusive behavior for Vaults created once `LendingProtocolV1_1` is enabled. +- `LEVersion = 1` (cash-basis): `AssetsTotal` excludes uncollected interest and increases only as interest is actually collected in cash. This is the exclusive behavior for Vaults created once `LendingProtocolV1_1` is enabled. See the `LendingProtocolV1_1` subsections of [XLS-66 §3.8](../XLS-0066-lending-protocol/README.md#38-transaction-loanset), [§3.10](../XLS-0066-lending-protocol/README.md#310-transaction-loanmanage), and [§3.11](../XLS-0066-lending-protocol/README.md#311-transaction-loanpay) for the precise accounting differences, all of which are gated on `Vault.LEVersion == 1`. diff --git a/XLS-0066-lending-protocol/66.1/README.md b/XLS-0066-lending-protocol/66.1/README.md index 241dc5ab7..4b7329ded 100644 --- a/XLS-0066-lending-protocol/66.1/README.md +++ b/XLS-0066-lending-protocol/66.1/README.md @@ -1,7 +1,7 @@
   xls: 66.1
   title: Principal-Only Debt Accounting
-  description: Makes Vault.AssetsTotal and LoanBroker.DebtTotal principal-only for cash-basis Vaults and restricts lending to closed-ended Vaults
+  description: Makes LoanBroker.DebtTotal principal-only and recognizes Vault interest on receipt for cash-basis Vaults
   author: Vytautas Vito Tumas , Aanchal Malhotra 
   implementation: https://github.com/XRPLF/rippled/pull/5270
   proposal-from: https://github.com/XRPLF/XRPL-Standards/discussions/190
@@ -16,78 +16,48 @@
 
 ## 1. Abstract
 
-This patch of [XLS-66](../README.md) records the changes the `LendingProtocolV1_1` amendment makes to the Lending Protocol. For a Vault with `LEVersion == 1` (cash basis, see [XLS-65.1](../../XLS-0065-single-asset-vault/65.1/README.md)), `Vault.AssetsTotal` and `LoanBroker.DebtTotal` track loan principal only; interest is recognised when it is collected rather than when a Loan is issued. A `LoanBroker` may only be attached to a closed-ended Vault.
+This patch of [XLS-66](../README.md) records the changes the `LendingProtocolV1_1` amendment makes to the Lending Protocol. For a Vault with `LEVersion == 1` (cash basis, see [XLS-65.1](../../XLS-0065-single-asset-vault/65.1/README.md)), `LoanBroker.DebtTotal` tracks loan principal only and `Vault.AssetsTotal` recognises interest when it is collected rather than when a Loan is issued.
 
 The consolidated specification is the top-level [README.md](../README.md).
 
 ## 2. Motivation
 
-Under accrual accounting, issuing a Loan immediately increases `Vault.AssetsTotal` and `LoanBroker.DebtTotal` by the principal plus the whole of the expected interest. Two consequences follow. The `AssetsMaximum` cap and the `DebtMaximum` cap are consumed by interest that has not been paid, so a Broker reaches its debt ceiling earlier than the principal at risk implies. And the first-loss capital requirement, which is a rate applied to `DebtTotal`, is sized against expected interest as well as principal.
-
-Restricting lending to closed-ended Vaults removes the second mismatch: an open-ended Vault allows redemptions at any time, so a depositor can exit at a share price that includes interest still owed by a Borrower.
+Under accrual accounting, issuing a Loan immediately increases `Vault.AssetsTotal` by the expected interest and `LoanBroker.DebtTotal` by the principal plus expected interest. The `AssetsMaximum` and `DebtMaximum` caps are therefore consumed by interest that has not been paid, and the first-loss capital requirement, which is a rate applied to `DebtTotal`, is sized against expected interest as well as principal.
 
 ## 3. Specification
 
-### 3.1 Transaction: `LoanBrokerSet`
-
-#### 3.1.1 Fields
-
-No fields are added or removed.
-
-#### 3.1.2 Failure Conditions
-
-When a new `LoanBroker` is created (no `LoanBrokerID` supplied), the following protocol-level failure applies in addition to the existing checks:
-
-1. The `Vault` identified by `VaultID` is not closed-ended, that is `Vault.VaultKind` is absent or not equal to `1`. (`tecNO_PERMISSION`)
-
-The check is evaluated before the asset-holding and reserve checks. Modifying an existing `LoanBroker` does not re-evaluate it.
-
-#### 3.1.3 State Changes
+### 3.1 Transaction: `LoanSet`
 
-Unchanged.
+#### 3.1.1 Failure Conditions
 
-#### 3.1.4 Example JSON
-
-```json
-{
-  "TransactionType": "LoanBrokerSet",
-  "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
-  "Fee": "10",
-  "Sequence": 12345,
-  "VaultID": "9CD5F03A9D0F4F7C0B8B4C5F5A4D3E2B1A0F9E8D7C6B5A493827160504030201",
-  "DebtMaximum": "100000"
-}
-```
-
-### 3.2 Transaction: `LoanSet`
-
-#### 3.2.1 Failure Conditions
-
-For a Vault with `LEVersion == 1`, origination no longer credits `InterestDue` into `Vault.AssetsTotal`, so the `AssetsMaximum` origination check does not apply. The Broker cap checks are evaluated against principal instead:
+For a Vault with `LEVersion == 1`, checks 6 and 14 of the parent specification do not apply, and the Broker cap checks 19 and 20 are replaced by principal-only checks:
 
 1. `LoanBroker.DebtMaximum != 0` and `LoanBroker.DebtMaximum < LoanBroker.DebtTotal + PrincipalRequested`. (`tecLIMIT_EXCEEDED`)
 2. `LoanBroker.CoverAvailable < (LoanBroker.DebtTotal + PrincipalRequested) × LoanBroker.CoverRateMinimum`. (`tecINSUFFICIENT_FUNDS`)
 
-For a Vault with `LEVersion` absent, the corresponding accrual-basis checks apply unchanged.
+For a Vault with `LEVersion` absent, parent checks 6, 14, 19, and 20 apply unchanged.
 
-#### 3.2.2 State Changes
+#### 3.1.2 State Changes
 
-For a Vault with `LEVersion == 1`, issuing a Loan increases `Vault.AssetsTotal` and `LoanBroker.DebtTotal` by `PrincipalRequested` only. For a Vault with `LEVersion` absent, both values increase by the principal plus the total expected interest.
+For a Vault with `LEVersion == 1`, issuing a Loan leaves `Vault.AssetsTotal` unchanged and increases `LoanBroker.DebtTotal` by `PrincipalRequested`. For a Vault with `LEVersion` absent, issuing a Loan increases `Vault.AssetsTotal` by `InterestDue` and increases `LoanBroker.DebtTotal` by `PrincipalRequested + InterestDue`.
 
-### 3.3 Transaction: `LoanPay`
+### 3.2 Transaction: `LoanPay`
 
-#### 3.3.1 State Changes
+#### 3.2.1 State Changes
 
 For a Vault with `LEVersion == 1`, a payment splits into principal and interest:
 
-- The principal portion reduces `Vault.AssetsTotal` and `LoanBroker.DebtTotal` and increases `Vault.AssetsAvailable`.
-- The interest portion, net of fees, increases `Vault.AssetsTotal` and `Vault.AssetsAvailable`. This is the point at which the Vault recognises the interest.
+- `Vault.AssetsTotal` increases by `interestPaid`.
+- `LoanBroker.DebtTotal` decreases by `principalPaid`.
+- `Vault.AssetsAvailable` increases by `totalToVault`, which is `principalPaid + interestPaid` rounded to the Vault's asset scale.
+
+Principal repayment does not reduce `Vault.AssetsTotal`. Because the cash-basis origination checks do not enforce `AssetsMaximum`, an interest receipt may increase `Vault.AssetsTotal` above `Vault.AssetsMaximum`; the cap restricts deposits, not interest receipts.
 
-For a Vault with `LEVersion` absent, the interest was already included in `AssetsTotal` when the Loan was issued, so a payment moves value from `AssetsTotal` to `AssetsAvailable` without changing the total.
+For a Vault with `LEVersion` absent, the parent accrual-basis state changes apply unchanged.
 
-### 3.4 Transaction: `LoanManage`
+### 3.3 Transaction: `LoanManage`
 
-#### 3.4.1 State Changes
+#### 3.3.1 State Changes
 
 For a Vault with `LEVersion == 1`, a default writes off principal only, because uncollected interest was never recognised. For a Vault with `LEVersion` absent, the write-off covers principal and accrued interest.
 
@@ -95,8 +65,6 @@ For a Vault with `LEVersion == 1`, a default writes off principal only, because
 
 Interest could have been recognised on a schedule, one payment period at a time, rather than on receipt. That was rejected because it reintroduces the original problem in a smaller form: the Vault would still credit itself with interest for a period in which the Borrower ends up not paying.
 
-The closed-ended restriction is enforced at `LoanBrokerSet` rather than at `LoanSet` so that the constraint is checked once, when the Vault is attached, rather than on every Loan.
-
 ## 5. Security Considerations
 
 Principal-only accounting makes `DebtTotal` a smaller number for the same set of Loans, so a `CoverRateMinimum` that was calibrated under accrual accounting yields less first-loss capital. A Broker migrating to a cash-basis Vault should re-derive the rate rather than reuse it.
diff --git a/XLS-0066-lending-protocol/README.md b/XLS-0066-lending-protocol/README.md
index 442aa452d..cacfd17e1 100644
--- a/XLS-0066-lending-protocol/README.md
+++ b/XLS-0066-lending-protocol/README.md
@@ -8,7 +8,7 @@
   category: Amendment
   requires: XLS-65, XLS-64
   created: 2024-10-18
-  updated: 2026-07-21
+  updated: 2026-09-04
   proposal-from: https://github.com/XRPLF/XRPL-Standards/discussions/190
 
@@ -118,7 +118,7 @@ The lending protocol charges a number of fees that the Loan Broker can configure ## Amendments -- `LendingProtocolV1_1` (not yet live, [XLS-66.1](./66.1/README.md)): Makes `Vault.AssetsTotal` and `LoanBroker.DebtTotal` principal-only for Vaults with `LEVersion = 1` and requires newly attached lending Vaults to be closed-ended. See [§3.3.3.3](#3333-protocol-level-failures-lendingprotocolv1_1), [§3.8.5.3](#3853-protocol-level-failures-lendingprotocolv1_1), [§3.8.6.1](#3861-state-changes-lendingprotocolv1_1), [§3.10.5.1](#31051-state-changes-lendingprotocolv1_1), and [§3.11.5.1](#31151-state-changes-lendingprotocolv1_1). +- [`LendingProtocolV1_1`](./66.1/README.md) introduces principal-only debt accounting and cash-basis interest recognition for Vaults with `LEVersion = 1`. ## 3. Specification @@ -155,7 +155,7 @@ The `LoanBroker` object has the following fields: | `Data` | No | No | `string` | `BLOB` | None | Arbitrary metadata about the `LoanBroker`. Limited to 256 bytes. | | `ManagementFeeRate` | Yes | No | `number` | `UINT16` | 0 | The 1/10th basis point fee charged by the Lending Protocol. Valid values are between 0 and 10000 inclusive. A value of 1 is equivalent to 1/10 bps or 0.001% | | `OwnerCount` | No | Yes | `number` | `UINT32` | 0 | The number of active Loans issued by the `LoanBroker`. | -| `DebtTotal` | No | Yes | `string` | `NUMBER` | 0 | The total asset amount the protocol owes the Vault, including interest. | +| `DebtTotal` | No | Yes | `string` | `NUMBER` | 0 | The total asset amount the protocol owes the Vault: principal plus interest for a legacy Vault, or principal only for a Vault with `LEVersion = 1`. | | `DebtMaximum` | No | Yes | `string` | `NUMBER` | 0 | The maximum amount the protocol can owe the Vault. The default value of 0 means there is no limit to the debt. | | `CoverAvailable` | No | Yes | `string` | `NUMBER` | 0 | The total amount of first-loss capital deposited into the Lending Protocol. | | `CoverRateMinimum` | No | Yes | `number` | `UINT32` | 0 | The 1/10th basis point of the `DebtTotal` that the first-loss capital must cover. Valid values are between 0 and 100000 inclusive. A value of 1 is equivalent to 1/10 bps or 0.001%. | @@ -242,7 +242,7 @@ _TBD_ #### 3.1.10 Accounting -The Lending Protocol tracks the funds owed to the associated Vault in the `DebtTotal` attribute. It captures the principal amount taken from the Vault and the interest due, excluding all fees. The `DebtMaximum` attribute controls the maximum debt a Lending Protocol may incur. Whenever the Lender issues a Loan, `DebtTotal` is incremented by the Loan principal and interest, excluding fees. When $DebtTotal \geq DebtMaximum$, the Lender cannot issue new loans until some of the debt is cleared. Furthermore, the Lender may not issue a loan that would cause the `DebtTotal` to exceed `DebtMaximum`. +The Lending Protocol tracks the funds owed to the associated Vault in the `DebtTotal` attribute. For a legacy Vault, it captures principal and interest due, excluding all fees; for a Vault with `LEVersion = 1`, it captures principal only. The `DebtMaximum` attribute controls the maximum debt a Lending Protocol may incur. When $DebtTotal \geq DebtMaximum$, the Lender cannot issue new loans until some of the debt is cleared. Furthermore, the Lender may not issue a loan that would cause the `DebtTotal` to exceed `DebtMaximum`. **Examples** @@ -606,12 +606,6 @@ This transaction uses the standard transaction fee. 10. Any value field (e.g., `DebtMaximum`) cannot be represented in the `Vault.Asset` type without precision loss (relevant for XRP and MPT). (`tecPRECISION_LOSS`) -##### 3.3.3.3 Protocol-Level Failures (`LendingProtocolV1_1`) - -When creating a new `LoanBroker` (`LoanBrokerID` is not specified), the following check is added after [3.3.3.2](#3332-protocol-level-failures) item 2. Modifying an existing `LoanBroker` does not re-evaluate it. - -1. The `Vault` identified by `VaultID` is not closed-ended (`Vault.VaultKind` is absent or not equal to `1`). (`tecNO_PERMISSION`) - #### 3.3.4 State Changes **If `LoanBrokerID` is not specified (creating new):** @@ -1101,11 +1095,13 @@ The account specified in the `Account` field pays the transaction fee. ##### 3.8.5.3 Protocol-Level Failures (`LendingProtocolV1_1`) -Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 1` (cash-basis, see [XLS-65 §3.1.2.2](../XLS-0065-single-asset-vault/README.md#3122-leversion-lendingprotocolv1_1)), `Vault.AssetsTotal` and `LoanBroker.DebtTotal` become principal-only. Check 14 of [3.8.5.2](#3852-protocol-level-failures) does not apply: origination no longer credits `InterestDue` into `AssetsTotal`, so it cannot push the Vault past `AssetsMaximum`. Checks 19 and 20 are replaced by the following (all other checks are unchanged). For a legacy (`LEVersion` absent) `Vault`, checks 14, 19, and 20 of [3.8.5.2](#3852-protocol-level-failures) continue to apply unchanged: +Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 1` (cash-basis, see [XLS-65 §3.1.2.2](../XLS-0065-single-asset-vault/README.md#3122-leversion-lendingprotocolv1_1)), checks 6 and 14 of [3.8.5.2](#3852-protocol-level-failures) do not apply, and checks 19 and 20 are replaced by the following; all other checks are unchanged: 19. `LoanBroker.DebtMaximum != 0` and `LoanBroker.DebtMaximum < LoanBroker.DebtTotal + PrincipalRequested` (exceeds maximum debt). (`tecLIMIT_EXCEEDED`) 20. `LoanBroker.CoverAvailable < (LoanBroker.DebtTotal + PrincipalRequested) × LoanBroker.CoverRateMinimum` (insufficient first-loss capital). (`tecINSUFFICIENT_FUNDS`) +For a legacy (`LEVersion` absent) `Vault`, checks 6, 14, 19, and 20 of [3.8.5.2](#3852-protocol-level-failures) continue to apply unchanged. + #### 3.8.6 State Changes 1. Create the `Loan` object with computed fields (`TotalValueOutstanding`, `PeriodicPayment`, `ManagementFeeOutstanding`, `LoanScale`, etc.). @@ -1142,11 +1138,11 @@ Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 1` ( ##### 3.8.6.1 State Changes (`LendingProtocolV1_1`) -Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 1` (cash-basis), `Vault.AssetsTotal` and `LoanBroker.DebtTotal` become principal-only — neither tracks interest. Steps 6 and 7 of [3.8.6](#386-state-changes) are replaced by the following; all other steps are unchanged. For a legacy (`LEVersion` absent) `Vault`, steps 6 and 7 of [3.8.6](#386-state-changes) continue to apply unchanged, even while this amendment is enabled: +Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 1` (cash-basis), origination does not recognise uncollected interest in `Vault.AssetsTotal`, and `LoanBroker.DebtTotal` tracks principal only. Steps 6 and 7 of [3.8.6](#386-state-changes) are replaced by the following; all other steps are unchanged. For a legacy (`LEVersion` absent) `Vault`, steps 6 and 7 of [3.8.6](#386-state-changes) continue to apply unchanged, even while this amendment is enabled: 6. Update `Vault` object: - Decrease `Vault.AssetsAvailable` by `PrincipalRequested`. - - `Vault.AssetsTotal` is **not** increased by `InterestDue` (`AssetsTotal` no longer tracks interest). + - `Vault.AssetsTotal` is unchanged (`InterestDue` is not recognised at origination). 7. Update `LoanBroker` object: - Increase `LoanBroker.DebtTotal` by `PrincipalRequested` only (`InterestDue` is excluded — `DebtTotal` no longer tracks interest). - Increment `LoanBroker.OwnerCount` by `1`. @@ -1342,7 +1338,7 @@ Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 1` ( ##### 3.10.5.1 State Changes (`LendingProtocolV1_1`) -Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 1` (cash-basis), `Vault.AssetsTotal` and `LoanBroker.DebtTotal` become principal-only. The `DefaultAmount`, `LossUnrealized`, and `LossReversed` computations in items 1–3 of [3.10.5](#3105-state-changes) are replaced by the following, collapsing each to a single, unified principal-only amount (all other steps — including the `MinimumCover`/`DefaultCovered`/`VaultLoss` mechanics, the `CoverAvailable` drawdown, and the `Loan` object updates — are unchanged). For a legacy (`LEVersion` absent) `Vault`, items 1–3 of [3.10.5](#3105-state-changes) continue to apply unchanged, even while this amendment is enabled: +Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 1` (cash-basis), `LoanBroker.DebtTotal` and the uncollected amount at risk are principal-only. The `DefaultAmount`, `LossUnrealized`, and `LossReversed` computations in items 1–3 of [3.10.5](#3105-state-changes) are replaced by the following, collapsing each to a single, unified principal-only amount (all other steps — including the `MinimumCover`/`DefaultCovered`/`VaultLoss` mechanics, the `CoverAvailable` drawdown, and the `Loan` object updates — are unchanged). For a legacy (`LEVersion` absent) `Vault`, items 1–3 of [3.10.5](#3105-state-changes) continue to apply unchanged, even while this amendment is enabled: 1. If the `tfLoanDefault` flag is specified: - Compute `DefaultAmount = Loan.PrincipalOutstanding` (principal only, replacing `Loan.TotalValueOutstanding - Loan.ManagementFeeOutstanding`). This single `DefaultAmount` is used, as before, for the `Vault` (`VaultLoss`/`DefaultCovered`) and `LoanBroker.DebtTotal` updates, and for `Vault.LossUnrealized` if the Loan was impaired. @@ -1416,7 +1412,7 @@ _Post-amendment (`LendingProtocolV1_1`):_ - In both trajectories, `LoanBroker.DebtTotal` and `Loan.PrincipalOutstanding`/`ManagementFeeOutstanding` fully clear to (or independent of) `0` at default, and `Vault.AssetsTotal ≈ Vault.AssetsAvailable` post-default (the Vault's books balance, up to the small rounding difference below) — the default mechanics are structurally unchanged. - The only real difference is that the pre-amendment `DefaultAmount`, `MinimumCover`, and `DefaultCovered` are all computed on figures that still include the (now-collected or now-forgiven) interest, while the post-amendment figures are computed on principal alone. This scales every downstream quantity (`MinimumCover`, `DefaultCovered`, `VaultLoss`, `CoverAvailable` drawn down) down proportionally by the amount of interest excluded — it does not change the _shape_ of the `CoverRateMinimum`/`CoverRateLiquidation` mechanics, only the base they are applied to. -- The `99,550.45` vs `99,550` difference in the final `Vault.AssetsTotal`/`AssetsAvailable` is exactly the `0.45` difference in `DefaultCovered` (`5.45` vs `5`), itself a consequence of `MinimumCover` being computed against a larger, interest-inclusive `DebtTotal` (`545`) pre-amendment vs. a principal-only `DebtTotal` (`500`) post-amendment. Since neither `AssetsTotal` nor `DebtTotal` carries interest information post-amendment, this `0.45` represents interest income the Vault would have (partially) recognized pre-amendment but never recognizes post-amendment — consistent with the loss-of-visibility tradeoff noted for a later conversation. +- The `99,550.45` vs `99,550` difference in the final `Vault.AssetsTotal`/`AssetsAvailable` is exactly the `0.45` difference in `DefaultCovered` (`5.45` vs `5`), itself a consequence of `MinimumCover` being computed against a larger, interest-inclusive `DebtTotal` (`545`) pre-amendment vs. a principal-only `DebtTotal` (`500`) post-amendment. This `0.45` is cover calculated from uncollected interest under accrual-basis accounting; cash-basis accounting never recognises that unpaid interest. - `Loan.ManagementFeeOutstanding` (`5`) is unaffected by the amendment in either trajectory: it was already excluded from `DefaultAmount` before the amendment (via the `- Loan.ManagementFeeOutstanding` term), and remains excluded after the amendment (since the amended `DefaultAmount` formula never references it). #### 3.10.6 Invariants @@ -1565,7 +1561,7 @@ These transfers are performed according to the asset type: ##### 3.11.5.1 State Changes (`LendingProtocolV1_1`) -Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 1` (cash-basis), `valueChange` is removed entirely from the `LoanBroker` and `Vault` accounting — since neither `LoanBroker.DebtTotal` nor `Vault.AssetsTotal` tracks interest anymore (see [3.8.6.1](#3861-state-changes-lendingprotocolv1_1)), there is nothing left to reconcile for late payments, overpayments, or early full repayments. Steps 6 and 7 of [3.11.5](#3115-state-changes) are replaced by the following (all other steps, including the asset transfers in section 4, are unchanged). For a legacy (`LEVersion` absent) `Vault`, steps 6 and 7 of [3.11.5](#3115-state-changes) continue to apply unchanged, even while this amendment is enabled: +Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 1` (cash-basis), `valueChange` is removed from the `LoanBroker` and `Vault` accounting: `LoanBroker.DebtTotal` tracks principal only, while `Vault.AssetsTotal` recognises actual interest receipts directly. Steps 6 and 7 of [3.11.5](#3115-state-changes) are replaced by the following (all other steps, including the asset transfers in section 4, are unchanged). For a legacy (`LEVersion` absent) `Vault`, steps 6 and 7 of [3.11.5](#3115-state-changes) continue to apply unchanged, even while this amendment is enabled: 6. **`LoanBroker` Updates**: - `LoanBroker.DebtTotal` is decreased by `principalPaid` only (`valueChange` does not apply) — the principal portion of the payment, from the same payment breakdown produced by [Appendix A-3](#a-3-loanpay-implementation-reference). @@ -1574,6 +1570,8 @@ Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 1` ( - `Vault.AssetsAvailable` increases by `totalToVault`. - `Vault.AssetsTotal` is increased by `interestPaid` only (`valueChange` does not apply) — the interest portion of the payment, net of management fee. +Because checks 6 and 14 of [3.8.5.2](#3852-protocol-level-failures) do not apply to a cash-basis Vault, collected interest may increase `Vault.AssetsTotal` above `Vault.AssetsMaximum`; the cap restricts deposits, not interest receipts. + `principalPaid` and `interestPaid` are computed by the same, unchanged `PeriodicPayment`/amortization logic in [Appendix A-3](#a-3-loanpay-implementation-reference) — no new machinery is introduced by this amendment; only the use of `valueChange` for `DebtTotal`/`AssetsTotal` is dropped. The `Loan` object's own `TotalValueOutstanding` is unaffected by this amendment and continues to be adjusted directly by the re-amortization recalculation described in step 5 of [3.11.5](#3115-state-changes). #### 3.11.6 Invariants From 0632fff8da0aaed582982eec60c93f33e63bc240 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Fri, 4 Sep 2026 16:57:57 +0200 Subject: [PATCH 09/32] docs: number the Amendments section as an Introduction subsection --- XLS-0065-single-asset-vault/README.md | 2 +- XLS-0066-lending-protocol/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/XLS-0065-single-asset-vault/README.md b/XLS-0065-single-asset-vault/README.md index 23d0464b1..6bbb755c3 100644 --- a/XLS-0065-single-asset-vault/README.md +++ b/XLS-0065-single-asset-vault/README.md @@ -69,7 +69,7 @@ Shares represent the ownership of a portion of the vault's assets. On-chain shar A protocol connecting to a Vault must track its debt. Furthermore, the updates to the Vault state when funds are removed or added back must be handled in the transactors of the protocol. For an example, please refer to the [Lending Protocol](../XLS-0066-lending-protocol/README.md) specification. -## Amendments +### 2.8. Amendments - [`LendingProtocolV1_1`](./65.1/README.md) introduces `LEVersion = 1` for cash-basis Vault accounting. diff --git a/XLS-0066-lending-protocol/README.md b/XLS-0066-lending-protocol/README.md index 6eddc46ab..93eddbcb6 100644 --- a/XLS-0066-lending-protocol/README.md +++ b/XLS-0066-lending-protocol/README.md @@ -116,7 +116,7 @@ The lending protocol charges a number of fees that the Loan Broker can configure ![Architecture Graph](./architecture.svg) -## Amendments +### 2.7 Amendments - [`LendingProtocolV1_1`](./66.1/README.md) introduces principal-only debt accounting and cash-basis interest recognition for Vaults with `LEVersion = 1`. From 1d0f16d56329349ee9a519520e0b50936176fb70 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:04:10 +0200 Subject: [PATCH 10/32] docs: consolidate the LendingProtocolV1_1 patches into one document per spec --- XLS-0065-single-asset-vault/65.1/README.md | 36 +++++++++++---------- XLS-0065-single-asset-vault/README.md | 5 +-- XLS-0066-lending-protocol/66.1/README.md | 37 ++++++++++++---------- XLS-0066-lending-protocol/README.md | 5 +-- 4 files changed, 46 insertions(+), 37 deletions(-) diff --git a/XLS-0065-single-asset-vault/65.1/README.md b/XLS-0065-single-asset-vault/65.1/README.md index a509477fe..1838d7265 100644 --- a/XLS-0065-single-asset-vault/65.1/README.md +++ b/XLS-0065-single-asset-vault/65.1/README.md @@ -1,7 +1,7 @@
   xls: 65.1
-  title: Vault Cash-Basis Accounting
-  description: Adds the Vault LEVersion field, which selects cash-basis accounting for the assets of a Vault
+  title: Single Asset Vault under LendingProtocolV1_1
+  description: Records the changes the LendingProtocolV1_1 amendment makes to XLS-65
   author: Vytautas Vito Tumas , Aanchal Malhotra 
   proposal-from: https://github.com/XRPLF/XRPL-Standards/discussions/192
   status: Draft
@@ -11,31 +11,35 @@
   updated: 2026-09-04
 
-# Vault Cash-Basis Accounting +# Single Asset Vault under `LendingProtocolV1_1` ## 1. Abstract -This patch of [XLS-65](../README.md) records the changes the `LendingProtocolV1_1` amendment makes to the `Vault` ledger entry. The amendment adds a `LEVersion` field that selects the accounting model a Vault applies to `AssetsTotal`. Vaults created while the amendment is enabled use cash-basis accounting, in which `AssetsTotal` excludes uncollected interest and grows only as interest is collected in cash. Vaults created before the amendment keep accrual-basis accounting, in which `AssetsTotal` includes interest as it accrues. +This patch of [XLS-65](../README.md) records the changes the `LendingProtocolV1_1` amendment makes to the Single Asset Vault. The amendment is not yet live. The consolidated specification is the top-level [README.md](../README.md). -The consolidated specification is the top-level [README.md](../README.md). +The amendment makes the following changes to XLS-65: + +- **Vault Cash-Basis Accounting** — Adds a `LEVersion` field that selects the accounting model a Vault applies to `AssetsTotal`: Vaults created while the amendment is enabled use cash-basis accounting, in which `AssetsTotal` excludes uncollected interest and grows only as interest is collected in cash, and Vaults created before the amendment keep accrual-basis accounting, in which `AssetsTotal` includes interest as it accrues. ## 2. Motivation -Accrual-basis accounting credits a Vault with interest that a Borrower has not paid yet, so the share exchange rate reflects income the Vault has not received. A depositor can redeem shares at a rate inflated by interest that later defaults, and the loss falls on the depositors who remain. Cash-basis accounting removes that mismatch by recognising interest only when it is paid. +**Vault Cash-Basis Accounting.** Accrual-basis accounting credits a Vault with interest that a Borrower has not paid yet, so the share exchange rate reflects income the Vault has not received. A depositor can redeem shares at a rate inflated by interest that later defaults, and the loss falls on the depositors who remain. Cash-basis accounting removes that mismatch by recognising interest only when it is paid. Existing Vaults cannot switch accounting models without changing the value of shares already issued, so the model has to be recorded per Vault rather than derived from the amendment state at the time of the transaction. ## 3. Specification -### 3.1 Ledger Entry: `Vault` +### 3.3 Vault Cash-Basis Accounting + +#### 3.3.1 Ledger Entry: `Vault` -#### 3.1.1 Fields +##### 3.3.1.1 Fields | Field Name | Constant | Required | Internal Type | Default Value | Description | | ----------- | :------: | :------: | :-----------: | :-----------: | ------------------------------------------------------------------------ | | `LEVersion` | Yes | No | `UINT8` | absent (`0`) | The accounting model the Vault applies to `AssetsTotal`. Absent is treated as `0` (accrual basis). Immutable once set. | -#### 3.1.2 `LEVersion` +##### 3.3.1.2 `LEVersion` `LEVersion` selects the accounting model of the Vault: @@ -44,12 +48,12 @@ Existing Vaults cannot switch accounting models without changing the value of sh The field is immutable. A Vault created while the amendment is enabled is always cash-basis, and a Vault created before it is always accrual-basis; neither can be converted to the other. -#### 3.1.3 Invariants +##### 3.3.1.3 Invariants - `Vault.LEVersion` is immutable once set. - `Vault.LEVersion` is either absent or `1`. -#### 3.1.4 Example JSON +##### 3.3.1.4 Example JSON ```json { @@ -62,24 +66,24 @@ The field is immutable. A Vault created while the amendment is enabled is always } ``` -### 3.2 Transaction: `VaultCreate` +#### 3.3.2 Transaction: `VaultCreate` -#### 3.2.1 Failure Conditions +##### 3.3.2.1 Failure Conditions No failure conditions are added or removed. `LEVersion` is not a transaction field and cannot be supplied by the submitter. -#### 3.2.2 State Changes +##### 3.3.2.2 State Changes When the amendment is enabled, the created `Vault` ledger entry has `LEVersion = 1`. When it is not enabled, the field is not written and the Vault is accrual-basis. ## 4. Rationale -The alternative to a per-entry version field is to derive the accounting model from the amendment state at the time each Loan transaction executes. That was rejected because it would silently change the share exchange rate of every existing Vault on the ledger at the moment the amendment activates. +**Vault Cash-Basis Accounting.** The alternative to a per-entry version field is to derive the accounting model from the amendment state at the time each Loan transaction executes. That was rejected because it would silently change the share exchange rate of every existing Vault on the ledger at the moment the amendment activates. `LEVersion` is a version number rather than a boolean flag so that a later accounting change can be expressed as a further version without another field. ## 5. Security Considerations -Cash-basis accounting reduces, but does not remove, the exposure of a depositor to an unpaid Loan: the Vault still carries the principal at face value until the Loan defaults or is impaired. `LossUnrealized` remains the mechanism for reporting an expected shortfall. +**Vault Cash-Basis Accounting.** Cash-basis accounting reduces, but does not remove, the exposure of a depositor to an unpaid Loan: the Vault still carries the principal at face value until the Loan defaults or is impaired. `LossUnrealized` remains the mechanism for reporting an expected shortfall. Because `LEVersion` is immutable, an implementation must read it from the `Vault` ledger entry rather than from the amendment state when computing the interest accounting of a Loan. Reading the amendment state instead would apply cash-basis rules to accrual-basis Vaults and misprice their shares. diff --git a/XLS-0065-single-asset-vault/README.md b/XLS-0065-single-asset-vault/README.md index 6bbb755c3..43cf69589 100644 --- a/XLS-0065-single-asset-vault/README.md +++ b/XLS-0065-single-asset-vault/README.md @@ -71,7 +71,8 @@ A protocol connecting to a Vault must track its debt. Furthermore, the updates t ### 2.8. Amendments -- [`LendingProtocolV1_1`](./65.1/README.md) introduces `LEVersion = 1` for cash-basis Vault accounting. +- `LendingProtocolV1_1` (not yet live), as described in [XLS-65.1](./65.1/README.md): + - introduces `LEVersion = 1` for cash-basis Vault accounting. ## 3. Specification @@ -1195,4 +1196,4 @@ No, neither of the transactions charge transfer fees when depositing or withdraw ## Appendix C: Changelog -- XLS-65.1: Cash-basis Vault accounting under `LendingProtocolV1_1`, not yet live — [XLS-65.1](./65.1/README.md) +- XLS-65.1: Single Asset Vault under `LendingProtocolV1_1`, not yet live — [XLS-65.1](./65.1/README.md) diff --git a/XLS-0066-lending-protocol/66.1/README.md b/XLS-0066-lending-protocol/66.1/README.md index 4b7329ded..e1b4661cf 100644 --- a/XLS-0066-lending-protocol/66.1/README.md +++ b/XLS-0066-lending-protocol/66.1/README.md @@ -1,34 +1,37 @@
   xls: 66.1
-  title: Principal-Only Debt Accounting
-  description: Makes LoanBroker.DebtTotal principal-only and recognizes Vault interest on receipt for cash-basis Vaults
+  title: Lending Protocol under LendingProtocolV1_1
+  description: Records the changes the LendingProtocolV1_1 amendment makes to XLS-66
   author: Vytautas Vito Tumas , Aanchal Malhotra 
-  implementation: https://github.com/XRPLF/rippled/pull/5270
   proposal-from: https://github.com/XRPLF/XRPL-Standards/discussions/190
   status: Draft
   category: Amendment
-  requires: [XLS-66](../README.md), [XLS-65.1](../../XLS-0065-single-asset-vault/65.1/README.md)
+  requires: [XLS-66](../README.md)
   created: 2026-09-04
   updated: 2026-09-04
 
-# Principal-Only Debt Accounting +# Lending Protocol under `LendingProtocolV1_1` ## 1. Abstract -This patch of [XLS-66](../README.md) records the changes the `LendingProtocolV1_1` amendment makes to the Lending Protocol. For a Vault with `LEVersion == 1` (cash basis, see [XLS-65.1](../../XLS-0065-single-asset-vault/65.1/README.md)), `LoanBroker.DebtTotal` tracks loan principal only and `Vault.AssetsTotal` recognises interest when it is collected rather than when a Loan is issued. +This patch of [XLS-66](../README.md) records the changes the `LendingProtocolV1_1` amendment makes to the Lending Protocol. The amendment is not yet live. The consolidated specification is the top-level [README.md](../README.md). -The consolidated specification is the top-level [README.md](../README.md). +The amendment makes the following changes to XLS-66: + +- **Principal-Only Debt Accounting** — For a Vault with `LEVersion == 1` (cash basis, see [XLS-65.1](../../XLS-0065-single-asset-vault/65.1/README.md)), `LoanBroker.DebtTotal` tracks loan principal only and `Vault.AssetsTotal` recognises interest when it is collected rather than when a Loan is issued. ## 2. Motivation -Under accrual accounting, issuing a Loan immediately increases `Vault.AssetsTotal` by the expected interest and `LoanBroker.DebtTotal` by the principal plus expected interest. The `AssetsMaximum` and `DebtMaximum` caps are therefore consumed by interest that has not been paid, and the first-loss capital requirement, which is a rate applied to `DebtTotal`, is sized against expected interest as well as principal. +**Principal-Only Debt Accounting.** Under accrual accounting, issuing a Loan immediately increases `Vault.AssetsTotal` by the expected interest and `LoanBroker.DebtTotal` by the principal plus expected interest. The `AssetsMaximum` and `DebtMaximum` caps are therefore consumed by interest that has not been paid, and the first-loss capital requirement, which is a rate applied to `DebtTotal`, is sized against expected interest as well as principal. ## 3. Specification -### 3.1 Transaction: `LoanSet` +### 3.1 Principal-Only Debt Accounting + +#### 3.1.1 Transaction: `LoanSet` -#### 3.1.1 Failure Conditions +##### 3.1.1.1 Failure Conditions For a Vault with `LEVersion == 1`, checks 6 and 14 of the parent specification do not apply, and the Broker cap checks 19 and 20 are replaced by principal-only checks: @@ -37,13 +40,13 @@ For a Vault with `LEVersion == 1`, checks 6 and 14 of the parent specification d For a Vault with `LEVersion` absent, parent checks 6, 14, 19, and 20 apply unchanged. -#### 3.1.2 State Changes +##### 3.1.1.2 State Changes For a Vault with `LEVersion == 1`, issuing a Loan leaves `Vault.AssetsTotal` unchanged and increases `LoanBroker.DebtTotal` by `PrincipalRequested`. For a Vault with `LEVersion` absent, issuing a Loan increases `Vault.AssetsTotal` by `InterestDue` and increases `LoanBroker.DebtTotal` by `PrincipalRequested + InterestDue`. -### 3.2 Transaction: `LoanPay` +#### 3.1.2 Transaction: `LoanPay` -#### 3.2.1 State Changes +##### 3.1.2.1 State Changes For a Vault with `LEVersion == 1`, a payment splits into principal and interest: @@ -55,18 +58,18 @@ Principal repayment does not reduce `Vault.AssetsTotal`. Because the cash-basis For a Vault with `LEVersion` absent, the parent accrual-basis state changes apply unchanged. -### 3.3 Transaction: `LoanManage` +#### 3.1.3 Transaction: `LoanManage` -#### 3.3.1 State Changes +##### 3.1.3.1 State Changes For a Vault with `LEVersion == 1`, a default writes off principal only, because uncollected interest was never recognised. For a Vault with `LEVersion` absent, the write-off covers principal and accrued interest. ## 4. Rationale -Interest could have been recognised on a schedule, one payment period at a time, rather than on receipt. That was rejected because it reintroduces the original problem in a smaller form: the Vault would still credit itself with interest for a period in which the Borrower ends up not paying. +**Principal-Only Debt Accounting.** Interest could have been recognised on a schedule, one payment period at a time, rather than on receipt. That was rejected because it reintroduces the original problem in a smaller form: the Vault would still credit itself with interest for a period in which the Borrower ends up not paying. ## 5. Security Considerations -Principal-only accounting makes `DebtTotal` a smaller number for the same set of Loans, so a `CoverRateMinimum` that was calibrated under accrual accounting yields less first-loss capital. A Broker migrating to a cash-basis Vault should re-derive the rate rather than reuse it. +**Principal-Only Debt Accounting.** Principal-only accounting makes `DebtTotal` a smaller number for the same set of Loans, so a `CoverRateMinimum` that was calibrated under accrual accounting yields less first-loss capital. A Broker migrating to a cash-basis Vault should re-derive the rate rather than reuse it. Both accounting models coexist on the ledger for as long as pre-amendment Vaults exist. An implementation must branch on `Vault.LEVersion` in every place it adjusts `AssetsTotal` or `DebtTotal`; branching in some places and not others corrupts the share exchange rate of the Vault. diff --git a/XLS-0066-lending-protocol/README.md b/XLS-0066-lending-protocol/README.md index 93eddbcb6..d0cc72999 100644 --- a/XLS-0066-lending-protocol/README.md +++ b/XLS-0066-lending-protocol/README.md @@ -118,7 +118,8 @@ The lending protocol charges a number of fees that the Loan Broker can configure ### 2.7 Amendments -- [`LendingProtocolV1_1`](./66.1/README.md) introduces principal-only debt accounting and cash-basis interest recognition for Vaults with `LEVersion = 1`. +- `LendingProtocolV1_1` (not yet live), as described in [XLS-66.1](./66.1/README.md): + - introduces principal-only debt accounting and cash-basis interest recognition for Vaults with `LEVersion = 1`. ## 3. Specification @@ -2797,4 +2798,4 @@ function make_payment(amount, currentTime) -> (principalPaid, interestPaid, valu ## Appendix C: Changelog -- XLS-66.1: Principal-only debt accounting under `LendingProtocolV1_1`, not yet live — [XLS-66.1](./66.1/README.md) +- XLS-66.1: Lending Protocol under `LendingProtocolV1_1`, not yet live — [XLS-66.1](./66.1/README.md) From 215f13a34302f2721f56a1b53677c6db7564dcce Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Mon, 7 Sep 2026 11:40:41 +0200 Subject: [PATCH 11/32] docs: restore cash-basis LoanBroker and LoanManage details Put the closed-ended Vault gate and principal-only impairment formulas back, and keep LEVersion marked Constant Yes with the other lifecycle fields. --- XLS-0066-lending-protocol/66.1/README.md | 51 +++++++++++++++++++----- XLS-0066-lending-protocol/README.md | 7 ++++ 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/XLS-0066-lending-protocol/66.1/README.md b/XLS-0066-lending-protocol/66.1/README.md index e1b4661cf..40ed6d145 100644 --- a/XLS-0066-lending-protocol/66.1/README.md +++ b/XLS-0066-lending-protocol/66.1/README.md @@ -6,7 +6,7 @@ proposal-from: https://github.com/XRPLF/XRPL-Standards/discussions/190 status: Draft category: Amendment - requires: [XLS-66](../README.md) + requires: [XLS-66](../README.md), [XLS-65.1](../../XLS-0065-single-asset-vault/65.1/README.md) created: 2026-09-04 updated: 2026-09-04 @@ -20,33 +20,46 @@ This patch of [XLS-66](../README.md) records the changes the `LendingProtocolV1_ The amendment makes the following changes to XLS-66: - **Principal-Only Debt Accounting** — For a Vault with `LEVersion == 1` (cash basis, see [XLS-65.1](../../XLS-0065-single-asset-vault/65.1/README.md)), `LoanBroker.DebtTotal` tracks loan principal only and `Vault.AssetsTotal` recognises interest when it is collected rather than when a Loan is issued. +- **Closed-Ended Lending Vaults** — A new `LoanBroker` can only attach to a closed-ended Vault. ## 2. Motivation **Principal-Only Debt Accounting.** Under accrual accounting, issuing a Loan immediately increases `Vault.AssetsTotal` by the expected interest and `LoanBroker.DebtTotal` by the principal plus expected interest. The `AssetsMaximum` and `DebtMaximum` caps are therefore consumed by interest that has not been paid, and the first-loss capital requirement, which is a rate applied to `DebtTotal`, is sized against expected interest as well as principal. +**Closed-Ended Lending Vaults.** An open-ended Vault allows redemptions while its assets are committed to Loans. Requiring a closed-ended Vault gives the lending protocol defined subscription, investment, and redemption phases. + ## 3. Specification -### 3.1 Principal-Only Debt Accounting +### 3.1 Closed-Ended Lending Vaults -#### 3.1.1 Transaction: `LoanSet` +#### 3.1.1 Transaction: `LoanBrokerSet` ##### 3.1.1.1 Failure Conditions +When creating a new `LoanBroker` (`LoanBrokerID` is not specified), the following protocol-level failure is added after item 2 of the parent specification. Modifying an existing `LoanBroker` does not re-evaluate it. + +1. The `Vault` identified by `VaultID` is not closed-ended (`Vault.VaultKind` is absent or not equal to `1`). (`tecNO_PERMISSION`) + +### 3.2 Principal-Only Debt Accounting + +#### 3.2.1 Transaction: `LoanSet` + +##### 3.2.1.1 Failure Conditions + For a Vault with `LEVersion == 1`, checks 6 and 14 of the parent specification do not apply, and the Broker cap checks 19 and 20 are replaced by principal-only checks: -1. `LoanBroker.DebtMaximum != 0` and `LoanBroker.DebtMaximum < LoanBroker.DebtTotal + PrincipalRequested`. (`tecLIMIT_EXCEEDED`) -2. `LoanBroker.CoverAvailable < (LoanBroker.DebtTotal + PrincipalRequested) × LoanBroker.CoverRateMinimum`. (`tecINSUFFICIENT_FUNDS`) +19. `LoanBroker.DebtMaximum != 0` and `LoanBroker.DebtMaximum < LoanBroker.DebtTotal + PrincipalRequested`. (`tecLIMIT_EXCEEDED`) +20. `LoanBroker.CoverAvailable < (LoanBroker.DebtTotal + PrincipalRequested) × LoanBroker.CoverRateMinimum`. (`tecINSUFFICIENT_FUNDS`) For a Vault with `LEVersion` absent, parent checks 6, 14, 19, and 20 apply unchanged. -##### 3.1.1.2 State Changes +##### 3.2.1.2 State Changes For a Vault with `LEVersion == 1`, issuing a Loan leaves `Vault.AssetsTotal` unchanged and increases `LoanBroker.DebtTotal` by `PrincipalRequested`. For a Vault with `LEVersion` absent, issuing a Loan increases `Vault.AssetsTotal` by `InterestDue` and increases `LoanBroker.DebtTotal` by `PrincipalRequested + InterestDue`. -#### 3.1.2 Transaction: `LoanPay` +#### 3.2.2 Transaction: `LoanPay` -##### 3.1.2.1 State Changes +##### 3.2.2.1 State Changes For a Vault with `LEVersion == 1`, a payment splits into principal and interest: @@ -58,16 +71,32 @@ Principal repayment does not reduce `Vault.AssetsTotal`. Because the cash-basis For a Vault with `LEVersion` absent, the parent accrual-basis state changes apply unchanged. -#### 3.1.3 Transaction: `LoanManage` +#### 3.2.3 Transaction: `LoanManage` + +##### 3.2.3.1 Failure Conditions -##### 3.1.3.1 State Changes +For a Vault with `LEVersion == 1`, parent check 8 is replaced by the following principal-only impairment check: -For a Vault with `LEVersion == 1`, a default writes off principal only, because uncollected interest was never recognised. For a Vault with `LEVersion` absent, the write-off covers principal and accrued interest. +8. `tfLoanImpair` is specified and `Vault.LossUnrealized + Loan.PrincipalOutstanding > Vault.AssetsTotal - Vault.AssetsAvailable`. (`tecLIMIT_EXCEEDED`) + +For a Vault with `LEVersion` absent, parent check 8 applies unchanged. + +##### 3.2.3.2 State Changes + +For a Vault with `LEVersion == 1`, the parent `LoanManage` state changes use the following principal-only amounts: + +- For `tfLoanDefault`, `DefaultAmount = Loan.PrincipalOutstanding`. +- For `tfLoanImpair`, `LossUnrealized = Loan.PrincipalOutstanding`. +- For `tfLoanUnimpair`, `LossReversed = Loan.PrincipalOutstanding`. + +All downstream default calculations and state changes use `DefaultAmount` unchanged. For a Vault with `LEVersion` absent, the parent accrual-basis state changes apply unchanged. ## 4. Rationale **Principal-Only Debt Accounting.** Interest could have been recognised on a schedule, one payment period at a time, rather than on receipt. That was rejected because it reintroduces the original problem in a smaller form: the Vault would still credit itself with interest for a period in which the Borrower ends up not paying. +**Closed-Ended Lending Vaults.** The restriction is enforced when the `LoanBroker` is created rather than for every `LoanSet`, because that is when the Vault is attached to the lending protocol. + ## 5. Security Considerations **Principal-Only Debt Accounting.** Principal-only accounting makes `DebtTotal` a smaller number for the same set of Loans, so a `CoverRateMinimum` that was calibrated under accrual accounting yields less first-loss capital. A Broker migrating to a cash-basis Vault should re-derive the rate rather than reuse it. diff --git a/XLS-0066-lending-protocol/README.md b/XLS-0066-lending-protocol/README.md index d0cc72999..06782294b 100644 --- a/XLS-0066-lending-protocol/README.md +++ b/XLS-0066-lending-protocol/README.md @@ -120,6 +120,7 @@ The lending protocol charges a number of fees that the Loan Broker can configure - `LendingProtocolV1_1` (not yet live), as described in [XLS-66.1](./66.1/README.md): - introduces principal-only debt accounting and cash-basis interest recognition for Vaults with `LEVersion = 1`. + - requires a new `LoanBroker` to attach to a closed-ended Vault. ## 3. Specification @@ -607,6 +608,12 @@ This transaction uses the standard transaction fee. 10. Any value field (e.g., `DebtMaximum`) cannot be represented in the `Vault.Asset` type without precision loss (relevant for XRP and MPT). (`tecPRECISION_LOSS`) +##### 3.3.3.3 Protocol-Level Failures (`LendingProtocolV1_1`) + +When creating a new `LoanBroker` (`LoanBrokerID` is not specified), the following check is added after item 2 of [3.3.3.2](#3332-protocol-level-failures). Modifying an existing `LoanBroker` does not re-evaluate it. + +1. The `Vault` identified by `VaultID` is not closed-ended (`Vault.VaultKind` is absent or not equal to `1`). (`tecNO_PERMISSION`) + #### 3.3.4 State Changes **If `LoanBrokerID` is not specified (creating new):** From df735b0dbdc3733df2a19eb4cfaf1f330d451a09 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Mon, 7 Sep 2026 15:23:43 +0200 Subject: [PATCH 12/32] docs: match Vault Constant column and LoanSet phase checks Align immutable Vault fields with the invariant, drop the invalid SharesTotal example field, and document the closed-ended LoanSet preclaim failures. --- XLS-0065-single-asset-vault/65.1/README.md | 19 ++++++---- XLS-0065-single-asset-vault/README.md | 44 +++++++++++----------- XLS-0066-lending-protocol/66.1/README.md | 10 ++++- XLS-0066-lending-protocol/README.md | 12 ++++-- 4 files changed, 50 insertions(+), 35 deletions(-) diff --git a/XLS-0065-single-asset-vault/65.1/README.md b/XLS-0065-single-asset-vault/65.1/README.md index 1838d7265..2bafbbbb5 100644 --- a/XLS-0065-single-asset-vault/65.1/README.md +++ b/XLS-0065-single-asset-vault/65.1/README.md @@ -8,7 +8,7 @@ category: Amendment requires: [XLS-65](../README.md) created: 2026-09-04 - updated: 2026-09-04 + updated: 2026-09-07 # Single Asset Vault under `LendingProtocolV1_1` @@ -35,8 +35,8 @@ Existing Vaults cannot switch accounting models without changing the value of sh ##### 3.3.1.1 Fields -| Field Name | Constant | Required | Internal Type | Default Value | Description | -| ----------- | :------: | :------: | :-----------: | :-----------: | ------------------------------------------------------------------------ | +| Field Name | Constant | Required | Internal Type | Default Value | Description | +| ----------- | :------: | :------: | :-----------: | :-----------: | ---------------------------------------------------------------------------------------------------------------------- | | `LEVersion` | Yes | No | `UINT8` | absent (`0`) | The accounting model the Vault applies to `AssetsTotal`. Absent is treated as `0` (accrual basis). Immutable once set. | ##### 3.3.1.2 `LEVersion` @@ -46,12 +46,13 @@ Existing Vaults cannot switch accounting models without changing the value of sh - `LEVersion` absent (treated as `0`, accrual basis): `AssetsTotal` includes the interest accrued over the life of the connected Loans. This is the pre-amendment behaviour and continues to apply to every Vault created before the amendment was enabled. - `LEVersion = 1` (cash basis): `AssetsTotal` excludes uncollected interest and increases only as interest is collected in cash. -The field is immutable. A Vault created while the amendment is enabled is always cash-basis, and a Vault created before it is always accrual-basis; neither can be converted to the other. +The field is written once by `VaultCreate` and is immutable after creation. A Vault created while the amendment is enabled is always cash-basis, and a Vault created before it is always accrual-basis; neither can be converted to the other. ##### 3.3.1.3 Invariants - `Vault.LEVersion` is immutable once set. -- `Vault.LEVersion` is either absent or `1`. + +Because `VaultCreate` is the only transaction that writes `LEVersion`, newly created Vaults have either an absent `LEVersion` or `LEVersion = 1`. ##### 3.3.1.4 Example JSON @@ -59,10 +60,12 @@ The field is immutable. A Vault created while the amendment is enabled is always { "LedgerEntryType": "Vault", "LEVersion": 1, - "Asset": { "currency": "USD", "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn" }, + "Asset": { + "currency": "USD", + "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn" + }, "AssetsTotal": "1000", - "AssetsAvailable": "1000", - "SharesTotal": "1000" + "AssetsAvailable": "1000" } ``` diff --git a/XLS-0065-single-asset-vault/README.md b/XLS-0065-single-asset-vault/README.md index 43cf69589..d67916b09 100644 --- a/XLS-0065-single-asset-vault/README.md +++ b/XLS-0065-single-asset-vault/README.md @@ -8,7 +8,7 @@ category: Amendment requires: [XLS-33](../XLS-0033-multi-purpose-tokens/README.md) created: 2024-04-12 - updated: 2026-09-04 + updated: 2026-09-07 # Single Asset Vault @@ -92,26 +92,26 @@ The key of the `Vault` object is the result of [`SHA512-Half`](https://xrpl.org/ A vault has the following fields: -| Field Name | Constant | Required | JSON Type | Internal Type | Default Value | Description | -| ------------------- | :------: | :------: | :----------------: | :-----------: | :-----------: | :----------------------------------------------------------------------------------------------------------------------------------------------------- | -| `LedgerEntryType` | No | Yes | `string` | `UINT16` | `0x0084` | Ledger object type. | -| `LedgerIndex` | No | Yes | `string` | `UINT16` | `N/A` | Ledger object identifier. | -| `Flags` | Yes | Yes | `string` | `UINT32` | 0 | Ledger object flags. | -| `PreviousTxnID` | No | Yes | `string` | `HASH256` | `N/A` | Identifies the transaction ID that most recently modified this object. | -| `PreviousTxnLgrSeq` | No | Yes | `number` | `UINT32` | `N/A` | The sequence of the ledger that contains the transaction that most recently modified this object. | -| `Sequence` | No | Yes | `number` | `UINT32` | `N/A` | The transaction sequence number that created the vault. | -| `OwnerNode` | No | Yes | `number` | `UINT64` | `N/A` | Identifies the page where this item is referenced in the owner's directory. | -| `Owner` | No | Yes | `string` | `AccountID` | `N/A` | The account address of the Vault Owner. | -| `Account` | No | Yes | `string` | `AccountID` | `N/A` | The address of the Vaults _pseudo-account_. | -| `Data` | Yes | No | `string` | `BLOB` | None | Arbitrary metadata about the Vault. Limited to 256 bytes. | -| `Asset` | No | Yes | `string or object` | `ISSUE` | `N/A` | The asset of the vault. The vault supports `XRP`, `IOU` and `MPT`. | -| `AssetsTotal` | No | Yes | `number` | `NUMBER` | 0 | The total value of the vault. | -| `AssetsAvailable` | No | Yes | `number` | `NUMBER` | 0 | The asset amount that is available in the vault. | -| `LossUnrealized` | No | Yes | `number` | `NUMBER` | 0 | The potential loss amount that is not yet realized expressed as the vaults asset. | -| `AssetsMaximum` | Yes | No | `number` | `NUMBER` | 0 | The maximum asset amount that can be held in the vault. Zero value `0` indicates there is no cap. | -| `ShareMPTID` | No | Yes | `number` | `UINT192` | 0 | The identifier of the share MPTokenIssuance object. | -| `WithdrawalPolicy` | No | Yes | `string` | `UINT8` | `N/A` | Indicates the withdrawal strategy used by the Vault. | -| `Scale` | No | Yes | `number` | `UINT8` | 6 | The `Scale` specifies the power of 10 ($10^{\text{scale}}$) to multiply an asset's value by when converting it into an integer-based number of shares. | +| Field Name | Constant | Required | JSON Type | Internal Type | Default Value | Description | +| ------------------- | :------: | :------: | :----------------: | :-----------: | :-----------: | :------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `LedgerEntryType` | No | Yes | `string` | `UINT16` | `0x0084` | Ledger object type. | +| `LedgerIndex` | No | Yes | `string` | `UINT16` | `N/A` | Ledger object identifier. | +| `Flags` | Yes | Yes | `string` | `UINT32` | 0 | Ledger object flags. | +| `PreviousTxnID` | No | Yes | `string` | `HASH256` | `N/A` | Identifies the transaction ID that most recently modified this object. | +| `PreviousTxnLgrSeq` | No | Yes | `number` | `UINT32` | `N/A` | The sequence of the ledger that contains the transaction that most recently modified this object. | +| `Sequence` | Yes | Yes | `number` | `UINT32` | `N/A` | The transaction sequence number that created the vault. | +| `OwnerNode` | Yes | Yes | `number` | `UINT64` | `N/A` | Identifies the page where this item is referenced in the owner's directory. | +| `Owner` | Yes | Yes | `string` | `AccountID` | `N/A` | The account address of the Vault Owner. | +| `Account` | Yes | Yes | `string` | `AccountID` | `N/A` | The address of the Vaults _pseudo-account_. | +| `Data` | No | No | `string` | `BLOB` | None | Arbitrary metadata about the Vault. Limited to 256 bytes. | +| `Asset` | Yes | Yes | `string or object` | `ISSUE` | `N/A` | The asset of the vault. The vault supports `XRP`, `IOU` and `MPT`. | +| `AssetsTotal` | No | Yes | `number` | `NUMBER` | 0 | The total value of the vault. | +| `AssetsAvailable` | No | Yes | `number` | `NUMBER` | 0 | The asset amount that is available in the vault. | +| `LossUnrealized` | No | Yes | `number` | `NUMBER` | 0 | The potential loss amount that is not yet realized expressed as the vaults asset. | +| `AssetsMaximum` | No | No | `number` | `NUMBER` | 0 | The maximum asset amount that can be held in the vault. Zero value `0` indicates there is no cap. | +| `ShareMPTID` | Yes | Yes | `number` | `UINT192` | 0 | The identifier of the share MPTokenIssuance object. | +| `WithdrawalPolicy` | Yes | Yes | `string` | `UINT8` | `N/A` | Indicates the withdrawal strategy used by the Vault. | +| `Scale` | Yes | Yes | `number` | `UINT8` | 6 | The `Scale` specifies the power of 10 ($10^{\text{scale}}$) to multiply an asset's value by when converting it into an integer-based number of shares. | | `LEVersion` | Yes | No | `number` | `UINT8` | absent (`0`) | Immutable protocol-selected ledger entry version. Absent is treated as `0` (legacy / accrual-basis). See [3.1.2.2](#3122-leversion-lendingprotocolv1_1). | ##### 3.1.2.1 Flags @@ -126,7 +126,7 @@ The `Vault` object supports the following flags: `LEVersion` is a generalized ledger entry schema version field, introduced by the `LendingProtocolV1_1` amendment. Rather than encoding schema variants as ledger entry flags — which structurally allow mutually exclusive states to be set simultaneously — `LEVersion` is a dedicated field that unambiguously identifies which schema/behavior revision a ledger entry follows. While `LEVersion` is a general-purpose mechanism applicable to any ledger entry type, `Vault` is its first consumer. -`LEVersion` is strictly protocol-driven: it is never a field of any transaction and cannot be set or changed by the transaction submitter. It is written exclusively by protocol logic, such as a Transactor's state changes at amendment activation. +`LEVersion` is strictly protocol-driven: it is never a field of any transaction and cannot be set or changed by the transaction submitter. It is written once, by `VaultCreate`, when the Vault is created. For the `Vault` object, `LEVersion` distinguishes the share-valuation and accounting behavior applied to the vault: diff --git a/XLS-0066-lending-protocol/66.1/README.md b/XLS-0066-lending-protocol/66.1/README.md index 40ed6d145..27b1d238b 100644 --- a/XLS-0066-lending-protocol/66.1/README.md +++ b/XLS-0066-lending-protocol/66.1/README.md @@ -8,7 +8,7 @@ category: Amendment requires: [XLS-66](../README.md), [XLS-65.1](../../XLS-0065-single-asset-vault/65.1/README.md) created: 2026-09-04 - updated: 2026-09-04 + updated: 2026-09-07 # Lending Protocol under `LendingProtocolV1_1` @@ -46,7 +46,13 @@ When creating a new `LoanBroker` (`LoanBrokerID` is not specified), the followin ##### 3.2.1.1 Failure Conditions -For a Vault with `LEVersion == 1`, checks 6 and 14 of the parent specification do not apply, and the Broker cap checks 19 and 20 are replaced by principal-only checks: +The following checks are added after parent check 5 and before parent check 6. They apply to every closed-ended Vault, regardless of `LEVersion`; an open-ended Vault has no phase and skips these checks: + +1. The Vault is in the Subscription phase. (`tecTOO_SOON`) +2. The Vault is in the Redemption phase. (`tecEXPIRED`) +3. The Vault is in the Investment phase, but the final scheduled payment at `StartDate + (PaymentInterval × PaymentTotal)` leaves fewer than 60 seconds before `Vault.RedemptionDate`. (`tecNO_PERMISSION`) + +After these phase and schedule checks, for a Vault with `LEVersion == 1`, parent checks 6 and 14 do not apply, and the Broker cap checks 19 and 20 are replaced by principal-only checks: 19. `LoanBroker.DebtMaximum != 0` and `LoanBroker.DebtMaximum < LoanBroker.DebtTotal + PrincipalRequested`. (`tecLIMIT_EXCEEDED`) 20. `LoanBroker.CoverAvailable < (LoanBroker.DebtTotal + PrincipalRequested) × LoanBroker.CoverRateMinimum`. (`tecINSUFFICIENT_FUNDS`) diff --git a/XLS-0066-lending-protocol/README.md b/XLS-0066-lending-protocol/README.md index 06782294b..95464a798 100644 --- a/XLS-0066-lending-protocol/README.md +++ b/XLS-0066-lending-protocol/README.md @@ -8,7 +8,7 @@ category: Amendment requires: XLS-65, XLS-64 created: 2024-10-18 - updated: 2026-09-04 + updated: 2026-09-07 proposal-from: https://github.com/XRPLF/XRPL-Standards/discussions/190 @@ -1103,7 +1103,13 @@ The account specified in the `Account` field pays the transaction fee. ##### 3.8.5.3 Protocol-Level Failures (`LendingProtocolV1_1`) -Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 1` (cash-basis, see [XLS-65 §3.1.2.2](../XLS-0065-single-asset-vault/README.md#3122-leversion-lendingprotocolv1_1)), checks 6 and 14 of [3.8.5.2](#3852-protocol-level-failures) do not apply, and checks 19 and 20 are replaced by the following; all other checks are unchanged: +Under the `LendingProtocolV1_1` amendment, the following checks are added after check 5 and before check 6 of [3.8.5.2](#3852-protocol-level-failures). They apply to every closed-ended Vault, regardless of `LEVersion`; an open-ended Vault has no phase and skips these checks: + +1. The Vault is in the Subscription phase. (`tecTOO_SOON`) +2. The Vault is in the Redemption phase. (`tecEXPIRED`) +3. The Vault is in the Investment phase, but the final scheduled payment at `StartDate + (PaymentInterval × PaymentTotal)` leaves fewer than 60 seconds before `Vault.RedemptionDate`. (`tecNO_PERMISSION`) + +After these phase and schedule checks, for a `Vault` with `LEVersion == 1` (cash-basis, see [XLS-65 §3.1.2.2](../XLS-0065-single-asset-vault/README.md#3122-leversion-lendingprotocolv1_1)), checks 6 and 14 of [3.8.5.2](#3852-protocol-level-failures) do not apply, and checks 19 and 20 are replaced by the following; all other parent checks are unchanged: 19. `LoanBroker.DebtMaximum != 0` and `LoanBroker.DebtMaximum < LoanBroker.DebtTotal + PrincipalRequested` (exceeds maximum debt). (`tecLIMIT_EXCEEDED`) 20. `LoanBroker.CoverAvailable < (LoanBroker.DebtTotal + PrincipalRequested) × LoanBroker.CoverRateMinimum` (insufficient first-loss capital). (`tecINSUFFICIENT_FUNDS`) @@ -2177,7 +2183,7 @@ The `valueChange` is a critical accounting mechanism that represents the change This `valueChange` is always split between the Vault (as a change in its net interest) and the Loan Broker (as a change in the `managementFee`). -**Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 1` (cash-basis)**, `valueChange` has no remaining purpose. For a legacy (`LEVersion` absent) `Vault`, `valueChange` continues to be computed and used exactly as described above, even while this amendment is enabled. In the pseudo-code in [A-3.3](#a-33-pseudo-code) below, `valueChange` is never used to derive the `Loan` object's own state — `loan.totalValueOutstanding`, `loan.principalOutstanding`, `loan.managementFeeOutstanding`, and `loan.roundedState`/`loan.periodicPayment` are all computed independently, directly from the amortization/re-amortization formulas (see `compute_late_payment_interest`, `compute_full_payment`, and `try_overpayment`). `valueChange` is *only* ever returned so that [`LoanPay`](#3115-state-changes) can reconcile `Vault.AssetsTotal` and `LoanBroker.DebtTotal` against it. Since the amendment removes that reconciliation entirely (see [3.11.5.1 State Changes (`LendingProtocolV1_1`)](#31151-state-changes-lendingprotocolv1_1)), `valueChange` becomes dead output under the amendment — an implementation only supporting `LendingProtocolV1_1` need not compute it at all. `principalPaid`, `interestPaid`, and `feePaid` are unaffected and continue to be computed and used exactly as described here. +**Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 1` (cash-basis)**, `valueChange` has no remaining purpose. For a legacy (`LEVersion` absent) `Vault`, `valueChange` continues to be computed and used exactly as described above, even while this amendment is enabled. In the pseudo-code in [A-3.3](#a-33-pseudo-code) below, `valueChange` is never used to derive the `Loan` object's own state — `loan.totalValueOutstanding`, `loan.principalOutstanding`, `loan.managementFeeOutstanding`, and `loan.roundedState`/`loan.periodicPayment` are all computed independently, directly from the amortization/re-amortization formulas (see `compute_late_payment_interest`, `compute_full_payment`, and `try_overpayment`). `valueChange` is _only_ ever returned so that [`LoanPay`](#3115-state-changes) can reconcile `Vault.AssetsTotal` and `LoanBroker.DebtTotal` against it. Since the amendment removes that reconciliation entirely (see [3.11.5.1 State Changes (`LendingProtocolV1_1`)](#31151-state-changes-lendingprotocolv1_1)), `valueChange` becomes dead output under the amendment — an implementation only supporting `LendingProtocolV1_1` need not compute it at all. `principalPaid`, `interestPaid`, and `feePaid` are unaffected and continue to be computed and used exactly as described here. #### A-3.2.1 Regular Payment From 07ae495138bbd230792232b07dd2fe00a70e1843 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Tue, 8 Sep 2026 18:15:59 +0200 Subject: [PATCH 13/32] docs: correct valueChange scope and cross-ref VaultKind to XLS-65.1 --- XLS-0065-single-asset-vault/65.1/README.md | 2 +- XLS-0065-single-asset-vault/README.md | 2 +- XLS-0066-lending-protocol/66.1/README.md | 4 +++- XLS-0066-lending-protocol/README.md | 8 ++++++-- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/XLS-0065-single-asset-vault/65.1/README.md b/XLS-0065-single-asset-vault/65.1/README.md index 2bafbbbb5..94ac08db4 100644 --- a/XLS-0065-single-asset-vault/65.1/README.md +++ b/XLS-0065-single-asset-vault/65.1/README.md @@ -8,7 +8,7 @@ category: Amendment requires: [XLS-65](../README.md) created: 2026-09-04 - updated: 2026-09-07 + updated: 2026-09-08 # Single Asset Vault under `LendingProtocolV1_1` diff --git a/XLS-0065-single-asset-vault/README.md b/XLS-0065-single-asset-vault/README.md index d67916b09..8ea47cbb7 100644 --- a/XLS-0065-single-asset-vault/README.md +++ b/XLS-0065-single-asset-vault/README.md @@ -8,7 +8,7 @@ category: Amendment requires: [XLS-33](../XLS-0033-multi-purpose-tokens/README.md) created: 2024-04-12 - updated: 2026-09-07 + updated: 2026-09-08 # Single Asset Vault diff --git a/XLS-0066-lending-protocol/66.1/README.md b/XLS-0066-lending-protocol/66.1/README.md index 27b1d238b..bd831da9c 100644 --- a/XLS-0066-lending-protocol/66.1/README.md +++ b/XLS-0066-lending-protocol/66.1/README.md @@ -8,7 +8,7 @@ category: Amendment requires: [XLS-66](../README.md), [XLS-65.1](../../XLS-0065-single-asset-vault/65.1/README.md) created: 2026-09-04 - updated: 2026-09-07 + updated: 2026-09-08 # Lending Protocol under `LendingProtocolV1_1` @@ -40,6 +40,8 @@ When creating a new `LoanBroker` (`LoanBrokerID` is not specified), the followin 1. The `Vault` identified by `VaultID` is not closed-ended (`Vault.VaultKind` is absent or not equal to `1`). (`tecNO_PERMISSION`) +`Vault.VaultKind` and the closed-ended Vault lifecycle are defined in [XLS-65.1](../../XLS-0065-single-asset-vault/65.1/README.md); this patch consumes the field and does not define it. + ### 3.2 Principal-Only Debt Accounting #### 3.2.1 Transaction: `LoanSet` diff --git a/XLS-0066-lending-protocol/README.md b/XLS-0066-lending-protocol/README.md index 95464a798..1e6ff4b40 100644 --- a/XLS-0066-lending-protocol/README.md +++ b/XLS-0066-lending-protocol/README.md @@ -8,7 +8,7 @@ category: Amendment requires: XLS-65, XLS-64 created: 2024-10-18 - updated: 2026-09-07 + updated: 2026-09-08 proposal-from: https://github.com/XRPLF/XRPL-Standards/discussions/190 @@ -614,6 +614,8 @@ When creating a new `LoanBroker` (`LoanBrokerID` is not specified), the followin 1. The `Vault` identified by `VaultID` is not closed-ended (`Vault.VaultKind` is absent or not equal to `1`). (`tecNO_PERMISSION`) +`Vault.VaultKind` and the closed-ended Vault lifecycle are defined in [XLS-65.1](../XLS-0065-single-asset-vault/65.1/README.md); this specification consumes the field and does not define it. + #### 3.3.4 State Changes **If `LoanBrokerID` is not specified (creating new):** @@ -2183,7 +2185,9 @@ The `valueChange` is a critical accounting mechanism that represents the change This `valueChange` is always split between the Vault (as a change in its net interest) and the Loan Broker (as a change in the `managementFee`). -**Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 1` (cash-basis)**, `valueChange` has no remaining purpose. For a legacy (`LEVersion` absent) `Vault`, `valueChange` continues to be computed and used exactly as described above, even while this amendment is enabled. In the pseudo-code in [A-3.3](#a-33-pseudo-code) below, `valueChange` is never used to derive the `Loan` object's own state — `loan.totalValueOutstanding`, `loan.principalOutstanding`, `loan.managementFeeOutstanding`, and `loan.roundedState`/`loan.periodicPayment` are all computed independently, directly from the amortization/re-amortization formulas (see `compute_late_payment_interest`, `compute_full_payment`, and `try_overpayment`). `valueChange` is _only_ ever returned so that [`LoanPay`](#3115-state-changes) can reconcile `Vault.AssetsTotal` and `LoanBroker.DebtTotal` against it. Since the amendment removes that reconciliation entirely (see [3.11.5.1 State Changes (`LendingProtocolV1_1`)](#31151-state-changes-lendingprotocolv1_1)), `valueChange` becomes dead output under the amendment — an implementation only supporting `LendingProtocolV1_1` need not compute it at all. `principalPaid`, `interestPaid`, and `feePaid` are unaffected and continue to be computed and used exactly as described here. +**Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 1` (cash-basis)**, `valueChange` is not used for `Vault` and `LoanBroker` accounting: `Vault.AssetsTotal` is increased by `interestPaid` and `LoanBroker.DebtTotal` is decreased by `principalPaid`, so neither is reconciled against `valueChange` (see [3.11.5.1 State Changes (`LendingProtocolV1_1`)](#31151-state-changes-lendingprotocolv1_1)). For a legacy (`LEVersion` absent) `Vault`, `valueChange` continues to be applied to `Vault.AssetsTotal` and `LoanBroker.DebtTotal` exactly as described above, even while this amendment is enabled. + +`valueChange` is still computed under the amendment, because it is also part of the `Loan` object's own state transition, which this amendment does not change. [`LoanPay`](#3115-state-changes) adjusts `Loan.TotalValueOutstanding` by the re-amortization `valueChange` after an overpayment (step 5 of [3.11.5](#3115-state-changes)), and `try_overpayment` rejects an overpayment that would increase the value of the Loan, that is when `valueChange > 0` (see the pseudo-code in [A-3.3](#a-33-pseudo-code)). Both behaviours are unchanged from the pre-amendment specification and from the reference implementation in `LoanPay.cpp`. `principalPaid`, `interestPaid`, and `feePaid` are likewise unaffected and continue to be computed and used exactly as described here. #### A-3.2.1 Regular Payment From 19a240a8bba95c18bf1813ccc74e71f496a7102c Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Wed, 9 Sep 2026 10:58:33 +0200 Subject: [PATCH 14/32] docs(XLS-66): leave closed-ended lending rules to XLS draft --- XLS-0066-lending-protocol/66.1/README.md | 41 ++++++------------------ XLS-0066-lending-protocol/README.md | 17 +--------- 2 files changed, 10 insertions(+), 48 deletions(-) diff --git a/XLS-0066-lending-protocol/66.1/README.md b/XLS-0066-lending-protocol/66.1/README.md index bd831da9c..570b80910 100644 --- a/XLS-0066-lending-protocol/66.1/README.md +++ b/XLS-0066-lending-protocol/66.1/README.md @@ -20,54 +20,33 @@ This patch of [XLS-66](../README.md) records the changes the `LendingProtocolV1_ The amendment makes the following changes to XLS-66: - **Principal-Only Debt Accounting** — For a Vault with `LEVersion == 1` (cash basis, see [XLS-65.1](../../XLS-0065-single-asset-vault/65.1/README.md)), `LoanBroker.DebtTotal` tracks loan principal only and `Vault.AssetsTotal` recognises interest when it is collected rather than when a Loan is issued. -- **Closed-Ended Lending Vaults** — A new `LoanBroker` can only attach to a closed-ended Vault. ## 2. Motivation **Principal-Only Debt Accounting.** Under accrual accounting, issuing a Loan immediately increases `Vault.AssetsTotal` by the expected interest and `LoanBroker.DebtTotal` by the principal plus expected interest. The `AssetsMaximum` and `DebtMaximum` caps are therefore consumed by interest that has not been paid, and the first-loss capital requirement, which is a rate applied to `DebtTotal`, is sized against expected interest as well as principal. -**Closed-Ended Lending Vaults.** An open-ended Vault allows redemptions while its assets are committed to Loans. Requiring a closed-ended Vault gives the lending protocol defined subscription, investment, and redemption phases. - ## 3. Specification -### 3.1 Closed-Ended Lending Vaults +### 3.1 Principal-Only Debt Accounting -#### 3.1.1 Transaction: `LoanBrokerSet` +#### 3.1.1 Transaction: `LoanSet` ##### 3.1.1.1 Failure Conditions -When creating a new `LoanBroker` (`LoanBrokerID` is not specified), the following protocol-level failure is added after item 2 of the parent specification. Modifying an existing `LoanBroker` does not re-evaluate it. - -1. The `Vault` identified by `VaultID` is not closed-ended (`Vault.VaultKind` is absent or not equal to `1`). (`tecNO_PERMISSION`) - -`Vault.VaultKind` and the closed-ended Vault lifecycle are defined in [XLS-65.1](../../XLS-0065-single-asset-vault/65.1/README.md); this patch consumes the field and does not define it. - -### 3.2 Principal-Only Debt Accounting - -#### 3.2.1 Transaction: `LoanSet` - -##### 3.2.1.1 Failure Conditions - -The following checks are added after parent check 5 and before parent check 6. They apply to every closed-ended Vault, regardless of `LEVersion`; an open-ended Vault has no phase and skips these checks: - -1. The Vault is in the Subscription phase. (`tecTOO_SOON`) -2. The Vault is in the Redemption phase. (`tecEXPIRED`) -3. The Vault is in the Investment phase, but the final scheduled payment at `StartDate + (PaymentInterval × PaymentTotal)` leaves fewer than 60 seconds before `Vault.RedemptionDate`. (`tecNO_PERMISSION`) - -After these phase and schedule checks, for a Vault with `LEVersion == 1`, parent checks 6 and 14 do not apply, and the Broker cap checks 19 and 20 are replaced by principal-only checks: +For a Vault with `LEVersion == 1`, parent checks 6 and 14 do not apply, and the Broker cap checks 19 and 20 are replaced by principal-only checks: 19. `LoanBroker.DebtMaximum != 0` and `LoanBroker.DebtMaximum < LoanBroker.DebtTotal + PrincipalRequested`. (`tecLIMIT_EXCEEDED`) 20. `LoanBroker.CoverAvailable < (LoanBroker.DebtTotal + PrincipalRequested) × LoanBroker.CoverRateMinimum`. (`tecINSUFFICIENT_FUNDS`) For a Vault with `LEVersion` absent, parent checks 6, 14, 19, and 20 apply unchanged. -##### 3.2.1.2 State Changes +##### 3.1.1.2 State Changes For a Vault with `LEVersion == 1`, issuing a Loan leaves `Vault.AssetsTotal` unchanged and increases `LoanBroker.DebtTotal` by `PrincipalRequested`. For a Vault with `LEVersion` absent, issuing a Loan increases `Vault.AssetsTotal` by `InterestDue` and increases `LoanBroker.DebtTotal` by `PrincipalRequested + InterestDue`. -#### 3.2.2 Transaction: `LoanPay` +#### 3.1.2 Transaction: `LoanPay` -##### 3.2.2.1 State Changes +##### 3.1.2.1 State Changes For a Vault with `LEVersion == 1`, a payment splits into principal and interest: @@ -79,9 +58,9 @@ Principal repayment does not reduce `Vault.AssetsTotal`. Because the cash-basis For a Vault with `LEVersion` absent, the parent accrual-basis state changes apply unchanged. -#### 3.2.3 Transaction: `LoanManage` +#### 3.1.3 Transaction: `LoanManage` -##### 3.2.3.1 Failure Conditions +##### 3.1.3.1 Failure Conditions For a Vault with `LEVersion == 1`, parent check 8 is replaced by the following principal-only impairment check: @@ -89,7 +68,7 @@ For a Vault with `LEVersion == 1`, parent check 8 is replaced by the following p For a Vault with `LEVersion` absent, parent check 8 applies unchanged. -##### 3.2.3.2 State Changes +##### 3.1.3.2 State Changes For a Vault with `LEVersion == 1`, the parent `LoanManage` state changes use the following principal-only amounts: @@ -103,8 +82,6 @@ All downstream default calculations and state changes use `DefaultAmount` unchan **Principal-Only Debt Accounting.** Interest could have been recognised on a schedule, one payment period at a time, rather than on receipt. That was rejected because it reintroduces the original problem in a smaller form: the Vault would still credit itself with interest for a period in which the Borrower ends up not paying. -**Closed-Ended Lending Vaults.** The restriction is enforced when the `LoanBroker` is created rather than for every `LoanSet`, because that is when the Vault is attached to the lending protocol. - ## 5. Security Considerations **Principal-Only Debt Accounting.** Principal-only accounting makes `DebtTotal` a smaller number for the same set of Loans, so a `CoverRateMinimum` that was calibrated under accrual accounting yields less first-loss capital. A Broker migrating to a cash-basis Vault should re-derive the rate rather than reuse it. diff --git a/XLS-0066-lending-protocol/README.md b/XLS-0066-lending-protocol/README.md index 1e6ff4b40..21c54068f 100644 --- a/XLS-0066-lending-protocol/README.md +++ b/XLS-0066-lending-protocol/README.md @@ -120,7 +120,6 @@ The lending protocol charges a number of fees that the Loan Broker can configure - `LendingProtocolV1_1` (not yet live), as described in [XLS-66.1](./66.1/README.md): - introduces principal-only debt accounting and cash-basis interest recognition for Vaults with `LEVersion = 1`. - - requires a new `LoanBroker` to attach to a closed-ended Vault. ## 3. Specification @@ -608,14 +607,6 @@ This transaction uses the standard transaction fee. 10. Any value field (e.g., `DebtMaximum`) cannot be represented in the `Vault.Asset` type without precision loss (relevant for XRP and MPT). (`tecPRECISION_LOSS`) -##### 3.3.3.3 Protocol-Level Failures (`LendingProtocolV1_1`) - -When creating a new `LoanBroker` (`LoanBrokerID` is not specified), the following check is added after item 2 of [3.3.3.2](#3332-protocol-level-failures). Modifying an existing `LoanBroker` does not re-evaluate it. - -1. The `Vault` identified by `VaultID` is not closed-ended (`Vault.VaultKind` is absent or not equal to `1`). (`tecNO_PERMISSION`) - -`Vault.VaultKind` and the closed-ended Vault lifecycle are defined in [XLS-65.1](../XLS-0065-single-asset-vault/65.1/README.md); this specification consumes the field and does not define it. - #### 3.3.4 State Changes **If `LoanBrokerID` is not specified (creating new):** @@ -1105,13 +1096,7 @@ The account specified in the `Account` field pays the transaction fee. ##### 3.8.5.3 Protocol-Level Failures (`LendingProtocolV1_1`) -Under the `LendingProtocolV1_1` amendment, the following checks are added after check 5 and before check 6 of [3.8.5.2](#3852-protocol-level-failures). They apply to every closed-ended Vault, regardless of `LEVersion`; an open-ended Vault has no phase and skips these checks: - -1. The Vault is in the Subscription phase. (`tecTOO_SOON`) -2. The Vault is in the Redemption phase. (`tecEXPIRED`) -3. The Vault is in the Investment phase, but the final scheduled payment at `StartDate + (PaymentInterval × PaymentTotal)` leaves fewer than 60 seconds before `Vault.RedemptionDate`. (`tecNO_PERMISSION`) - -After these phase and schedule checks, for a `Vault` with `LEVersion == 1` (cash-basis, see [XLS-65 §3.1.2.2](../XLS-0065-single-asset-vault/README.md#3122-leversion-lendingprotocolv1_1)), checks 6 and 14 of [3.8.5.2](#3852-protocol-level-failures) do not apply, and checks 19 and 20 are replaced by the following; all other parent checks are unchanged: +For a `Vault` with `LEVersion == 1` (cash-basis, see [XLS-65 §3.1.2.2](../XLS-0065-single-asset-vault/README.md#3122-leversion-lendingprotocolv1_1)), checks 6 and 14 of [3.8.5.2](#3852-protocol-level-failures) do not apply, and checks 19 and 20 are replaced by the following; all other parent checks are unchanged: 19. `LoanBroker.DebtMaximum != 0` and `LoanBroker.DebtMaximum < LoanBroker.DebtTotal + PrincipalRequested` (exceeds maximum debt). (`tecLIMIT_EXCEEDED`) 20. `LoanBroker.CoverAvailable < (LoanBroker.DebtTotal + PrincipalRequested) × LoanBroker.CoverRateMinimum` (insufficient first-loss capital). (`tecINSUFFICIENT_FUNDS`) From 40201e925edcc7cdadf6f6ff66138fdc28bc59b6 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Wed, 9 Sep 2026 12:10:51 +0200 Subject: [PATCH 15/32] docs: format cash-basis patches and inline amendment behaviour Name 65.1 and 66.1 after the change they make, match the XLS heading layout, and fold LendingProtocolV1_1 failure conditions and state changes into the parent numbered lists instead of extra subsections. --- XLS-0065-single-asset-vault/65.1/README.md | 39 +++--- XLS-0065-single-asset-vault/README.md | 18 ++- XLS-0066-lending-protocol/66.1/README.md | 69 +++++------ XLS-0066-lending-protocol/README.md | 131 ++++++++------------- 4 files changed, 110 insertions(+), 147 deletions(-) diff --git a/XLS-0065-single-asset-vault/65.1/README.md b/XLS-0065-single-asset-vault/65.1/README.md index 94ac08db4..458005e35 100644 --- a/XLS-0065-single-asset-vault/65.1/README.md +++ b/XLS-0065-single-asset-vault/65.1/README.md @@ -1,25 +1,20 @@
   xls: 65.1
-  title: Single Asset Vault under LendingProtocolV1_1
-  description: Records the changes the LendingProtocolV1_1 amendment makes to XLS-65
-  author: Vytautas Vito Tumas , Aanchal Malhotra 
+  title: Single Asset Vault Cash-Basis Accounting
+  description: Introduces Cash-Basis accounting for the Single Asset Vault
+  author: Vytautas Vito Tumas 
   proposal-from: https://github.com/XRPLF/XRPL-Standards/discussions/192
   status: Draft
   category: Amendment
-  requires: [XLS-65](../README.md)
   created: 2026-09-04
   updated: 2026-09-08
 
-# Single Asset Vault under `LendingProtocolV1_1` +# Single Asset Vault Cash-Basis Accounting ## 1. Abstract -This patch of [XLS-65](../README.md) records the changes the `LendingProtocolV1_1` amendment makes to the Single Asset Vault. The amendment is not yet live. The consolidated specification is the top-level [README.md](../README.md). - -The amendment makes the following changes to XLS-65: - -- **Vault Cash-Basis Accounting** — Adds a `LEVersion` field that selects the accounting model a Vault applies to `AssetsTotal`: Vaults created while the amendment is enabled use cash-basis accounting, in which `AssetsTotal` excludes uncollected interest and grows only as interest is collected in cash, and Vaults created before the amendment keep accrual-basis accounting, in which `AssetsTotal` includes interest as it accrues. +Under the `LendingProtocolV1_1` amendment, a Single Asset Vault accounts for the interest earned by its Loans on a cash basis. `AssetsTotal` counts interest only once a Borrower has paid it, rather than counting the interest a Loan is expected to earn from the moment the Loan is issued. The share exchange rate of the Vault therefore tracks the assets the Vault holds instead of the income it is owed, and a Borrower who stops paying costs the Vault only the principal it lent out. A Vault created before the amendment keeps accrual-basis accounting, in which `AssetsTotal` includes interest as it accrues, because changing the accounting model of an existing Vault would change the value of the shares it has already issued. Which model a Vault uses is recorded in its `LEVersion` field and fixed when the Vault is created. ## 2. Motivation @@ -29,17 +24,15 @@ Existing Vaults cannot switch accounting models without changing the value of sh ## 3. Specification -### 3.3 Vault Cash-Basis Accounting +### 3.1 Ledger Entry: `Vault` -#### 3.3.1 Ledger Entry: `Vault` - -##### 3.3.1.1 Fields +#### 3.1.1 Fields | Field Name | Constant | Required | Internal Type | Default Value | Description | | ----------- | :------: | :------: | :-----------: | :-----------: | ---------------------------------------------------------------------------------------------------------------------- | | `LEVersion` | Yes | No | `UINT8` | absent (`0`) | The accounting model the Vault applies to `AssetsTotal`. Absent is treated as `0` (accrual basis). Immutable once set. | -##### 3.3.1.2 `LEVersion` +#### 3.1.2 `LEVersion` `LEVersion` selects the accounting model of the Vault: @@ -48,13 +41,11 @@ Existing Vaults cannot switch accounting models without changing the value of sh The field is written once by `VaultCreate` and is immutable after creation. A Vault created while the amendment is enabled is always cash-basis, and a Vault created before it is always accrual-basis; neither can be converted to the other. -##### 3.3.1.3 Invariants - -- `Vault.LEVersion` is immutable once set. +#### 3.1.3 Invariants -Because `VaultCreate` is the only transaction that writes `LEVersion`, newly created Vaults have either an absent `LEVersion` or `LEVersion = 1`. +- `Vault.LEVersion == Vault'.LEVersion` -##### 3.3.1.4 Example JSON +#### 3.1.4 Example JSON ```json { @@ -69,15 +60,15 @@ Because `VaultCreate` is the only transaction that writes `LEVersion`, newly cre } ``` -#### 3.3.2 Transaction: `VaultCreate` +### 3.2 Transaction: `VaultCreate` -##### 3.3.2.1 Failure Conditions +#### 3.2.1 Failure Conditions No failure conditions are added or removed. `LEVersion` is not a transaction field and cannot be supplied by the submitter. -##### 3.3.2.2 State Changes +#### 3.2.2 State Changes -When the amendment is enabled, the created `Vault` ledger entry has `LEVersion = 1`. When it is not enabled, the field is not written and the Vault is accrual-basis. +1. Create a new `Vault` ledger object, setting `Vault.LEVersion = 1`. ## 4. Rationale diff --git a/XLS-0065-single-asset-vault/README.md b/XLS-0065-single-asset-vault/README.md index 8ea47cbb7..0ee6a59cf 100644 --- a/XLS-0065-single-asset-vault/README.md +++ b/XLS-0065-single-asset-vault/README.md @@ -71,7 +71,7 @@ A protocol connecting to a Vault must track its debt. Furthermore, the updates t ### 2.8. Amendments -- `LendingProtocolV1_1` (not yet live), as described in [XLS-65.1](./65.1/README.md): +- `LendingProtocolV1_1`, as described in [XLS-65.1](./65.1/README.md): - introduces `LEVersion = 1` for cash-basis Vault accounting. ## 3. Specification @@ -131,7 +131,7 @@ The `Vault` object supports the following flags: For the `Vault` object, `LEVersion` distinguishes the share-valuation and accounting behavior applied to the vault: - **Absent (default value `0`)**: The Vault was created before the `LendingProtocolV1_1` amendment was enabled, or otherwise predates ledger entry versioning. It uses the legacy, accrual-basis accounting described throughout this document and in [XLS-66](../XLS-0066-lending-protocol/README.md) — `AssetsTotal` includes accrued interest. -- **`1`**: The Vault was created while the `LendingProtocolV1_1` amendment was enabled. It exclusively uses the cash-basis accounting introduced by that amendment — `AssetsTotal` excludes uncollected interest and increases when interest is paid (see [3.1.7.4](#3174-cash-basis-vs-accrual-basis-accounting-lendingprotocolv1_1) and the `LendingProtocolV1_1` subsections of [XLS-66](../XLS-0066-lending-protocol/README.md)). +- **`1`**: The Vault was created while the `LendingProtocolV1_1` amendment was enabled. It exclusively uses the cash-basis accounting introduced by that amendment — `AssetsTotal` excludes uncollected interest and increases when interest is paid (see [3.1.7.4](#3174-cash-basis-vs-accrual-basis-accounting-lendingprotocolv1_1) and the `LendingProtocolV1_1` entries in [XLS-66](../XLS-0066-lending-protocol/README.md)). Any Vault created once `LendingProtocolV1_1` is enabled is always assigned `LEVersion = 1` — cash-basis accounting is not optional or user-selectable at creation time. A future revision of the `Vault` ledger entry would increment `LEVersion` to `2`, and so on. @@ -337,7 +337,7 @@ Under the `LendingProtocolV1_1` amendment, `Vault.LEVersion` (see [3.1.2.2](#312 - `LEVersion` absent (`0`, legacy/accrual-basis): `AssetsTotal` includes interest upfront, as accrued over the life of connected Loans. This is the pre-amendment behavior and continues unchanged for Vaults created before `LendingProtocolV1_1` was enabled. - `LEVersion = 1` (cash-basis): `AssetsTotal` excludes uncollected interest and increases only as interest is actually collected in cash. This is the exclusive behavior for Vaults created once `LendingProtocolV1_1` is enabled. -See the `LendingProtocolV1_1` subsections of [XLS-66 §3.8](../XLS-0066-lending-protocol/README.md#38-transaction-loanset), [§3.10](../XLS-0066-lending-protocol/README.md#310-transaction-loanmanage), and [§3.11](../XLS-0066-lending-protocol/README.md#311-transaction-loanpay) for the precise accounting differences, all of which are gated on `Vault.LEVersion == 1`. +See the `LendingProtocolV1_1` entries in the failure conditions and state changes of [XLS-66 §3.8](../XLS-0066-lending-protocol/README.md#38-transaction-loanset), [§3.10](../XLS-0066-lending-protocol/README.md#310-transaction-loanmanage), and [§3.11](../XLS-0066-lending-protocol/README.md#311-transaction-loanpay) for the precise accounting differences, all of which are gated on `Vault.LEVersion == 1`. #### 3.1.8 Frozen Assets @@ -417,7 +417,9 @@ _TBD_ #### 3.2.6 State Changes -1. Create a new `Vault` ledger object. +1. + - `SingleAssetVault`: Create a new `Vault` ledger object. + - `LendingProtocolV1_1`: Create a new `Vault` ledger object, setting `Vault.LEVersion = 1` (see [3.1.2.2](#3122-leversion-lendingprotocolv1_1)). `LEVersion` is not a transaction field and is not settable by the Vault Owner — every Vault created while this amendment is enabled is unconditionally assigned cash-basis accounting. 2. Create a new `MPTokenIssuance` ledger object for the vault shares, and assign its MPTID to `Vault.ShareMPTID`. 1. If the `DomainID` is provided: 1. `MPTokenIssuance(Vault.ShareMPTID).DomainID = DomainID` (Set the Permissioned Domain ID). @@ -430,12 +432,6 @@ _TBD_ 5. If `Vault.Asset` is an `MPT`: 1. Create `MPToken` object for the _pseudo-account_ for the `Asset.MPTokenIssuance`. -##### 3.2.6.1 State Changes (`LendingProtocolV1_1`) - -Under the `LendingProtocolV1_1` amendment, step 1 of [3.2.6](#326-state-changes) is amended; all other steps are unchanged: - -1. Create a new `Vault` ledger object, setting `Vault.LEVersion = 1` (see [3.1.2.2](#3122-leversion-lendingprotocolv1_1)). `LEVersion` is not a transaction field and is not settable by the Vault Owner — every Vault created while this amendment is enabled is unconditionally assigned cash-basis accounting. - #### 3.2.7 Invariants **TBD** @@ -1196,4 +1192,4 @@ No, neither of the transactions charge transfer fees when depositing or withdraw ## Appendix C: Changelog -- XLS-65.1: Single Asset Vault under `LendingProtocolV1_1`, not yet live — [XLS-65.1](./65.1/README.md) +- [XLS-65.1](./65.1/README.md): Introduces cash-basis Vault accounting, recorded in `LEVersion`, so `AssetsTotal` counts interest only when a Borrower pays it. diff --git a/XLS-0066-lending-protocol/66.1/README.md b/XLS-0066-lending-protocol/66.1/README.md index 570b80910..783afda6b 100644 --- a/XLS-0066-lending-protocol/66.1/README.md +++ b/XLS-0066-lending-protocol/66.1/README.md @@ -1,25 +1,20 @@
   xls: 66.1
-  title: Lending Protocol under LendingProtocolV1_1
-  description: Records the changes the LendingProtocolV1_1 amendment makes to XLS-66
-  author: Vytautas Vito Tumas , Aanchal Malhotra 
+  title: Lending Protocol Cash-Basis Accounting
+  description: Introduces Cash-Basis accounting for the Lending Protocol
+  author: Vytautas Vito Tumas 
   proposal-from: https://github.com/XRPLF/XRPL-Standards/discussions/190
   status: Draft
   category: Amendment
-  requires: [XLS-66](../README.md), [XLS-65.1](../../XLS-0065-single-asset-vault/65.1/README.md)
   created: 2026-09-04
   updated: 2026-09-08
 
-# Lending Protocol under `LendingProtocolV1_1` +# Lending Protocol Cash-Basis Accounting ## 1. Abstract -This patch of [XLS-66](../README.md) records the changes the `LendingProtocolV1_1` amendment makes to the Lending Protocol. The amendment is not yet live. The consolidated specification is the top-level [README.md](../README.md). - -The amendment makes the following changes to XLS-66: - -- **Principal-Only Debt Accounting** — For a Vault with `LEVersion == 1` (cash basis, see [XLS-65.1](../../XLS-0065-single-asset-vault/65.1/README.md)), `LoanBroker.DebtTotal` tracks loan principal only and `Vault.AssetsTotal` recognises interest when it is collected rather than when a Loan is issued. +Under the `LendingProtocolV1_1` amendment, for a Vault with `LEVersion == 1` (cash basis, see [XLS-65.1](../../XLS-0065-single-asset-vault/65.1/README.md)), `LoanBroker.DebtTotal` tracks loan principal only and `Vault.AssetsTotal` recognises interest when it is collected rather than when a Loan is issued. ## 2. Motivation @@ -27,11 +22,9 @@ The amendment makes the following changes to XLS-66: ## 3. Specification -### 3.1 Principal-Only Debt Accounting - -#### 3.1.1 Transaction: `LoanSet` +### 3.1 Transaction: `LoanSet` -##### 3.1.1.1 Failure Conditions +#### 3.1.1 Failure Conditions For a Vault with `LEVersion == 1`, parent checks 6 and 14 do not apply, and the Broker cap checks 19 and 20 are replaced by principal-only checks: @@ -40,27 +33,36 @@ For a Vault with `LEVersion == 1`, parent checks 6 and 14 do not apply, and the For a Vault with `LEVersion` absent, parent checks 6, 14, 19, and 20 apply unchanged. -##### 3.1.1.2 State Changes +#### 3.1.2 State Changes -For a Vault with `LEVersion == 1`, issuing a Loan leaves `Vault.AssetsTotal` unchanged and increases `LoanBroker.DebtTotal` by `PrincipalRequested`. For a Vault with `LEVersion` absent, issuing a Loan increases `Vault.AssetsTotal` by `InterestDue` and increases `LoanBroker.DebtTotal` by `PrincipalRequested + InterestDue`. +For a Vault with `LEVersion == 1`, steps 6 and 7 of [3.8.6 State Changes](../README.md#386-state-changes) are replaced by the following; all other steps are unchanged. For a Vault with `LEVersion` absent, steps 6 and 7 apply unchanged: -#### 3.1.2 Transaction: `LoanPay` +6. Update `Vault` object: + - Decrease `Vault.AssetsAvailable` by `PrincipalRequested`. + - `Vault.AssetsTotal` is unchanged; `InterestDue` is not recognised at origination. +7. Update `LoanBroker` object: + - Increase `LoanBroker.DebtTotal` by `PrincipalRequested` only; `InterestDue` is excluded. + - Increment `LoanBroker.OwnerCount` by `1`. + - Increment `LoanBroker.LoanSequence` by `1`. -##### 3.1.2.1 State Changes +### 3.2 Transaction: `LoanPay` -For a Vault with `LEVersion == 1`, a payment splits into principal and interest: +#### 3.2.1 State Changes -- `Vault.AssetsTotal` increases by `interestPaid`. -- `LoanBroker.DebtTotal` decreases by `principalPaid`. -- `Vault.AssetsAvailable` increases by `totalToVault`, which is `principalPaid + interestPaid` rounded to the Vault's asset scale. +For a Vault with `LEVersion == 1`, steps 6 and 7 of [3.11.5 State Changes](../README.md#3115-state-changes) are replaced by the following; all other steps, including the asset transfers, are unchanged. For a Vault with `LEVersion` absent, steps 6 and 7 apply unchanged: -Principal repayment does not reduce `Vault.AssetsTotal`. Because the cash-basis origination checks do not enforce `AssetsMaximum`, an interest receipt may increase `Vault.AssetsTotal` above `Vault.AssetsMaximum`; the cap restricts deposits, not interest receipts. +6. **`LoanBroker` Updates**: + - `LoanBroker.DebtTotal` is decreased by `principalPaid` only; `valueChange` does not apply. + - If fees were directed to the cover pool, `LoanBroker.CoverAvailable` increases by `totalToBroker`. +7. **`Vault` Updates**: + - `Vault.AssetsAvailable` increases by `totalToVault`, which is `principalPaid + interestPaid` rounded to the Vault's asset scale. + - `Vault.AssetsTotal` is increased by `interestPaid` only; `valueChange` does not apply. -For a Vault with `LEVersion` absent, the parent accrual-basis state changes apply unchanged. +Principal repayment does not reduce `Vault.AssetsTotal`. Because the cash-basis origination checks do not enforce `AssetsMaximum`, an interest receipt may increase `Vault.AssetsTotal` above `Vault.AssetsMaximum`; the cap restricts deposits, not interest receipts. -#### 3.1.3 Transaction: `LoanManage` +### 3.3 Transaction: `LoanManage` -##### 3.1.3.1 Failure Conditions +#### 3.3.1 Failure Conditions For a Vault with `LEVersion == 1`, parent check 8 is replaced by the following principal-only impairment check: @@ -68,15 +70,16 @@ For a Vault with `LEVersion == 1`, parent check 8 is replaced by the following p For a Vault with `LEVersion` absent, parent check 8 applies unchanged. -##### 3.1.3.2 State Changes - -For a Vault with `LEVersion == 1`, the parent `LoanManage` state changes use the following principal-only amounts: +#### 3.3.2 State Changes -- For `tfLoanDefault`, `DefaultAmount = Loan.PrincipalOutstanding`. -- For `tfLoanImpair`, `LossUnrealized = Loan.PrincipalOutstanding`. -- For `tfLoanUnimpair`, `LossReversed = Loan.PrincipalOutstanding`. +For a Vault with `LEVersion == 1`, items 1 to 3 of [3.10.5 State Changes](../README.md#3105-state-changes) are replaced by the following; all other steps are unchanged. For a Vault with `LEVersion` absent, items 1 to 3 apply unchanged: -All downstream default calculations and state changes use `DefaultAmount` unchanged. For a Vault with `LEVersion` absent, the parent accrual-basis state changes apply unchanged. +1. If the `tfLoanDefault` flag is specified: + - Compute `DefaultAmount = Loan.PrincipalOutstanding`, replacing `Loan.TotalValueOutstanding - Loan.ManagementFeeOutstanding`. All downstream calculations use `DefaultAmount` unchanged. +2. If the `tfLoanImpair` flag is specified: + - Compute `LossUnrealized = Loan.PrincipalOutstanding`, replacing `Loan.TotalValueOutstanding - Loan.ManagementFeeOutstanding`. +3. If the `tfLoanUnimpair` flag is specified: + - Compute `LossReversed = Loan.PrincipalOutstanding`, replacing `Loan.TotalValueOutstanding - Loan.ManagementFeeOutstanding`. ## 4. Rationale diff --git a/XLS-0066-lending-protocol/README.md b/XLS-0066-lending-protocol/README.md index 21c54068f..a70e1b4ed 100644 --- a/XLS-0066-lending-protocol/README.md +++ b/XLS-0066-lending-protocol/README.md @@ -118,7 +118,7 @@ The lending protocol charges a number of fees that the Loan Broker can configure ### 2.7 Amendments -- `LendingProtocolV1_1` (not yet live), as described in [XLS-66.1](./66.1/README.md): +- `LendingProtocolV1_1`, as described in [XLS-66.1](./66.1/README.md): - introduces principal-only debt accounting and cash-basis interest recognition for Vaults with `LEVersion = 1`. ## 3. Specification @@ -1074,7 +1074,9 @@ The account specified in the `Account` field pays the transaction fee. 3. `LoanBroker` object with the specified `LoanBrokerID` does not exist on the ledger. (`tecNO_ENTRY`) 4. Neither the `Account` nor the `Counterparty` field are the `LoanBroker.Owner`. (`tecNO_PERMISSION`) 5. The `Borrower` `AccountRoot` object does not exist. (`terNO_ACCOUNT`) -6. `Vault.AssetsMaximum != 0` and `Vault.AssetsTotal >= Vault.AssetsMaximum` (vault at capacity). (`tecLIMIT_EXCEEDED`) +6. + - `LendingProtocol`: `Vault.AssetsMaximum != 0` and `Vault.AssetsTotal >= Vault.AssetsMaximum` (vault at capacity). (`tecLIMIT_EXCEEDED`) + - `LendingProtocolV1_1`: The check does not apply if `Vault.LEVersion == 1`. 7. Any value field (e.g., `PrincipalRequested`, `LoanOriginationFee`) cannot be represented in the `Vault.Asset` type without precision loss. (`tecPRECISION_LOSS`) 8. Cannot add asset holding for the `Vault.Asset` (e.g., MPToken or TrustLine issues). (`tecNO_PERMISSION`) 9. The Vault _pseudo-account_ is frozen for the asset. (`tecFROZEN` for IOUs, `tecLOCKED` for MPTs) @@ -1082,27 +1084,24 @@ The account specified in the `Account` field pays the transaction fee. 11. The Borrower is frozen for the asset. (`tecFROZEN` for IOUs, `tecLOCKED` for MPTs) 12. The `LoanBroker.Owner` is deep frozen for the asset. (`tecFROZEN` for IOUs, `tecLOCKED` for MPTs) 13. `Vault.AssetsAvailable < PrincipalRequested` (insufficient assets in the Vault). (`tecINSUFFICIENT_FUNDS`) -14. `Vault.AssetsMaximum != 0` and `Vault.AssetsTotal + InterestDue > Vault.AssetsMaximum` (expected interest would exceed vault assets cap). (`tecLIMIT_EXCEEDED`) +14. + - `LendingProtocol`: `Vault.AssetsMaximum != 0` and `Vault.AssetsTotal + InterestDue > Vault.AssetsMaximum` (expected interest would exceed vault assets cap). (`tecLIMIT_EXCEEDED`) + - `LendingProtocolV1_1`: The check does not apply if `Vault.LEVersion == 1`. 15. The combination of `PrincipalRequested`, `InterestRate`, `PaymentTotal`, and `PaymentInterval` results in a total interest amount that is zero or negative due to precision limitations. (`tecPRECISION_LOSS`) 16. The loan terms result in a periodic payment that is too small to cover the interest accrued in the first period, leaving no amount to pay down the principal. (`tecPRECISION_LOSS`) 17. The calculated periodic payment is so small that it rounds down to zero when adjusted for the asset's precision. (`tecPRECISION_LOSS`) 18. The rounding of the periodic payment (due to asset precision) is significant enough that the total number of payments required to settle the loan differs from the specified `PaymentTotal`. (`tecPRECISION_LOSS`) -19. `LoanBroker.DebtMaximum != 0` and `LoanBroker.DebtMaximum < LoanBroker.DebtTotal + PrincipalRequested + InterestDue` (exceeds maximum debt). (`tecLIMIT_EXCEEDED`) -20. `LoanBroker.CoverAvailable < (LoanBroker.DebtTotal + PrincipalRequested + InterestDue) × LoanBroker.CoverRateMinimum` (insufficient first-loss capital). (`tecINSUFFICIENT_FUNDS`) +19. + - `LendingProtocol`: `LoanBroker.DebtMaximum != 0` and `LoanBroker.DebtMaximum < LoanBroker.DebtTotal + PrincipalRequested + InterestDue` (exceeds maximum debt). (`tecLIMIT_EXCEEDED`) + - `LendingProtocolV1_1`: If `Vault.LEVersion == 1`, `LoanBroker.DebtMaximum != 0` and `LoanBroker.DebtMaximum < LoanBroker.DebtTotal + PrincipalRequested` (exceeds maximum debt). (`tecLIMIT_EXCEEDED`) +20. + - `LendingProtocol`: `LoanBroker.CoverAvailable < (LoanBroker.DebtTotal + PrincipalRequested + InterestDue) × LoanBroker.CoverRateMinimum` (insufficient first-loss capital). (`tecINSUFFICIENT_FUNDS`) + - `LendingProtocolV1_1`: If `Vault.LEVersion == 1`, `LoanBroker.CoverAvailable < (LoanBroker.DebtTotal + PrincipalRequested) × LoanBroker.CoverRateMinimum` (insufficient first-loss capital). (`tecINSUFFICIENT_FUNDS`) 21. The Borrower does not have sufficient reserve for the `Loan` object. (`tecINSUFFICIENT_RESERVE`) 22. The Borrower is not authorized for the asset. (`tecNO_AUTH`) 23. The `LoanBroker.Owner` is not authorized for the asset. (`tecNO_AUTH`) 24. The `LoanBroker.LoanSequence` has reached its maximum value. (`tecMAX_SEQUENCE_REACHED`) -##### 3.8.5.3 Protocol-Level Failures (`LendingProtocolV1_1`) - -For a `Vault` with `LEVersion == 1` (cash-basis, see [XLS-65 §3.1.2.2](../XLS-0065-single-asset-vault/README.md#3122-leversion-lendingprotocolv1_1)), checks 6 and 14 of [3.8.5.2](#3852-protocol-level-failures) do not apply, and checks 19 and 20 are replaced by the following; all other parent checks are unchanged: - -19. `LoanBroker.DebtMaximum != 0` and `LoanBroker.DebtMaximum < LoanBroker.DebtTotal + PrincipalRequested` (exceeds maximum debt). (`tecLIMIT_EXCEEDED`) -20. `LoanBroker.CoverAvailable < (LoanBroker.DebtTotal + PrincipalRequested) × LoanBroker.CoverRateMinimum` (insufficient first-loss capital). (`tecINSUFFICIENT_FUNDS`) - -For a legacy (`LEVersion` absent) `Vault`, checks 6, 14, 19, and 20 of [3.8.5.2](#3852-protocol-level-failures) continue to apply unchanged. - #### 3.8.6 State Changes 1. Create the `Loan` object with computed fields (`TotalValueOutstanding`, `PeriodicPayment`, `ManagementFeeOutstanding`, `LoanScale`, etc.). @@ -1126,29 +1125,26 @@ For a legacy (`LEVersion` absent) `Vault`, checks 6, 14, 19, and 20 of [3.8.5.2] - Decrease the `MPToken.MPTAmount` of the `Vault` _pseudo-account_ `MPToken` object by `PrincipalRequested`. - Increase the `MPToken.MPTAmount` of the `Borrower` `MPToken` object by `PrincipalRequested - LoanOriginationFee`. - Increase the `MPToken.MPTAmount` of the `LoanBroker.Owner` `MPToken` object by `LoanOriginationFee`. -6. Update `Vault` object: - - Decrease `Vault.AssetsAvailable` by `PrincipalRequested`. - - Increase `Vault.AssetsTotal` by `InterestDue` (interest owed to the Vault, excluding management fee). -7. Update `LoanBroker` object: - - Increase `LoanBroker.DebtTotal` by `PrincipalRequested + InterestDue`. - - Increment `LoanBroker.OwnerCount` by `1`. - - Increment `LoanBroker.LoanSequence` by `1`. +6. + - `LendingProtocol`: Update `Vault` object: + - Decrease `Vault.AssetsAvailable` by `PrincipalRequested`. + - Increase `Vault.AssetsTotal` by `InterestDue` (interest owed to the Vault, excluding management fee). + - `LendingProtocolV1_1`: If `Vault.LEVersion == 1`, update `Vault` object: + - Decrease `Vault.AssetsAvailable` by `PrincipalRequested`. + - `Vault.AssetsTotal` is unchanged; `InterestDue` is not recognised at origination. +7. + - `LendingProtocol`: Update `LoanBroker` object: + - Increase `LoanBroker.DebtTotal` by `PrincipalRequested + InterestDue`. + - Increment `LoanBroker.OwnerCount` by `1`. + - Increment `LoanBroker.LoanSequence` by `1`. + - `LendingProtocolV1_1`: If `Vault.LEVersion == 1`, update `LoanBroker` object: + - Increase `LoanBroker.DebtTotal` by `PrincipalRequested` only; `InterestDue` is excluded. + - Increment `LoanBroker.OwnerCount` by `1`. + - Increment `LoanBroker.LoanSequence` by `1`. 8. Directory linking: - Add `LoanID` to the `OwnerDirectory` of the `LoanBroker` _pseudo-account_ (sets `LoanBrokerNode`). - Add `LoanID` to the `OwnerDirectory` of the `Borrower` (sets `OwnerNode`). -##### 3.8.6.1 State Changes (`LendingProtocolV1_1`) - -Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 1` (cash-basis), origination does not recognise uncollected interest in `Vault.AssetsTotal`, and `LoanBroker.DebtTotal` tracks principal only. Steps 6 and 7 of [3.8.6](#386-state-changes) are replaced by the following; all other steps are unchanged. For a legacy (`LEVersion` absent) `Vault`, steps 6 and 7 of [3.8.6](#386-state-changes) continue to apply unchanged, even while this amendment is enabled: - -6. Update `Vault` object: - - Decrease `Vault.AssetsAvailable` by `PrincipalRequested`. - - `Vault.AssetsTotal` is unchanged (`InterestDue` is not recognised at origination). -7. Update `LoanBroker` object: - - Increase `LoanBroker.DebtTotal` by `PrincipalRequested` only (`InterestDue` is excluded — `DebtTotal` no longer tracks interest). - - Increment `LoanBroker.OwnerCount` by `1`. - - Increment `LoanBroker.LoanSequence` by `1`. - #### 3.8.7 Invariants **TBD** @@ -1277,18 +1273,16 @@ This transaction uses the standard transaction fee. 5. `Loan.PaymentRemaining == 0` (fully paid loan cannot be modified). (`tecNO_PERMISSION`) 6. `tfLoanDefault` flag is specified and `Loan.NextPaymentDueDate + Loan.GracePeriod` has not yet passed. (`tecTOO_SOON`) 7. The submitter is not the `LoanBroker.Owner`. (`tecNO_PERMISSION`) -8. `tfLoanImpair` flag is specified and `Vault.LossUnrealized + (Loan.TotalValueOutstanding - Loan.ManagementFeeOutstanding) > Vault.AssetsTotal - Vault.AssetsAvailable` (impairment would exceed vault's unavailable assets). (`tecLIMIT_EXCEEDED`) - -##### 3.10.4.3 Protocol-Level Failures (`LendingProtocolV1_1`) - -Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 1` (cash-basis), check 8 of [3.10.4.2](#31042-protocol-level-failures) is replaced by the following, consistent with the amended impairment amount in [3.10.5.1](#31051-state-changes-lendingprotocolv1_1) (all other checks are unchanged). For a legacy (`LEVersion` absent) `Vault`, check 8 of [3.10.4.2](#31042-protocol-level-failures) continues to apply unchanged: - -8. `tfLoanImpair` flag is specified and `Vault.LossUnrealized + Loan.PrincipalOutstanding > Vault.AssetsTotal - Vault.AssetsAvailable` (impairment would exceed vault's unavailable assets). (`tecLIMIT_EXCEEDED`) +8. + - `LendingProtocol`: `tfLoanImpair` flag is specified and `Vault.LossUnrealized + (Loan.TotalValueOutstanding - Loan.ManagementFeeOutstanding) > Vault.AssetsTotal - Vault.AssetsAvailable` (impairment would exceed vault's unavailable assets). (`tecLIMIT_EXCEEDED`) + - `LendingProtocolV1_1`: If `Vault.LEVersion == 1`, `tfLoanImpair` flag is specified and `Vault.LossUnrealized + Loan.PrincipalOutstanding > Vault.AssetsTotal - Vault.AssetsAvailable` (impairment would exceed vault's unavailable assets). (`tecLIMIT_EXCEEDED`) #### 3.10.5 State Changes 1. If the `tfLoanDefault` flag is specified: - - Compute `DefaultAmount = Loan.TotalValueOutstanding - Loan.ManagementFeeOutstanding` (principal + interest owed to Vault). + - Compute `DefaultAmount`: + - `LendingProtocol`: `DefaultAmount = Loan.TotalValueOutstanding - Loan.ManagementFeeOutstanding` (principal + interest owed to Vault). + - `LendingProtocolV1_1`: If `Vault.LEVersion == 1`, `DefaultAmount = Loan.PrincipalOutstanding` (principal only). `Loan.ManagementFeeOutstanding` is excluded under both amendments. - Compute `MinimumCover = LoanBroker.DebtTotal × LoanBroker.CoverRateMinimum`. - Compute `DefaultCovered = min(MinimumCover × LoanBroker.CoverRateLiquidation, DefaultAmount, LoanBroker.CoverAvailable)`. - Compute `VaultLoss = DefaultAmount - DefaultCovered`. @@ -1318,7 +1312,9 @@ Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 1` ( - Decrease the `MPToken.MPTAmount` of the `LoanBroker` _pseudo-account_ `MPToken` object by `DefaultCovered`. - Increase the `MPToken.MPTAmount` of the `Vault` _pseudo-account_ `MPToken` object by `DefaultCovered`. 2. If the `tfLoanImpair` flag is specified: - - Compute `LossUnrealized = Loan.TotalValueOutstanding - Loan.ManagementFeeOutstanding`. + - Compute `LossUnrealized`: + - `LendingProtocol`: `LossUnrealized = Loan.TotalValueOutstanding - Loan.ManagementFeeOutstanding`. + - `LendingProtocolV1_1`: If `Vault.LEVersion == 1`, `LossUnrealized = Loan.PrincipalOutstanding`. - Update `Vault` object: - Increase `Vault.LossUnrealized` by `LossUnrealized`. - Update `Loan` object: @@ -1326,7 +1322,9 @@ Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 1` ( - If `Loan.NextPaymentDueDate` has not yet passed: - Set `Loan.NextPaymentDueDate = currentTime`. 3. If the `tfLoanUnimpair` flag is specified: - - Compute `LossReversed = Loan.TotalValueOutstanding - Loan.ManagementFeeOutstanding`. + - Compute `LossReversed`: + - `LendingProtocol`: `LossReversed = Loan.TotalValueOutstanding - Loan.ManagementFeeOutstanding`. + - `LendingProtocolV1_1`: If `Vault.LEVersion == 1`, `LossReversed = Loan.PrincipalOutstanding`. - Update `Vault` object: - Decrease `Vault.LossUnrealized` by `LossReversed`. - Update `Loan` object: @@ -1337,21 +1335,7 @@ Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 1` ( - Otherwise: - Set `Loan.NextPaymentDueDate = currentTime + Loan.PaymentInterval`. -##### 3.10.5.1 State Changes (`LendingProtocolV1_1`) - -Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 1` (cash-basis), `LoanBroker.DebtTotal` and the uncollected amount at risk are principal-only. The `DefaultAmount`, `LossUnrealized`, and `LossReversed` computations in items 1–3 of [3.10.5](#3105-state-changes) are replaced by the following, collapsing each to a single, unified principal-only amount (all other steps — including the `MinimumCover`/`DefaultCovered`/`VaultLoss` mechanics, the `CoverAvailable` drawdown, and the `Loan` object updates — are unchanged). For a legacy (`LEVersion` absent) `Vault`, items 1–3 of [3.10.5](#3105-state-changes) continue to apply unchanged, even while this amendment is enabled: - -1. If the `tfLoanDefault` flag is specified: - - Compute `DefaultAmount = Loan.PrincipalOutstanding` (principal only, replacing `Loan.TotalValueOutstanding - Loan.ManagementFeeOutstanding`). This single `DefaultAmount` is used, as before, for the `Vault` (`VaultLoss`/`DefaultCovered`) and `LoanBroker.DebtTotal` updates, and for `Vault.LossUnrealized` if the Loan was impaired. - - `Loan.ManagementFeeOutstanding` is unaffected by this amendment — it was already excluded from `DefaultAmount` pre-amendment (via the `- Loan.ManagementFeeOutstanding` term), and remains excluded post-amendment (the amended formula never references it). -2. If the `tfLoanImpair` flag is specified: - - Compute `LossUnrealized = Loan.PrincipalOutstanding` (principal only, replacing `Loan.TotalValueOutstanding - Loan.ManagementFeeOutstanding`), consistent with the amended `DefaultAmount` above. -3. If the `tfLoanUnimpair` flag is specified: - - Compute `LossReversed = Loan.PrincipalOutstanding` (principal only, replacing `Loan.TotalValueOutstanding - Loan.ManagementFeeOutstanding`), mirroring the amended `tfLoanImpair` calculation so the increase/decrease pair on `Vault.LossUnrealized` stays balanced. - -See [3.10.5.2](#31052-worked-example-pre--vs-post-lendingprotocolv1_1-loan-default) below for a full numeric walkthrough comparing the pre- and post-amendment trajectories. - -##### 3.10.5.2 Worked Example: Pre- vs Post-`LendingProtocolV1_1` Loan Default +##### 3.10.5.1 Worked Example: Pre- vs Post-`LendingProtocolV1_1` Loan Default This example walks a single Loan through `LoanSet` → `LoanPay` → `LoanManage(tfLoanDefault)`, computing both the current (pre-amendment) and `LendingProtocolV1_1` (post-amendment) trajectories side by side. It uses the same figures as the [`LoanBroker` Accounting](#3110-accounting) and [First-Loss Capital](#3111-first-loss-capital) examples above, for continuity. @@ -1529,13 +1513,17 @@ The `Loan` object is updated to reflect the payment. The `LoanBroker` and `Vault` objects are updated to reflect the new accounting state. The `valueChange`—representing the net change in the loan's total future interest—is applied to both the `LoanBroker` and the `Vault`, but with an important distinction for late payments. 6. **`LoanBroker` Updates**: - - `LoanBroker.DebtTotal` is decreased by `totalToVault - valueChange` (the principal and interest paid back, adjusted for any value change). - - For late payments, `valueChange > 0`, so the debt reduction is less than `totalToVault`. - - For overpayments/early full repayments, `valueChange` is typically negative, so the debt reduction is greater than `totalToVault`. + - `LoanBroker.DebtTotal`: + - `LendingProtocol`: decreased by `totalToVault - valueChange` (the principal and interest paid back, adjusted for any value change). + - For late payments, `valueChange > 0`, so the debt reduction is less than `totalToVault`. + - For overpayments/early full repayments, `valueChange` is typically negative, so the debt reduction is greater than `totalToVault`. + - `LendingProtocolV1_1`: If `Vault.LEVersion == 1`, decreased by `principalPaid` only; `valueChange` does not apply. - If fees were directed to the cover pool, `LoanBroker.CoverAvailable` increases by `totalToBroker`. 7. **`Vault` Updates**: - `Vault.AssetsAvailable` increases by `totalToVault`. - - `Vault.AssetsTotal` is adjusted by `valueChange`, reflecting the net change in the vault's expected future earnings from the loan. + - `Vault.AssetsTotal`: + - `LendingProtocol`: adjusted by `valueChange`, reflecting the net change in the vault's expected future earnings from the loan. + - `LendingProtocolV1_1`: If `Vault.LEVersion == 1`, increased by `interestPaid` only; `valueChange` does not apply. Collected interest may raise `Vault.AssetsTotal` above `Vault.AssetsMaximum`, because check 6 and check 14 of [3.8.5.2](#3852-protocol-level-failures) do not apply to a cash-basis Vault; the cap restricts deposits, not interest receipts. **4. Asset Transfers** @@ -1560,21 +1548,6 @@ These transfers are performed according to the asset type: - The `MPTAmount` in the `Vault` pseudo-account's `MPToken` object is increased. - The `MPTAmount` in the destination account for fees' `MPToken` object is increased. -##### 3.11.5.1 State Changes (`LendingProtocolV1_1`) - -Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 1` (cash-basis), `valueChange` is removed from the `LoanBroker` and `Vault` accounting: `LoanBroker.DebtTotal` tracks principal only, while `Vault.AssetsTotal` recognises actual interest receipts directly. Steps 6 and 7 of [3.11.5](#3115-state-changes) are replaced by the following (all other steps, including the asset transfers in section 4, are unchanged). For a legacy (`LEVersion` absent) `Vault`, steps 6 and 7 of [3.11.5](#3115-state-changes) continue to apply unchanged, even while this amendment is enabled: - -6. **`LoanBroker` Updates**: - - `LoanBroker.DebtTotal` is decreased by `principalPaid` only (`valueChange` does not apply) — the principal portion of the payment, from the same payment breakdown produced by [Appendix A-3](#a-3-loanpay-implementation-reference). - - If fees were directed to the cover pool, `LoanBroker.CoverAvailable` increases by `totalToBroker`. -7. **`Vault` Updates**: - - `Vault.AssetsAvailable` increases by `totalToVault`. - - `Vault.AssetsTotal` is increased by `interestPaid` only (`valueChange` does not apply) — the interest portion of the payment, net of management fee. - -Because checks 6 and 14 of [3.8.5.2](#3852-protocol-level-failures) do not apply to a cash-basis Vault, collected interest may increase `Vault.AssetsTotal` above `Vault.AssetsMaximum`; the cap restricts deposits, not interest receipts. - -`principalPaid` and `interestPaid` are computed by the same, unchanged `PeriodicPayment`/amortization logic in [Appendix A-3](#a-3-loanpay-implementation-reference) — no new machinery is introduced by this amendment; only the use of `valueChange` for `DebtTotal`/`AssetsTotal` is dropped. The `Loan` object's own `TotalValueOutstanding` is unaffected by this amendment and continues to be adjusted directly by the re-amortization recalculation described in step 5 of [3.11.5](#3115-state-changes). - #### 3.11.6 Invariants **TBD** @@ -2170,7 +2143,7 @@ The `valueChange` is a critical accounting mechanism that represents the change This `valueChange` is always split between the Vault (as a change in its net interest) and the Loan Broker (as a change in the `managementFee`). -**Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 1` (cash-basis)**, `valueChange` is not used for `Vault` and `LoanBroker` accounting: `Vault.AssetsTotal` is increased by `interestPaid` and `LoanBroker.DebtTotal` is decreased by `principalPaid`, so neither is reconciled against `valueChange` (see [3.11.5.1 State Changes (`LendingProtocolV1_1`)](#31151-state-changes-lendingprotocolv1_1)). For a legacy (`LEVersion` absent) `Vault`, `valueChange` continues to be applied to `Vault.AssetsTotal` and `LoanBroker.DebtTotal` exactly as described above, even while this amendment is enabled. +**Under the `LendingProtocolV1_1` amendment, for a `Vault` with `LEVersion == 1` (cash-basis)**, `valueChange` is not used for `Vault` and `LoanBroker` accounting: `Vault.AssetsTotal` is increased by `interestPaid` and `LoanBroker.DebtTotal` is decreased by `principalPaid`, so neither is reconciled against `valueChange` (see steps 6 and 7 of [3.11.5 State Changes](#3115-state-changes)). For a legacy (`LEVersion` absent) `Vault`, `valueChange` continues to be applied to `Vault.AssetsTotal` and `LoanBroker.DebtTotal` exactly as described above, even while this amendment is enabled. `valueChange` is still computed under the amendment, because it is also part of the `Loan` object's own state transition, which this amendment does not change. [`LoanPay`](#3115-state-changes) adjusts `Loan.TotalValueOutstanding` by the re-amortization `valueChange` after an overpayment (step 5 of [3.11.5](#3115-state-changes)), and `try_overpayment` rejects an overpayment that would increase the value of the Loan, that is when `valueChange > 0` (see the pseudo-code in [A-3.3](#a-33-pseudo-code)). Both behaviours are unchanged from the pre-amendment specification and from the reference implementation in `LoanPay.cpp`. `principalPaid`, `interestPaid`, and `feePaid` are likewise unaffected and continue to be computed and used exactly as described here. @@ -2800,4 +2773,4 @@ function make_payment(amount, currentTime) -> (principalPaid, interestPaid, valu ## Appendix C: Changelog -- XLS-66.1: Lending Protocol under `LendingProtocolV1_1`, not yet live — [XLS-66.1](./66.1/README.md) +- [XLS-66.1](./66.1/README.md): For a cash-basis Vault, `LoanBroker.DebtTotal` tracks principal only and `Vault.AssetsTotal` recognises interest when it is collected rather than when a Loan is issued. From 0c8d45c2ff4c004fc1bf817f1c98ecf4a6c1be8c Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Wed, 9 Sep 2026 13:28:05 +0200 Subject: [PATCH 16/32] docs: scope LEVersion immutability and label the Vault excerpt The patch invariant only applies on update of an existing Vault, and the JSON example is a partial excerpt rather than a complete ledger entry. --- XLS-0065-single-asset-vault/65.1/README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/XLS-0065-single-asset-vault/65.1/README.md b/XLS-0065-single-asset-vault/65.1/README.md index 458005e35..2f15e7fb1 100644 --- a/XLS-0065-single-asset-vault/65.1/README.md +++ b/XLS-0065-single-asset-vault/65.1/README.md @@ -43,9 +43,11 @@ The field is written once by `VaultCreate` and is immutable after creation. A Va #### 3.1.3 Invariants -- `Vault.LEVersion == Vault'.LEVersion` +- On an update to an existing Vault: `Vault.LEVersion == Vault'.LEVersion`. -#### 3.1.4 Example JSON +#### 3.1.4 Partial Ledger Entry Example + +The following excerpt shows the accounting fields introduced or affected by this patch; it is not a complete serialized `Vault` ledger entry: ```json { From bb136b8af838039096312929f1abdd5d2fff31df Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Wed, 9 Sep 2026 13:57:23 +0200 Subject: [PATCH 17/32] docs: make DefaultAmount principal-only for cash-basis Vaults in formula 34 Keep LoanManage nested state updates; only the amount computations change. --- XLS-0066-lending-protocol/66.1/README.md | 2 +- XLS-0066-lending-protocol/README.md | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/XLS-0066-lending-protocol/66.1/README.md b/XLS-0066-lending-protocol/66.1/README.md index 783afda6b..fa58c82d4 100644 --- a/XLS-0066-lending-protocol/66.1/README.md +++ b/XLS-0066-lending-protocol/66.1/README.md @@ -72,7 +72,7 @@ For a Vault with `LEVersion` absent, parent check 8 applies unchanged. #### 3.3.2 State Changes -For a Vault with `LEVersion == 1`, items 1 to 3 of [3.10.5 State Changes](../README.md#3105-state-changes) are replaced by the following; all other steps are unchanged. For a Vault with `LEVersion` absent, items 1 to 3 apply unchanged: +For a Vault with `LEVersion == 1`, the amount computations in items 1 to 3 of [3.10.5 State Changes](../README.md#3105-state-changes) are replaced by the following; nested substeps (flags, transfers, `DebtTotal`, and field clears) are unchanged except that they use these amounts. For a Vault with `LEVersion` absent, the parent amount computations in items 1 to 3 apply unchanged: 1. If the `tfLoanDefault` flag is specified: - Compute `DefaultAmount = Loan.PrincipalOutstanding`, replacing `Loan.TotalValueOutstanding - Loan.ManagementFeeOutstanding`. All downstream calculations use `DefaultAmount` unchanged. diff --git a/XLS-0066-lending-protocol/README.md b/XLS-0066-lending-protocol/README.md index a70e1b4ed..4a88d9bc5 100644 --- a/XLS-0066-lending-protocol/README.md +++ b/XLS-0066-lending-protocol/README.md @@ -2020,7 +2020,12 @@ Applied when a loan defaults. #### 8.1 Default Amounts $$ -DefaultAmount = PrincipalOutstanding + InterestOutstanding_{net} \quad \text{(34)} +DefaultAmount = +\begin{cases} +PrincipalOutstanding & \text{if } Vault.LEVersion = 1 \\ +PrincipalOutstanding + InterestOutstanding_{net} & \text{otherwise} +\end{cases} +\quad \text{(34)} $$ $$ From edce47003047871d82c8473aa3dca41da80ceb3b Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Wed, 9 Sep 2026 13:57:43 +0200 Subject: [PATCH 18/32] docs: note that formula 34 is principal-only when LEVersion is 1 --- XLS-0066-lending-protocol/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/XLS-0066-lending-protocol/README.md b/XLS-0066-lending-protocol/README.md index 4a88d9bc5..05ea89447 100644 --- a/XLS-0066-lending-protocol/README.md +++ b/XLS-0066-lending-protocol/README.md @@ -2044,6 +2044,7 @@ $$ - `PrincipalOutstanding` = Outstanding principal balance (`Loan.PrincipalOutstanding`) - $InterestOutstanding_{net}$ = Remaining net interest excluding management fee (formula 33 applied to ledger values) +- For a cash-basis Vault (`Vault.LEVersion = 1`), formula (34) is principal only; unpaid interest is not charged to the Vault. - `DebtTotal` = Total debt owed to vault (`LoanBroker.DebtTotal`) - `CoverRateMinimum` = Required coverage percentage (`LoanBroker.CoverRateMinimum`) - `CoverRateLiquidation` = Portion of minimum cover to liquidate (`LoanBroker.CoverRateLiquidation`) From 4c2a50adc765021d4e2061b0a7a69637f9c90ce7 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Wed, 9 Sep 2026 15:00:59 +0200 Subject: [PATCH 19/32] docs: point closed-ended LendingProtocolV1_1 checks at PR 587 Cash-basis remains this patch. Soften VaultCreate so it only claims no cash-basis failure conditions. --- XLS-0065-single-asset-vault/65.1/README.md | 2 +- XLS-0065-single-asset-vault/README.md | 1 + XLS-0066-lending-protocol/README.md | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/XLS-0065-single-asset-vault/65.1/README.md b/XLS-0065-single-asset-vault/65.1/README.md index 2f15e7fb1..f34f95388 100644 --- a/XLS-0065-single-asset-vault/65.1/README.md +++ b/XLS-0065-single-asset-vault/65.1/README.md @@ -66,7 +66,7 @@ The following excerpt shows the accounting fields introduced or affected by this #### 3.2.1 Failure Conditions -No failure conditions are added or removed. `LEVersion` is not a transaction field and cannot be supplied by the submitter. +No failure conditions related to cash-basis accounting are added or removed. `LEVersion` is not a transaction field and cannot be supplied by the submitter. #### 3.2.2 State Changes diff --git a/XLS-0065-single-asset-vault/README.md b/XLS-0065-single-asset-vault/README.md index 0ee6a59cf..26188683c 100644 --- a/XLS-0065-single-asset-vault/README.md +++ b/XLS-0065-single-asset-vault/README.md @@ -73,6 +73,7 @@ A protocol connecting to a Vault must track its debt. Furthermore, the updates t - `LendingProtocolV1_1`, as described in [XLS-65.1](./65.1/README.md): - introduces `LEVersion = 1` for cash-basis Vault accounting. + - Closed-ended Vault checks also gated by this amendment (`VaultKind`, `SubscriptionDate`, `RedemptionDate`, and the phase checks on `VaultDeposit` / `VaultWithdraw`) are specified in [PR #587](https://github.com/XRPLF/XRPL-Standards/pull/587), not in this patch. ## 3. Specification diff --git a/XLS-0066-lending-protocol/README.md b/XLS-0066-lending-protocol/README.md index 05ea89447..c6ff6f67f 100644 --- a/XLS-0066-lending-protocol/README.md +++ b/XLS-0066-lending-protocol/README.md @@ -120,6 +120,7 @@ The lending protocol charges a number of fees that the Loan Broker can configure - `LendingProtocolV1_1`, as described in [XLS-66.1](./66.1/README.md): - introduces principal-only debt accounting and cash-basis interest recognition for Vaults with `LEVersion = 1`. + - Closed-ended Vault phase checks on `LoanSet` and the closed-ended requirement on `LoanBrokerSet` are specified in [PR #587](https://github.com/XRPLF/XRPL-Standards/pull/587), not in this patch. ## 3. Specification From 85be555a990d41832e3ffe736cb23920711b3dc1 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Wed, 9 Sep 2026 15:21:33 +0200 Subject: [PATCH 20/32] docs: add CoverAvailable to formula 35 and vault_info LEVersion Align appendix default cover with 3.10.5, and document vault.LEVersion on vault_info. Clarify that AssetsTotal equals AssetsAvailable on each default trajectory and that proportional scaling holds only when CoverAvailable is not binding. --- XLS-0065-single-asset-vault/README.md | 2 ++ XLS-0066-lending-protocol/README.md | 9 +++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/XLS-0065-single-asset-vault/README.md b/XLS-0065-single-asset-vault/README.md index 26188683c..f2b468d5f 100644 --- a/XLS-0065-single-asset-vault/README.md +++ b/XLS-0065-single-asset-vault/README.md @@ -828,6 +828,7 @@ This RPC retrieves the Vault ledger entry and the IDs associated with it. | `vault.shares.index` | Yes | `string` | Unique index of the shares ledger entry. | | `vault.shares.mpt_issuance_id` | No | `string` | The ID of the `MPTokenIssuance` object. It will always be equal to `vault.ShareMPTID`. | | `vault.Scale` | Yes | `number` | The `Scale` specifies the power of 10 ($10^{\text{scale}}$) to multiply an asset's value by when converting it into an integer-based number of shares. | +| `vault.LEVersion` | No | `number` | Accounting model. Absent or `0` is accrual-basis; `1` is cash-basis under `LendingProtocolV1_1`. Clients use this to interpret `AssetsTotal`. | #### 3.9.3 Failure Conditions @@ -883,6 +884,7 @@ Vault holding an `IOU`: "mpt_issuance_id" : "00000001C752C42A1EBD6BF2403134F7CFD2F1D835AFD26E" }, "Scale": 6, + "LEVersion": 1 } } ``` diff --git a/XLS-0066-lending-protocol/README.md b/XLS-0066-lending-protocol/README.md index c6ff6f67f..c2aadb09f 100644 --- a/XLS-0066-lending-protocol/README.md +++ b/XLS-0066-lending-protocol/README.md @@ -1396,8 +1396,8 @@ _Post-amendment (`LendingProtocolV1_1`):_ **Observations** -- In both trajectories, `LoanBroker.DebtTotal` and `Loan.PrincipalOutstanding`/`ManagementFeeOutstanding` fully clear to (or independent of) `0` at default, and `Vault.AssetsTotal ≈ Vault.AssetsAvailable` post-default (the Vault's books balance, up to the small rounding difference below) — the default mechanics are structurally unchanged. -- The only real difference is that the pre-amendment `DefaultAmount`, `MinimumCover`, and `DefaultCovered` are all computed on figures that still include the (now-collected or now-forgiven) interest, while the post-amendment figures are computed on principal alone. This scales every downstream quantity (`MinimumCover`, `DefaultCovered`, `VaultLoss`, `CoverAvailable` drawn down) down proportionally by the amount of interest excluded — it does not change the _shape_ of the `CoverRateMinimum`/`CoverRateLiquidation` mechanics, only the base they are applied to. +- In both trajectories, `LoanBroker.DebtTotal` and `Loan.PrincipalOutstanding`/`ManagementFeeOutstanding` fully clear to (or independent of) `0` at default, and `Vault.AssetsTotal` equals `Vault.AssetsAvailable` after default in each trajectory — the default mechanics are structurally unchanged. +- The only real difference is that the pre-amendment `DefaultAmount`, `MinimumCover`, and `DefaultCovered` are all computed on figures that still include the (now-collected or now-forgiven) interest, while the post-amendment figures are computed on principal alone. In this example `CoverAvailable` is not binding, so those quantities scale with the interest excluded. That is not general: a `min` capped by `CoverAvailable` can leave the cover drawdown unchanged, and an aggregate broker `DebtTotal` can produce another ratio. The `CoverRateMinimum`/`CoverRateLiquidation` shape is unchanged. - The `99,550.45` vs `99,550` difference in the final `Vault.AssetsTotal`/`AssetsAvailable` is exactly the `0.45` difference in `DefaultCovered` (`5.45` vs `5`), itself a consequence of `MinimumCover` being computed against a larger, interest-inclusive `DebtTotal` (`545`) pre-amendment vs. a principal-only `DebtTotal` (`500`) post-amendment. This `0.45` is cover calculated from uncollected interest under accrual-basis accounting; cash-basis accounting never recognises that unpaid interest. - `Loan.ManagementFeeOutstanding` (`5`) is unaffected by the amendment in either trajectory: it was already excluded from `DefaultAmount` before the amendment (via the `- Loan.ManagementFeeOutstanding` term), and remains excluded after the amendment (since the amended `DefaultAmount` formula never references it). @@ -2030,7 +2030,7 @@ PrincipalOutstanding + InterestOutstanding_{net} & \text{otherwise} $$ $$ -DefaultCovered = \min((DebtTotal \times CoverRateMinimum) \times CoverRateLiquidation, DefaultAmount) \quad \text{(35)} +DefaultCovered = \min((DebtTotal \times CoverRateMinimum) \times CoverRateLiquidation, DefaultAmount, CoverAvailable) \quad \text{(35)} $$ $$ @@ -2049,10 +2049,11 @@ $$ - `DebtTotal` = Total debt owed to vault (`LoanBroker.DebtTotal`) - `CoverRateMinimum` = Required coverage percentage (`LoanBroker.CoverRateMinimum`) - `CoverRateLiquidation` = Portion of minimum cover to liquidate (`LoanBroker.CoverRateLiquidation`) +- `CoverAvailable` = First-loss capital available (`LoanBroker.CoverAvailable`) **Process:** -1. Calculate coverage: `DefaultCovered = min((DebtTotal × CoverRateMinimum) × CoverRateLiquidation, DefaultAmount)` using formula (35) +1. Calculate coverage: `DefaultCovered = min((DebtTotal × CoverRateMinimum) × CoverRateLiquidation, DefaultAmount, CoverAvailable)` using formula (35) 2. Determine loss: `Loss = DefaultAmount - DefaultCovered` using formula (36) 3. Return covered amount to vault: `FundsReturned = DefaultCovered` using formula (37) 4. Decrease first-loss capital: `CoverAvailable -= DefaultCovered` From f2219ad2cf95d2ca9eb5697b305fed33503a8f92 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:22:41 +0200 Subject: [PATCH 21/32] style: apply prettier to vault and lending specs Satisfy the pre-commit hook that was failing CI. --- XLS-0065-single-asset-vault/README.md | 27 +++++++++++++-------------- XLS-0066-lending-protocol/README.md | 21 +++++++-------------- 2 files changed, 20 insertions(+), 28 deletions(-) diff --git a/XLS-0065-single-asset-vault/README.md b/XLS-0065-single-asset-vault/README.md index f2b468d5f..6e8563010 100644 --- a/XLS-0065-single-asset-vault/README.md +++ b/XLS-0065-single-asset-vault/README.md @@ -418,20 +418,19 @@ _TBD_ #### 3.2.6 State Changes -1. - - `SingleAssetVault`: Create a new `Vault` ledger object. - - `LendingProtocolV1_1`: Create a new `Vault` ledger object, setting `Vault.LEVersion = 1` (see [3.1.2.2](#3122-leversion-lendingprotocolv1_1)). `LEVersion` is not a transaction field and is not settable by the Vault Owner — every Vault created while this amendment is enabled is unconditionally assigned cash-basis accounting. -2. Create a new `MPTokenIssuance` ledger object for the vault shares, and assign its MPTID to `Vault.ShareMPTID`. - 1. If the `DomainID` is provided: - 1. `MPTokenIssuance(Vault.ShareMPTID).DomainID = DomainID` (Set the Permissioned Domain ID). - 2. Create an `MPToken` object for the Vault Owner to hold Vault Shares. -3. Create a new `AccountRoot`[_pseudo-account_](../XLS-0064-pseudo-account/README.md) object setting the `PseudoOwner` to `VaultID`. - -4. If `Vault.Asset` is an `IOU`: - 1. Create a `RippleState` object between the _pseudo-account_ `AccountRoot` and `Issuer` `AccountRoot`. - -5. If `Vault.Asset` is an `MPT`: - 1. Create `MPToken` object for the _pseudo-account_ for the `Asset.MPTokenIssuance`. +1. - `SingleAssetVault`: Create a new `Vault` ledger object. + - `LendingProtocolV1_1`: Create a new `Vault` ledger object, setting `Vault.LEVersion = 1` (see [3.1.2.2](#3122-leversion-lendingprotocolv1_1)). `LEVersion` is not a transaction field and is not settable by the Vault Owner — every Vault created while this amendment is enabled is unconditionally assigned cash-basis accounting. +2. Create a new `MPTokenIssuance` ledger object for the vault shares, and assign its MPTID to `Vault.ShareMPTID`. + 1. If the `DomainID` is provided: + 1. `MPTokenIssuance(Vault.ShareMPTID).DomainID = DomainID` (Set the Permissioned Domain ID). + 2. Create an `MPToken` object for the Vault Owner to hold Vault Shares. +3. Create a new `AccountRoot`[_pseudo-account_](../XLS-0064-pseudo-account/README.md) object setting the `PseudoOwner` to `VaultID`. + +4. If `Vault.Asset` is an `IOU`: + 1. Create a `RippleState` object between the _pseudo-account_ `AccountRoot` and `Issuer` `AccountRoot`. + +5. If `Vault.Asset` is an `MPT`: + 1. Create `MPToken` object for the _pseudo-account_ for the `Asset.MPTokenIssuance`. #### 3.2.7 Invariants diff --git a/XLS-0066-lending-protocol/README.md b/XLS-0066-lending-protocol/README.md index c2aadb09f..62113bf2e 100644 --- a/XLS-0066-lending-protocol/README.md +++ b/XLS-0066-lending-protocol/README.md @@ -1075,8 +1075,7 @@ The account specified in the `Account` field pays the transaction fee. 3. `LoanBroker` object with the specified `LoanBrokerID` does not exist on the ledger. (`tecNO_ENTRY`) 4. Neither the `Account` nor the `Counterparty` field are the `LoanBroker.Owner`. (`tecNO_PERMISSION`) 5. The `Borrower` `AccountRoot` object does not exist. (`terNO_ACCOUNT`) -6. - - `LendingProtocol`: `Vault.AssetsMaximum != 0` and `Vault.AssetsTotal >= Vault.AssetsMaximum` (vault at capacity). (`tecLIMIT_EXCEEDED`) +6. - `LendingProtocol`: `Vault.AssetsMaximum != 0` and `Vault.AssetsTotal >= Vault.AssetsMaximum` (vault at capacity). (`tecLIMIT_EXCEEDED`) - `LendingProtocolV1_1`: The check does not apply if `Vault.LEVersion == 1`. 7. Any value field (e.g., `PrincipalRequested`, `LoanOriginationFee`) cannot be represented in the `Vault.Asset` type without precision loss. (`tecPRECISION_LOSS`) 8. Cannot add asset holding for the `Vault.Asset` (e.g., MPToken or TrustLine issues). (`tecNO_PERMISSION`) @@ -1085,18 +1084,15 @@ The account specified in the `Account` field pays the transaction fee. 11. The Borrower is frozen for the asset. (`tecFROZEN` for IOUs, `tecLOCKED` for MPTs) 12. The `LoanBroker.Owner` is deep frozen for the asset. (`tecFROZEN` for IOUs, `tecLOCKED` for MPTs) 13. `Vault.AssetsAvailable < PrincipalRequested` (insufficient assets in the Vault). (`tecINSUFFICIENT_FUNDS`) -14. - - `LendingProtocol`: `Vault.AssetsMaximum != 0` and `Vault.AssetsTotal + InterestDue > Vault.AssetsMaximum` (expected interest would exceed vault assets cap). (`tecLIMIT_EXCEEDED`) +14. - `LendingProtocol`: `Vault.AssetsMaximum != 0` and `Vault.AssetsTotal + InterestDue > Vault.AssetsMaximum` (expected interest would exceed vault assets cap). (`tecLIMIT_EXCEEDED`) - `LendingProtocolV1_1`: The check does not apply if `Vault.LEVersion == 1`. 15. The combination of `PrincipalRequested`, `InterestRate`, `PaymentTotal`, and `PaymentInterval` results in a total interest amount that is zero or negative due to precision limitations. (`tecPRECISION_LOSS`) 16. The loan terms result in a periodic payment that is too small to cover the interest accrued in the first period, leaving no amount to pay down the principal. (`tecPRECISION_LOSS`) 17. The calculated periodic payment is so small that it rounds down to zero when adjusted for the asset's precision. (`tecPRECISION_LOSS`) 18. The rounding of the periodic payment (due to asset precision) is significant enough that the total number of payments required to settle the loan differs from the specified `PaymentTotal`. (`tecPRECISION_LOSS`) -19. - - `LendingProtocol`: `LoanBroker.DebtMaximum != 0` and `LoanBroker.DebtMaximum < LoanBroker.DebtTotal + PrincipalRequested + InterestDue` (exceeds maximum debt). (`tecLIMIT_EXCEEDED`) +19. - `LendingProtocol`: `LoanBroker.DebtMaximum != 0` and `LoanBroker.DebtMaximum < LoanBroker.DebtTotal + PrincipalRequested + InterestDue` (exceeds maximum debt). (`tecLIMIT_EXCEEDED`) - `LendingProtocolV1_1`: If `Vault.LEVersion == 1`, `LoanBroker.DebtMaximum != 0` and `LoanBroker.DebtMaximum < LoanBroker.DebtTotal + PrincipalRequested` (exceeds maximum debt). (`tecLIMIT_EXCEEDED`) -20. - - `LendingProtocol`: `LoanBroker.CoverAvailable < (LoanBroker.DebtTotal + PrincipalRequested + InterestDue) × LoanBroker.CoverRateMinimum` (insufficient first-loss capital). (`tecINSUFFICIENT_FUNDS`) +20. - `LendingProtocol`: `LoanBroker.CoverAvailable < (LoanBroker.DebtTotal + PrincipalRequested + InterestDue) × LoanBroker.CoverRateMinimum` (insufficient first-loss capital). (`tecINSUFFICIENT_FUNDS`) - `LendingProtocolV1_1`: If `Vault.LEVersion == 1`, `LoanBroker.CoverAvailable < (LoanBroker.DebtTotal + PrincipalRequested) × LoanBroker.CoverRateMinimum` (insufficient first-loss capital). (`tecINSUFFICIENT_FUNDS`) 21. The Borrower does not have sufficient reserve for the `Loan` object. (`tecINSUFFICIENT_RESERVE`) 22. The Borrower is not authorized for the asset. (`tecNO_AUTH`) @@ -1126,15 +1122,13 @@ The account specified in the `Account` field pays the transaction fee. - Decrease the `MPToken.MPTAmount` of the `Vault` _pseudo-account_ `MPToken` object by `PrincipalRequested`. - Increase the `MPToken.MPTAmount` of the `Borrower` `MPToken` object by `PrincipalRequested - LoanOriginationFee`. - Increase the `MPToken.MPTAmount` of the `LoanBroker.Owner` `MPToken` object by `LoanOriginationFee`. -6. - - `LendingProtocol`: Update `Vault` object: +6. - `LendingProtocol`: Update `Vault` object: - Decrease `Vault.AssetsAvailable` by `PrincipalRequested`. - Increase `Vault.AssetsTotal` by `InterestDue` (interest owed to the Vault, excluding management fee). - `LendingProtocolV1_1`: If `Vault.LEVersion == 1`, update `Vault` object: - Decrease `Vault.AssetsAvailable` by `PrincipalRequested`. - `Vault.AssetsTotal` is unchanged; `InterestDue` is not recognised at origination. -7. - - `LendingProtocol`: Update `LoanBroker` object: +7. - `LendingProtocol`: Update `LoanBroker` object: - Increase `LoanBroker.DebtTotal` by `PrincipalRequested + InterestDue`. - Increment `LoanBroker.OwnerCount` by `1`. - Increment `LoanBroker.LoanSequence` by `1`. @@ -1274,8 +1268,7 @@ This transaction uses the standard transaction fee. 5. `Loan.PaymentRemaining == 0` (fully paid loan cannot be modified). (`tecNO_PERMISSION`) 6. `tfLoanDefault` flag is specified and `Loan.NextPaymentDueDate + Loan.GracePeriod` has not yet passed. (`tecTOO_SOON`) 7. The submitter is not the `LoanBroker.Owner`. (`tecNO_PERMISSION`) -8. - - `LendingProtocol`: `tfLoanImpair` flag is specified and `Vault.LossUnrealized + (Loan.TotalValueOutstanding - Loan.ManagementFeeOutstanding) > Vault.AssetsTotal - Vault.AssetsAvailable` (impairment would exceed vault's unavailable assets). (`tecLIMIT_EXCEEDED`) +8. - `LendingProtocol`: `tfLoanImpair` flag is specified and `Vault.LossUnrealized + (Loan.TotalValueOutstanding - Loan.ManagementFeeOutstanding) > Vault.AssetsTotal - Vault.AssetsAvailable` (impairment would exceed vault's unavailable assets). (`tecLIMIT_EXCEEDED`) - `LendingProtocolV1_1`: If `Vault.LEVersion == 1`, `tfLoanImpair` flag is specified and `Vault.LossUnrealized + Loan.PrincipalOutstanding > Vault.AssetsTotal - Vault.AssetsAvailable` (impairment would exceed vault's unavailable assets). (`tecLIMIT_EXCEEDED`) #### 3.10.5 State Changes From fe59dfc5624f8db020fa10ed073a0c0e28994389 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:35:46 +0200 Subject: [PATCH 22/32] docs: drop patch-title prefixes and name parent amendments Match the XLS-65 review: section lead-ins no longer repeat the patch title, and Amendments names the original feature. --- XLS-0065-single-asset-vault/65.1/README.md | 6 +++--- XLS-0065-single-asset-vault/README.md | 1 + XLS-0066-lending-protocol/66.1/README.md | 6 +++--- XLS-0066-lending-protocol/README.md | 1 + 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/XLS-0065-single-asset-vault/65.1/README.md b/XLS-0065-single-asset-vault/65.1/README.md index f34f95388..c0a59f403 100644 --- a/XLS-0065-single-asset-vault/65.1/README.md +++ b/XLS-0065-single-asset-vault/65.1/README.md @@ -18,7 +18,7 @@ Under the `LendingProtocolV1_1` amendment, a Single Asset Vault accounts for the ## 2. Motivation -**Vault Cash-Basis Accounting.** Accrual-basis accounting credits a Vault with interest that a Borrower has not paid yet, so the share exchange rate reflects income the Vault has not received. A depositor can redeem shares at a rate inflated by interest that later defaults, and the loss falls on the depositors who remain. Cash-basis accounting removes that mismatch by recognising interest only when it is paid. +Accrual-basis accounting credits a Vault with interest that a Borrower has not paid yet, so the share exchange rate reflects income the Vault has not received. A depositor can redeem shares at a rate inflated by interest that later defaults, and the loss falls on the depositors who remain. Cash-basis accounting removes that mismatch by recognising interest only when it is paid. Existing Vaults cannot switch accounting models without changing the value of shares already issued, so the model has to be recorded per Vault rather than derived from the amendment state at the time of the transaction. @@ -74,12 +74,12 @@ No failure conditions related to cash-basis accounting are added or removed. `LE ## 4. Rationale -**Vault Cash-Basis Accounting.** The alternative to a per-entry version field is to derive the accounting model from the amendment state at the time each Loan transaction executes. That was rejected because it would silently change the share exchange rate of every existing Vault on the ledger at the moment the amendment activates. +The alternative to a per-entry version field is to derive the accounting model from the amendment state at the time each Loan transaction executes. That was rejected because it would silently change the share exchange rate of every existing Vault on the ledger at the moment the amendment activates. `LEVersion` is a version number rather than a boolean flag so that a later accounting change can be expressed as a further version without another field. ## 5. Security Considerations -**Vault Cash-Basis Accounting.** Cash-basis accounting reduces, but does not remove, the exposure of a depositor to an unpaid Loan: the Vault still carries the principal at face value until the Loan defaults or is impaired. `LossUnrealized` remains the mechanism for reporting an expected shortfall. +Cash-basis accounting reduces, but does not remove, the exposure of a depositor to an unpaid Loan: the Vault still carries the principal at face value until the Loan defaults or is impaired. `LossUnrealized` remains the mechanism for reporting an expected shortfall. Because `LEVersion` is immutable, an implementation must read it from the `Vault` ledger entry rather than from the amendment state when computing the interest accounting of a Loan. Reading the amendment state instead would apply cash-basis rules to accrual-basis Vaults and misprice their shares. diff --git a/XLS-0065-single-asset-vault/README.md b/XLS-0065-single-asset-vault/README.md index 6e8563010..d59535825 100644 --- a/XLS-0065-single-asset-vault/README.md +++ b/XLS-0065-single-asset-vault/README.md @@ -71,6 +71,7 @@ A protocol connecting to a Vault must track its debt. Furthermore, the updates t ### 2.8. Amendments +- `SingleAssetVault` (`featureSingleAssetVault`): the original vault amendment. It introduced the `Vault` ledger entry and the vault transactions; that behaviour is the parent of the patches below. - `LendingProtocolV1_1`, as described in [XLS-65.1](./65.1/README.md): - introduces `LEVersion = 1` for cash-basis Vault accounting. - Closed-ended Vault checks also gated by this amendment (`VaultKind`, `SubscriptionDate`, `RedemptionDate`, and the phase checks on `VaultDeposit` / `VaultWithdraw`) are specified in [PR #587](https://github.com/XRPLF/XRPL-Standards/pull/587), not in this patch. diff --git a/XLS-0066-lending-protocol/66.1/README.md b/XLS-0066-lending-protocol/66.1/README.md index fa58c82d4..548685774 100644 --- a/XLS-0066-lending-protocol/66.1/README.md +++ b/XLS-0066-lending-protocol/66.1/README.md @@ -18,7 +18,7 @@ Under the `LendingProtocolV1_1` amendment, for a Vault with `LEVersion == 1` (ca ## 2. Motivation -**Principal-Only Debt Accounting.** Under accrual accounting, issuing a Loan immediately increases `Vault.AssetsTotal` by the expected interest and `LoanBroker.DebtTotal` by the principal plus expected interest. The `AssetsMaximum` and `DebtMaximum` caps are therefore consumed by interest that has not been paid, and the first-loss capital requirement, which is a rate applied to `DebtTotal`, is sized against expected interest as well as principal. +Under accrual accounting, issuing a Loan immediately increases `Vault.AssetsTotal` by the expected interest and `LoanBroker.DebtTotal` by the principal plus expected interest. The `AssetsMaximum` and `DebtMaximum` caps are therefore consumed by interest that has not been paid, and the first-loss capital requirement, which is a rate applied to `DebtTotal`, is sized against expected interest as well as principal. ## 3. Specification @@ -83,10 +83,10 @@ For a Vault with `LEVersion == 1`, the amount computations in items 1 to 3 of [3 ## 4. Rationale -**Principal-Only Debt Accounting.** Interest could have been recognised on a schedule, one payment period at a time, rather than on receipt. That was rejected because it reintroduces the original problem in a smaller form: the Vault would still credit itself with interest for a period in which the Borrower ends up not paying. +Interest could have been recognised on a schedule, one payment period at a time, rather than on receipt. That was rejected because it reintroduces the original problem in a smaller form: the Vault would still credit itself with interest for a period in which the Borrower ends up not paying. ## 5. Security Considerations -**Principal-Only Debt Accounting.** Principal-only accounting makes `DebtTotal` a smaller number for the same set of Loans, so a `CoverRateMinimum` that was calibrated under accrual accounting yields less first-loss capital. A Broker migrating to a cash-basis Vault should re-derive the rate rather than reuse it. +Principal-only accounting makes `DebtTotal` a smaller number for the same set of Loans, so a `CoverRateMinimum` that was calibrated under accrual accounting yields less first-loss capital. A Broker migrating to a cash-basis Vault should re-derive the rate rather than reuse it. Both accounting models coexist on the ledger for as long as pre-amendment Vaults exist. An implementation must branch on `Vault.LEVersion` in every place it adjusts `AssetsTotal` or `DebtTotal`; branching in some places and not others corrupts the share exchange rate of the Vault. diff --git a/XLS-0066-lending-protocol/README.md b/XLS-0066-lending-protocol/README.md index 62113bf2e..8cea828ce 100644 --- a/XLS-0066-lending-protocol/README.md +++ b/XLS-0066-lending-protocol/README.md @@ -118,6 +118,7 @@ The lending protocol charges a number of fees that the Loan Broker can configure ### 2.7 Amendments +- `LendingProtocol` (`featureLendingProtocol`): the original lending amendment. That behaviour is the parent of the patches below. - `LendingProtocolV1_1`, as described in [XLS-66.1](./66.1/README.md): - introduces principal-only debt accounting and cash-basis interest recognition for Vaults with `LEVersion = 1`. - Closed-ended Vault phase checks on `LoanSet` and the closed-ended requirement on `LoanBrokerSet` are specified in [PR #587](https://github.com/XRPLF/XRPL-Standards/pull/587), not in this patch. From 8e22494bdc8b4da70d10f3f9d2fba431f5041a19 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Fri, 11 Sep 2026 22:44:24 +0200 Subject: [PATCH 23/32] docs: state origination InterestDue recognition and patch-over-parent branches --- XLS-0065-single-asset-vault/65.1/vault-cash-basis.md | 2 +- XLS-0066-lending-protocol/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/XLS-0065-single-asset-vault/65.1/vault-cash-basis.md b/XLS-0065-single-asset-vault/65.1/vault-cash-basis.md index c0a59f403..ad15ac07b 100644 --- a/XLS-0065-single-asset-vault/65.1/vault-cash-basis.md +++ b/XLS-0065-single-asset-vault/65.1/vault-cash-basis.md @@ -14,7 +14,7 @@ ## 1. Abstract -Under the `LendingProtocolV1_1` amendment, a Single Asset Vault accounts for the interest earned by its Loans on a cash basis. `AssetsTotal` counts interest only once a Borrower has paid it, rather than counting the interest a Loan is expected to earn from the moment the Loan is issued. The share exchange rate of the Vault therefore tracks the assets the Vault holds instead of the income it is owed, and a Borrower who stops paying costs the Vault only the principal it lent out. A Vault created before the amendment keeps accrual-basis accounting, in which `AssetsTotal` includes interest as it accrues, because changing the accounting model of an existing Vault would change the value of the shares it has already issued. Which model a Vault uses is recorded in its `LEVersion` field and fixed when the Vault is created. +Under the `LendingProtocolV1_1` amendment, a Single Asset Vault accounts for the interest earned by its Loans on a cash basis. `AssetsTotal` counts interest only once a Borrower has paid it, rather than counting the interest a Loan is expected to earn from the moment the Loan is issued. The share exchange rate of the Vault therefore tracks the assets the Vault holds instead of the income it is owed, and a Borrower who stops paying costs the Vault only the principal it lent out. A Vault created before the amendment keeps accrual-basis accounting, in which `LoanSet` immediately adds all expected net `InterestDue` to `AssetsTotal` at origination (XLS-66 §3.8.6), because changing the accounting model of an existing Vault would change the value of the shares it has already issued. Which model a Vault uses is recorded in its `LEVersion` field and fixed when the Vault is created. ## 2. Motivation diff --git a/XLS-0066-lending-protocol/README.md b/XLS-0066-lending-protocol/README.md index 2f1530bc5..63ccc1bd5 100644 --- a/XLS-0066-lending-protocol/README.md +++ b/XLS-0066-lending-protocol/README.md @@ -125,7 +125,7 @@ The lending protocol charges a number of fees that the Loan Broker can configure - `fixCleanup3_4_0`, as described in [XLS-66.2](./66.2/README.md): - prevents impairing a loan before it is late, stops impairment and unimpairment from rewriting `Loan.NextPaymentDueDate`, and makes the due-date and grace-period boundaries exclusive. -Throughout this specification, amendment-labelled bullets describe the behaviour when that amendment is enabled. +Throughout this specification, amendment-labelled bullets describe the behaviour when that amendment is enabled. Because `LendingProtocolV1_1` depends on `LendingProtocol`, both amendments are enabled together. An applicable patch branch replaces the corresponding parent branch; the `LendingProtocol` branch remains the fallback. ## 3. Specification From 8a686520780d6d0a540e8b62739447df775f5027 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Mon, 14 Sep 2026 16:52:13 +0200 Subject: [PATCH 24/32] docs(XLS-65): gate the VaultSet cap invariant on fixCleanup3_4_0 --- XLS-0065-single-asset-vault/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/XLS-0065-single-asset-vault/README.md b/XLS-0065-single-asset-vault/README.md index 3d66637f8..1718eb14b 100644 --- a/XLS-0065-single-asset-vault/README.md +++ b/XLS-0065-single-asset-vault/README.md @@ -76,6 +76,7 @@ A protocol connecting to a Vault must track its debt. Furthermore, the updates t - introduces `LEVersion = 1` for [cash-basis Vault accounting](./65.1/vault-cash-basis.md). - adds an optional [`MemoData` field to `VaultDelete`](./65.1/vault-memo.md) that, if present, must be 1–256 bytes. - Closed-ended Vault checks also gated by this amendment (`VaultKind`, `SubscriptionDate`, `RedemptionDate`, and the phase checks on `VaultDeposit` / `VaultWithdraw`) are specified in [PR #587](https://github.com/XRPLF/XRPL-Standards/pull/587), not in this patch. +- `fixCleanup3_4_0`: a later amendment on the `SingleAssetVault` parent. It narrows the `VaultSet` cap invariant in [§3.3.4](#334-invariants), so that a `Vault.AssetsTotal` already above the cap no longer blocks a `VaultSet` that leaves the cap alone. Cash-basis interest receipts are one way the cap is exceeded without a deposit. ## 3. Specification @@ -480,7 +481,8 @@ The `VaultSet` updates an existing `Vault` ledger object. 1. `VaultSet` must not change the vault pseudo-account's asset balance. 2. `VaultSet` must not change `Vault.AssetsTotal` or `Vault.AssetsAvailable`. 3. `VaultSet` must not change `MPTokenIssuance(Vault.ShareMPTID).OutstandingAmount`. -4. If `Vault.AssetsMaximum > 0`: `Vault.AssetsTotal <= Vault.AssetsMaximum`. +4. - `SingleAssetVault`: If `Vault.AssetsMaximum > 0`: `Vault.AssetsTotal <= Vault.AssetsMaximum`. + - `fixCleanup3_4_0`: This is checked only if the transaction supplies `AssetsMaximum` or otherwise changes the cap. ### 3.4 Transaction: `VaultDelete` From 34f8b77c24391cde10d209a68f447b8f6bda2eba Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Tue, 15 Sep 2026 10:36:12 +0200 Subject: [PATCH 25/32] docs: fix DebtTotal valueChange, AssetsMaximum cap, and 66.1 requires --- XLS-0065-single-asset-vault/README.md | 2 +- XLS-0066-lending-protocol/66.1/README.md | 3 ++- XLS-0066-lending-protocol/README.md | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/XLS-0065-single-asset-vault/README.md b/XLS-0065-single-asset-vault/README.md index 4bca69bf1..1d2014419 100644 --- a/XLS-0065-single-asset-vault/README.md +++ b/XLS-0065-single-asset-vault/README.md @@ -115,7 +115,7 @@ A vault has the following fields: | `AssetsTotal` | No | Yes | `number` | `NUMBER` | 0 | The total value of the vault. | | `AssetsAvailable` | No | Yes | `number` | `NUMBER` | 0 | The asset amount that is available in the vault. | | `LossUnrealized` | No | Yes | `number` | `NUMBER` | 0 | The potential loss amount that is not yet realized expressed as the vaults asset. | -| `AssetsMaximum` | No | No | `number` | `NUMBER` | 0 | The maximum asset amount that can be held in the vault. Zero value `0` indicates there is no cap. | +| `AssetsMaximum` | No | No | `number` | `NUMBER` | 0 | Cap on new deposits. Zero (`0`) means no cap. Accrued or collected interest may raise `AssetsTotal` above it; deposits still must not. | | `ShareMPTID` | Yes | Yes | `number` | `UINT192` | 0 | The identifier of the share MPTokenIssuance object. | | `WithdrawalPolicy` | Yes | Yes | `string` | `UINT8` | `N/A` | Indicates the withdrawal strategy used by the Vault. | | `Scale` | Yes | Yes | `number` | `UINT8` | 6 | The `Scale` specifies the power of 10 ($10^{\text{scale}}$) to multiply an asset's value by when converting it into an integer-based number of shares. | diff --git a/XLS-0066-lending-protocol/66.1/README.md b/XLS-0066-lending-protocol/66.1/README.md index 566360834..4edcbf43e 100644 --- a/XLS-0066-lending-protocol/66.1/README.md +++ b/XLS-0066-lending-protocol/66.1/README.md @@ -6,8 +6,9 @@ proposal-from: https://github.com/XRPLF/XRPL-Standards/discussions/190 status: Draft category: Amendment + requires: [XLS-65.1.3](../../XLS-0065-single-asset-vault/65.1/65.1.3-vault-cash-basis.md) created: 2026-09-04 - updated: 2026-09-08 + updated: 2026-09-15 # 66.1 Lending Protocol Cash-Basis Accounting diff --git a/XLS-0066-lending-protocol/README.md b/XLS-0066-lending-protocol/README.md index 63ccc1bd5..f658768ee 100644 --- a/XLS-0066-lending-protocol/README.md +++ b/XLS-0066-lending-protocol/README.md @@ -2260,7 +2260,7 @@ $$ valueChange = latePaymentInterest_{gross} - managementFee_{late} $$ -For a legacy (`LEVersion` absent) Vault, this `valueChange` represents the net increase in the loan's value and must be reflected in `Vault.AssetsTotal`. For a cash-basis Vault with `LEVersion == 1`, `valueChange` is not applied to `Vault.AssetsTotal`; the Vault instead recognises the `interestPaid` portion of the payment. Under either accounting model, this value change is not reflected in `Loan.TotalValueOutstanding` or `LoanBroker.DebtTotal`. It is an unanticipated increase in value. Note that `valueChange > 0` for late payments. +For a legacy (`LEVersion` absent) Vault, this `valueChange` represents the net increase in the loan's value and must be reflected in `Vault.AssetsTotal` and in `LoanBroker.DebtTotal`, which §3.11.5 decreases by `totalToVault - valueChange`. Omitting that adjustment on a late payment would subtract newly paid late interest that was never in the outstanding debt. For a cash-basis Vault with `LEVersion == 1`, `valueChange` is not applied to either field; the Vault recognises the `interestPaid` portion of the payment and `LoanBroker.DebtTotal` decreases by `principalPaid` only. Under either model this `valueChange` is not reflected in `Loan.TotalValueOutstanding`, because late interest was not part of the original loan value. Note that `valueChange > 0` for late payments. #### A-3.2.3 Loan Overpayment From 7840c9b87bb4c480bf80ab815594d3f17b5ea217 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Tue, 15 Sep 2026 10:36:42 +0200 Subject: [PATCH 26/32] docs: match VaultCreate AssetsMaximum to the deposit cap --- XLS-0065-single-asset-vault/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/XLS-0065-single-asset-vault/README.md b/XLS-0065-single-asset-vault/README.md index 1d2014419..ccd68f947 100644 --- a/XLS-0065-single-asset-vault/README.md +++ b/XLS-0065-single-asset-vault/README.md @@ -391,7 +391,7 @@ The `VaultCreate` transaction creates a new `Vault` object. | `Flags` | Yes | `number` | `UINT32` | 0 | Specifies the flags for the Vault. | | `Data` | No | `string` | `BLOB` | | Arbitrary Vault metadata, limited to 256 bytes. | | `Asset` | Yes | `string or object` | `ISSUE` | `N/A` | The asset (`XRP`, `IOU` or `MPT`) of the Vault. | -| `AssetsMaximum` | No | `number` | `NUMBER` | 0 | The maximum asset amount that can be held in a vault. | +| `AssetsMaximum` | No | `number` | `NUMBER` | 0 | Cap on new deposits. Zero (`0`) means no cap. Interest may raise `AssetsTotal` above it. | | `MPTokenMetadata` | No | `string` | `BLOB` | | Arbitrary metadata about the share `MPT`, in hex format, limited to 1024 bytes. | | `WithdrawalPolicy` | No | `number` | `UINT8` | `"FirstComeFirstServe"` | Indicates the withdrawal strategy used by the Vault. | | `DomainID` | No | `string` | `HASH256` | | The `PermissionedDomain` object ID associated with the shares of this Vault. | From 4d3ae679af20802d169467b5169e3a50b9c4fb6c Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Tue, 15 Sep 2026 11:16:18 +0200 Subject: [PATCH 27/32] docs(XLS-65): split LoanSet cap invariant for cash-basis Vaults --- XLS-0065-single-asset-vault/README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/XLS-0065-single-asset-vault/README.md b/XLS-0065-single-asset-vault/README.md index ccd68f947..b3ee9234b 100644 --- a/XLS-0065-single-asset-vault/README.md +++ b/XLS-0065-single-asset-vault/README.md @@ -8,7 +8,7 @@ category: Amendment requires: [XLS-33](../XLS-0033-multi-purpose-tokens/README.md) created: 2024-04-12 - updated: 2026-09-11 + updated: 2026-09-15 # Single Asset Vault @@ -370,7 +370,9 @@ The Vault does not apply the [Transfer Fee](https://xrpl.org/docs/concepts/token 8. The `MPTokenIssuance` identified by `Vault.ShareMPTID` must exist, and its `OutstandingAmount` must not exceed its `MaximumAmount`. `VaultCreate` leaves `MaximumAmount` absent (§3.1.6.2.1), in which case the bound is the protocol maximum `0x7FFFFFFFFFFFFFFF`. 9. On creation, `Vault.AssetsTotal`, `Vault.AssetsAvailable`, `Vault.LossUnrealized` and the share `MPTokenIssuance.OutstandingAmount` are all zero. The share issuance has `Issuer == Vault.Account`, and `AccountRoot(Vault.Account)` is a _pseudo-account_ whose `VaultID` points to the new vault. Only `VaultCreate` may create a `Vault`. 10. Only `VaultDelete` may delete a `Vault`. It must also delete the share `MPTokenIssuance`, and the deleted vault must have `AssetsTotal == 0`, `AssetsAvailable == 0` and no outstanding shares. -11. There is no invariant requiring `Vault.AssetsTotal <= Vault.AssetsMaximum` to hold on every modification of the entry, because `Vault.AssetsTotal` grows with accrued interest and interest is not a deposit. The cap is instead enforced per transaction, by the checks in items 12 and 13 and, unchanged by either amendment, by `LoanSet`, which fails when `Vault.AssetsMaximum` is non-zero and either `Vault.AssetsTotal >= Vault.AssetsMaximum` or `Vault.AssetsTotal + InterestDue > Vault.AssetsMaximum` (see the [Lending Protocol](../XLS-0066-lending-protocol/README.md) §3.8.5.2): +11. There is no invariant requiring `Vault.AssetsTotal <= Vault.AssetsMaximum` to hold on every modification of the entry, because `Vault.AssetsTotal` grows with interest and interest is not a deposit. The cap is instead enforced per transaction, by the checks in items 12 and 13 and by `LoanSet` (see the [Lending Protocol](../XLS-0066-lending-protocol/README.md) §3.8.5.2 items 6 and 14): + - `LendingProtocol` / legacy Vault (`LEVersion` absent): `LoanSet` fails when `Vault.AssetsMaximum` is non-zero and either `Vault.AssetsTotal >= Vault.AssetsMaximum` or `Vault.AssetsTotal + InterestDue > Vault.AssetsMaximum`. + - `LendingProtocolV1_1`: If `Vault.LEVersion == 1`, those two `LoanSet` checks do not apply; collected interest may raise `AssetsTotal` above the cap. For a legacy Vault they remain as above. 12. `VaultDeposit` fails when `Vault.AssetsMaximum` is non-zero and the post-deposit `Vault.AssetsTotal` exceeds it. 13. - `SingleAssetVault`: `VaultSet` fails when `Vault.AssetsMaximum` is non-zero and `Vault.AssetsTotal` exceeds it. - `fixCleanup3_4_0`: `VaultSet` fails when `Vault.AssetsMaximum` is non-zero, `Vault.AssetsTotal` exceeds it, and the transaction supplies `AssetsMaximum` or the cap otherwise changes. A cap already exceeded by accrued interest no longer blocks a `VaultSet` that omits `AssetsMaximum` and does not otherwise change the cap. From 90d9c0043ab5077836eb262ef70351fd4b9b9569 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Tue, 15 Sep 2026 11:47:22 +0200 Subject: [PATCH 28/32] docs(XLS-66): bump updated date and label legacy accounting examples --- XLS-0066-lending-protocol/README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/XLS-0066-lending-protocol/README.md b/XLS-0066-lending-protocol/README.md index f658768ee..e32fba8cf 100644 --- a/XLS-0066-lending-protocol/README.md +++ b/XLS-0066-lending-protocol/README.md @@ -8,7 +8,7 @@ category: Amendment requires: XLS-65, XLS-64 created: 2024-10-18 - updated: 2026-09-11 + updated: 2026-09-15 proposal-from: https://github.com/XRPLF/XRPL-Standards/discussions/190 @@ -251,7 +251,9 @@ _TBD_ The Lending Protocol tracks the funds owed to the associated Vault in the `DebtTotal` attribute. For a legacy Vault, it captures principal and interest due, excluding all fees; for a Vault with `LEVersion = 1`, it captures principal only. The `DebtMaximum` attribute controls the maximum debt a Lending Protocol may incur. When $DebtTotal \geq DebtMaximum$, the Lender cannot issue new loans until some of the debt is cleared. Furthermore, the Lender may not issue a loan that would cause the `DebtTotal` to exceed `DebtMaximum`. -**Examples** +**Examples (legacy Vault, `LEVersion` absent)** + +These examples credit expected interest at origination. For a cash-basis Vault (`LEVersion = 1`) see [§3.10.5.1](#31051-worked-example-pre--vs-post-lendingprotocolv1_1-loan-default). **Example 1: Issuing a Loan** From c7f12a4cca78010616e88afe52497c8c66841bc4 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Tue, 15 Sep 2026 12:16:31 +0200 Subject: [PATCH 29/32] docs(XLS-66): label first-loss default example as legacy --- XLS-0066-lending-protocol/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/XLS-0066-lending-protocol/README.md b/XLS-0066-lending-protocol/README.md index e32fba8cf..912df6768 100644 --- a/XLS-0066-lending-protocol/README.md +++ b/XLS-0066-lending-protocol/README.md @@ -348,7 +348,9 @@ Whenever the available cover falls below the minimum cover required, two consequ - The Lender cannot issue new Loans. - The Lender cannot directly receive fees. The fees are instead added to the First-Loss Capital to cover the deficit. -**Examples** +**Examples (legacy Vault, `LEVersion` absent)** + +`DefaultAmount` here is principal plus interest. For a cash-basis Vault (`LEVersion = 1`), `DefaultAmount` is principal only; see [§3.10.5.1](#31051-worked-example-pre--vs-post-lendingprotocolv1_1-loan-default). **Example 1: Loan Default** From 375a7b00da5a3ca264dcfe54b692bf1456dcd283 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Tue, 15 Sep 2026 12:47:19 +0200 Subject: [PATCH 30/32] docs(XLS-66): gate remaining valueChange prose to legacy Vaults --- XLS-0066-lending-protocol/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/XLS-0066-lending-protocol/README.md b/XLS-0066-lending-protocol/README.md index 912df6768..7cb223676 100644 --- a/XLS-0066-lending-protocol/README.md +++ b/XLS-0066-lending-protocol/README.md @@ -1522,7 +1522,7 @@ The `Loan` object is updated to reflect the payment. **3. `LoanBroker` and `Vault` Object State Changes** -The `LoanBroker` and `Vault` objects are updated to reflect the new accounting state. The `valueChange`—representing the net change in the loan's total future interest—is applied to both the `LoanBroker` and the `Vault`, but with an important distinction for late payments. +The `LoanBroker` and `Vault` objects are updated to reflect the new accounting state. For a legacy Vault (`LEVersion` absent), `valueChange`—the net change in the loan's total future interest—is applied to both the `LoanBroker` and the `Vault`, with an important distinction for late payments. For a cash-basis Vault (`LEVersion == 1`), `valueChange` is not applied to either field; steps 6 and 7 use `principalPaid` and `interestPaid` instead. 6. **`LoanBroker` Updates**: - `LoanBroker.DebtTotal`: @@ -1837,7 +1837,8 @@ $$ - `valueChange > 0` (always positive for late payments) - Not reflected in `Loan.TotalValueOutstanding` (unanticipated value increase) -- Applied directly to `Vault.AssetsTotal` and `LoanBroker.DebtTotal` +- Legacy Vault (`LEVersion` absent): applied directly to `Vault.AssetsTotal` and `LoanBroker.DebtTotal` +- Cash-basis Vault (`LEVersion == 1`): not applied to either field; `Vault.AssetsTotal` increases by `interestPaid` and `LoanBroker.DebtTotal` decreases by `principalPaid` ### 5. Overpayment Processing From 3c259da3f4a510fe3840b3014cf144174e036fa6 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Wed, 16 Sep 2026 18:38:12 +0200 Subject: [PATCH 31/32] docs: call legacy Vault accounting instant interest recognition The pre-amendment Vault books all expected InterestDue at origination, which is not accrual-basis accounting. --- .../65.1/65.1.3-vault-cash-basis.md | 16 +-- XLS-0065-single-asset-vault/README.md | 118 +++++++++--------- XLS-0066-lending-protocol/66.1/README.md | 4 +- XLS-0066-lending-protocol/README.md | 2 +- 4 files changed, 70 insertions(+), 70 deletions(-) diff --git a/XLS-0065-single-asset-vault/65.1/65.1.3-vault-cash-basis.md b/XLS-0065-single-asset-vault/65.1/65.1.3-vault-cash-basis.md index 87abbc390..535d2a0a7 100644 --- a/XLS-0065-single-asset-vault/65.1/65.1.3-vault-cash-basis.md +++ b/XLS-0065-single-asset-vault/65.1/65.1.3-vault-cash-basis.md @@ -14,11 +14,11 @@ ## 1. Abstract -Under the `LendingProtocolV1_1` amendment, a Single Asset Vault accounts for the interest earned by its Loans on a cash basis. `AssetsTotal` counts interest only once a Borrower has paid it, rather than counting the interest a Loan is expected to earn from the moment the Loan is issued. The share exchange rate of the Vault therefore tracks the assets the Vault holds instead of the income it is owed, and a Borrower who stops paying costs the Vault only the principal it lent out. A Vault created before the amendment keeps accrual-basis accounting, in which `LoanSet` immediately adds all expected net `InterestDue` to `AssetsTotal` at origination (XLS-66 §3.8.6), because changing the accounting model of an existing Vault would change the value of the shares it has already issued. Which model a Vault uses is recorded in its `LEVersion` field and fixed when the Vault is created. +Under the `LendingProtocolV1_1` amendment, a Single Asset Vault accounts for the interest earned by its Loans on a cash basis. `AssetsTotal` counts interest only once a Borrower has paid it, rather than counting the interest a Loan is expected to earn from the moment the Loan is issued. The share exchange rate of the Vault therefore tracks the assets the Vault holds instead of the income it is owed, and a Borrower who stops paying costs the Vault only the principal it lent out. A Vault created before the amendment keeps instant interest recognition, in which `LoanSet` immediately adds all expected net `InterestDue` to `AssetsTotal` at origination (XLS-66 §3.8.6), because changing the accounting model of an existing Vault would change the value of the shares it has already issued. Which model a Vault uses is recorded in its `LEVersion` field and fixed when the Vault is created. ## 2. Motivation -Accrual-basis accounting credits a Vault with interest that a Borrower has not paid yet, so the share exchange rate reflects income the Vault has not received. A depositor can redeem shares at a rate inflated by interest that later defaults, and the loss falls on the depositors who remain. Cash-basis accounting removes that mismatch by recognising interest only when it is paid. +Instant interest recognition credits a Vault with interest that a Borrower has not paid yet, so the share exchange rate reflects income the Vault has not received. A depositor can redeem shares at a rate inflated by interest that later defaults, and the loss falls on the depositors who remain. Cash-basis accounting removes that mismatch by recognising interest only when it is paid. Existing Vaults cannot switch accounting models without changing the value of shares already issued, so the model has to be recorded per Vault rather than derived from the amendment state at the time of the transaction. @@ -28,18 +28,18 @@ Existing Vaults cannot switch accounting models without changing the value of sh #### 3.1.1 Fields -| Field Name | Constant | Required | Internal Type | Default Value | Description | -| ----------- | :------: | :------: | :-----------: | :-----------: | ---------------------------------------------------------------------------------------------------------------------- | -| `LEVersion` | Yes | No | `UINT8` | absent (`0`) | The accounting model the Vault applies to `AssetsTotal`. Absent is treated as `0` (accrual basis). Immutable once set. | +| Field Name | Constant | Required | Internal Type | Default Value | Description | +| ----------- | :------: | :------: | :-----------: | :-----------: | ------------------------------------------------------------------------------------------------------------------------------------- | +| `LEVersion` | Yes | No | `UINT8` | absent (`0`) | The accounting model the Vault applies to `AssetsTotal`. Absent is treated as `0` (instant interest recognition). Immutable once set. | #### 3.1.2 `LEVersion` `LEVersion` selects the accounting model of the Vault: -- `LEVersion` absent (treated as `0`, accrual basis): `AssetsTotal` includes the interest accrued over the life of the connected Loans. This is the pre-amendment behaviour and continues to apply to every Vault created before the amendment was enabled. +- `LEVersion` absent (treated as `0`, instant interest recognition): `AssetsTotal` includes the full expected net `InterestDue` booked at origination. This is the pre-amendment behaviour and continues to apply to every Vault created before the amendment was enabled. - `LEVersion = 1` (cash basis): `AssetsTotal` excludes uncollected interest and increases only as interest is collected in cash. -The field is written once by `VaultCreate` and is immutable after creation. A Vault created while the amendment is enabled is always cash-basis, and a Vault created before it is always accrual-basis; neither can be converted to the other. +The field is written once by `VaultCreate` and is immutable after creation. A Vault created while the amendment is enabled is always cash-basis, and a Vault created before it always uses instant interest recognition; neither can be converted to the other. #### 3.1.3 Invariants @@ -82,4 +82,4 @@ The alternative to a per-entry version field is to derive the accounting model f Cash-basis accounting reduces, but does not remove, the exposure of a depositor to an unpaid Loan: the Vault still carries the principal at face value until the Loan defaults or is impaired. `LossUnrealized` remains the mechanism for reporting an expected shortfall. -Because `LEVersion` is immutable, an implementation must read it from the `Vault` ledger entry rather than from the amendment state when computing the interest accounting of a Loan. Reading the amendment state instead would apply cash-basis rules to accrual-basis Vaults and misprice their shares. +Because `LEVersion` is immutable, an implementation must read it from the `Vault` ledger entry rather than from the amendment state when computing the interest accounting of a Loan. Reading the amendment state instead would apply cash-basis rules to Vaults that use instant interest recognition and misprice their shares. diff --git a/XLS-0065-single-asset-vault/README.md b/XLS-0065-single-asset-vault/README.md index b3ee9234b..5ab12f922 100644 --- a/XLS-0065-single-asset-vault/README.md +++ b/XLS-0065-single-asset-vault/README.md @@ -99,27 +99,27 @@ The key of the `Vault` object is the result of [`SHA512-Half`](https://xrpl.org/ A vault has the following fields: -| Field Name | Constant | Required | JSON Type | Internal Type | Default Value | Description | -| ------------------- | :------: | :------: | :----------------: | :-----------: | :-----------: | :------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `LedgerEntryType` | No | Yes | `string` | `UINT16` | `0x0084` | Ledger object type. | -| `LedgerIndex` | No | Yes | `string` | `UINT16` | `N/A` | Ledger object identifier. | -| `Flags` | Yes | Yes | `string` | `UINT32` | 0 | Ledger object flags. | -| `PreviousTxnID` | No | Yes | `string` | `HASH256` | `N/A` | Identifies the transaction ID that most recently modified this object. | -| `PreviousTxnLgrSeq` | No | Yes | `number` | `UINT32` | `N/A` | The sequence of the ledger that contains the transaction that most recently modified this object. | -| `Sequence` | Yes | Yes | `number` | `UINT32` | `N/A` | The transaction sequence number that created the vault. | -| `OwnerNode` | Yes | Yes | `number` | `UINT64` | `N/A` | Identifies the page where this item is referenced in the owner's directory. | -| `Owner` | Yes | Yes | `string` | `AccountID` | `N/A` | The account address of the Vault Owner. | -| `Account` | Yes | Yes | `string` | `AccountID` | `N/A` | The address of the Vaults _pseudo-account_. | -| `Data` | No | No | `string` | `BLOB` | None | Arbitrary metadata about the Vault. Limited to 256 bytes. | -| `Asset` | Yes | Yes | `string or object` | `ISSUE` | `N/A` | The asset of the vault. The vault supports `XRP`, `IOU` and `MPT`. | -| `AssetsTotal` | No | Yes | `number` | `NUMBER` | 0 | The total value of the vault. | -| `AssetsAvailable` | No | Yes | `number` | `NUMBER` | 0 | The asset amount that is available in the vault. | -| `LossUnrealized` | No | Yes | `number` | `NUMBER` | 0 | The potential loss amount that is not yet realized expressed as the vaults asset. | -| `AssetsMaximum` | No | No | `number` | `NUMBER` | 0 | Cap on new deposits. Zero (`0`) means no cap. Accrued or collected interest may raise `AssetsTotal` above it; deposits still must not. | -| `ShareMPTID` | Yes | Yes | `number` | `UINT192` | 0 | The identifier of the share MPTokenIssuance object. | -| `WithdrawalPolicy` | Yes | Yes | `string` | `UINT8` | `N/A` | Indicates the withdrawal strategy used by the Vault. | -| `Scale` | Yes | Yes | `number` | `UINT8` | 6 | The `Scale` specifies the power of 10 ($10^{\text{scale}}$) to multiply an asset's value by when converting it into an integer-based number of shares. | -| `LEVersion` | Yes | No | `number` | `UINT8` | absent (`0`) | Immutable protocol-selected ledger entry version. Absent is treated as `0` (legacy / accrual-basis). See [3.1.2.2](#3122-leversion-lendingprotocolv1_1). | +| Field Name | Constant | Required | JSON Type | Internal Type | Default Value | Description | +| ------------------- | :------: | :------: | :----------------: | :-----------: | :-----------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `LedgerEntryType` | No | Yes | `string` | `UINT16` | `0x0084` | Ledger object type. | +| `LedgerIndex` | No | Yes | `string` | `UINT16` | `N/A` | Ledger object identifier. | +| `Flags` | Yes | Yes | `string` | `UINT32` | 0 | Ledger object flags. | +| `PreviousTxnID` | No | Yes | `string` | `HASH256` | `N/A` | Identifies the transaction ID that most recently modified this object. | +| `PreviousTxnLgrSeq` | No | Yes | `number` | `UINT32` | `N/A` | The sequence of the ledger that contains the transaction that most recently modified this object. | +| `Sequence` | Yes | Yes | `number` | `UINT32` | `N/A` | The transaction sequence number that created the vault. | +| `OwnerNode` | Yes | Yes | `number` | `UINT64` | `N/A` | Identifies the page where this item is referenced in the owner's directory. | +| `Owner` | Yes | Yes | `string` | `AccountID` | `N/A` | The account address of the Vault Owner. | +| `Account` | Yes | Yes | `string` | `AccountID` | `N/A` | The address of the Vaults _pseudo-account_. | +| `Data` | No | No | `string` | `BLOB` | None | Arbitrary metadata about the Vault. Limited to 256 bytes. | +| `Asset` | Yes | Yes | `string or object` | `ISSUE` | `N/A` | The asset of the vault. The vault supports `XRP`, `IOU` and `MPT`. | +| `AssetsTotal` | No | Yes | `number` | `NUMBER` | 0 | The total value of the vault. | +| `AssetsAvailable` | No | Yes | `number` | `NUMBER` | 0 | The asset amount that is available in the vault. | +| `LossUnrealized` | No | Yes | `number` | `NUMBER` | 0 | The potential loss amount that is not yet realized expressed as the vaults asset. | +| `AssetsMaximum` | No | No | `number` | `NUMBER` | 0 | Cap on new deposits. Zero (`0`) means no cap. Accrued or collected interest may raise `AssetsTotal` above it; deposits still must not. | +| `ShareMPTID` | Yes | Yes | `number` | `UINT192` | 0 | The identifier of the share MPTokenIssuance object. | +| `WithdrawalPolicy` | Yes | Yes | `string` | `UINT8` | `N/A` | Indicates the withdrawal strategy used by the Vault. | +| `Scale` | Yes | Yes | `number` | `UINT8` | 6 | The `Scale` specifies the power of 10 ($10^{\text{scale}}$) to multiply an asset's value by when converting it into an integer-based number of shares. | +| `LEVersion` | Yes | No | `number` | `UINT8` | absent (`0`) | Immutable protocol-selected ledger entry version. Absent is treated as `0` (legacy / instant interest recognition). See [3.1.2.2](#3122-leversion-lendingprotocolv1_1). | ##### 3.1.2.1 Flags @@ -137,8 +137,8 @@ The `Vault` object supports the following flags: For the `Vault` object, `LEVersion` distinguishes the share-valuation and accounting behavior applied to the vault: -- **Absent (default value `0`)**: The Vault was created before the `LendingProtocolV1_1` amendment was enabled, or otherwise predates ledger entry versioning. It uses the legacy, accrual-basis accounting described throughout this document and in [XLS-66](../XLS-0066-lending-protocol/README.md) — `AssetsTotal` includes accrued interest. -- **`1`**: The Vault was created while the `LendingProtocolV1_1` amendment was enabled. It exclusively uses the cash-basis accounting introduced by that amendment — `AssetsTotal` excludes uncollected interest and increases when interest is paid (see [3.1.7.4](#3174-cash-basis-vs-accrual-basis-accounting-lendingprotocolv1_1) and the `LendingProtocolV1_1` entries in [XLS-66](../XLS-0066-lending-protocol/README.md)). +- **Absent (default value `0`)**: The Vault was created before the `LendingProtocolV1_1` amendment was enabled, or otherwise predates ledger entry versioning. It uses the legacy instant interest recognition described throughout this document and in [XLS-66](../XLS-0066-lending-protocol/README.md) — `AssetsTotal` includes expected interest booked at origination. +- **`1`**: The Vault was created while the `LendingProtocolV1_1` amendment was enabled. It exclusively uses the cash-basis accounting introduced by that amendment — `AssetsTotal` excludes uncollected interest and increases when interest is paid (see [3.1.7.4](#3174-cash-basis-vs-instant-interest-recognition-lendingprotocolv1_1) and the `LendingProtocolV1_1` entries in [XLS-66](../XLS-0066-lending-protocol/README.md)). Any Vault created once `LendingProtocolV1_1` is enabled is always assigned `LEVersion = 1` — cash-basis accounting is not optional or user-selectable at creation time. A future revision of the `Vault` ledger entry would increment `LEVersion` to `2`, and so on. @@ -337,13 +337,13 @@ Withdrawal policy controls the logic used when removing liquidity from a vault. The First Come, First Serve strategy treats all requests equally, allowing a depositor to redeem any amount of assets provided they have a sufficient number of shares. -##### 3.1.7.4 Cash-Basis vs. Accrual-Basis Accounting (`LendingProtocolV1_1`) +##### 3.1.7.4 Cash-Basis vs Instant Interest Recognition (`LendingProtocolV1_1`) -The exchange rate algorithms in [3.1.7.2](#3172-exchange-rate-algorithms) above operate on $\Gamma_{assets}$ (`Vault.AssetsTotal`) without regard to how that value is accrued — that accrual behavior is defined by the connected protocol (see [XLS-66](../XLS-0066-lending-protocol/README.md)), not by the Vault's own exchange-rate math, which is unchanged by this amendment. +The exchange rate algorithms in [3.1.7.2](#3172-exchange-rate-algorithms) above operate on $\Gamma_{assets}$ (`Vault.AssetsTotal`) without regard to how interest is recognised into that value — that recognition behavior is defined by the connected protocol (see [XLS-66](../XLS-0066-lending-protocol/README.md)), not by the Vault's own exchange-rate math, which is unchanged by this amendment. Under the `LendingProtocolV1_1` amendment, `Vault.LEVersion` (see [3.1.2.2](#3122-leversion-lendingprotocolv1_1)) determines which of the two accounting models a Vault's `AssetsTotal` follows: -- `LEVersion` absent (`0`, legacy/accrual-basis): `AssetsTotal` includes interest upfront, as accrued over the life of connected Loans. This is the pre-amendment behavior and continues unchanged for Vaults created before `LendingProtocolV1_1` was enabled. +- `LEVersion` absent (`0`, legacy / instant interest recognition): `AssetsTotal` includes interest upfront at origination. This is the pre-amendment behavior and continues unchanged for Vaults created before `LendingProtocolV1_1` was enabled. - `LEVersion = 1` (cash-basis): `AssetsTotal` excludes uncollected interest and increases only as interest is actually collected in cash. This is the exclusive behavior for Vaults created once `LendingProtocolV1_1` is enabled. See the `LendingProtocolV1_1` entries in the failure conditions and state changes of [XLS-66 §3.8](../XLS-0066-lending-protocol/README.md#38-transaction-loanset), [§3.10](../XLS-0066-lending-protocol/README.md#310-transaction-loanmanage), and [§3.11](../XLS-0066-lending-protocol/README.md#311-transaction-loanpay) for the precise accounting differences, all of which are gated on `Vault.LEVersion == 1`. @@ -858,39 +858,39 @@ This RPC retrieves the Vault ledger entry and the IDs associated with it. #### 3.9.2 Response Fields -| Field Name | Always Present? | JSON Type | Description | -| -------------------------------- | :-------------: | :-------: | ------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `vault` | Yes | `object` | Root object representing the vault. | -| `vault.Account` | Yes | `string` | The pseudo-account ID of the vault. | -| `vault.Asset` | Yes | `object` | Object representing the asset held in the vault. | -| `vault.Asset.currency` | Yes | `string` | Currency code of the asset stored in the vault. | -| `vault.Asset.issuer` | No | `string` | Issuer address of the asset. | -| `vault.AssetsAvailable` | Yes | `string` | Amount of assets currently available for withdrawal. | -| `vault.AssetsTotal` | Yes | `string` | Total amount of assets in the vault. | -| `vault.Flags` | No | `number` | Bit-field flags associated with the vault. | -| `vault.LedgerEntryType` | Yes | `string` | Ledger entry type, always "Vault". | -| `vault.LossUnrealized` | No | `string` | Unrealized loss associated with the vault. | -| `vault.Owner` | Yes | `string` | ID of the Vault Owner account. | -| `vault.OwnerNode` | No | `string` | Identifier for the owner node in the ledger tree. | -| `vault.PreviousTxnID` | Yes | `string` | Transaction ID of the last modification to this vault. | -| `vault.PreviousTxnLgrSeq` | Yes | `number` | Ledger sequence number of the last transaction modifying this vault. | -| `vault.Sequence` | Yes | `number` | Sequence number of the vault entry. | -| `vault.ShareMPTID` | No | `string` | Multi-purpose token ID associated with this vault. | -| `vault.WithdrawalPolicy` | No | `number` | Policy defining withdrawal conditions. | -| `vault.index` | Yes | `string` | Unique index of the vault ledger entry. | -| `vault.shares` | Yes | `object` | Object containing details about issued shares. | -| `vault.shares.Flags` | No | `number` | Bit-field flags associated with the shares issuance. | -| `vault.shares.Issuer` | Yes | `string` | The ID of the Issuer of the Share. It will always be the pseudo-account ID. | -| `vault.shares.LedgerEntryType` | Yes | `string` | Ledger entry type, always "MPTokenIssuance". | -| `vault.shares.OutstandingAmount` | Yes | `string` | Total outstanding shares issued. | -| `vault.shares.OwnerNode` | No | `string` | Identifier for the owner node of the shares. | -| `vault.shares.PreviousTxnID` | Yes | `string` | Transaction ID of the last modification to the shares issuance. | -| `vault.shares.PreviousTxnLgrSeq` | Yes | `number` | Ledger sequence number of the last transaction modifying the shares issuance. | -| `vault.shares.Sequence` | Yes | `number` | Sequence number of the shares issuance entry. | -| `vault.shares.index` | Yes | `string` | Unique index of the shares ledger entry. | -| `vault.shares.mpt_issuance_id` | No | `string` | The ID of the `MPTokenIssuance` object. It will always be equal to `vault.ShareMPTID`. | -| `vault.Scale` | Yes | `number` | The `Scale` specifies the power of 10 ($10^{\text{scale}}$) to multiply an asset's value by when converting it into an integer-based number of shares. | -| `vault.LEVersion` | No | `number` | Accounting model. Absent or `0` is accrual-basis; `1` is cash-basis under `LendingProtocolV1_1`. Clients use this to interpret `AssetsTotal`. | +| Field Name | Always Present? | JSON Type | Description | +| -------------------------------- | :-------------: | :-------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `vault` | Yes | `object` | Root object representing the vault. | +| `vault.Account` | Yes | `string` | The pseudo-account ID of the vault. | +| `vault.Asset` | Yes | `object` | Object representing the asset held in the vault. | +| `vault.Asset.currency` | Yes | `string` | Currency code of the asset stored in the vault. | +| `vault.Asset.issuer` | No | `string` | Issuer address of the asset. | +| `vault.AssetsAvailable` | Yes | `string` | Amount of assets currently available for withdrawal. | +| `vault.AssetsTotal` | Yes | `string` | Total amount of assets in the vault. | +| `vault.Flags` | No | `number` | Bit-field flags associated with the vault. | +| `vault.LedgerEntryType` | Yes | `string` | Ledger entry type, always "Vault". | +| `vault.LossUnrealized` | No | `string` | Unrealized loss associated with the vault. | +| `vault.Owner` | Yes | `string` | ID of the Vault Owner account. | +| `vault.OwnerNode` | No | `string` | Identifier for the owner node in the ledger tree. | +| `vault.PreviousTxnID` | Yes | `string` | Transaction ID of the last modification to this vault. | +| `vault.PreviousTxnLgrSeq` | Yes | `number` | Ledger sequence number of the last transaction modifying this vault. | +| `vault.Sequence` | Yes | `number` | Sequence number of the vault entry. | +| `vault.ShareMPTID` | No | `string` | Multi-purpose token ID associated with this vault. | +| `vault.WithdrawalPolicy` | No | `number` | Policy defining withdrawal conditions. | +| `vault.index` | Yes | `string` | Unique index of the vault ledger entry. | +| `vault.shares` | Yes | `object` | Object containing details about issued shares. | +| `vault.shares.Flags` | No | `number` | Bit-field flags associated with the shares issuance. | +| `vault.shares.Issuer` | Yes | `string` | The ID of the Issuer of the Share. It will always be the pseudo-account ID. | +| `vault.shares.LedgerEntryType` | Yes | `string` | Ledger entry type, always "MPTokenIssuance". | +| `vault.shares.OutstandingAmount` | Yes | `string` | Total outstanding shares issued. | +| `vault.shares.OwnerNode` | No | `string` | Identifier for the owner node of the shares. | +| `vault.shares.PreviousTxnID` | Yes | `string` | Transaction ID of the last modification to the shares issuance. | +| `vault.shares.PreviousTxnLgrSeq` | Yes | `number` | Ledger sequence number of the last transaction modifying the shares issuance. | +| `vault.shares.Sequence` | Yes | `number` | Sequence number of the shares issuance entry. | +| `vault.shares.index` | Yes | `string` | Unique index of the shares ledger entry. | +| `vault.shares.mpt_issuance_id` | No | `string` | The ID of the `MPTokenIssuance` object. It will always be equal to `vault.ShareMPTID`. | +| `vault.Scale` | Yes | `number` | The `Scale` specifies the power of 10 ($10^{\text{scale}}$) to multiply an asset's value by when converting it into an integer-based number of shares. | +| `vault.LEVersion` | No | `number` | Accounting model. Absent or `0` is instant interest recognition; `1` is cash-basis under `LendingProtocolV1_1`. Clients use this to interpret `AssetsTotal`. | #### 3.9.3 Failure Conditions diff --git a/XLS-0066-lending-protocol/66.1/README.md b/XLS-0066-lending-protocol/66.1/README.md index 4edcbf43e..10a310be1 100644 --- a/XLS-0066-lending-protocol/66.1/README.md +++ b/XLS-0066-lending-protocol/66.1/README.md @@ -19,7 +19,7 @@ Under the `LendingProtocolV1_1` amendment, for a Vault with `LEVersion == 1` (ca ## 2. Motivation -Under accrual accounting, issuing a Loan immediately increases `Vault.AssetsTotal` by the expected interest and `LoanBroker.DebtTotal` by the principal plus expected interest. The `AssetsMaximum` and `DebtMaximum` caps are therefore consumed by interest that has not been paid, and the first-loss capital requirement, which is a rate applied to `DebtTotal`, is sized against expected interest as well as principal. +Under instant interest recognition, issuing a Loan immediately increases `Vault.AssetsTotal` by the expected interest and `LoanBroker.DebtTotal` by the principal plus expected interest. The `AssetsMaximum` and `DebtMaximum` caps are therefore consumed by interest that has not been paid, and the first-loss capital requirement, which is a rate applied to `DebtTotal`, is sized against expected interest as well as principal. ## 3. Specification @@ -88,6 +88,6 @@ Interest could have been recognised on a schedule, one payment period at a time, ## 5. Security Considerations -Principal-only accounting makes `DebtTotal` a smaller number for the same set of Loans, so a `CoverRateMinimum` that was calibrated under accrual accounting yields less first-loss capital. A Broker migrating to a cash-basis Vault should re-derive the rate rather than reuse it. +Principal-only accounting makes `DebtTotal` a smaller number for the same set of Loans, so a `CoverRateMinimum` that was calibrated under instant interest recognition yields less first-loss capital. A Broker migrating to a cash-basis Vault should re-derive the rate rather than reuse it. Both accounting models coexist on the ledger for as long as pre-amendment Vaults exist. An implementation must branch on `Vault.LEVersion` in every place it adjusts `AssetsTotal` or `DebtTotal`; branching in some places and not others corrupts the share exchange rate of the Vault. diff --git a/XLS-0066-lending-protocol/README.md b/XLS-0066-lending-protocol/README.md index 7cb223676..a7ad2538b 100644 --- a/XLS-0066-lending-protocol/README.md +++ b/XLS-0066-lending-protocol/README.md @@ -1407,7 +1407,7 @@ _Post-amendment (`LendingProtocolV1_1`):_ - In both trajectories, `LoanBroker.DebtTotal` and `Loan.PrincipalOutstanding`/`ManagementFeeOutstanding` fully clear to (or independent of) `0` at default, and `Vault.AssetsTotal` equals `Vault.AssetsAvailable` after default in each trajectory — the default mechanics are structurally unchanged. - The only real difference is that the pre-amendment `DefaultAmount`, `MinimumCover`, and `DefaultCovered` are all computed on figures that still include the (now-collected or now-forgiven) interest, while the post-amendment figures are computed on principal alone. In this example `CoverAvailable` is not binding, so those quantities scale with the interest excluded. That is not general: a `min` capped by `CoverAvailable` can leave the cover drawdown unchanged, and an aggregate broker `DebtTotal` can produce another ratio. The `CoverRateMinimum`/`CoverRateLiquidation` shape is unchanged. -- The `99,550.45` vs `99,550` difference in the final `Vault.AssetsTotal`/`AssetsAvailable` is exactly the `0.45` difference in `DefaultCovered` (`5.45` vs `5`), itself a consequence of `MinimumCover` being computed against a larger, interest-inclusive `DebtTotal` (`545`) pre-amendment vs. a principal-only `DebtTotal` (`500`) post-amendment. This `0.45` is cover calculated from uncollected interest under accrual-basis accounting; cash-basis accounting never recognises that unpaid interest. +- The `99,550.45` vs `99,550` difference in the final `Vault.AssetsTotal`/`AssetsAvailable` is exactly the `0.45` difference in `DefaultCovered` (`5.45` vs `5`), itself a consequence of `MinimumCover` being computed against a larger, interest-inclusive `DebtTotal` (`545`) pre-amendment vs. a principal-only `DebtTotal` (`500`) post-amendment. This `0.45` is cover calculated from uncollected interest under instant interest recognition; cash-basis accounting never recognises that unpaid interest. - `Loan.ManagementFeeOutstanding` (`5`) is unaffected by the amendment in either trajectory: it was already excluded from `DefaultAmount` before the amendment (via the `- Loan.ManagementFeeOutstanding` term), and remains excluded after the amendment (since the amended `DefaultAmount` formula never references it). #### 3.10.6 Invariants From 5f81ed4a78ef14a7f79fa3e9ca4c739c58c58141 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:15:52 +0200 Subject: [PATCH 32/32] docs(XLS-66.1.1): keep parent LoanSet check numbers, add test plan, plain requires --- .../66.1/66.1.1-loan-cash-basis.md | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/XLS-0066-lending-protocol/66.1/66.1.1-loan-cash-basis.md b/XLS-0066-lending-protocol/66.1/66.1.1-loan-cash-basis.md index 69ced0e2a..c6f27dd90 100644 --- a/XLS-0066-lending-protocol/66.1/66.1.1-loan-cash-basis.md +++ b/XLS-0066-lending-protocol/66.1/66.1.1-loan-cash-basis.md @@ -6,7 +6,7 @@ proposal-from: https://github.com/XRPLF/XRPL-Standards/discussions/190 status: Draft category: Amendment - requires: [XLS-65.1.3](../../XLS-0065-single-asset-vault/65.1/65.1.3-vault-cash-basis.md) + requires: XLS-65.1.3 created: 2026-09-04 updated: 2026-10-01 @@ -46,14 +46,18 @@ The examples in parent [§3.1.10](../README.md#3110-accounting) and [§3.1.11](. Amend these checks in parent [§3.8.5.2](../README.md#3852-protocol-level-failures): -6. - `LendingProtocol (parent)`: `Vault.AssetsMaximum != 0` and `Vault.AssetsTotal >= Vault.AssetsMaximum` (vault at capacity). (`tecLIMIT_EXCEEDED`) - - `LendingProtocolV1_1`: The check does not apply if `Vault.LEVersion == 1`. -7. - `LendingProtocol (parent)`: `Vault.AssetsMaximum != 0` and `Vault.AssetsTotal + InterestDue > Vault.AssetsMaximum` (expected interest would exceed vault assets cap). (`tecLIMIT_EXCEEDED`) - - `LendingProtocolV1_1`: The check does not apply if `Vault.LEVersion == 1`. -8. - `LendingProtocol (parent)`: `LoanBroker.DebtMaximum != 0` and `LoanBroker.DebtMaximum < LoanBroker.DebtTotal + PrincipalRequested + InterestDue` (exceeds maximum debt). (`tecLIMIT_EXCEEDED`) - - `LendingProtocolV1_1`: If `Vault.LEVersion == 1`, `LoanBroker.DebtMaximum != 0` and `LoanBroker.DebtMaximum < LoanBroker.DebtTotal + PrincipalRequested` (exceeds maximum debt). (`tecLIMIT_EXCEEDED`) -9. - `LendingProtocol (parent)`: `LoanBroker.CoverAvailable < (LoanBroker.DebtTotal + PrincipalRequested + InterestDue) × LoanBroker.CoverRateMinimum` (insufficient first-loss capital). (`tecINSUFFICIENT_FUNDS`) - - `LendingProtocolV1_1`: If `Vault.LEVersion == 1`, `LoanBroker.CoverAvailable < (LoanBroker.DebtTotal + PrincipalRequested) × LoanBroker.CoverRateMinimum` (insufficient first-loss capital). (`tecINSUFFICIENT_FUNDS`) +- Parent check 6: + - `LendingProtocol (parent)`: `Vault.AssetsMaximum != 0` and `Vault.AssetsTotal >= Vault.AssetsMaximum` (vault at capacity). (`tecLIMIT_EXCEEDED`) + - `LendingProtocolV1_1`: The check does not apply if `Vault.LEVersion == 1`. +- Parent check 14: + - `LendingProtocol (parent)`: `Vault.AssetsMaximum != 0` and `Vault.AssetsTotal + InterestDue > Vault.AssetsMaximum` (expected interest would exceed vault assets cap). (`tecLIMIT_EXCEEDED`) + - `LendingProtocolV1_1`: The check does not apply if `Vault.LEVersion == 1`. +- Parent check 19: + - `LendingProtocol (parent)`: `LoanBroker.DebtMaximum != 0` and `LoanBroker.DebtMaximum < LoanBroker.DebtTotal + PrincipalRequested + InterestDue` (exceeds maximum debt). (`tecLIMIT_EXCEEDED`) + - `LendingProtocolV1_1`: If `Vault.LEVersion == 1`, `LoanBroker.DebtMaximum != 0` and `LoanBroker.DebtMaximum < LoanBroker.DebtTotal + PrincipalRequested` (exceeds maximum debt). (`tecLIMIT_EXCEEDED`) +- Parent check 20: + - `LendingProtocol (parent)`: `LoanBroker.CoverAvailable < (LoanBroker.DebtTotal + PrincipalRequested + InterestDue) × LoanBroker.CoverRateMinimum` (insufficient first-loss capital). (`tecINSUFFICIENT_FUNDS`) + - `LendingProtocolV1_1`: If `Vault.LEVersion == 1`, `LoanBroker.CoverAvailable < (LoanBroker.DebtTotal + PrincipalRequested) × LoanBroker.CoverRateMinimum` (insufficient first-loss capital). (`tecINSUFFICIENT_FUNDS`) Checks 6 and 14 do not apply to a cash-basis Vault because origination does not credit `InterestDue` into `Vault.AssetsTotal`. A replacement such as `Vault.AssetsTotal > Vault.AssetsMaximum` is not used: it would reject origination from a Vault that is over the cap only because of collected interest. The matching change to the Vault cap invariant is in [XLS-65.1.3](../../XLS-0065-single-asset-vault/65.1/65.1.3-vault-cash-basis.md) §3.1.4. @@ -210,7 +214,15 @@ _Cash basis:_ Interest could have been recognised on a schedule, one payment period at a time, rather than on receipt. That was rejected because it reintroduces the original problem in a smaller form: the Vault would still credit itself with interest for a period in which the Borrower ends up not paying. -## 5. Security Considerations +## 5. Test Plan + +Tests must run each case against a Vault with `LEVersion` absent and a Vault with `LEVersion == 1`, with `LendingProtocolV1_1` enabled, and against a pre-amendment Vault with the amendment disabled: + +- `LoanSet`: parent checks 6, 14, 19 and 20 at their boundaries, including origination from a cash-basis Vault whose `AssetsTotal` is above `AssetsMaximum` only because of collected interest, and the `DebtTotal`, `AssetsTotal` and `AssetsAvailable` changes of §3.2.2. +- `LoanPay`: regular, late, overpayment and full payments, checking that `DebtTotal` decreases by `principalPaid` and `AssetsTotal` increases by `interestPaid` on a cash-basis Vault, and by the parent `valueChange` amounts otherwise. +- `LoanManage`: impairment, unimpairment and default, checking `LossUnrealized`, `LossReversed`, `DefaultAmount`, `DefaultCovered` and the impairment limit of check 8, including the worked example in §3.5. + +## 6. Security Considerations Principal-only accounting makes `DebtTotal` a smaller number for the same set of Loans, so a `CoverRateMinimum` that was calibrated under instant interest recognition yields less first-loss capital. A Broker migrating to a cash-basis Vault should re-derive the rate rather than reuse it.