diff --git a/XLS-0065-single-asset-vault/65.3/65.3.3-vault-assets-reserved.md b/XLS-0065-single-asset-vault/65.3/65.3.3-vault-assets-reserved.md new file mode 100644 index 000000000..17b42edc9 --- /dev/null +++ b/XLS-0065-single-asset-vault/65.3/65.3.3-vault-assets-reserved.md @@ -0,0 +1,112 @@ +
+  xls: 65.3.3
+  title: Vault Assets Reserved
+  description: Adds Vault.AssetsReserved to track Vault assets set aside for pending Loans
+  author: Vytautas Vito Tumas 
+  proposal-from: https://github.com/XRPLF/XRPL-Standards/discussions/192
+  status: Draft
+  category: Amendment
+  created: 2026-06-24
+  updated: 2026-10-01
+
+ +# 65.3.3 Vault Assets Reserved + +## 1. Abstract + +Under `LendingProtocolV1_2`, the `Vault` ledger entry gains an `AssetsReserved` field. It records the part of `Vault.AssetsTotal` that is set aside for pending Loans created by the two-step loan flow in [XLS-66.3.1](../../XLS-0066-lending-protocol/66.3/66.3.1-two-step-loan-creation.md). Reserved assets stay in the Vault _pseudo-account_ but are not part of `Vault.AssetsAvailable`, so they cannot be withdrawn or lent again. The `vault_info` RPC returns the new field. + +## 2. Motivation + +In the two-step loan flow, the Loan Broker proposes a Loan and the Borrower accepts it later. Between the two steps the principal must stay in the Vault, because the Borrower has not accepted the Loan yet. It must also be unavailable to depositors and to other Loans, or the Borrower could accept a Loan that the Vault can no longer fund. + +`Vault.AssetsAvailable` cannot express this alone. If the principal is only removed from `AssetsAvailable`, the Vault _pseudo-account_ balance no longer matches any Vault field, and an observer cannot tell lent assets from assets held for pending Loans. `AssetsReserved` makes the held amount explicit. + +## 3. Specification + +This patch changes the parent [XLS-65](../README.md) sections named below. All other parent behavior is unchanged. + +### 3.1 Ledger Entry: `Vault` + +#### 3.1.1 Fields + +Add this field to parent [§3.1.2](../README.md#312-fields): + +| Field Name | Constant | Required | JSON Type | Internal Type | Default Value | Description | +| ---------------- | :------: | :------: | :-------: | :-----------: | :-----------: | -------------------------------------------------------------------------------- | +| `AssetsReserved` | No | Yes | `number` | `NUMBER` | `0` | The amount of Vault assets reserved for pending Loans that are not yet accepted. | + +`AssetsReserved` is part of `AssetsTotal` and is not part of `AssetsAvailable`. The assets are still held by the Vault _pseudo-account_. As with `LossUnrealized`, the default value is `0`. + +Only these transactions change `AssetsReserved`, as specified in [XLS-66.3.1](../../XLS-0066-lending-protocol/66.3/66.3.1-two-step-loan-creation.md): + +- `LoanSet` that proposes a Loan increases it by the Loan principal. +- `LoanAccept` decreases it by the Loan principal when the principal is sent to the Borrower. +- `LoanDelete` of a pending Loan decreases it by the Loan principal and returns that amount to `AssetsAvailable`. + +#### 3.1.2 Invariants + +When the amendment is enabled, amend parent [§3.1.10](../README.md#3110-invariants) as follows. + +Parent invariant 9 also requires `Vault.AssetsReserved` to be zero on creation. Parent invariant 10 also requires the deleted Vault to have `AssetsReserved == 0`. + +Parent invariant 5, including its `fixCleanup3_4_0` form from [XLS-65.2](../65.2/README.md), uses `Vault.AssetsTotal - Vault.AssetsAvailable - Vault.AssetsReserved` in place of `Vault.AssetsTotal - Vault.AssetsAvailable`. `Vault.LossUnrealized` is therefore bounded by the assets lent out by active Loans, not by assets reserved for pending Loans. The one-unit `IOU` slack is unchanged. + +In parent invariant 16, add `LoanAccept` with `MustModifyVault` and `LoanDelete` with `MayModifyVault` to the list of transaction types that may modify a `Vault`. `LoanDelete` modifies the Vault only when it deletes a pending Loan. + +Append these invariants: + +18. `Vault.AssetsReserved >= 0`. +19. `Vault.AssetsAvailable + Vault.AssetsReserved <= Vault.AssetsTotal`. +20. `Vault.AssetsReserved` may only be changed by `LoanSet`, `LoanAccept` and `LoanDelete`. Every other transaction that modifies the entry must leave it unchanged. + +When the amendment is not enabled, `AssetsReserved` is absent and these changes do not apply. + +#### 3.1.3 Example JSON + +```json +{ + "LedgerEntryType": "Vault", + "Account": "rwCNM7SeUHTajEBQDiNqxDG8p1Mreizw85", + "Asset": { + "currency": "USD", + "issuer": "rXJSJiZMxaLuH3kQBUV5DLipnYtrE6iVb" + }, + "AssetsAvailable": "900", + "AssetsReserved": "100", + "AssetsTotal": "1000", + "Flags": 0, + "LossUnrealized": "0", + "Owner": "rNGHoQwNG753zyfDrib4qDvvswbrtmV8Es", + "ShareMPTID": "000000014B3B3E1D2D8F4B5A9C2E7F1D3A6B8C9D0E1F2A3B", + "WithdrawalPolicy": 1 +} +``` + +### 3.2 Unchanged Transactions + +`VaultWithdraw` and `VaultClawback` are unchanged. They are already limited by `Vault.AssetsAvailable`, so reserved assets cannot be withdrawn or clawed back while a Loan is pending. `VaultDelete` is unchanged: it requires `AssetsTotal == 0`, which by invariant 19 also requires `AssetsReserved == 0`. + +### 3.3 RPC: `vault_info` + +#### 3.3.1 Response Fields + +Add this field to parent [§3.9.2](../README.md#392-response-fields): + +| Field Name | Always Present? | JSON Type | Description | +| ---------------------- | :-------------: | :-------: | ---------------------------------------------------------------------------- | +| `vault.AssetsReserved` | No | `string` | Amount of assets reserved for pending Loans that have not yet been accepted. | + +The field is absent when the amendment is not enabled. + +## 4. Rationale + +Keeping reserved assets inside `AssetsTotal` means that, in a cash-basis Vault ([XLS-65.1.3](../65.1/65.1.3-vault-cash-basis.md)), a pending Loan does not change the value of Vault shares on its own. In an accrual-basis Vault the proposal also books the expected interest into `AssetsTotal`; see [XLS-66.3.1](../../XLS-0066-lending-protocol/66.3/66.3.1-two-step-loan-creation.md) §7. Taking them out of `AssetsAvailable` stops a depositor from withdrawing them and stops a second Loan from using them. Excluding reserved assets from the `LossUnrealized` bound in invariant 5 keeps an unrealized loss from discounting assets that are still in the Vault. A separate field, rather than a sum over pending Loans, lets invariants and RPC clients read the reserved amount from the Vault alone. + +## 5. Test Plan + +The reference implementation tests that `AssetsReserved` is zero on a new Vault, increases when a Loan is proposed, decreases when it is accepted or deleted, and stays unchanged for every other Vault and Lending transaction. It tests that `VaultWithdraw` cannot withdraw reserved assets, checks invariants 18-20 after each of these transactions, and checks that `vault_info` returns the field. It runs the same tests with the amendment disabled and confirms that the field is absent. + +## 6. Security Considerations + +Reserved assets are held by the Vault _pseudo-account_ but are committed to a pending Loan. If `AssetsReserved` were not checked, a Borrower could accept a Loan after the principal had been withdrawn or lent to someone else. Invariant 19 rules that out. A pending Loan can hold Vault liquidity until it is accepted or deleted; [XLS-66.3.1](../../XLS-0066-lending-protocol/66.3/66.3.1-two-step-loan-creation.md) describes how a pending Loan is removed. diff --git a/XLS-0065-single-asset-vault/65.3/README.md b/XLS-0065-single-asset-vault/65.3/README.md new file mode 100644 index 000000000..3ab13c32b --- /dev/null +++ b/XLS-0065-single-asset-vault/65.3/README.md @@ -0,0 +1,36 @@ +
+  xls: 65.3
+  title: LendingProtocolV1_2 Vault Changes
+  description: Index of Vault changes introduced by the LendingProtocolV1_2 amendment
+  author: Vytautas Vito Tumas 
+  proposal-from: https://github.com/XRPLF/XRPL-Standards/discussions/192
+  status: Draft
+  category: Amendment
+  created: 2026-10-01
+  updated: 2026-10-01
+
+ +# 65.3 LendingProtocolV1_2 Vault Changes + +## 1. Abstract + +This index groups the Vault changes introduced by the `LendingProtocolV1_2` amendment. + +## 2. Specifications + +These specifications are introduced by the `LendingProtocolV1_2` amendment: + +| Spec | Description | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | +| [65.3.1 Fixed Precision](./65.3.1-fixed-precision.md) | Fixes one precision grid per Vault for the life of the Vault, its LoanBrokers, and its Loans. | +| [65.3.2 Closed-Ended Vault Early-Exit Fee](./65.3.2-early-exit-fee.md) | Adds an optional immutable `EarlyExitFeeRate` that allows fee-bearing Investment-phase exits. | +| [65.3.3 Vault Assets Reserved](./65.3.3-vault-assets-reserved.md) | Adds `Vault.AssetsReserved` to track assets reserved for pending Loans. | +| [65.3.4 Vault Deposit Block](./65.3.4-vault-deposit-block.md) | Lets an opted-in Vault Owner block and unblock deposits. | + +## 3. Rationale + +The amendment covers independent Vault changes. Keeping each change in a focused specification makes its behavior and motivation explicit while this index records their shared amendment. + +## 4. Security Considerations + +This index introduces no additional protocol behavior. The security considerations for each change are documented in its linked specification. diff --git a/XLS-0065-single-asset-vault/README.md b/XLS-0065-single-asset-vault/README.md index 376825cb6..6c2baefe4 100644 --- a/XLS-0065-single-asset-vault/README.md +++ b/XLS-0065-single-asset-vault/README.md @@ -96,6 +96,7 @@ A protocol connecting to a Vault must track its debt. Furthermore, the updates t - [65.1.4 Closed-Ended Vault](./65.1/65.1.4-closed-ended-vault.md): adds the `ClosedEnded` vault kind with `SubscriptionDate` and `RedemptionDate`, and phase-gates `VaultDeposit` and `VaultWithdraw` on such vaults - `fixCleanup3_4_0`, as described in [XLS-65.2](./65.2/README.md): - admits one unit of rounding slack in the `LossUnrealized` invariant and in IOU accounting and state-change deltas for `VaultDeposit`, `VaultWithdraw` and `VaultClawback`, requires `LossUnrealized` to be non-negative, and narrows `VaultSet` cap enforcement to transactions that supply `AssetsMaximum` or otherwise change the cap +- `LendingProtocolV1_2`, as described in [XLS-65.3](./65.3/README.md). ## 3. Specification diff --git a/XLS-0066-lending-protocol/66.3/66.3.1-two-step-loan-creation.md b/XLS-0066-lending-protocol/66.3/66.3.1-two-step-loan-creation.md new file mode 100644 index 000000000..392b10d1c --- /dev/null +++ b/XLS-0066-lending-protocol/66.3/66.3.1-two-step-loan-creation.md @@ -0,0 +1,389 @@ +
+  xls: 66.3.1
+  title: Two-Step Loan Creation
+  description: Adds a propose-and-accept Loan flow through LoanSet and a new LoanAccept transaction
+  author: Vytautas Vito Tumas 
+  proposal-from: https://github.com/XRPLF/XRPL-Standards/discussions/190
+  status: Draft
+  category: Amendment
+  requires: [XLS-65.3.3](../../XLS-0065-single-asset-vault/65.3/65.3.3-vault-assets-reserved.md), [XLS-65.1.3](../../XLS-0065-single-asset-vault/65.1/65.1.3-vault-cash-basis.md), [XLS-66.1.1](../66.1/66.1.1-loan-cash-basis.md)
+  created: 2026-06-24
+  updated: 2026-10-01
+
+ +# 66.3.1 Two-Step Loan Creation + +## 1. Abstract + +Under `LendingProtocolV1_2`, a Loan can be created in two steps, each signed with a standard signature. First, the `LoanBroker.Owner` submits `LoanSet` with the `Borrower` and `StartDate` fields. This creates a pending `Loan` and moves the principal from `Vault.AssetsAvailable` to `Vault.AssetsReserved`. Then the Borrower submits the new `LoanAccept` transaction before `StartDate`. This sends the principal to the Borrower and activates the Loan. A pending Loan cannot be paid or managed, and either party can delete it with `LoanDelete`. The existing single-transaction flow with `CounterpartySignature` is unchanged. + +## 2. Motivation + +The parent `LoanSet` must be signed by both the Borrower and the `LoanBroker.Owner`, and one party signs inside `CounterpartySignature`. Wallets and custodians must support this custom dual-signature format before their users can borrow. Many do not, which ties Borrowers to the custodians that have built the integration. + +The two-step flow uses two ordinary transactions. The Loan Broker proposes the terms on the ledger, and the Borrower accepts them with a normal single-signer transaction. Any wallet that can sign a standard XRPL transaction can take part. + +## 3. Specification + +This patch changes the parent [XLS-66](../README.md) sections named below. It uses `Vault.AssetsReserved`, which is defined by [XLS-65.3.3](../../XLS-0065-single-asset-vault/65.3/65.3.3-vault-assets-reserved.md). All other parent behavior is unchanged. + +A **pending** Loan is a `Loan` with the `lsfLoanPending` flag set. A **proposal** is a `LoanSet` that creates a pending Loan, as defined in §3.2.2. + +This patch depends on cash-basis accounting, introduced by the `LendingProtocolV1_1` patches [XLS-65.1.3](../../XLS-0065-single-asset-vault/65.1/65.1.3-vault-cash-basis.md) and [XLS-66.1.1](../66.1/66.1.1-loan-cash-basis.md). `Vault.LEVersion` (XLS-65.1.3 §3.1.2) selects the accounting model of each Vault, and the proposal, acceptance and deletion steps below branch on it. Where this patch says **cash-basis Vault**, it means a Vault that XLS-65.1.3 accounts for on a cash basis: `Vault.AssetsTotal` and `LoanBroker.DebtTotal` track Loan principal only. Where it says **accrual-basis Vault**, it means a Vault with `LEVersion` absent (`0`), which keeps the parent's instant interest recognition. All other accounting follows XLS-66.1.1. + +### 3.1 Ledger Entry: `Loan` + +#### 3.1.1 Fields + +Amend these rows of parent [§3.2.2](../README.md#322-fields): + +| Field Name | Constant | Required | JSON Type | Internal Type | Default Value | Description | +| ----------- | :------: | :---------: | :-------: | :-----------: | :-----------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `OwnerNode` | Yes | Conditional | `number` | `UINT64` | `N/A` | Identifies the page where this item is referenced in the `Borrower` owner's directory. Absent while the Loan is pending; set when the Borrower accepts the Loan. | +| `StartDate` | Yes | Yes | `number` | `UINT32` | See below. | The timestamp at which the Loan starts, in [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time). | + +`StartDate` is set as follows: + +- A Loan created by the single-transaction flow uses the parent default, the current ledger timestamp. +- A Loan created by a proposal uses `LoanSet.StartDate`. This is a time in the future. For a pending Loan it is also the acceptance deadline: the Borrower must accept the Loan while the current ledger timestamp is less than `StartDate`. + +The payment schedule always starts at `StartDate`. `NextPaymentDueDate` is `StartDate + PaymentInterval` at creation and does not change when the Loan is accepted. + +`OwnerNode` keeps the `Constant` value `Yes`: once it is set at acceptance, it does not change. + +#### 3.1.2 Flags + +Add this flag to parent [§3.2.3](../README.md#323-flags): + +| Flag Name | Flag Value | Modifiable? | Description | +| ---------------- | :----------: | :---------: | ---------------------------------------------------------------------- | +| `lsfLoanPending` | `0x00080000` | `Yes` | If set, indicates that the Loan is pending acceptance by the Borrower. | + +Only `LoanSet` sets `lsfLoanPending`, and only `LoanAccept` clears it. A Loan that has been accepted is never pending again. + +#### 3.1.3 Ownership + +Amend parent [§3.2.4](../README.md#324-ownership): a pending Loan is in only one Owner Directory, the one of the `LoanBroker` _pseudo-account_, using `LoanBrokerNode`. It is not in the `Borrower`'s Owner Directory, and `OwnerNode` is absent. When the Borrower accepts the Loan, it is added to the `Borrower`'s Owner Directory and `OwnerNode` is set. From then on, the parent rules apply. + +#### 3.1.4 Reserves + +Amend parent [§3.2.5](../README.md#325-reserves): a pending Loan costs one owner reserve for the `LoanBroker.Owner`, not for the `Borrower`. When the Borrower accepts the Loan, that reserve is released from the `LoanBroker.Owner` and charged to the `Borrower`. + +#### 3.1.5 Deletion + +Amend parent [§3.2.6](../README.md#326-deletion): either the `LoanBroker.Owner` or the `Borrower` can delete a pending Loan at any time with `LoanDelete`, before or after `StartDate`. A pending Loan does not need to be fully paid or defaulted. See §3.4. + +A pending Loan does not block deletion of the Borrower's account, because it is not in the Borrower's Owner Directory. It counts toward `LoanBroker.OwnerCount`, so it blocks `LoanBrokerDelete` until it is accepted or deleted. + +#### 3.1.6 Invariants + +Replace the parent [§3.2.7](../README.md#327-invariants) placeholder with these invariants for Loans created or modified under the amendment: + +1. If `lsfLoanPending` is set, `OwnerNode` is absent. If it is not set, `OwnerNode` is present. +2. Only `LoanSet` creates a Loan with `lsfLoanPending` set, and only `LoanAccept` clears it. No transaction sets `lsfLoanPending` on an existing Loan. +3. If `lsfLoanPending` is set, `lsfLoanDefault` and `lsfLoanImpaired` are not set, and `PaymentRemaining`, `PrincipalOutstanding` and `TotalValueOutstanding` are unchanged from their values at creation. + +### 3.2 Transaction: `LoanSet` + +#### 3.2.1 Fields + +Add these fields to parent [§3.8.1](../README.md#381-fields), and change `CounterpartySignature` from `Yes` to `Conditional`: + +| Field Name | Required | JSON Type | Internal Type | Default Value | Description | +| ----------------------- | :---------: | :-------: | :-----------: | :-----------: | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| `Borrower` | Conditional | `string` | `ACCOUNTID` | `N/A` | The address of the Borrower. Required for a proposal and not allowed otherwise. Only the `LoanBroker.Owner` can submit a `LoanSet` with it. | +| `StartDate` | Conditional | `number` | `UINT32` | `N/A` | The timestamp at which the proposed Loan starts. Required for a proposal and not allowed otherwise. The proposal expires at this time. | +| `CounterpartySignature` | Conditional | `string` | `STOBJECT` | `N/A` | The signature of the counterparty over the transaction. Required for the single-transaction flow outside a `Batch`. Not allowed for a proposal. | + +#### 3.2.2 Signing + +Rename parent [§3.8.3](../README.md#383-multi-signing) from "Multi-Signing" to "Signing". The parent text describes the **single-transaction flow** and still applies to it unchanged. + +A `LoanSet` is a **proposal** if and only if all of the following are true: + +1. The `Counterparty` field is not specified. +2. The `CounterpartySignature` field is not specified. +3. The `Borrower` field is specified. +4. The `StartDate` field is specified. + +The `LoanBroker.Owner` uses either `Counterparty` and `CounterpartySignature` (single-transaction flow) or `Borrower` and `StartDate` (two-step flow). The two pairs cannot be combined. The data-verification checks in §3.2.3.1 reject every other combination. + +The two-step flow is: + +- **Step 1: Proposal (Loan Broker)** + 1. The `LoanBroker.Owner` creates a `LoanSet` transaction with the Loan terms, `Borrower` set to the Borrower's account, and `StartDate` set to the time the Loan starts. The Borrower must accept the Loan before this time. + 2. The `LoanBroker.Owner` signs the transaction with a standard signature and submits it. + 3. A pending `Loan` is created. No funds are transferred. `Vault.AssetsAvailable` decreases and `Vault.AssetsReserved` increases by `PrincipalRequested`. + 4. If the Borrower does not accept the Loan before `StartDate`, the proposal expires. An expired pending Loan is not removed automatically: the `LoanBroker.Owner` or the `Borrower` must submit `LoanDelete` to delete it and release the reserved assets. +- **Step 2: Acceptance (Borrower)** + 1. The `Borrower` creates a `LoanAccept` transaction with `LoanID` set to the pending Loan. + 2. The `Borrower` signs the transaction with a standard signature and submits it. + 3. The freeze, authorization and reserve checks are run again (§3.3.4.2). If one fails, the pending Loan is not deleted. The Borrower can fix the cause and retry until `StartDate`. + 4. On success, `lsfLoanPending` is cleared, the principal is sent to the Borrower, and the Loan becomes active. + +#### 3.2.3 Failure Conditions + +##### 3.2.3.1 Data Verification + +When the amendment is not enabled, the presence of `Borrower` or `StartDate` fails with `temDISABLED`. + +When the amendment is enabled, run these checks before parent check 2 of [§3.8.5.1](../README.md#3851-data-verification), so that a wrong combination of fields fails with `temINVALID` and not `temBAD_SIGNER`: + +- `Borrower` and `Counterparty` are both specified. (`temINVALID`) +- `Borrower` and `CounterpartySignature` are both specified. (`temINVALID`) +- `Borrower` is specified and `StartDate` is not specified. (`temINVALID`) +- `StartDate` is specified and `Borrower` is not specified. (`temINVALID`) + +After these checks, a `LoanSet` with `Borrower` or `StartDate` is a proposal. + +Amend parent checks 2 and 3 so that they do not apply to a proposal: + +2. `CounterpartySignature` is not present, the transaction is not a `Batch` inner transaction, and `Borrower` is not specified. (`temBAD_SIGNER`) +3. The transaction is a `Batch` inner transaction, and neither `Counterparty` nor `Borrower` is specified. (`temBAD_SIGNER`) + +All other parent data-verification checks apply unchanged. + +##### 3.2.3.2 Protocol-Level Failures + +For a single-transaction `LoanSet`, parent [§3.8.5.2](../README.md#3852-protocol-level-failures) is unchanged. + +For a proposal, the parent checks apply with these changes: + +- Parent check 1 does not apply, because a proposal has no `CounterpartySignature`. +- Parent check 4 becomes: the `Account` submitting the transaction is not the `LoanBroker.Owner`. (`tecNO_PERMISSION`) +- Parent check 5 checks the account in `Borrower`. +- Parent check 21 is replaced: the `LoanBroker.Owner` does not have enough reserve for the `Loan` object. (`tecINSUFFICIENT_RESERVE`) +- In parent check 27, `StartDate` is `LoanSet.StartDate`, not the current ledger timestamp. + +Append this check for a proposal: + +28. - `LendingProtocol (parent)`: The check does not apply. + - `LendingProtocolV1_2`: The transaction is a proposal and the current ledger timestamp is greater than or equal to `StartDate`. The proposal would be expired when created. (`tecEXPIRED`) + +#### 3.2.4 State Changes + +For a single-transaction `LoanSet`, parent [§3.8.6](../README.md#386-state-changes) is unchanged. + +For a proposal, these steps replace the parent steps: + +1. Create the `Loan` object with the computed fields (`TotalValueOutstanding`, `PeriodicPayment`, `ManagementFeeOutstanding`, `LoanScale`, and so on). Set `Borrower` and `StartDate` from the transaction, and set `lsfLoanPending`. +2. Increment `AccountRoot(LoanBroker.Owner).OwnerCount` by `1`. +3. Update the `Vault` object: + - Decrease `Vault.AssetsAvailable` by `PrincipalRequested`. + - Increase `Vault.AssetsReserved` by `PrincipalRequested`. + - For an accrual-basis Vault, increase `Vault.AssetsTotal` by `InterestDue` (interest owed to the Vault, excluding the management fee). For a cash-basis Vault, do not change `Vault.AssetsTotal`. +4. Update the `LoanBroker` object: + - For an accrual-basis Vault, increase `LoanBroker.DebtTotal` by `PrincipalRequested + InterestDue`. For a cash-basis Vault, increase it by `PrincipalRequested`. + - Increment `LoanBroker.OwnerCount` by `1`. + - Increment `LoanBroker.LoanSequence` by `1`. +5. Add `LoanID` to the Owner Directory of the `LoanBroker` _pseudo-account_ (sets `LoanBrokerNode`). Do not add it to the Borrower's Owner Directory, and do not set `OwnerNode`. + +A proposal does not create asset holdings and does not transfer funds. `LoanAccept` does both. + +#### 3.2.5 Example JSON + +A proposal: + +```json +{ + "TransactionType": "LoanSet", + "Account": "rMXpwfCWRGe8L9MCmF1zur9JRKzEJUNFBZ", + "LoanBrokerID": "A9470DEFB52F18D1A11C2208D366D575EB4DE5A5D202AFED812F09FDA5B8D614", + "Flags": 0, + "Borrower": "rEY12QsZrCoCPJuRiVz2XDWdmFDXhwH8Ws", + "LoanOriginationFee": "0", + "LoanServiceFee": "0", + "LatePaymentFee": "0", + "ClosePaymentFee": "0", + "OverpaymentFee": 0, + "InterestRate": 500, + "LateInterestRate": 0, + "CloseInterestRate": 0, + "OverpaymentInterestRate": 0, + "PrincipalRequested": "1000", + "PaymentTotal": 12, + "PaymentInterval": 3600, + "GracePeriod": 60, + "StartDate": 825165502, + "Fee": "1", + "Sequence": 3964250 +} +``` + +### 3.3 Transaction: `LoanAccept` + +The Borrower submits `LoanAccept` to accept a pending Loan. On success, the principal is sent to the Borrower and the Loan becomes active. + +#### 3.3.1 Fields + +| Field Name | Required? | JSON Type | Internal Type | Default Value | Description | +| ----------------- | :-------: | :-------: | :-----------: | :-----------: | ---------------------------------------------- | +| `TransactionType` | Yes | `string` | `UINT16` | `LoanAccept` | Identifies this as a `LoanAccept` transaction. | +| `LoanID` | Yes | `string` | `HASH256` | `N/A` | The ID of the pending `Loan` object to accept. | + +The transaction has no other transaction-specific fields. Its numeric transaction type code is assigned by the implementation. It MUST be a new, unused code; the parent codes `80` (`LoanSet`) to `83` (`LoanPay`) do not change. + +#### 3.3.2 Flags + +`LoanAccept` has no transaction-specific flags. + +#### 3.3.3 Transaction Fee + +This transaction uses the standard transaction fee. + +#### 3.3.4 Failure Conditions + +When the amendment is not enabled, the transaction fails with `temDISABLED`. + +##### 3.3.4.1 Data Verification + +1. `LoanID` is zero. (`temINVALID`) + +##### 3.3.4.2 Protocol-Level Failures + +1. The `Loan` object with the specified `LoanID` does not exist on the ledger. (`tecNO_ENTRY`) +2. The `Loan` object does not have the `lsfLoanPending` flag set. (`tecNO_PERMISSION`) +3. The `Account` submitting the transaction is not the `Loan.Borrower`. (`tecNO_PERMISSION`) +4. The current ledger timestamp is greater than or equal to `Loan.StartDate`, so the proposal has expired. (`tecEXPIRED`) + - The pending `Loan` object is not deleted, and none of its fields change. + - The `LoanBroker.Owner` or the `Borrower` must submit `LoanDelete` to delete the pending Loan and release the reserved assets. +5. The Borrower does not have enough reserve for the `Loan` object. (`tecINSUFFICIENT_RESERVE`) +6. The Vault _pseudo-account_ is frozen for the asset. (`tecFROZEN` for IOUs, `tecLOCKED` for MPTs) +7. The LoanBroker _pseudo-account_ is deep frozen for the asset. (`tecFROZEN` for IOUs, `tecLOCKED` for MPTs) +8. The Borrower is frozen for the asset. (`tecFROZEN` for IOUs, `tecLOCKED` for MPTs) +9. The `LoanBroker.Owner` is deep frozen for the asset. (`tecFROZEN` for IOUs, `tecLOCKED` for MPTs) +10. A holding must be created for the Borrower, or for the `LoanBroker.Owner` when `Loan.LoanOriginationFee > 0`, and the asset does not allow a new holding: + - If `Vault.Asset` is an `IOU`, the issuer does not have `lsfDefaultRipple` set. (`terNO_RIPPLE`) + - If `Vault.Asset` is an `MPT`, the `MPTokenIssuance` does not have `lsfMPTCanTransfer` set. (`tecNO_AUTH`) +11. The Borrower is not authorized for the asset. (`tecNO_AUTH`) +12. The `LoanBroker.Owner` is not authorized for the asset. (`tecNO_AUTH`) + +A failure leaves the pending Loan unchanged. The Borrower can fix the cause of checks 5-12 and submit `LoanAccept` again before `Loan.StartDate`. + +#### 3.3.5 State Changes + +**On Success (`tesSUCCESS`):** + +1. Clear the `lsfLoanPending` flag on the `Loan` object. +2. Release the reserve from the Loan Broker: decrement `AccountRoot(LoanBroker.Owner).OwnerCount` by `1`. +3. Charge the reserve to the Borrower: increment `AccountRoot(Borrower).OwnerCount` by `1`. +4. Create an asset holding for the Borrower if one does not exist: + - If `Vault.Asset` is an `IOU`: create a `RippleState` object between the `Issuer` and the `Borrower`. + - If `Vault.Asset` is an `MPT`: create an `MPToken` object for the `Borrower`. +5. Create an asset holding for the `LoanBroker.Owner` if one does not exist and `Loan.LoanOriginationFee > 0`: + - If `Vault.Asset` is an `IOU`: create a `RippleState` object between the `Issuer` and the `LoanBroker.Owner`. + - If `Vault.Asset` is an `MPT`: create an `MPToken` object for the `LoanBroker.Owner`. +6. Transfer funds from the Vault _pseudo-account_ (transfer fee waived): + - If `Vault.Asset` is `XRP`: + - Decrease the `Balance` of the Vault _pseudo-account_ `AccountRoot` by `Loan.PrincipalOutstanding`. + - Increase the `Balance` of the `Borrower` `AccountRoot` by `Loan.PrincipalOutstanding - Loan.LoanOriginationFee`. + - Increase the `Balance` of the `LoanBroker.Owner` `AccountRoot` by `Loan.LoanOriginationFee`. + - If `Vault.Asset` is an `IOU`: + - Decrease the `RippleState` balance between the Vault _pseudo-account_ and the `Issuer` by `Loan.PrincipalOutstanding`. + - Increase the `RippleState` balance between the `Borrower` and the `Issuer` by `Loan.PrincipalOutstanding - Loan.LoanOriginationFee`. + - Increase the `RippleState` balance between the `LoanBroker.Owner` and the `Issuer` by `Loan.LoanOriginationFee`. + - If `Vault.Asset` is an `MPT`: + - Decrease the `MPToken.MPTAmount` of the Vault _pseudo-account_ `MPToken` by `Loan.PrincipalOutstanding`. + - Increase the `MPToken.MPTAmount` of the `Borrower` `MPToken` by `Loan.PrincipalOutstanding - Loan.LoanOriginationFee`. + - Increase the `MPToken.MPTAmount` of the `LoanBroker.Owner` `MPToken` by `Loan.LoanOriginationFee`. +7. Update the `Vault` object: decrease `Vault.AssetsReserved` by `Loan.PrincipalOutstanding`. +8. Add `LoanID` to the Owner Directory of the `Borrower` (sets `OwnerNode`). + +`Loan.StartDate`, `Loan.NextPaymentDueDate`, `LoanBroker.DebtTotal` and `Vault.AssetsTotal` do not change. The proposal already recorded them. + +#### 3.3.6 Example JSON + +```json +{ + "TransactionType": "LoanAccept", + "Account": "rEY12QsZrCoCPJuRiVz2XDWdmFDXhwH8Ws", + "LoanID": "A85F331533BFD21557C30F92DC3432BDEBEC85436A937C41FFCBB21EA9C07AED", + "Fee": "1", + "Sequence": 3964251 +} +``` + +### 3.4 Transaction: `LoanDelete` + +#### 3.4.1 Failure Conditions + +Amend parent check 2 of [§3.9.3.2](../README.md#3932-protocol-level-failures) so that it does not apply to a pending Loan: + +2. `Loan.PaymentRemaining > 0` and `lsfLoanPending` is not set (the Loan is still active). (`tecHAS_OBLIGATIONS`) + +Parent checks 1 and 3 apply unchanged. Either the `LoanBroker.Owner` or the `Loan.Borrower` can delete a pending Loan, before or after `Loan.StartDate`. + +#### 3.4.2 State Changes + +For a Loan that is not pending, parent [§3.9.4](../README.md#394-state-changes) is unchanged. + +For a pending Loan, these steps replace the parent steps: + +1. Remove `LoanID` from the Owner Directory of the `LoanBroker` _pseudo-account_ (using `LoanBrokerNode`). +2. Delete the `Loan` object. +3. Update the `Vault` object: + - Increase `Vault.AssetsAvailable` by `Loan.PrincipalOutstanding`. + - Decrease `Vault.AssetsReserved` by `Loan.PrincipalOutstanding`. + - For an accrual-basis Vault, decrease `Vault.AssetsTotal` by `InterestDue`, reversing the interest added by the proposal. For a cash-basis Vault, do not change `Vault.AssetsTotal`. +4. Update the `LoanBroker` object: + - For an accrual-basis Vault, decrease `LoanBroker.DebtTotal` by `Loan.PrincipalOutstanding + InterestDue`. For a cash-basis Vault, decrease it by `Loan.PrincipalOutstanding`. + - Decrement `LoanBroker.OwnerCount` by `1`. +5. Release the reserve from the Loan Broker: decrement `AccountRoot(LoanBroker.Owner).OwnerCount` by `1`. + +`InterestDue` is the same amount the proposal added in §3.2.4. + +### 3.5 Transaction: `LoanManage` + +#### 3.5.1 Failure Conditions + +Add this check to parent [§3.10.4.2](../README.md#31042-protocol-level-failures). It runs immediately after parent check 1, and the parent checks keep their numbers: + +- `LendingProtocol (parent)`: The check does not apply. +- `LendingProtocolV1_2`: `Loan.Flags` has `lsfLoanPending` set. A pending Loan cannot be managed. (`tecNO_PERMISSION`) + +Amend check 8 in parent [§3.10.4.2](../README.md#31042-protocol-level-failures). Reserved assets are still held by the Vault, so they are excluded from the amount an impairment may cover: + +8. - `LendingProtocol (parent)`: `tfLoanImpair` is specified and `Vault.LossUnrealized + (Loan.TotalValueOutstanding - Loan.ManagementFeeOutstanding) > Vault.AssetsTotal - Vault.AssetsAvailable` (impairment would exceed vault's unavailable assets). (`tecLIMIT_EXCEEDED`) + - `LendingProtocolV1_1`: As defined by [XLS-66.1.1](../66.1/66.1.1-loan-cash-basis.md). + - `LendingProtocolV1_2`: `tfLoanImpair` is specified and `Vault.LossUnrealized + impairedAmount > Vault.AssetsTotal - Vault.AssetsAvailable - Vault.AssetsReserved`, where `impairedAmount` is `Loan.PrincipalOutstanding` for a cash-basis Vault and `Loan.TotalValueOutstanding - Loan.ManagementFeeOutstanding` for an accrual-basis Vault (impairment would exceed the assets lent out by active Loans). (`tecLIMIT_EXCEEDED`) + +### 3.6 Transaction: `LoanPay` + +#### 3.6.1 Failure Conditions + +Add this check to parent [§3.11.4.2](../README.md#31142-protocol-level-failures). It runs immediately after parent check 1, and the parent checks keep their numbers: + +- `LendingProtocol (parent)`: The check does not apply. +- `LendingProtocolV1_2`: `Loan.Flags` has `lsfLoanPending` set. A pending Loan cannot be paid. (`tecNO_PERMISSION`) + +## 4. Rationale + +`LoanManage` check 8 bounds `Vault.LossUnrealized` by the assets that are out on Loan. Under the parent, that amount is `Vault.AssetsTotal - Vault.AssetsAvailable`. With pending Loans, that difference also includes `Vault.AssetsReserved`, which is still held by the Vault. Subtracting it keeps the bound at the assets actually lent out, so an unrealized loss can never discount reserved assets. A pending Loan cannot be impaired, so every valid ledger state still passes the tighter check. + +The Loan Broker sets the terms on the ledger and the Borrower accepts them later, so neither party has to sign the other's transaction. Both steps use standard signatures, and any wallet can support them. + +`StartDate` is both the acceptance deadline and the start of the payment schedule. A single date keeps the schedule fixed by the proposal, so `LoanAccept` does not recalculate any Loan field. A Borrower who accepts close to `StartDate` gets the same schedule as one who accepts early. + +The principal is reserved at proposal time, so the Loan stays fundable until it is accepted or deleted. The reserve for the `Loan` object is charged to the `LoanBroker.Owner`, who created it, until the Borrower accepts. An unwanted proposal therefore costs the Borrower nothing. + +An expired pending Loan is not deleted automatically, because the ledger does not run a transaction at `StartDate`. Either party can delete it, so neither depends on the other to release the reserved assets. + +For a closed-ended Vault ([XLS-66.1.2](../66.1/66.1.2-closed-ended-loan-gates.md)), the proposal runs the phase and maturity checks with the proposed `StartDate`. Acceptance happens after the proposal and before `StartDate`, so it is also inside the Investment phase. + +## 5. Backwards Compatibility + +The single-transaction flow with `CounterpartySignature` is unchanged. Before the amendment, `LoanSet` with `Borrower` or `StartDate` and every `LoanAccept` fail with `temDISABLED`. Loans created before the amendment are never pending. Parent transaction type codes are unchanged. + +## 6. Test Plan + +The reference implementation tests a proposal followed by `LoanAccept` for `XRP`, `IOU` and `MPT` Vaults, with and without an origination fee. It checks the reserve move from the `LoanBroker.Owner` to the Borrower, the move from `Vault.AssetsAvailable` to `Vault.AssetsReserved` and back to a transfer, and that `OwnerNode` is set only after acceptance. It tests every data-verification combination of `Borrower`, `StartDate`, `Counterparty` and `CounterpartySignature`, including inside a `Batch`. It tests `LoanAccept` checks 1-12, including the `StartDate` boundary at, one second before, and one second after, and a retry after a failed check. It tests `LoanDelete` of a pending Loan by each party, before and after `StartDate`, for accrual-basis and cash-basis Vaults, and that `LoanManage` and `LoanPay` reject a pending Loan. It runs the same tests with the amendment disabled. + +## 7. Security Considerations + +A pending Loan reserves Vault liquidity until it is accepted or deleted, and there is no automatic deletion. The Loan Broker controls proposals and pays the reserve for each one, which limits how many it can create. The Loan Broker should delete proposals that have expired. + +For an accrual-basis Vault, the proposal adds the expected interest to `Vault.AssetsTotal` before any principal leaves the Vault, and deleting the pending Loan removes it again. Share value goes up while the Loan is pending and back down when it is deleted. A depositor who redeems shares while a Loan is pending is paid at the higher value, and the loss falls on the depositors who remain. Depositors and Vault operators should treat pending interest as unearned. + +The `Borrower` of a pending Loan can delete their account, because the pending Loan is not in their Owner Directory. The pending Loan then cannot be accepted, and the `LoanBroker.Owner` must delete it. + +The freeze and authorization checks run again at acceptance. A freeze or a removed authorization after the proposal blocks acceptance. diff --git a/XLS-0066-lending-protocol/66.3/README.md b/XLS-0066-lending-protocol/66.3/README.md new file mode 100644 index 000000000..f16a16b5f --- /dev/null +++ b/XLS-0066-lending-protocol/66.3/README.md @@ -0,0 +1,35 @@ +
+  xls: 66.3
+  title: LendingProtocolV1_2 Loan Changes
+  description: Index of Lending Protocol changes introduced by the LendingProtocolV1_2 amendment
+  author: Vytautas Vito Tumas 
+  proposal-from: https://github.com/XRPLF/XRPL-Standards/discussions/190
+  status: Draft
+  category: Amendment
+  created: 2026-10-01
+  updated: 2026-10-01
+
+ +# 66.3 LendingProtocolV1_2 Loan Changes + +## 1. Abstract + +This index groups the Lending Protocol changes introduced by the `LendingProtocolV1_2` amendment. + +## 2. Specifications + +These specifications are introduced by the `LendingProtocolV1_2` amendment: + +| Spec | Description | +| ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | +| [66.3.1 Two-Step Loan Creation](./66.3.1-two-step-loan-creation.md) | Adds a propose-and-accept Loan flow through `LoanSet` and a new `LoanAccept` transaction. | +| [66.3.2 Conditional LoanBroker VaultID](./66.3.2-conditional-vault-id.md) | Requires `VaultID` on `LoanBrokerSet` creation and forbids it on modification. | +| [66.3.3 Private LoanBroker](./66.3.3-private-loan-broker.md) | Restricts Loan origination to Borrowers holding credentials accepted by a Permissioned Domain. | + +## 3. Rationale + +The amendment covers independent Lending Protocol changes. Keeping each change in a focused specification makes its behavior and motivation explicit while this index records their shared amendment. + +## 4. Security Considerations + +This index introduces no additional protocol behavior. The security considerations for each change are documented in its linked specification. diff --git a/XLS-0066-lending-protocol/README.md b/XLS-0066-lending-protocol/README.md index ca293f0b6..8393b933e 100644 --- a/XLS-0066-lending-protocol/README.md +++ b/XLS-0066-lending-protocol/README.md @@ -127,6 +127,7 @@ The parent protocol is `LendingProtocol` (`featureLendingProtocol`). Amendment c - [66.1.2 Closed-Ended Loan Gates](./66.1/66.1.2-closed-ended-loan-gates.md): restricts `LoanSet` to the Investment phase with a pre-Redemption maturity buffer and requires closed-ended Vaults for new Loan Brokers. - `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. +- `LendingProtocolV1_2`, as described in [XLS-66.3](./66.3/README.md). ## 3. Specification