From ba3a7504eeca18ab575e28b6445a0446bbfcffa7 Mon Sep 17 00:00:00 2001 From: Vito Date: Fri, 18 Oct 2024 18:24:23 +0100 Subject: [PATCH 01/77] adds lending protocol spec --- XLS-0066d-lending-protocol/README.md | 1435 ++++++++++++++++++++++++++ 1 file changed, 1435 insertions(+) create mode 100644 XLS-0066d-lending-protocol/README.md diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md new file mode 100644 index 00000000..95bad938 --- /dev/null +++ b/XLS-0066d-lending-protocol/README.md @@ -0,0 +1,1435 @@ +
    
+Title:        Lending Protocol
+Revision:     1 (2024-10-18)
+
+
Authors: + Vytautas Vito Tumas + Aanchal Malhotra + +Affiliation: + Ripple +
+ +# Lending Protocol + +## _Abstract_ + +Decentralized Finance (DeFi) lending represents a transformative force within the blockchain ecosystem. It revolutionizes traditional financial services by offering a peer-to-peer alternative without intermediaries like banks or financial institutions. At its core, DeFi lending platforms empower users to borrow and lend digital assets directly, fostering financial inclusion, transparency, and efficiency. + +This proposal introduces fundamental primitives for an XRP Ledger-native Lending Protocol. The protocol offers straightforward on-chain uncollateralized fixed-term loans, utilizing pooled funds with pre-set terms for interest-accruing loans. The design relies on off-chain underwriting and risk management to assess the creditworthiness of the borrowers. However, the First-Loss Capital protection scheme absorbs some of the losses in case of a Loan Default. + +This version intentionally skips the complex mechanisms of automated on-chain collateral and liquidation management. Instead, it focuses on the primitives and the essential components for on-chain credit origination. Therefore, the primary design principle is flexibility and reusability to enable the introduction of additional complex features in the future. + +## Index + +- [**1. Introduction**](#1-introduction) + - [**1.1. Overview**](#11-overview) + - [**1.2. Compliance Features**](#12-compliance-features) + - [**1.3. Risk Management**](#13-risk-management) + - [**1.4. Interest Rates**](#14-interest-rates) + - [**1.5. Fees**](#15-fees) + - [**1.6. Terminology**](#16-terminology) + - [**1.7. System Diagram**](#17-system-diagram) +- [**2. Ledger Entries**](#2-ledger-entries) + - [**2.1. LoanBroker Ledger Entry**](#21-loanbroker-ledger-entry) + - [**2.1.1. Object Identifier**](#211-object-identifier) + - [**2.1.2. Fields**](#212-fields) + - [**2.1.3. LoanBroker _pseudo-account_**](#213-loanbroker-pseudo-account) + - [**2.1.4. Ownership**](#214-ownership) + - [**2.1.5. Reserves**](#215-reserves) + - [**2.1.6. Accounting**](#216-accounting) + - [**2.1.7. First-Loss Capital**](#217-first-loss-capital) + - [**2.2. Loan Ledger Entry**](#22-loan-ledger-entry) + - [**2.2.1. Object Identifier**](#221-object-identifier) + - [**2.2.2. Fields**](#222-fields) + - [**2.2.3. Ownership**](#223-ownership) + - [**2.2.4. Reserves**](#224-reserves) + - [**2.2.5. Impairment**](#225-impairment) +- [**3. Transactions**](#3-transactions) + - [**3.1. LoanBroker Transactions**](#31-loanbroker-transactions) + - [**3.1.1. LoanBrokerSet Transaction**](#311-loanbrokerset) + - [**3.1.2. LoanBrokerDelete Transaction**](#312-loanbrokerdelete) + - [**3.1.3. LoanBrokerCovereposit Transaction**](#313-loanbrokercoverdeposit) + - [**3.1.4. LoanBrokerCoverWithdraw Transaction**](#314-loanbrokercoverwithdraw) + - [**3.2 Loan Transactions**](#32-loan-transactions) + - [**3.2.1. LoanSet Transaction**](#321-loanset-transaction) + - [**3.2.2. LoanDelete Transaction**](#322-loandelete-transaction) + - [**3.2.3. LoanManage Transaction**](#323-loanmanage-transaction) + - [**3.2.4. LoanDraw Transaction**](#324-loandraw-transaction) + - [**3.2.5 LoanPay Transaction**](#325-loanpay-transaction) +- [**Appendix**](#appendix) + +## 1. Introduction + +### 1.1 Overview + +The Lending Protocol uses the [Vault](https://github.com/XRPLF/XRPL-Standards/discussions/192) on-chain object to provision assets from one or more depositors. A Loan Broker is responsible for managing the Lending Protocol and the associated Vault. The Vault Owner and Loan Broker must be on the same account, but this may change in the future. + +The specification introduces two new ledger entries: `LoanBroker` and `Loan`. The `LoanBroker` object captures the Lending Protocol-specific details, such as fees and First-Loss Capital cover. Furthermore, it tracks the funds taken from the `Vault`. The `Loan` object captures the Loan agreement between the Loan Broker and the Borrower. + +The specification introduces the following transactions: + +- **`LoanBrokerSet`**: A transaction to create a new `LoanBroker` object. +- **`LoanBrokerDelete`**: A transaction to delete an existing `LoanBroker` object. +- **`LoanBrokerCoverDeposit`**: A transaction to deposit First-Loss Capital. +- **`LoanBrokerCoverWithdraw`**: A transaction to withdraw First-Loss Capital. +- **`LoanSet`**: A transaction to create a new `Loan` object. +- **`LoanDelete`**: A transaction to delete an existing `Loan` object. +- **`LoanManage`**: A transaction to manage an existing `Loan`. +- **`LoanDraw`**: A transaction to drawdown `Loan` funds. +- **`LoanPay`**: A transaction to make a `Loan` payment. + +The flow of the lending protocol is as follows: + +1. The Loan Broker creates a `Vault` ledger entry. +2. The Loan Broker creates a `LoanBroker` ledger entry with a `LoanBrokerSet` transaction. +3. The Depositors deposit assets into the `Vault`. +4. Optionally, the Loan Broker deposits First-Loss Capital into the `LoanBroker` with the `LoanBrokerCoverDeposit` transaction. +5. The Loan Broker and Borrower create a `Loan` object with a `LoanSet` transaction. +6. The Borrower can draw funds with the `LoanDraw` transaction and make payments with the `LoanPay`. +7. If the Borrower fails to pay the Loan, the Loan Broker can default the `Loan` using the `LoanManage` transaction. +8. Once the Loan has matured (or defaulted), the Borrower or the Loan Broker can delete it using a `LoanDelete` transaction. +9. Optionally, the Loan Broker can withdraw the First-Loss Capital using the `LoanBrokerCoverWithdraw` transaction. +10. When all `Loan` objects are deleted, the Loan Broker can delete the `LoanBroker` object with a `LoanBrokerDelete` transaction. +11. When all `LoanBroker` objects are deleted, the Loan Broker can delete the `Vault` object. + +### 1.2 Compliance Features + +### 1.2.1 Clawback + +Clawback is a mechanism by which an asset Issuer (IOU or MPT, not XRP) claws back the funds. It can be performed on the Vault, not the Lending Protocol. + +### 1.2.2 Freeze + +Freeze is a mechanism by which an asset Issuer (IOUT or MPT, not XRP) freezes an `Account`, preventing that account from sending or receiving the Asset. Furthermore, an Issuer may enact a global freeze, which prevents everyone from sending or receiving the Asset. Note that in both single-account and global freezes, the Asset can be sent to the Issuer. + +If the Issuer freezes a Borrower's account, the Borrower cannot make loan payments or draw down funds. A frozen account does not lift the obligation to repay a Loan. +If a Loan Broker's account is frozen, the Broker will not receive any Loan fees. They will be able to create new loans, and existing loans will not be affected. However, the Loan Broker cannot deposit or withdraw First-Loss Capital. + +Finally, the exact behaviour has yet to be defined in a global freeze. **TBD** + +### 1.3 Risk Management + +Risk management involves mechanisms that mitigate the risks associated with lending. To protect investors' assets, we have introduced an optional first-loss capital protection scheme. This scheme requires the Loan Broker to deposit a fund that can be partially liquidated to cover losses in the event of a loan default. The amount of first-loss capital required is a percentage of the total debt owed to the Vault. In case of a default, a portion of the first-loss capital will be liquidated based on the minimum required cover. The liquidated capital is placed back into the Vault to cover some of the loss. + +### 1.4 Interest Rates + +There are three basic interest rates associated with a Loan: + +- **`Interest Rate`**: The regular interest rate based on the principal amount. It is the cost of borrowing funds. +- **`Late Interest Rate`**: A higher interest rate charged for a late payment. +- **`Full Payment Rate`**: An interest rate charged for repaying the total Loan early. + +### 1.5 Fees + +The lending protocol charges a number of fees that the Loan Broker can configure. The protocol will not charge the fees if the Loan Broker has not deposited enough First-Loss Capital. + +- **`Management Fee`**: This is a percentage of interest charged by the Loan Broker. Intuitively, the Vault depositors pay this fee. +- **`Loan Origination Fee`**: A nominal fee paid to the Loan Broker taken from the principal lent. +- **`Loan Service Fee`**: A nominal fee paid on top of each loan payment. +- **`Late Payment Fee`**: A nominal fee paid on top of a late payment. +- **`Early Payment Fee`**: A nominal fee paid on top of an early payment. + +### 1.6 Terminology + +#### 1.6.1 Terms + +- **`Fixed-Term Loan`**: A type of Loan with a known end date and a constant periodic payment schedule. +- **`Principal`**: The original sum of money borrowed that must be repaid, excluding interest or other fees. +- **`Interest`**: The cost of borrowing the Asset, calculated as a percentage of the loan principal, which the Borrower pays to the Lender over time. +- **`Drawdown`**: The process where a borrower accesses part or all of the loan funds after the Loan has been created. +- **`Default`**: The failure by the Borrower to meet the obligations of a loan, such as missing payments. +- **`First-Loss Capital`**: The portion of capital that absorbs initial losses in case of a Default, protecting the Vault from loss. +- **`Term`**: The period over which a Borrower must repay the Loan. +- **`Amortization`**: The gradual repayment of a loan through scheduled payments that cover both interest and principal over time. +- **`Repayment Schedule`**: A detailed plan that outlines when and how much a borrower must pay to repay the Loan fLoan. +- **`Grace Period`**: A set period after the Loan's due date after which the Loan Broker can default the Loan +- **`Origination Fee`**: A nominal one-time fee the loan broker charges for processing a new loan application. +- **`Service Fee`**: A recurring nominal charge the Borrower pays during Loan payment. +- **`Management Fee`**: A percentage fee charged by the Borrower on the loan interest before returning the interest to the Vault. +- **`Late Payment Fee`**: A penalty charged to the Borrower for failing to make a payment on or before its due date. +- **`Interest Rate`**: The percentage charged by the loan broker on the loan principal, representing the cost of borrowing. +- **`Late Interest Rate`**: A higher interest rate applied to overdue loan payments as a penalty for late repayment. +- **`Closing Interest Rate`**: The final interest rate charged when the Loan is closed or fully repaid. + +### 1.6.2 Actors + +- **`LoanBroker`**: The entity issuing the Loan. +- **`Borrower`**: The account that is borrowing funds. + +### 1.7 System Diagram + +``` ++-----------------+ +-----------------+ +-----------------+ +| Depositor | | LoanBroker | | Borrower | +| AccountRoot | | AccountRoot | | AccountRoot | +|-----------------| |-----------------| |-----------------| +| Owner Directory | | Owner Directory | | Owner Directory | ++-----------------+ +-----------------+ +-----------------+ + ^ | | | + | Reserve ____________Reserve____________ Reserve + Account | | | | + | V V V V ++-----------------+ +-----------------+ +-----------------+ +-----------------+ +| | | |1 N| |1 N| | +| MPToken | | Vault |--------->| LoanBroker |--------->| Loan | +| | | | |-----------------| | | ++-----------------+ +-----------------+ | Owner Directory | +-----------------+ + | ^ +-----------------+ ^ + Issuance | ___________ ____________^ |_________Link_________| + | | Account | + V ^ | ^ ++-----------------+ | +-----------------+ | +| Share | | | Pseudo-Account | | +| MPTokenIssuance |<------Issuer------| -----| AccountRoot | | +| | |_Link_|-----------------|_Link_| ++-----------------+ | Owner Directory | + +-----------------+ +``` + +[**Return to Index**](#index) + +## 2. Ledger Entries + +### 2.1. LoanBroker Ledger Entry + +The `LoanBroker` object captures attributes of the Lending Protocol. + +#### 2.1.1 Object Identifier + +The key of the `LoanBroker` object is the result of [`SHA512-Half`](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#hashes) of the following values concatenated in order: + +- The `LoanBroker` space key `0x006C` (Lower-case `l`). +- The `AccountID`(https://xrpl.org/docs/references/protocol/binary-format/#accountid-fields) of the account submitting the `LoanBrokerSet` transaction, i.e. `Lender`. +- The transaction `Sequence` number. If the transaction used a [Ticket](https://xrpl.org/docs/concepts/accounts/tickets/), use the `TicketSequence` value. + +#### 2.1.2 Fields + +The `LoanBroker` object has the following fields: + +| Field Name | Modifiable? | Required? | JSON Type | Internal Type | Default Value | Description | +| ---------------------- | :---------: | :----------------: | :-------: | :-----------: | :-----------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `LedgerEntryType` | `N/A` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | Ledger object type. | +| `LedgerIndex` | `N/A` | :heavy_check_mark: | `string` | `UINT16` | `N/A` | Ledger object identifier. | +| `Flags` | `Yes` | :heavy_check_mark: | `string` | `UINT32` | 0 | Ledger object flags. | +| `PreviousTxnID` | `N/A` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the transaction that last modified this object. | +| `PreviousTxnLgrSeq` | `N/A` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The sequence of the ledger containing the transaction that last modified this object. | +| `Sequence` | `N/A` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The transaction sequence number that created the `LoanBroker`. | +| `OwnerNode` | `N/A` | :heavy_check_mark: | `number` | `UINT64` | `N/A` | Identifies the page where this item is referenced in the owner's directory. | +| `VaultID` | `No` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the `Vault` object associated with this Lending Protocol Instance. | +| `Owner` | `No` | :heavy_check_mark: | `string` | `AccountID` | `N/A` | The address of the account that is the Loan Broker. | +| `Data` | `Yes` | | `string` | `BLOB` | None | Arbitrary metadata about the `LoanBroker`. Limited to 256 bytes. | +| `ManagementFeeRate` | `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` | `N/A` | :heavy_check_mark: | `number` | `UINT32` | 0 | The number of active Loans issued by the `LoanBroker`. | +| `DebtTotal` | `N/A` | :heavy_check_mark: | `number` | `NUMBER` | 0 | The total asset amount the protocol owes the Vault, including interest. | +| `DebtMaximum` | `Yes` | :heavy_check_mark: | `number` | `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` | `N/A` | :heavy_check_mark: | `number` | `NUMBER` | 0 | The total amount of first-loss capital deposited into the Lending Protocol. | +| `CoverRateMinimum` | `No` | :heavy_check_mark: | `number` | `UINT16` | 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%. | +| `CoverRateLiquidation` | `No` | :heavy_check_mark: | `number` | `UINT16` | 0 | The 1/10th basis point of minimum required first loss capital that is liquidated to cover a Loan default. Valid values are between 0 and 100000 inclusive. A value of 1 is equivalent to 1/10 bps or 0.001%. | + +#### 2.1.3 `LoanBroker `_pseudo-account_` + +The Lending Protocol uses the `_pseudo-account_` of the associated `Vault` object to hold the First-Loss Capital. + +#### 2.1.4 Ownership + +The lending protocol object is stored in the ledger and tracked in an [Owner Directory](https://xrpl.org/docs/references/protocol/ledger-data/ledger-entry-types/directorynode) owned by the account submitting the `LoanBrokerSet` transaction. Furthermore, the object is also tracked in the `OwnerDirectory` of the _`pseudo-account`_. + +The `LoanBroker` requires tracking associated `Loan` objects to prevent the `LoanBroker` object from being deleted while loans are active as well as future RPC endpoints. Therefore, the `LoanBroker` has an associated [Owner Directory](https://xrpl.org/docs/references/protocol/ledger-data/ledger-entry-types/directorynode) object. + +The `RootIndex` of the `DirectoryNode` object is the result of [`SHA512-Half`](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#hashes) of the following values concatenated in order: + +- The `OwnerDirectory` space key `0x004F` +- The `LoanBrokerID` + +#### 2.1.5 Reserves + +The `LoanBroker` object costs one owner reserve for the account creating it. + +#### 2.1.6 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`. + +**Example** + +``` +Example 1: # Issuing a Loan # + +** Initial States ** + +-- Vault -- +AssetsTotal = 100,000 Tokens +AssetsAvailable = 100,000 Tokens +SharesTotal = 100,000 Shares + +-- Lending Protocol -- +DebtTotal = 0 +# The fee charged by the Lending Protocol against any interest. +ManagementFeeRate = 0.1 (10%) + + +# The Lender issues the following Loan +-- Loan -- +LoanPrincipal = 1,000 Tokens +LoanInterestRate = 0.1 (10%) + +# SIMPLIfIED +LoanInterest = LoanPrincipal x LoanInterestRate + = 100 Tokens + +** State Changes ** + +-- Vault -- +# Increase the potential value of the Vault +AssetsTotal = AssetsTotal + ((LoanInterest - (LoanInterest x ManagementFeeRate))) + = 100,000 + (100 - (100 x 0.1)) = 100,000 + 90 + = 100,090 Tokens + +# Decrease Assets Available in the Vault +AssetsAvailable = AssetsAvailable - LoanPrincipal + = 100,000 - 1,000 + = 99,000 Tokens + +SharesTotal = (UNCHANGED) + +-- Lending Protocol -- +# Increase Lending Protocol Debt +DebtTotal = DebtTotal + LoanPrincipal + (LoanInterest - (LoanInterest x ManagementFeeRate)) + = 0 + 1,000 + (100 - (100 x 0.1)) = 1,000 + 90 + = 1,090 Tokens + + +--------------------------------------------------------------------------------------------------- + +Example 2: # Loan Payment # + +** Initial States ** + +-- Vault -- +AssetsTotal = 100,090 Tokens +AssetsAvailable = 99,000 Tokens +SharesTotal = 100,000 Shares + +-- Lending Protocol -- +DebtTotal = 1,090 Tokens +# The fee charged by the Lending Protocol against any interest. +ManagementFeeRate = 0.1 (10%) + +-- Loan -- +LoanPrincipal = 1,000 Tokens +LoanInterestRate = 0.1 (10%) +# SIMPLIfIED +LoanPayments = 2 + +# SIMPLIfIED +LoanInterest = LoanPrincipal x LoanInterestRate + = 100 Tokens + + +# The Borrower makes a single payment + +PaymentAmount = 550 Tokens +PaymentPrincipalPortion = 500 Tokens +PaymentInterestPortion = 50 Tokens + + +** State Changes ** + +-- Vault -- +AssetsTotal = (UNCHANGED) + +# Increase Assets Available in the Vault +AssetsAvailable = AssetsAvailable + PaymentPrincipalPortion + (PaymentInterestPortion - (PaymentInterestPortion x ManagementFeeRate) + = 99,000 + 500 + (50 - (50 x 0.1)) + = 99,545 Tokens + +SharesTotal = (UNCHANGED) + +-- Lending Protocol -- + +# Decrease Lending Protocol Debt +DebtTotal = DebtTotal - PaymentPrincipalAmount - (PaymentInterestPortion - (PaymentInterestPortion x ManagementFeeRate) + = 1,090 - 500 - (50 - (50 x 0.1)) + = 545 Tokens + +``` + +#### 2.1.7 First-Loss Capital + +The First-Loss Capital is an optional mechanism to protect the Vault depositors from incurring a loss in case of a Loan default. The first loss of capital absorbs some of the loss. The following parameters control the First-Loss Capital: + +- `CoverAvailable` - the total amount of cover deposited by the Lending Protocol Owner. +- `CoverRateMinimum` - the percentage of `DebtTotal` that must be covered by the `CoverAvailable`. +- `CoverRateLiquidation` - the maximum percentage of the minimum required cover ($DebtTotal \times CoverRateMinimum$) that will be liquidated to cover a Loan Default. + +Whenever the available cover falls below the minimum cover required, two consequences occur: + +- The Lender cannot issue new Loans. +- The Lender cannot receive fees. Borrower fees are ignored (i.e. the Borrower does not have to pay a Loan payment fee), and the Management Fee is instead deposited into a Vault. + +**Examples** + +``` +Example 1: Loan Default + +** Initial States ** + +-- Vault -- +AssetsTotal = 100,090 Tokens +AssetsAvailable = 99,000 Tokens +SharesTotal = 100,000 Tokens + +-- Lending Protocol -- +DebtTotal = 1,090 Tokens +CoverRateMinimum = 0.1 (10%) +CoverRateLiquidation = 0.1 (10%) +CoverAvailable = 1,000 Tokens + +-- Loan -- +DefaultAmount = 1,090 Tokens + + +# First-Loss Capital liquidation maths + +# The amount of the default that the first-loss capital scheme will cover +DefaultCovered = min((DebtTotal x CoverRateMinimum) x CoverRateLiquidation, DefaultAmount) + = min((1,090 * 0.1) * 0.1, 1,090) = min(10.9, 1,090) + = 10.9 Tokens + +DefaultRemaining = DefaultAmount - DefaultCovered + = 1,090 - 10.9 + = 1,079.1 Tokens + +** State Changes ** + +-- Vault -- +AssetsTotal = AssetsTotal - DefaultRemaining + = 100,090 - 1,079.1 + = 99,010.9 Tokens + +AssetsAvailable = AssetsAvailable + DefaultCovered + = 99,000 + 10.9 + = 99,010.9 Tokens + +SharesTotal = (UNCHANGED) + +-- Lending Protocol -- +DebtTotal = DebtTotal - DefaultAmount + = 1,090 - 1,090 + = 0 Tokens + +CoverAvailable = CoverAvailable - DefaultCovered + = 1,000 - 10.9 + = 989.1 Tokens +``` + +[**Return to Index**](#index) + +### 2.2. `Loan` Ledger Entry + +A Loan ledger entry captures various Loan terms on-chain. It is an agreement between the Borrower and the loan issuer. + +#### 2.2.1 Object Identifier + +The `LoanID` is calculated as follows: + +- Calculate [`SHA512-Half`](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#hashes) of the following values: + - The `Loan` space key `0x004C` (capital L) + - The [`AccountID`](https://xrpl.org/docs/references/protocol/binary-format/#accountid-fields) of the Borrower account. + - The `LoanBrokerID` of the associated `LoanBroker` object. + - The `Sequence` number of the **`LoanSet`** transaction. If the transaction used a [Ticket](https://xrpl.org/docs/concepts/accounts/tickets/), use the `TicketSequence` value. + +#### 2.2.2 Fields + +| Field Name | Modifiable? | Required? | JSON Type | Internal Type | Default Value | Description | +| ------------------------- | :---------: | :----------------: | :-------: | :-----------: | :-------------------------------------------------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `LedgerEntryType` | `N/A` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | Ledger object type. | +| `LedgerIndex` | `N/A` | :heavy_check_mark: | `string` | `UINT16` | `N/A` | Ledger object identifier. | +| `Flags` | `Yes` | | `string` | `UINT32` | 0 | Ledger object flags. | +| `PreviousTxnID` | `N/A` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the transaction that last modified this object. | +| `PreviousTxnLgrSeq` | `N/A` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The ledger sequence containing the transaction that last modified this object. | +| `Sequence` | `N/A` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The transaction sequence number that created the loan. | +| `OwnerNode` | `N/A` | :heavy_check_mark: | `number` | `UINT64` | `N/A` | Identifies the page where this item is referenced in the owner's directory. | +| `LoanBrokerID` | `No` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the `LoanBroker` associated with this Loan Instance. | +| `Borrower` | `No` | :heavy_check_mark: | `string` | `AccountID` | `N/A` | The address of the account that is the borrower. | +| `LoanOriginationFee` | `No` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | A nominal funds amount paid to the `LoanBroker.Owner` when the Loan is created. | +| `LoanServiceFee` | `No` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | A nominal funds amount paid to the `LoanBroker.Owner` with every Loan payment. | +| `LatePaymentFee` | `No` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | A nominal funds amount paid to the `LoanBroker.Owner` when a payment is late. | +| `ClosePaymentFee` | `No` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | A nominal funds amount paid to the `LoanBroker.Owner` when a payment full payment is made. | +| `OveraymentFee` | `No` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | A fee charged on overpayments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | +| `InterestRate` | `No` | :heavy_check_mark: | `number` | `UINT16` | `N/A` | Annualized interest rate of the Loan in 1/10th basis points. | +| `LateInterestRate` | `No` | :heavy_check_mark: | `number` | `UINT16` | `N/A` | A premium is added to the interest rate for late payments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | +| `CloseInterestRate` | `No` | :heavy_check_mark: | `number` | `UINT16` | `N/A` | An interest rate charged for repaying the Loan early in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | +| `OverpaymentInterestRate` | `No` | :heavy_check_mark: | `number` | `UINT16` | `N/A` | An interest rate charged on overpayments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | +| `StartDate` | `No` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The timestamp of when the Loan starts [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time). | +| `PaymentInterval` | `No` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | Number of seconds between Loan payments. | +| `GracePeriod` | `No` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The number of seconds after the Payment Due Date that the Loan can be Defaulted. | +| `PreviousPaymentDate` | `N/A` | :heavy_check_mark: | `number` | `UINT32` | `0` | The timestamp of when the previous payment was made in [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time). | +| `NextPaymentDueDate` | `N/A` | :heavy_check_mark: | `number` | `UINT32` | `LoanSet.StartDate + LoanSet.PaymentInterval` | The timestamp of when the next payment is due in [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time). | +| `PaymentsRemaining` | `N/A` | :heavy_check_mark: | `number` | `UINT32` | `LoanSet.PaymentsTotal` | The number of payments remaining on the Loan. | +| `AssetsAvailable` | `N/A` | :heavy_check_mark: | `number` | `NUMBER` | `LoanSet.[PrincipalRequested - LoanOriginationFee]` | The asset amount that is available in the Loan. | +| `PrincipalOutstanding` | `No` | :heavy_check_mark: | `number` | `NUMBER` | `LoanSet.PrincipalRequested` | The principal amount requested by the Borrower. | + +##### 2.2.2.1 Flags + +The `Loan` object supports the following flags: + +| Flag Name | Flag Value | Modifiable? | Description | +| -------------------- | :--------: | :---------: | :----------------------------------------------------: | +| `lsfLoanDefault` | `0x0001` | `No` | If set, indicates that the Loan is defaulted. | +| `lsfLoanImpaired` | `0x0002` | `Yes` | If set, indicates that the Loan is impaired. | +| `lsfLoanOverpayment` | `0x0003` | `No` | If set, indicates that the Loan supports overpayments. | + +#### 2.2.3 Ownership + +The `Loan` objects are stored in the ledger and tracked in an [Owner Directory](https://xrpl.org/docs/references/protocol/ledger-data/ledger-entry-types/directorynode) owned by the `Borrower`. + +Furthermore, to facilitate the `Loan` object lookup from the `LoanBroker`, the object is also tracked in the `OwnerDirectory` associated with the `LoanBroker` object. + +#### 2.2.4 Reserves + +The `Loan` object costs one owner reserve for the `Borrower`. + +#### 2.2.5 Impairment + +When the Loan Broker discovers that the Borower cannot make an upcoming payment, impairment allows the Loan Broker to register a "paper loss" with the Vault. The impairment mechanism moves the Next Payment Due Date to the time the Loan was impaired, allowing to default the Loan more quickly. However, if the Borrower makes a payment, the impairment status is automatically cleared. + +[**Return to Index**](#index) + +## 3. Transactions + +### 3.1. `LoanBroker` Transactions + +In this section we specify the transactions associated with the `LoanBroker` ledger entry. + +#### 3.1.1 `LoanBrokerSet` + +The transaction creates a new `LoanBroker` object or updates an existing one. + +| Field Name | Required? | JSON Type | Internal Type | Default Value | Description | +| ---------------------- | :----------------: | :-------: | :-----------: | :-----------: | :------------------------------------------------------------------------------------------------------------------------------------------------- | +| `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | The transaction type. | +| `VaultID` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The Vault ID that the Lending Protocol will use to access liquidity. | +| `LoanBrokerID` | | `string` | `HASH256` | `N/A` | The Loan Broker ID that the transaction is modifying. | +| `Flags` | | `string` | `UINT32` | 0 | Specifies the flags for the Lending Protocol. | +| `Data` | | `string` | `BLOB` | None | Arbitrary metadata in hex format. The field is limited to 256 bytes. | +| `ManagementFeeRate` | | `number` | `UINT16` | 0 | The 1/10th basis point fee charged by the Lending Protocol Owner. Valid values are between 0 and 10000 inclusive. | +| `DebtMaximum` | | `number` | `NUMBER` | 0 | The maximum amount the protocol can owe the Vault. The default value of 0 means there is no limit to the debt. | +| `CoverRateMinimum` | | `number` | `UINT16` | 0 | The 1/10th basis point `DebtTotal` that the first loss capital must cover. Valid values are between 0 and 100000 inclusive. | +| `CoverRateLiquidation` | | `number` | `UINT16` | 0 | The 1/10th basis point of minimum required first loss capital liquidated to cover a Loan default. Valid values are between 0 and 100000 inclusive. | + +##### 3.1.1.1 Failure Conditions + +- If `LoanBrokerID` is not specified: + + - `Vault` object with the specified `VaultID` does not exist on the ledger. + - The submitter `AccountRoot.Account != Vault(VaultID).Owner`. + +- If `LoanBrokerID` is specified: + + - `LoanBroker` object with the specified `LoanBrokerID` does not exist on the ledger. + - The submitter `AccountRoot.Account != LoanBroker(LoanBrokerID).Owner`. + - The submitter is attempting to modify fixed fields. + +- Any of the fields are _invalid_. + +##### 3.1.1.2 State Changes + +- If `LoanBrokerID` is not specified: + + - Add `LoanBrokerID` to the `OwnerDirectory` of the submitting account. + - Add `LoanBrokerID` to the `OwnerDirectory` of the Vault's `_pseudo-account_`. + +- If `LoanBrokerID` is specified: + - Update appropriate fields. + +##### 3.1.1.3 Invariants + +**TBD** + +[**Return to Index**](#index) + +#### 3.1.2 `LoanBrokerDelete` + +| Field Name | Required? | JSON Type | Internal Type | Default Value | Description | +| ----------------- | :----------------: | :-------: | :-----------: | :-----------: | :--------------------------------------------------- | +| `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | The transaction type. | +| `LoanBrokerID` | | `string` | `HASH256` | `N/A` | The Loan Broker ID that the transaction is deleting. | + +##### 3.1.2.1 Failure Conditions + +- `LoanBroker` object with the specified `LoanBrokerID` does not exist on the ledger. +- The submitter `AccountRoot.Account != LoanBroker(LoanBrokerID).Owner`. + +- The `OwnerCount` field is greater than zero. +- `CoverAvailable` is greater than zero. + +##### 3.1.2.2 State Changes + +- Delete `LoanBrokerID` from the `OwnerDirectory` of the submitting account. +- Delete `LoanBrokerID` from the `OwnerDirectory` of the Vault's `_pseudo-account_`. +- Delete the `OwnerDirectory` associated with the `LoanBroker` object. + +##### 3.1.2.3 Invariants + +**TBD** + +[**Return to Index**](#index) + +#### 3.1.3 `LoanBrokerCoverDeposit` + +The transaction creates a new `LoanBroker` object or updates an existing one. + +| Field Name | Required? | JSON Type | Internal Type | Default Value | Description | +| ----------------- | :----------------: | :-------: | :-----------: | :-----------: | :------------------------------------------------ | +| `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | The transaction type. | +| `LoanBrokerID` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The Loan Broker ID to deposit First-Loss Capital. | +| `Amount` | :heavy_check_mark: | `object` | `NUMBER` | 0 | The Fist-Loss Capital amount to deposit. | + +##### 3.1.3.1 Failure Conditions + +- `LoanBroker` object with the specified `LoanBrokerID` does not exist on the ledger. +- The submitter `AccountRoot.Account != LoanBroker(LoanBrokerID).Owner`. + +- If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `IOU`: + + - The `RippleState` object between the submitter account and the `Issuer` of the asset has the `lsfLowFreeze` or `lsfHighFreeze` flag set. + - The `AccountRoot` object of the `Issuer` has the `lsfGlobalFreeze` flag set. + - The trustline `Balance` < `Amount`. + +- If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `MPT`: + + - The `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the submitter `AccountRoot`: + - Has `lsfMPTLocked` flag set. + - `MPTAmount` < `Amount`. + - The `MPTokenIssuance` object of the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` has the `lsfMPTLocked` flag set. + +##### 3.1.3.2 State Changes + +- If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is `XRP`: + + - Increase the `Balance` field of _pseudo-account_ `AccountRoot` by `Amount`. + - Decrease the `Balance` field of the submitter `AccountRoot` by `Amount`. + +- If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `IOU`: + + - Increase the `RippleState` balance between the _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `Amount`. + - Decrease the `RippleState` balance between the submitter `AccountRoot` and the `Issuer` `AccountRoot` by `Amount`. + +- If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `MPT`: + + - Increase the `MPToken.MPTAmount` by `Amount` of the _pseudo-account_ `MPToken` object for the `Vault.Asset`. + - Decrease the `MPToken.MPTAmount` by `Amount` of the submitter `MPToken` object for the `Vault.Asset`. + +- Increase `LoanBroker.CoverAvailable` by `Amount`. + +##### 3.1.3.3 Invariants + +**TBD** + +[**Return to Index**](#index) + +#### 3.1.4 `LoanBrokerCoverWithdraw` + +The `LoanBrokerCoverWithdraw` transaction withdraws the First-Loss Capital from the `LoanBroker`. + +| Field Name | Required? | JSON Type | Internal Type | Default Value | Description | +| ----------------- | :----------------: | :-------: | :-----------: | :-----------: | :------------------------------------------------------------ | +| `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | Transaction type. | +| `LoanBrokerID` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The Loan Broker ID from which to withdraw First-Loss Capital. | +| `Amount` | :heavy_check_mark: | `object` | `NUMBER` | 0 | The amount of Vault asset to withdraw. | + +##### 3.2.2.1 Failure conditions + +- `LoanBroker` object with the specified `LoanBrokerID` does not exist on the ledger. +- The submitter `AccountRoot.Account != LoanBroker(LoanBrokerID).Owner`. + +- If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `IOU`: + + - The trustline between the submitter account and the `Issuer` of the asset is frozen. + - The `AccountRoot` object of the `Issuer` has the `lsfGlobalFreeze` flag set. + +- If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `MPT`: + + - The `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the submitter `AccountRoot`: + - Has `lsfMPTLocked` flag set. + - The `MPTokenIssuance` object of the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` has the `lsfMPTLocked` flag set. + +- The submitter is attempting to withdraw an Asset that does not match the Asset of the Vault. + +- The `LoanBroker.CoverAvailable` < `Amount`. + +##### 3.2.2.2 State Changes + +- If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is `XRP`: + + - Decrease the `Balance` field of _pseudo-account_ `AccountRoot` by `Amount`. + - Increase the `Balance` field of the submitter `AccountRoot` by `Amount`. + +- If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `IOU`: + + - Decrease the `RippleState` balance between the _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `Amount`. + - Increase the `RippleState` balance between the submitter `AccountRoot` and the `Issuer` `AccountRoot` by `Amount`. + +- If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `MPT`: + + - Decrease the `MPToken.MPTAmount` by `Amount` of the _pseudo-account_ `MPToken` object for the `Vault.Asset`. + - Increase the `MPToken.MPTAmount` by `Amount` of the submitter `MPToken` object for the `Vault.Asset`. + +- Decrease `LoanBroker.CoverAvailable` by `Amount`. + +[**Return to Index**](#index) + +### 3.2. `Loan` Transactions + +In this section we specify transactions associated with the `Loan` ledger entry. + +#### 3.2.1 `LoanSet` Transaction + +The transaction creates a new `Loan` object. + +| Field Name | Required? | JSON Type | Internal Type | Default Value | Description | +| -------------------- | :----------------: | :-------: | :-----------: | :-----------: | :-------------------------------------------------------------------------------------------------------------------------------------------- | +| `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | The transaction type. | +| `LoanBrokerID` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The Loan Broker ID associated with the loan. | +| `Flags` | | `string` | `UINT32` | 0 | Specifies the flags for the Loan. | +| `Data` | | `string` | `BLOB` | None | Arbitrary metadata in hex format. The field is limited to 256 bytes. | +| `Borrower` | :heavy_check_mark: | `string` | `AccountID` | `N/A` | The address of the account that is the borrower. | +| `LoanOriginationFee` | | `number` | `NUMBER` | 0 | A nominal funds amount paid to the `LoanBroker.Owner` when the Loan is created. | +| `LoanServiceFee` | | `number` | `NUMBER` | 0 | A nominal amount paid to the `LoanBroker.Owner` with every Loan payment. | +| `LatePaymentFee` | | `number` | `NUMBER` | 0 | A nominal funds amount paid to the `LoanBroker.Owner` when a payment is late. | +| `ClosePaymentFee` | | `number` | `NUMBER` | 0 | A nominal funds amount paid to the `LoanBroker.Owner` when an early full repayment is made. | +| `InterestRate` | | `number` | `UINT16` | 0 | Annualized interest rate of the Loan in basis points. | +| `LateInterestRate` | | `number` | `UINT16` | 0 | A premium added to the interest rate for late payments in basis points. Valid values are between 0 and 10000 inclusive. (0 - 100%) | +| `CloseInterestRate` | | `number` | `UINT16` | 0 | A Fee Rate charged for repaying the Loan early in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | +| `PrincipalRequested` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | The principal amount requested by the Borrower. | +| `StartDate` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The timestamp of when the Loan starts [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time). | +| `PaymentsTotal` | | `number` | `UINT32` | 1 | The total number of payments to be made against the Loan. | +| `PaymentInterval` | | `number` | `UINT32` | 60 | Number of seconds between Loan payments. | +| `GracePeriod` | | `number` | `UINT32` | 60 | The number of seconds after the Loan's Payment Due Date can be Defaulted. | +| `Lender` | :heavy_check_mark: | `object` | `STObject` | `N/A` | An inner object that contains the signature of the Lender over the transaction. | + +##### 3.2.1.1 `Flags` + +| Flag Name | Flag Value | Description | +| ------------------- | :--------: | :---------------------------------------------- | +| `tfLoanOverpayment` | `0x0001` | Indicates that the vault supports overpayments. | + +##### 3.2.1.2 `Lender` + +An inner object that contains the signature of the Lender over the transaction. The fields contained in this object are: + +| Field Name | Required? | JSON Type | Internal Type | Default Value | Description | +| --------------- | :----------------: | :-------: | :-----------: | :-----------: | :--------------------------------------------------------------------------------------------------------------------- | +| `SigningPubKey` | :heavy_check_mark: | `string` | `STBlob` | `N/A` | The Public Key to be used to verify the validity of the signature. | +| `Signature` | :heavy_check_mark: | `string` | `STBlob` | `N/A` | The signature of over all signing fields, including the `Signature` of the Borrower. | +| `Signers` | :heavy_check_mark: | `list` | `STArray` | `N/A` | An array of transaction signatures from the `LoanBroker.Owner` signers to indicate their approval of this transaction. | + +The final transaction must include `Signature` or `Signers`. + +If the `Signers` field is necessary, then the total fee for the transaction will be increased due to the extra signatures that need to be processed, similar to the additional fees for multisigning. The minimum fee will be $(|signatures| + 1) \times base\_fee$ + +The total fee calculation for signatures will now be $(1 + |tx.Signers| + |tx.Lender.Signers|) \times base\_fee$. + +This field is not a signing field (it will not be included in transaction signatures, though the `Signature` or `Signers` field will be included in the stored transaction). + +##### 3.2.1.3 Multi-Signing + +The `LoanSet` transaction is a mutual agreement between the `Borrower` and the `LoanBroke.Owner` to create a Loan. Therefore, the `LoanSet` transaction must be signed by both parties. The multi-signature flow is as follows: + +1. The `Borrower` creates a new transaction with the pre-agreed terms of the Loan and signs the transaction. +2. The `Lender` signs over all signing fields, including the signature of the `Borrower`. + +##### 3.2.1.4 Failure Conditions + +- `LoanBroker` object with the specified `LoanBrokerID` does not exist on the ledger. +- The submitter `AccountRoot.Account != LoanBroker(LoanBrokerID).Owner`. +- `Lender. Signature` is invalid. + +- If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `IOU`: + + - The trustline between the submitter account and the `Issuer` of the asset is frozen. + - The `AccountRoot` object of the `Issuer` has the `lsfGlobalFreeze` flag set. + +- If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `MPT`: + + - The `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the submitter `AccountRoot`: + - Has `lsfMPTLocked` flag set. + - The `MPTokenIssuance` object of the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` has the `lsfMPTLocked` flag set. + +- Either of the `tfDefault`, `tfImpair` or `tfUnimpair` flags are set. + +- The `Borrower` `AccountRoot` object does not exist. + +- `PaymentInterval` is less than `60` seconds. +- `GracePeriod` is greater than the `PaymentInterval`. +- `Loan.StartDate < CurrentTime`. + +- Insufficient assets in the Vault: + + - `Vault(LoanBroker(LoanBrokerID).VaultID).AssetsAvailable` < `Loan.PrincipalRequested`. + +- Exceeds maximum Debt of the LoanBroker: + + - `LoanBroker(LoanBrokerID).DebtMaximum` < `LoanBroker(LoanBrokerID).DebtTotal + Loan.PrincipalRequested` + +- Insufficient First-Loss Capital: + - `LoanBroker(LoanBrokerID).CoverAvailable` < `(LoanBroker(LoanBrokerID).DebtTotal + Loan.PrincipalRequested) x LoanBroker(LoanBrokerID).CoverRateMinimum` + +##### 3.2.1.5 State Changes + +- If the Loan Asset is an `IOU`: + + - Create a `Trustline` between the `Issuer` and the `Borrower` if one does not exist. + +- If the Loan Asset is an `MPT`: + + - Create an `MPToken` object for the `Borrower` if one does not exist. + +- `Vault(LoanBroker(LoanBrokerID).VaultID)` object state changes: + + - Decrease Assets Available in the Vault: + + - `Vault.AssetsAvailable -= Loan.PrincipalRequested`. + + - Increase the Total Value of the Vault: + - `Vault.AssetsTotal += LoanInterest - (LoanInterest x LoanBroker.ManagementFeeRate)` where `LoanInterest` is the Loan's total interest. + +- `LoanBroker(LoanBrokerID)` object changes: + + - `LoanBroker.DebtTotal += Loan.PrincipalRequested + (LoanInterest - (LoanInterest x LoanBroker.ManagementFeeRate)` + - `LoanBroker.OwnerCount += 1` + + - If the `DirectoryNode` for the `LoanBroker` does not exist, create one. + - Add `LoanID` to `DirectoryNode.Indexes`. + +##### 3.2.1.4 Invariants + +**TBD** + +[**Return to Index**](#index) + +#### 3.2.2 `LoanDelete` Transaction + +The transaction deletes an existing `Loan` object. + +| Field Name | Required? | JSON Type | Internal Type | Default Value | Description | +| ----------------- | :----------------: | :-------: | :-----------: | :-----------: | :--------------------------------------- | +| `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | The transaction type. | +| `LoanID` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the Loan object to be deleted. | + +##### 3.2.2.1 Failure Conditions + +- A `Loan` object with the specified `LoanID` does not exist on the ledger. +- The Account submitting the `LoanDelete` is not the `LoanBroker.Owner` or the `Loan.Borrower`. +- The Loan is active: + - `Loan.PaymentsRemaining > 0` + +##### 3.2.2.2 State Changes + +- Remove `LoanID` from the Owner Directory associated with the `LoanBroker`. +- `LoanBroker.OwnerCount -= 1` +- Delete the `Loan` object. +- Release reserve funds back to the Borrower. + +[**Return to Index**](#index) + +#### 3.2.3 `LoanManage` Transaction + +| Field Name | Required? | JSON Type | Internal Type | Default Value | Description | +| ----------------- | :----------------: | :-------: | :-----------: | :-----------: | :--------------------------------------- | +| `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | The transaction type. | +| `LoanID` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the Loan object to be updated. | +| `Flags` | | `string` | `UINT32` | 0 | Specifies the flags for the Loan. | + +##### 3.2.3.1 `Flags` + +| Flag Name | Flag Value | Description | +| ---------------- | :--------: | :--------------------------------------------- | +| `tfLoanDefault` | `0x0001` | Indicates that the Loan should be defaulted. | +| `tfLoanImpair` | `0x0002` | Indicates that the Loan should be impaired. | +| `tfLoanUnimpair` | `0x0003` | Indicates that the Loan should be un-impaired. | + +##### 3.2.3.1 Failure Conditions + +- A `Loan` object with the specified `LoanID` does not exist on the ledger. +- The `Account` submitting the transaction is not the `LoanBroker.Owner`. +- The `lsfLoanDefault` flag is set on the Loan object. Once a Loan is defaulted, it cannot be modified. + +- If `Loan(LoanID).Flags == lsfLoanImpaired` AND `tfLoanImpair` flag is provided. + +- `Loan.PaymentsRemaining == 0`. + +- The `tfDefault` flag is specified and: + - `CurrentTime` < `Loan.NextPaymentDueDate + Loan.GracePeriod`. + +##### 3.2.3.2 State Changes + +- If the `tfDefault` flag is specified: + + - Calculate the amount of the Default that First-Loss Capital covers: + + - The default Amount equals the outstanding principal and interest, excluding any funds unclaimed by the Borrower. + - `DefaultAmount = (Loan.PrincipalOutstanding + Loan.InterestOutstanding) - Loan.AssetsAvailable`. + - Apply the First-Loss Capital to the Default Amount + - `DefaultCovered = min((LoanBroker(Loan.LoanBrokerID).DebtTotal x LoanBroker(Loan.LoanBrokerID).CoverRateMinimum) x LoanBroker(Loan.LoanBrokerID).CoverRateLiquidation, DefaultAmount)` + - `DefaultAmount -= DefaultCovered` + + - Update the `Vault` object: + + - Decrease the Total Value of the Vault: + - `Vault(LoanBroker(LoanBrokerID).VaultID).AssetsTotal -= DefaultAmount`. + - Increase the Assets Available of the Vault by liquidated First-Loss Capital and any unclaimed funds amount: + - `Vault(LoanBroker(LoanBrokerID).VaultID).AssetsAvailable += DefaultCovered + Loan.AssetsAvailable`. + +- Update the `LoanBroker` object: + + - Decrease the Debt of the LoanBroker: + - `LoanBroker(LoanBrokerID).DebtTotal -= ` + - `Loan.PrincipalOutstanding + Loan.InterestOutstanding + Loan.AssetsAvailable` + - Decrease the First-Loss Capital Cover Available: + - `LoanBroker(LoanBrokerID).CoverAvailable -= DefaultCovered` + - Decrease the number of active Loans: + - `LoanBroker(LoanBrokerID).OwnerCount -= 1` + +- Update the `Loan` object: + + - `Loan(LoanID).Flags = lsfLoanDefault` + - `Loan(LoanID).PaymentsRemaining = 0` + - `Loan(LoanID).AssetsAvailable = 0` + - `Loan(LoanID).PrincipalOutstanding = 0` + +- If `tfLoanImpair` flag is specified: + + - Update the `Vault` object (set "paper loss"): + + - `Vault(LoanBroker(LoanBrokerID).VaultID).LossUnrealized += Loan.PrincipalOutstanding + TotalInterestOutstanding()` (Please refer to section [**3.2.5.1. Payment Types**](#3251-payment-types), which outlines how to calculate total interest outstanding) + + - Update the `Loan` object: + - `Loan(LoanID).Flags = lsfLoanImpaired` + - If `currentTime < Loan(LoanID).NextPaymentDueDate` (if the loan payment is not yet late): + - `Loan(LoanID).NextPaymentDueDate = currentTime` (move the next payment due date to now) + +- If the `tfLoanUnimpair` flag is specified: + + - Update the `Vault` object (clear "paper loss"): + - `Vault(LoanBroker(LoanBrokerID).VaultID).LossUnrealized -= Loan.PrincipalOutstanding + TotalInterestOutstanding()` (Please refer to section [**3.2.5.1. Payment Types**](#3251-payment-types), which outlines how to calculate total interest outstanding) + + - Update the `Loan` object: + + - `Loan(LoanID).Flags = 0` + - If `Loan(LoanID).PreviousPaymentDate + Loan(LoanID).PaymentInterval > currentTime` (the loan was unimpaired within the payment interval): + + - `Loan(LoanID).NextPaymentDueDate = Loan(LoanID).PreviousPaymentDate + Loan(LoanID).PaymentInterval` + + - If `Loan(LoanID).PreviousPaymentDate + Loan(LoanID).PaymentInterval < currentTime` (the loan was unimpaired after the original payment due date): + - `Loan(LoanID).NextPaymentDueDate = currentTime + Loan(LoanID).PaymentInterval` + +##### 3.2.3.3 Invariants + +**TBD** + +[**Return to Index**](#index) + +#### 3.2.4 `LoanDraw` Transaction + +The Borrower submits a `LoanDraw` transaction to draw funds from the Loan. + +| Field Name | Required? | JSON Type | Internal Type | Default Value | Description | +| ----------------- | :----------------: | :-------: | :-----------: | :-----------: | :------------------------------------------ | +| `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | The transaction type. | +| `LoanID` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the Loan object to be drawn from. | +| `Amount` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | The amount of funds to drawdown. | + +##### 3.2.4.1 Failure Conditions + +- A `Loan` object with the specified `LoanID` does not exist on the ledger. +- The `AccountRoot.Account` of the submitter is not `Loan.Borrower`. +- The Loan has not started: + - `Loan.StartDate > CurrentTime`. +- There are insufficient assets: + + - `Loan.AssetsAvailable` < `Amount`. + +- The `Loan` has `lsfLoanImpaired` or `lsfLoanDefault` flags set. + +- If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `IOU`: + + - The trustline between the submitter account and the `Issuer` of the asset is frozen. + - The `AccountRoot` object of the `Issuer` has the `lsfGlobalFreeze` flag set. + +- If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `MPT`: + + - The `MPToken` object for the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` of the submitter `AccountRoot`: + - Has `lsfMPTLocked` flag set. + - The `MPTokenIssuance` object of the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` has the `lsfMPTLocked` flag set. + +- The `Borrower` missed a payment: + - `CurrentTime > Loan.NextPaymentDueDate`. + +##### 3.2.4.2 State Changes + +- If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is `XRP`: + + - Decrease the `Balance` field of _pseudo-account_ `AccountRoot` by `Amount`. + - Increase the `Balance` field of the submitter `AccountRoot` by `Amount`. + +- If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `IOU`: + + - Decrease the `RippleState` balance between the _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `Amount`. + - Increase the `RippleState` balance between the submitter `AccountRoot` and the `Issuer` `AccountRoot` by `Amount`. + +- If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `MPT`: + + - Decrease the `MPToken.MPTAmount` by `Amount` of the _pseudo-account_ `MPToken` object for the `Vault.Asset`. + - Increase the `MPToken.MPTAmount` by `Amount` of the submitter `MPToken` object for the `Vault.Asset`. + +- Decrease `Loan.AssetsAvailable` by `Amount`. + +##### 3.2.4.3 Invariants + +**TBD** + +[**Return to Index**](#index) + +#### 3.2.5 `LoanPay` Transaction + +The Borrower submits a `LoanPay` transaction to make a Payment on the Loan. + +| Field Name | Required? | JSON Type | Internal Type | Default Value | Description | +| ----------------- | :----------------: | :-------: | :-----------: | :-----------: | :------------------------------------------ | +| `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | The transaction type. | +| `LoanID` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the Loan object to be drawn from. | +| `Amount` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | The amount of funds to pay. | + +##### 3.2.5.1 Payment Types + +A Loan payment has four types: + +- Regular payment is made on time, where the payment size and schedule are calculated with a standard amortization [formula](https://en.wikipedia.org/wiki/Amortization_calculator). + +- A _late_ payment, when a Borrower makes a payment after `netxPaymentDueDate`. Late payments include a `LatePaymentFee` and `LateInterestRate`. + +- An early _full_ payment is when a Borrower pays the outstanding principal. A `CloseInterestRate` is charged on the outstanding principal. + +- An overpayment occurs when a borrower makes a payment that exceeds the required minimum payment amount. + +The payment amount and timing determine the type of payment. A payment made before the `Loan.NextPaymentDueDate` is a regular payment and follows the standard amortization calculation. Any payment made after this date is considered a late payment. + +The following diagram depicts how a payment is handled based on the amount paid. + +``` + Rejected Overpayment Overpayment Overpayment Not charged +|------------|---------------|---------------|---------------|-------------| + Periodic/Late Periodic Periodic Full + Payment Amount Payment Amount Payment Amount Payment Amount + I II N - 1 + + Payment Amount +``` + +The minimum payment required is determined by whether the borrower makes the payment before or on the `NextPaymentDueDate` or if it is late. Any payment below the minimum amount required is rejected. With a single `LoanPay` transaction, the Borrower can make multiple loan payments. For example, if the periodic payment amount is 400 Tokens and the Borrower makes a payment of 900 Tokens, the payment will be treated as two periodic payments, moving the NextPaymentDueDate forward by two payment intervals, and the remaining 100 Tokens will be an overpayment. + +If the Loan Broker and the borrower have agreed to allow overpayments, any amount above the periodic payment is treated as an overpayment. However, if overpayments are not supported, the excess amount will not be charged and will remain with the borrower. + +Each payment comprises three parts, `principal`, `interest` and `fee`. The `principal` is an amount paid against the principal of the Loan, `interest` is the interest portion of the Loan, and `fee` is the fee part paid by the Borrower on top of `principal` and `interest`. + +###### 3.2.5.1.1 Regular Payment + +A periodic payment amount is calculated using the amortization payment formula: + +$$ +totalDue = periodicPayment + loanServiceFee +$$ + +$$ +periodicPayment = principalOutstanding \times \frac{periodicRate \times (1 + periodicRate)^{PaymentsRemaining}}{(1 + periodicRate)^{PaymentsRemaining} - 1} +$$ + +where the periodic interest rate is the interest rate charged per payment period: + +$$ +periodicRate = \frac{interestRate \times paymentInterval}{365 \times 24 \times 60 \times 60} +$$ + +The `principal` and `interest` portions can be derived as follows: + +$$ +interest = principalOutstanding \times periodicRate +$$ + +$$ +principal = periodicPayment - interest +$$ + +###### 3.2.5.1.2 Late Payment + +When a Borrower makes a payment after `NextPaymentDueDate`, they must pay a nominal late payment fee and an additional interest rate charged on the overdue amount for the unpaid period. The formula is as follows: + +$$ +totalDue = periodicPayment + latePaymentFee + latePaymentInterest +$$ + +A special, late payment interest rate is applied for the over-due period: + +$$ +latePaymentInterest = principalOutstanding \times \frac{lateInterestRate \times secondsSinceLastPayment}{365 \times 24 \times 60 \times 60} +$$ + +A late payment pays more interest than calculated when increasing the Vault value in the `LoanSet` transaction. Therefore, the total Vault value captured by `Vault.AssetsTotal` must be recalculate +d. +Assume the function `PeriodicPayment()` returns the expected periodic payment, split into `principalPeriodic` and `interestPeriodic`. Furthermore, assume the function `LatePayment()` that implements the Late Payment formula. The function returns the late payment split into `principalLate` and `interestLate`, where `interestLate` is calculated using the formula above. Note that `principalPeriodic == principalLate` and `interestLate > interestPeriodic` are used only when the payment is late. Otherwise, `interestLate == interestPeriodic`. + +$$ +valueChange = interestLate - interestPeriodic +$$ + +Note that `valueChange >= 0`. + +###### 3.2.5.1.3 Loan Overpayment + +- Let $\mathcal{P}$ and $\mathcal{p}$ represent the total and outstanding Loan principal. +- Let $\mathcal{I}$ and $\mathcal{i}$ represent the total and outstanding Loan interest computed from $\mathcal{P}$ and $\mathcal{p}$ respectively. + +$$ +excess = min(\mathcal{p}, paymentAmountMade - minimumPaymentAmount) +$$ + +$$ +interestPortion = excess \times overpaymentInterestRate +$$ + +$$ +feePortion = excess \times overpaymentFee +$$ + +$$ +principalPortion = excess - interestPortion - feePortion +$$ + +$$ +\mathcal{p'} = \mathcal{p} - principalPortion +$$ + +Let $\mathcal{i}$ denote the outstanding interest computed from $\mathcal{p}$. Simillarly, let $\mathcal{i'}$ denote the outstanding interest computed from $\mathcal{p'}$. We compute the loan interest change as follows: + +$$ +valueChange = \mathcal{i} - \mathcal{i'} +$$ + +###### 3.2.5.1.4 Early Full Repayment + +A Borrower can close a Loan early by submitting the total amount needed to do so. This amount is the sum of the remaining balance, any accrued interest, a prepayment penalty, and a prepayment fee. + +$$ +totalDue = principalOutstanding + accruedInterest + prepaymentPenalty + ClosePaymentFee +$$ + +Accrued interest up to the point of early closure is calculated as follows: + +$$ +accruedInterest = principalOutstanding \times periodicRate \times \frac{secondsSinceLastPayment}{paymentInterval} +$$ + +Finally, the Lender may charge a prepayment penalty for paying a loan early, which is calculated as follows: + +$$ +prepaymentPenalty = principalOutstanding \times closeInterestRate +$$ + +An early payment pays less interest than calculated when increasing the Vault value in the `LoanSet` transaction. Therefore, the Vault value (captured by `Vault.AssetsTotal`) must be recalculated after an early payment. + +Assume a function `CurrentValue()` that returns `principalOutstanding` and `interestOutstanding` of the Loan. Furthermore, assume a function `ClosePayment()` that implements the Full Payment calculation. The function returns the total full payment due split into `principal` and `interest`. + +The value change for an early full repayment is calculated as follows: + +$$ +valueChange = (prepaymentPenalty) - (interestOutstanding - accruedInterest) +$$ + +Note that `valueChange <= 0` as an early repayment reduces the total value of the Loan. + +###### 3.2.5.1.5 Management Fee Calculations + +The `LoanBroker` Management fee is charged against the interest portion of the Loan and subtracted from the total Loan value at Loan creation. However, the fee is charged only during Loan payments. Early and Late payments change the total value of the Loan by decreasing or increasing the value of total interest. Therefore, when an early, late or an overpayment payment is made, the management fee must be updated. + +To update the management fee, we need to compute the new total management fee based on the new total interest after executing the early or late payment. Therefore, we need to capture the Loan value before the payment is made and the new value after the payment is made. + +For the calculation, assume the following variables: + +- Let $\mathcal{P}$ and $\mathcal{p}$ represent the total and outstanding Loan principal. +- Let $\mathcal{I}$ and $\mathcal{i}$ represent the total and outstanding Loan interest computed from $\mathcal{P}$ and $\mathcal{p}$ respectively. +- Let $\mathcal{V}$ and $\mathcal{v}$ represent the total and outstanding value of the Loan. $\mathcal{V} = \mathcal{P} + \mathcal{I}$ and $\mathcal{v} = \mathcal{p} + \mathcal{i}$. +- Finally, let $\mathcal{m}$ represent the management fee rate of the Loan Broker. + +Assume $f(\mathcal{v})$ is a Loan payment, $f(\mathcal{v}) = \mathcal{v'}$, the new outstanding loan value is equal to the application of the payment transaction to the current outstanding value. Furthermore, assume $\mathcal{V} \xrightarrow{f(\mathcal{v})} \mathcal{V'}$, is the change in the Loan total value as the result of applying $f(\mathcal{v})$. + +we say that $\mathcal{V'} = \mathcal{P'} + \mathcal{I'}$. It's important to note that a payment transaction must never change the total principal. I.e. $\mathcal{P} = \mathcal{P'}$, the change in total value is caused by the change in total principal only. + +$\Delta_{\mathcal{V}} = \mathcal{I'} - \mathcal{I}$ is the total value change of the Loan. When $\Delta_{\mathcal{V}} > 0$ the total value of the Loan increased, when $\Delta_{\mathcal{V}} < 0$ the total value decreased, and if $\Delta_{\mathcal{V}} = 0$ the value remained the same. + +The total management fee is calculated as follows: + +$$ +managementFeeTotal = \mathcal{I} \times \mathcal{m} +$$ + +We compute the management fee paid so far as follows: + +$$ +managementFeePaid = (\mathcal{I} - \mathcal{i}) \times \mathcal{m} +$$ + +$$ +managementFeeDue = managementFeeTotal - managementFeePaid +$$ + +Finally, we compute the change in management fee as follows: + +$$ +managementFeeChange = \mathcal{i'} \times \mathcal{m} - managementFeeDue +$$ + +The above calculation can be simplified to: + +$$ +managementFeeChange = \Delta_{\mathcal{V}} \times \mathcal{m} +$$ + +When the management fee change is negative, the Loan's value decreases, and thus, the Loan Broker's debt decreases. +Intuitively, a negative fee change suggests that the fee must be returned, increasing the loan broker's debt. + +In contrast, if the management fee change is positive, the Loan's value increases, and a further fee must be deducted from the debt. +Intuitively, a positive fee change suggests that an additional fee must be paid due to the increase in the interest paid. + +The LoanBroker debt is then updated as: + +$$ +LoanBroker.DebtTotal = LoanBroker.DebtTotal - managementFeeChange +$$ + +##### 3.2.5.2 Transaction Pseudo-code + +The following is the pseudo-code for handling a Loan payment transaction. + +``` +function make_payment(amount, current_time) -> (principal_paid, interest_paid, value_change, fee_paid): + if loan.payments_remaining is 0 || loan.principal_outstanding is 0 { + return "loan complete" error + } + + // the payment is late + if loan.next_payment_due_date < current_time { + let late_payment = loan.compute_late_payment(current_time) + if amount < late_payment { + return "insufficient amount paid" error + } + + loan.payments_remaining -= 1 + loan.principal_outstanding -= late_payment.principal + + loan.last_payment_date = loan.next_payment_due_date + loan.next_payment_due_date = loan.next_payment_due_date + loan.payment_interval + + let periodic_payment = loan.compute_periodic_payment() + + // A late payment increases the value of the loan by the difference between periodic and late payment interest + return (late_payment.principal, late_payment.interest, late_payment.interest - periodic_payment.interest, loan.late_payment_fee) + } + + let full_payment = loan.compute_full_payment(current_time) + + // if the payment is equal or higher than full payment amount + // and there is more than one payment remaining, make a full payment + if amount >= full_payment && loan.payments_remaining > 1 { + loan.payments_remaining = 0 + loan.principal_outstanding = 0 + + // A full payment decreases the value of the loan by the difference between the interest paid and the expected outstanding interest + return (full_payment.principal, full_payment.interest, full_payment.interest - loan.compute_current_value().interest, full_payment.fee) + } + + // if the payment is not late nor if it's a full payment, then it must be a periodic once + + let periodic_payment = loan.compute_periodic_payment() + + let full_periodic_payments = floor(amount / periodic_payment) + if full_periodic_payments < 1 { + return "insufficient amount paid" error + } + + loan.payments_remaining -= full_periodic_payments + loan.next_payment_due_date = loan.next_payment_due_date + loan.payment_interval * full_periodic_payments + loan.last_payment_date = loan.next_payment_due_date - loan.payment_interval + + + let total_principal_paid = 0 + let total_interest_paid = 0 + let loan_value_change = 0 + let total_fee_paid = loan.service_fee * full_periodic_payments + + while full_periodic_payments > 0 { + total_principal_paid += periodic_payment.principal + total_interest_paid += periodic_payment.interest + periodic_payment = loan.compute_periodic_payment() + full_periodic_payments -= 1 + } + + loan.principal_outstanding -= total_principal_paid + + let overpayment = min(loan.principal_outstanding, amount % periodic_payment) + if overpayment > 0 && is_set(lsfOverayment) { + let interest_portion = overpayment * loan.overpayment_interest_rate + let fee_portion = overpayment * loan.overpayment_fee + let remainder = overpayment - interest_portion - fee_portion + + total_principal_paid += remainder + total_interest_paid += interest_portion + total_fee_paid += fee_portion + + let current_value = loan.compute_current_value() + loan.principal_outstanding -= remainder + let new_value = loan.compute_current_value() + + loan_value_change = (new_value.interest - current_value.interest) + interest_portion + } + + return (total_principal_paid, total_interest_paid, loan_value_change, total_fee_paid) +``` + +##### 3.2.5.3 Failure Conditions + +- A `Loan` object with specified `LoanID` does not exist on the ledger. + +- The Loan has not started yet: `Loan.StartDate > CurrentTime`. + +- The submitter `AccountRoot.Account` is not equal to `Loan.Borrower`. + +- `Loan.PaymentsRemaining` or `Loan.PrincipalOutstanding` is `0`. + +- If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `IOU`: + + - The trustline between the submitter account and the `Issuer` of the asset is frozen. + - The `AccountRoot` object of the `Issuer` has the `lsfGlobalFreeze` flag set. + +- If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `MPT`: + + - The `MPToken` object for the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` of the submitter `AccountRoot`: + - Has `lsfMPTLocked` flag set. + - The `MPTokenIssuance` object of the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` has the `lsfMPTLocked` flag set. + +- If `CurrentTime > Loan.NextPaymentDueDate` and `Amount` < `LatePaymentAmount()` + +- If `CurrentTime <= Loan.NextPaymentDueDate` and `Amount` < `PeriodicPaymentAmount()` + +##### 3.2.5.4 State Changes + +Assume the payment is split into `principal`, `interest` and `fee`, and `totalDue = principal + interest + fee`. + +Assume the payment is handled by a function that implements the [Pseudo-Code](#3252-transaction-pseudo-code) that returns `principal_paid`, `interest_paid`, `value_change` and `fee_paid`, where: + +- `principal_paid` is the amount of principal that the payment covered. +- `interest_paid` is the amount of interest that the payment covered. +- `fee_paid` is the amount of fee that the payment covered. +- `value_change` is the amount by which the total value of the Loan changed. + - If `value_change` < `0`, Loan value decreased. + - If `value_change` > `0`, Loan value increased. + +Furthermore, assume `full_periodic_payments` variable represents the number of payment intervals that the payment covered. + +- `Loan` object state changes: + + - If `Loan(LoanID).Flags == lsfLoanImpaired`: + + - `Loan(LoanID).Flags = 0` + + - Decrease `Loan.PaymentsRemaining` by `full_periodic_payments`. + - Decrease `Loan.PrincipalOutstanding` by `principal_paid`. + + - If `Loan.PaymentsRemaining > 0` and `LoanPrincipalOutstanding > 0`: + + - Set the next payment date: `Loan.NextPaymentDueDate += Loan.PaymentInterval * full_periodic_payments`. + - Set the previous payment date: `Loan.PreviousPaymentDate = Loan.NextPaymentDueDate - Loan.PaymentInterval`. + +- `LoanBroker(Loan.LoanBrokerID)` object state changes: + + - Compute the management fee: + + - `feeManagement = interest_paid x LoanBroker.ManagementFeeRate` + + - If there is **not enough** first-loss capital: `LoanBroker.CoverAvailable < LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`: + + - Do not charge the management, add it to the total debt: + - `LoanBroker.DebtTotal += feeManagement` + + - If there is **enough** first-loss capital: `LoanBroker.CoverAvailable >= LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`: + + - Decrease the management fee from totalPaid amount: + - `totalPaid = totalPaid - feeManagement` + + - Decrease LoanBroker Debt by the amount paid: + + - `LoanBroker.DebtTotal -= totalPaid` + + - Update the LoanBroker Debt by the Loan value change: + + - `LoanBroker.DebtTotal += valueChange` + + - Update the LoanBroker Debt by the change in the management fee: + - `LoanBroker.DebtTotal -= (valueChange x LoanBroker.ManagementFeeRate)` + - If `LoanPaymentsRemaining == 0` and `LoanPrincipalOutstanding == 0`: + - Decrease active loans: + - `LoanBroker.OwnerCount = LoanBroker.OwnerCount - 1` + +- `Vault(LoanBroker(Loan.LoanBrokerID).VaultID)` state changes: + + - Increase available assets in the Vault by the amount paid: + + - `Vault.AssetsAvailable = Vault.AssetsAvailable + totalPaid` + + - Update the Vault total value by the change in the Loan total value: + + - `Vault.AssetsTotal = Vault.AssetsTotal + valueChange` + + - Update the Vault total value by the change in the management fee: + + - `Vault.AssetsTotal = Vault.AssetsTotal - (vaultChange x LoanBroker.managementFeeRate)` + + - If there is **not enough** first-loss capital: `LoanBroker.CoverAvailable < LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`: + - The management fee was not charged; decrease Vault TotalValue: + - `Loan.AssetsTotal = Loan.AssetsTotal + feeManagement` + +- If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is `XRP`: + + - Increase the `Balance` field of _pseudo-account_ `AccountRoot` by `principal_paid + (interest_paid - management_fee)`. + - If `LoanBroker.CoverAvailable >= LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`: + + - Increase the `Balance` field of the `LoanBroker.Owner` `AccountRoot` by `fee_paid + management_fee`. + + - Decrease the `Balance` field of the submitter `AccountRoot` by `principal_paid + interest_paid + fee_paid`. + +- If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `IOU`: + + - Increase the `RippleState` balance between the _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `principal_paid + (interest_paid - management_fee)`. + - If `LoanBroker.CoverAvailable >= LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum` : + + - Increase the `RippleState` balance between the `LoanBroker.Owner` `AccountRoot` and the `Issuer` `AccountRoot` by `fee_paid + management_fee`. + + - Decrease the `RippleState` balance between the submitter `AccountRoot` and the `Issuer` `AccountRoot` by `principal_paid + interest_paid + fee_paid`. + +- If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `MPT`: + + - Increase the `MPToken.MPTAmount` by `principal_paid + (interest_paid - management_fee)` of the _pseudo-account_ `MPToken` object for the `Vault.Asset`. + - If `LoanBroker.CoverAvailable >= LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`: + + - Increase the `MPToken.MPTAmount` by `fee_paid + management_fee` of the `LoanBroker.Owner` `MPToken` object for the `Vault.Asset`. + + - Decrease the `MPToken.MPTAmount` by `principal_paid + interest_paid + fee_paid` of the submitter `MPToken` object for the `Vault.Asset`. + +[**Return to Index**](#index) + +##### 3.2.5.4 Invariants + +**TBD** + +# Appendix From d3e61159c4bb94b29528c44f9b316ef301687604 Mon Sep 17 00:00:00 2001 From: Vito Date: Tue, 19 Nov 2024 16:10:29 +0000 Subject: [PATCH 02/77] improves the description of the management fee --- XLS-0066d-lending-protocol/README.md | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index 95bad938..0f037e13 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -124,7 +124,7 @@ There are three basic interest rates associated with a Loan: The lending protocol charges a number of fees that the Loan Broker can configure. The protocol will not charge the fees if the Loan Broker has not deposited enough First-Loss Capital. -- **`Management Fee`**: This is a percentage of interest charged by the Loan Broker. Intuitively, the Vault depositors pay this fee. +- **`Management Fee`**: This is a fee charged by the Loan Broker, calculated as a percentage of the interest earned on loans. It's deducted from the interest that would otherwise go to the Vault depositors. Essentially, borrowers pay the full interest, but before that interest reaches depositors, the Loan Broker takes their cut. - **`Loan Origination Fee`**: A nominal fee paid to the Loan Broker taken from the principal lent. - **`Loan Service Fee`**: A nominal fee paid on top of each loan payment. - **`Late Payment Fee`**: A nominal fee paid on top of a late payment. @@ -144,13 +144,6 @@ The lending protocol charges a number of fees that the Loan Broker can configure - **`Amortization`**: The gradual repayment of a loan through scheduled payments that cover both interest and principal over time. - **`Repayment Schedule`**: A detailed plan that outlines when and how much a borrower must pay to repay the Loan fLoan. - **`Grace Period`**: A set period after the Loan's due date after which the Loan Broker can default the Loan -- **`Origination Fee`**: A nominal one-time fee the loan broker charges for processing a new loan application. -- **`Service Fee`**: A recurring nominal charge the Borrower pays during Loan payment. -- **`Management Fee`**: A percentage fee charged by the Borrower on the loan interest before returning the interest to the Vault. -- **`Late Payment Fee`**: A penalty charged to the Borrower for failing to make a payment on or before its due date. -- **`Interest Rate`**: The percentage charged by the loan broker on the loan principal, representing the cost of borrowing. -- **`Late Interest Rate`**: A higher interest rate applied to overdue loan payments as a penalty for late repayment. -- **`Closing Interest Rate`**: The final interest rate charged when the Loan is closed or fully repaid. ### 1.6.2 Actors From b297e6ea0a5d16de070f32e68ca9cb144da480a5 Mon Sep 17 00:00:00 2001 From: Vito Date: Tue, 7 Jan 2025 17:41:20 +0000 Subject: [PATCH 03/77] renames field names from plural to singular --- XLS-0066d-lending-protocol/README.md | 98 ++++++++++++++-------------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index 0f037e13..b7111705 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -251,9 +251,9 @@ Example 1: # Issuing a Loan # ** Initial States ** -- Vault -- -AssetsTotal = 100,000 Tokens -AssetsAvailable = 100,000 Tokens -SharesTotal = 100,000 Shares +AssetTotal = 100,000 Tokens +AssetAvailable = 100,000 Tokens +ShareTotal = 100,000 Shares -- Lending Protocol -- DebtTotal = 0 @@ -274,16 +274,16 @@ LoanInterest = LoanPrincipal x LoanInterestRate -- Vault -- # Increase the potential value of the Vault -AssetsTotal = AssetsTotal + ((LoanInterest - (LoanInterest x ManagementFeeRate))) +AssetTotal = AssetTotal + ((LoanInterest - (LoanInterest x ManagementFeeRate))) = 100,000 + (100 - (100 x 0.1)) = 100,000 + 90 = 100,090 Tokens -# Decrease Assets Available in the Vault -AssetsAvailable = AssetsAvailable - LoanPrincipal +# Decrease Asset Available in the Vault +AssetAvailable = AssetAvailable - LoanPrincipal = 100,000 - 1,000 = 99,000 Tokens -SharesTotal = (UNCHANGED) +ShareTotal = (UNCHANGED) -- Lending Protocol -- # Increase Lending Protocol Debt @@ -299,9 +299,9 @@ Example 2: # Loan Payment # ** Initial States ** -- Vault -- -AssetsTotal = 100,090 Tokens -AssetsAvailable = 99,000 Tokens -SharesTotal = 100,000 Shares +AssetTotal = 100,090 Tokens +AssetAvailable = 99,000 Tokens +ShareTotal = 100,000 Shares -- Lending Protocol -- DebtTotal = 1,090 Tokens @@ -329,14 +329,14 @@ PaymentInterestPortion = 50 Tokens ** State Changes ** -- Vault -- -AssetsTotal = (UNCHANGED) +AssetTotal = (UNCHANGED) -# Increase Assets Available in the Vault -AssetsAvailable = AssetsAvailable + PaymentPrincipalPortion + (PaymentInterestPortion - (PaymentInterestPortion x ManagementFeeRate) +# Increase Asset Available in the Vault +AssetAvailable = AssetAvailable + PaymentPrincipalPortion + (PaymentInterestPortion - (PaymentInterestPortion x ManagementFeeRate) = 99,000 + 500 + (50 - (50 x 0.1)) = 99,545 Tokens -SharesTotal = (UNCHANGED) +ShareTotal = (UNCHANGED) -- Lending Protocol -- @@ -368,9 +368,9 @@ Example 1: Loan Default ** Initial States ** -- Vault -- -AssetsTotal = 100,090 Tokens -AssetsAvailable = 99,000 Tokens -SharesTotal = 100,000 Tokens +AssetTotal = 100,090 Tokens +AssetAvailable = 99,000 Tokens +ShareTotal = 100,000 Tokens -- Lending Protocol -- DebtTotal = 1,090 Tokens @@ -396,15 +396,15 @@ DefaultRemaining = DefaultAmount - DefaultCovered ** State Changes ** -- Vault -- -AssetsTotal = AssetsTotal - DefaultRemaining +AssetTotal = AssetTotal - DefaultRemaining = 100,090 - 1,079.1 = 99,010.9 Tokens -AssetsAvailable = AssetsAvailable + DefaultCovered +AssetAvailable = AssetAvailable + DefaultCovered = 99,000 + 10.9 = 99,010.9 Tokens -SharesTotal = (UNCHANGED) +ShareTotal = (UNCHANGED) -- Lending Protocol -- DebtTotal = DebtTotal - DefaultAmount @@ -459,8 +459,8 @@ The `LoanID` is calculated as follows: | `GracePeriod` | `No` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The number of seconds after the Payment Due Date that the Loan can be Defaulted. | | `PreviousPaymentDate` | `N/A` | :heavy_check_mark: | `number` | `UINT32` | `0` | The timestamp of when the previous payment was made in [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time). | | `NextPaymentDueDate` | `N/A` | :heavy_check_mark: | `number` | `UINT32` | `LoanSet.StartDate + LoanSet.PaymentInterval` | The timestamp of when the next payment is due in [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time). | -| `PaymentsRemaining` | `N/A` | :heavy_check_mark: | `number` | `UINT32` | `LoanSet.PaymentsTotal` | The number of payments remaining on the Loan. | -| `AssetsAvailable` | `N/A` | :heavy_check_mark: | `number` | `NUMBER` | `LoanSet.[PrincipalRequested - LoanOriginationFee]` | The asset amount that is available in the Loan. | +| `PaymentRemaining` | `N/A` | :heavy_check_mark: | `number` | `UINT32` | `LoanSet.PaymentTotal` | The number of payments remaining on the Loan. | +| `AssetAvailable` | `N/A` | :heavy_check_mark: | `number` | `NUMBER` | `LoanSet.[PrincipalRequested - LoanOriginationFee]` | The asset amount that is available in the Loan. | | `PrincipalOutstanding` | `No` | :heavy_check_mark: | `number` | `NUMBER` | `LoanSet.PrincipalRequested` | The principal amount requested by the Borrower. | ##### 2.2.2.1 Flags @@ -697,7 +697,7 @@ The transaction creates a new `Loan` object. | `CloseInterestRate` | | `number` | `UINT16` | 0 | A Fee Rate charged for repaying the Loan early in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | | `PrincipalRequested` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | The principal amount requested by the Borrower. | | `StartDate` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The timestamp of when the Loan starts [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time). | -| `PaymentsTotal` | | `number` | `UINT32` | 1 | The total number of payments to be made against the Loan. | +| `PaymentTotal` | | `number` | `UINT32` | 1 | The total number of payments to be made against the Loan. | | `PaymentInterval` | | `number` | `UINT32` | 60 | Number of seconds between Loan payments. | | `GracePeriod` | | `number` | `UINT32` | 60 | The number of seconds after the Loan's Payment Due Date can be Defaulted. | | `Lender` | :heavy_check_mark: | `object` | `STObject` | `N/A` | An inner object that contains the signature of the Lender over the transaction. | @@ -760,7 +760,7 @@ The `LoanSet` transaction is a mutual agreement between the `Borrower` and the ` - Insufficient assets in the Vault: - - `Vault(LoanBroker(LoanBrokerID).VaultID).AssetsAvailable` < `Loan.PrincipalRequested`. + - `Vault(LoanBroker(LoanBrokerID).VaultID).AssetAvailable` < `Loan.PrincipalRequested`. - Exceeds maximum Debt of the LoanBroker: @@ -781,12 +781,12 @@ The `LoanSet` transaction is a mutual agreement between the `Borrower` and the ` - `Vault(LoanBroker(LoanBrokerID).VaultID)` object state changes: - - Decrease Assets Available in the Vault: + - Decrease Asset Available in the Vault: - - `Vault.AssetsAvailable -= Loan.PrincipalRequested`. + - `Vault.AssetAvailable -= Loan.PrincipalRequested`. - Increase the Total Value of the Vault: - - `Vault.AssetsTotal += LoanInterest - (LoanInterest x LoanBroker.ManagementFeeRate)` where `LoanInterest` is the Loan's total interest. + - `Vault.AssetTotal += LoanInterest - (LoanInterest x LoanBroker.ManagementFeeRate)` where `LoanInterest` is the Loan's total interest. - `LoanBroker(LoanBrokerID)` object changes: @@ -816,7 +816,7 @@ The transaction deletes an existing `Loan` object. - A `Loan` object with the specified `LoanID` does not exist on the ledger. - The Account submitting the `LoanDelete` is not the `LoanBroker.Owner` or the `Loan.Borrower`. - The Loan is active: - - `Loan.PaymentsRemaining > 0` + - `Loan.PaymentRemaining > 0` ##### 3.2.2.2 State Changes @@ -851,7 +851,7 @@ The transaction deletes an existing `Loan` object. - If `Loan(LoanID).Flags == lsfLoanImpaired` AND `tfLoanImpair` flag is provided. -- `Loan.PaymentsRemaining == 0`. +- `Loan.PaymentRemaining == 0`. - The `tfDefault` flag is specified and: - `CurrentTime` < `Loan.NextPaymentDueDate + Loan.GracePeriod`. @@ -863,7 +863,7 @@ The transaction deletes an existing `Loan` object. - Calculate the amount of the Default that First-Loss Capital covers: - The default Amount equals the outstanding principal and interest, excluding any funds unclaimed by the Borrower. - - `DefaultAmount = (Loan.PrincipalOutstanding + Loan.InterestOutstanding) - Loan.AssetsAvailable`. + - `DefaultAmount = (Loan.PrincipalOutstanding + Loan.InterestOutstanding) - Loan.AssetAvailable`. - Apply the First-Loss Capital to the Default Amount - `DefaultCovered = min((LoanBroker(Loan.LoanBrokerID).DebtTotal x LoanBroker(Loan.LoanBrokerID).CoverRateMinimum) x LoanBroker(Loan.LoanBrokerID).CoverRateLiquidation, DefaultAmount)` - `DefaultAmount -= DefaultCovered` @@ -871,15 +871,15 @@ The transaction deletes an existing `Loan` object. - Update the `Vault` object: - Decrease the Total Value of the Vault: - - `Vault(LoanBroker(LoanBrokerID).VaultID).AssetsTotal -= DefaultAmount`. - - Increase the Assets Available of the Vault by liquidated First-Loss Capital and any unclaimed funds amount: - - `Vault(LoanBroker(LoanBrokerID).VaultID).AssetsAvailable += DefaultCovered + Loan.AssetsAvailable`. + - `Vault(LoanBroker(LoanBrokerID).VaultID).AssetTotal -= DefaultAmount`. + - Increase the Asset Available of the Vault by liquidated First-Loss Capital and any unclaimed funds amount: + - `Vault(LoanBroker(LoanBrokerID).VaultID).AssetAvailable += DefaultCovered + Loan.AssetAvailable`. - Update the `LoanBroker` object: - Decrease the Debt of the LoanBroker: - `LoanBroker(LoanBrokerID).DebtTotal -= ` - - `Loan.PrincipalOutstanding + Loan.InterestOutstanding + Loan.AssetsAvailable` + - `Loan.PrincipalOutstanding + Loan.InterestOutstanding + Loan.AssetAvailable` - Decrease the First-Loss Capital Cover Available: - `LoanBroker(LoanBrokerID).CoverAvailable -= DefaultCovered` - Decrease the number of active Loans: @@ -888,8 +888,8 @@ The transaction deletes an existing `Loan` object. - Update the `Loan` object: - `Loan(LoanID).Flags = lsfLoanDefault` - - `Loan(LoanID).PaymentsRemaining = 0` - - `Loan(LoanID).AssetsAvailable = 0` + - `Loan(LoanID).PaymentRemaining = 0` + - `Loan(LoanID).AssetAvailable = 0` - `Loan(LoanID).PrincipalOutstanding = 0` - If `tfLoanImpair` flag is specified: @@ -942,7 +942,7 @@ The Borrower submits a `LoanDraw` transaction to draw funds from the Loan. - `Loan.StartDate > CurrentTime`. - There are insufficient assets: - - `Loan.AssetsAvailable` < `Amount`. + - `Loan.AssetAvailable` < `Amount`. - The `Loan` has `lsfLoanImpaired` or `lsfLoanDefault` flags set. @@ -977,7 +977,7 @@ The Borrower submits a `LoanDraw` transaction to draw funds from the Loan. - Decrease the `MPToken.MPTAmount` by `Amount` of the _pseudo-account_ `MPToken` object for the `Vault.Asset`. - Increase the `MPToken.MPTAmount` by `Amount` of the submitter `MPToken` object for the `Vault.Asset`. -- Decrease `Loan.AssetsAvailable` by `Amount`. +- Decrease `Loan.AssetAvailable` by `Amount`. ##### 3.2.4.3 Invariants @@ -1036,7 +1036,7 @@ totalDue = periodicPayment + loanServiceFee $$ $$ -periodicPayment = principalOutstanding \times \frac{periodicRate \times (1 + periodicRate)^{PaymentsRemaining}}{(1 + periodicRate)^{PaymentsRemaining} - 1} +periodicPayment = principalOutstanding \times \frac{periodicRate \times (1 + periodicRate)^{PaymentRemaining}}{(1 + periodicRate)^{PaymentRemaining} - 1} $$ where the periodic interest rate is the interest rate charged per payment period: @@ -1069,7 +1069,7 @@ $$ latePaymentInterest = principalOutstanding \times \frac{lateInterestRate \times secondsSinceLastPayment}{365 \times 24 \times 60 \times 60} $$ -A late payment pays more interest than calculated when increasing the Vault value in the `LoanSet` transaction. Therefore, the total Vault value captured by `Vault.AssetsTotal` must be recalculate +A late payment pays more interest than calculated when increasing the Vault value in the `LoanSet` transaction. Therefore, the total Vault value captured by `Vault.AssetTotal` must be recalculate d. Assume the function `PeriodicPayment()` returns the expected periodic payment, split into `principalPeriodic` and `interestPeriodic`. Furthermore, assume the function `LatePayment()` that implements the Late Payment formula. The function returns the late payment split into `principalLate` and `interestLate`, where `interestLate` is calculated using the formula above. Note that `principalPeriodic == principalLate` and `interestLate > interestPeriodic` are used only when the payment is late. Otherwise, `interestLate == interestPeriodic`. @@ -1130,7 +1130,7 @@ $$ prepaymentPenalty = principalOutstanding \times closeInterestRate $$ -An early payment pays less interest than calculated when increasing the Vault value in the `LoanSet` transaction. Therefore, the Vault value (captured by `Vault.AssetsTotal`) must be recalculated after an early payment. +An early payment pays less interest than calculated when increasing the Vault value in the `LoanSet` transaction. Therefore, the Vault value (captured by `Vault.AssetTotal`) must be recalculated after an early payment. Assume a function `CurrentValue()` that returns `principalOutstanding` and `interestOutstanding` of the Loan. Furthermore, assume a function `ClosePayment()` that implements the Full Payment calculation. The function returns the total full payment due split into `principal` and `interest`. @@ -1298,7 +1298,7 @@ function make_payment(amount, current_time) -> (principal_paid, interest_paid, v - The submitter `AccountRoot.Account` is not equal to `Loan.Borrower`. -- `Loan.PaymentsRemaining` or `Loan.PrincipalOutstanding` is `0`. +- `Loan.PaymentRemaining` or `Loan.PrincipalOutstanding` is `0`. - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `IOU`: @@ -1336,10 +1336,10 @@ Furthermore, assume `full_periodic_payments` variable represents the number of p - `Loan(LoanID).Flags = 0` - - Decrease `Loan.PaymentsRemaining` by `full_periodic_payments`. + - Decrease `Loan.PaymentRemaining` by `full_periodic_payments`. - Decrease `Loan.PrincipalOutstanding` by `principal_paid`. - - If `Loan.PaymentsRemaining > 0` and `LoanPrincipalOutstanding > 0`: + - If `Loan.PaymentRemaining > 0` and `LoanPrincipalOutstanding > 0`: - Set the next payment date: `Loan.NextPaymentDueDate += Loan.PaymentInterval * full_periodic_payments`. - Set the previous payment date: `Loan.PreviousPaymentDate = Loan.NextPaymentDueDate - Loan.PaymentInterval`. @@ -1370,7 +1370,7 @@ Furthermore, assume `full_periodic_payments` variable represents the number of p - Update the LoanBroker Debt by the change in the management fee: - `LoanBroker.DebtTotal -= (valueChange x LoanBroker.ManagementFeeRate)` - - If `LoanPaymentsRemaining == 0` and `LoanPrincipalOutstanding == 0`: + - If `LoanPaymentRemaining == 0` and `LoanPrincipalOutstanding == 0`: - Decrease active loans: - `LoanBroker.OwnerCount = LoanBroker.OwnerCount - 1` @@ -1378,19 +1378,19 @@ Furthermore, assume `full_periodic_payments` variable represents the number of p - Increase available assets in the Vault by the amount paid: - - `Vault.AssetsAvailable = Vault.AssetsAvailable + totalPaid` + - `Vault.AssetAvailable = Vault.AssetAvailable + totalPaid` - Update the Vault total value by the change in the Loan total value: - - `Vault.AssetsTotal = Vault.AssetsTotal + valueChange` + - `Vault.AssetTotal = Vault.AssetTotal + valueChange` - Update the Vault total value by the change in the management fee: - - `Vault.AssetsTotal = Vault.AssetsTotal - (vaultChange x LoanBroker.managementFeeRate)` + - `Vault.AssetTotal = Vault.AssetTotal - (vaultChange x LoanBroker.managementFeeRate)` - If there is **not enough** first-loss capital: `LoanBroker.CoverAvailable < LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`: - The management fee was not charged; decrease Vault TotalValue: - - `Loan.AssetsTotal = Loan.AssetsTotal + feeManagement` + - `Loan.AssetTotal = Loan.AssetTotal + feeManagement` - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is `XRP`: From aac7f3ece9b8dbc2ce8ca214b97b4fc855d41b0a Mon Sep 17 00:00:00 2001 From: Vito Date: Mon, 27 Jan 2025 16:40:28 +0000 Subject: [PATCH 04/77] This commit introduces the following changes: - Adds VaultNode to LoanBroker object to track in which owner directory of the Vaults pseudo-account the LoanBroker object is referenced. - Adds LoanBrokerNode to Loan object to track in which owner directory of the LoanBroker object the Loan is references. - Replaces CurrentTime to LastClosedLedger.CloseTime. - Changes the LoanBroker.Delete transaction to automatically return any outstanding Cover to the LoanBroker.Owner. - Adds a balance check to the LoanBrokerCoverDeposit transaction when depositing XRP. - Adds a check to LoanBrokerCoverWithdraw to ensure the CoverAvailable does not drop below Mimimum Cover Required. --- XLS-0066d-lending-protocol/README.md | 55 +++++++++++++++++++--------- 1 file changed, 38 insertions(+), 17 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index b7111705..87be5c2f 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -209,6 +209,7 @@ The `LoanBroker` object has the following fields: | `PreviousTxnLgrSeq` | `N/A` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The sequence of the ledger containing the transaction that last modified this object. | | `Sequence` | `N/A` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The transaction sequence number that created the `LoanBroker`. | | `OwnerNode` | `N/A` | :heavy_check_mark: | `number` | `UINT64` | `N/A` | Identifies the page where this item is referenced in the owner's directory. | +| `VaultNode` | `N/A` | :heavy_check_mark: | `number` | `UINT64` | `N/A` | Identifies the page where this item is referenced in the Vault's _pseudo-account_ owner's directory. | | `VaultID` | `No` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the `Vault` object associated with this Lending Protocol Instance. | | `Owner` | `No` | :heavy_check_mark: | `string` | `AccountID` | `N/A` | The address of the account that is the Loan Broker. | | `Data` | `Yes` | | `string` | `BLOB` | None | Arbitrary metadata about the `LoanBroker`. Limited to 256 bytes. | @@ -226,7 +227,7 @@ The Lending Protocol uses the `_pseudo-account_` of the associated `Vault` objec #### 2.1.4 Ownership -The lending protocol object is stored in the ledger and tracked in an [Owner Directory](https://xrpl.org/docs/references/protocol/ledger-data/ledger-entry-types/directorynode) owned by the account submitting the `LoanBrokerSet` transaction. Furthermore, the object is also tracked in the `OwnerDirectory` of the _`pseudo-account`_. +The lending protocol object is stored in the ledger and tracked in an [Owner Directory](https://xrpl.org/docs/references/protocol/ledger-data/ledger-entry-types/directorynode) owned by the account submitting the `LoanBrokerSet` transaction. Furthermore, the object is also tracked in the `OwnerDirectory` of the _`pseudo-account`_. The `_pseudo_account_` `OwnerDirectory` page is captured by the `VaultNode` field. The `LoanBroker` requires tracking associated `Loan` objects to prevent the `LoanBroker` object from being deleted while loans are active as well as future RPC endpoints. Therefore, the `LoanBroker` has an associated [Owner Directory](https://xrpl.org/docs/references/protocol/ledger-data/ledger-entry-types/directorynode) object. @@ -443,6 +444,7 @@ The `LoanID` is calculated as follows: | `PreviousTxnLgrSeq` | `N/A` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The ledger sequence containing the transaction that last modified this object. | | `Sequence` | `N/A` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The transaction sequence number that created the loan. | | `OwnerNode` | `N/A` | :heavy_check_mark: | `number` | `UINT64` | `N/A` | Identifies the page where this item is referenced in the owner's directory. | +| `LoanBrokerNode` | `N/A` | :heavy_check_mark: | `number` | `UINT64` | `N/A` | Identifies the page where this item is referenced in the `LoanBroker`s owner directory. | | `LoanBrokerID` | `No` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the `LoanBroker` associated with this Loan Instance. | | `Borrower` | `No` | :heavy_check_mark: | `string` | `AccountID` | `N/A` | The address of the account that is the borrower. | | `LoanOriginationFee` | `No` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | A nominal funds amount paid to the `LoanBroker.Owner` when the Loan is created. | @@ -459,8 +461,8 @@ The `LoanID` is calculated as follows: | `GracePeriod` | `No` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The number of seconds after the Payment Due Date that the Loan can be Defaulted. | | `PreviousPaymentDate` | `N/A` | :heavy_check_mark: | `number` | `UINT32` | `0` | The timestamp of when the previous payment was made in [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time). | | `NextPaymentDueDate` | `N/A` | :heavy_check_mark: | `number` | `UINT32` | `LoanSet.StartDate + LoanSet.PaymentInterval` | The timestamp of when the next payment is due in [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time). | -| `PaymentRemaining` | `N/A` | :heavy_check_mark: | `number` | `UINT32` | `LoanSet.PaymentTotal` | The number of payments remaining on the Loan. | -| `AssetAvailable` | `N/A` | :heavy_check_mark: | `number` | `NUMBER` | `LoanSet.[PrincipalRequested - LoanOriginationFee]` | The asset amount that is available in the Loan. | +| `PaymentRemaining` | `N/A` | :heavy_check_mark: | `number` | `UINT32` | `LoanSet.PaymentTotal` | The number of payments remaining on the Loan. | +| `AssetAvailable` | `N/A` | :heavy_check_mark: | `number` | `NUMBER` | `LoanSet.[PrincipalRequested - LoanOriginationFee]` | The asset amount that is available in the Loan. | | `PrincipalOutstanding` | `No` | :heavy_check_mark: | `number` | `NUMBER` | `LoanSet.PrincipalRequested` | The principal amount requested by the Borrower. | ##### 2.2.2.1 Flags @@ -471,13 +473,13 @@ The `Loan` object supports the following flags: | -------------------- | :--------: | :---------: | :----------------------------------------------------: | | `lsfLoanDefault` | `0x0001` | `No` | If set, indicates that the Loan is defaulted. | | `lsfLoanImpaired` | `0x0002` | `Yes` | If set, indicates that the Loan is impaired. | -| `lsfLoanOverpayment` | `0x0003` | `No` | If set, indicates that the Loan supports overpayments. | +| `lsfLoanOverpayment` | `0x0004` | `No` | If set, indicates that the Loan supports overpayments. | #### 2.2.3 Ownership The `Loan` objects are stored in the ledger and tracked in an [Owner Directory](https://xrpl.org/docs/references/protocol/ledger-data/ledger-entry-types/directorynode) owned by the `Borrower`. -Furthermore, to facilitate the `Loan` object lookup from the `LoanBroker`, the object is also tracked in the `OwnerDirectory` associated with the `LoanBroker` object. +Furthermore, to facilitate the `Loan` object lookup from the `LoanBroker`, the object is also tracked in the `OwnerDirectory` associated with the `LoanBroker` object. The `OwnerDirectory` page of the `LoanBroker` is captured by the `LoanBrokerNode` field. #### 2.2.4 Reserves @@ -563,6 +565,22 @@ The transaction creates a new `LoanBroker` object or updates an existing one. - Delete `LoanBrokerID` from the `OwnerDirectory` of the Vault's `_pseudo-account_`. - Delete the `OwnerDirectory` associated with the `LoanBroker` object. +- If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is `XRP`: + + - Decrease the `Balance` field of _pseudo-account_ `AccountRoot` by `CoverAvailable`. + - Increase the `Balance` field of the submitter `AccountRoot` by `CoverAvailable`. + +- If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `IOU`: + + - Decrease the `RippleState` balance between the _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `CoverAvailable`. + - Increase the `RippleState` balance between the submitter `AccountRoot` and the `Issuer` `AccountRoot` by `CoverAvailable`. + +- If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `MPT`: + + - Decrease the `MPToken.MPTAmount` by `CoverAvailable` of the _pseudo-account_ `MPToken` object for the `Vault.Asset`. + - Increase the `MPToken.MPTAmount` by `CoverAvailable` of the submitter `MPToken` object for the `Vault.Asset`. + + ##### 3.1.2.3 Invariants **TBD** @@ -571,7 +589,7 @@ The transaction creates a new `LoanBroker` object or updates an existing one. #### 3.1.3 `LoanBrokerCoverDeposit` -The transaction creates a new `LoanBroker` object or updates an existing one. +The transaction deposits First Loss Capital into the `LoanBroker` object. | Field Name | Required? | JSON Type | Internal Type | Default Value | Description | | ----------------- | :----------------: | :-------: | :-----------: | :-----------: | :------------------------------------------------ | @@ -584,6 +602,9 @@ The transaction creates a new `LoanBroker` object or updates an existing one. - `LoanBroker` object with the specified `LoanBrokerID` does not exist on the ledger. - The submitter `AccountRoot.Account != LoanBroker(LoanBrokerID).Owner`. +- If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is `XRP`: + - `AccountRoot(LoanBroker.Owner).Balance < Amount` (LoanBroker does not have sufficient funds to deposit the First Loss Capital). + - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `IOU`: - The `RippleState` object between the submitter account and the `Issuer` of the asset has the `lsfLowFreeze` or `lsfHighFreeze` flag set. @@ -648,10 +669,10 @@ The `LoanBrokerCoverWithdraw` transaction withdraws the First-Loss Capital from - Has `lsfMPTLocked` flag set. - The `MPTokenIssuance` object of the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` has the `lsfMPTLocked` flag set. -- The submitter is attempting to withdraw an Asset that does not match the Asset of the Vault. - - The `LoanBroker.CoverAvailable` < `Amount`. +- `LoanBroker.CoverAvailable - Amount` < `LoanBroker.DebtTotal * LoanBroker.CoverRateMinimum` + ##### 3.2.2.2 State Changes - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is `XRP`: @@ -697,7 +718,7 @@ The transaction creates a new `Loan` object. | `CloseInterestRate` | | `number` | `UINT16` | 0 | A Fee Rate charged for repaying the Loan early in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | | `PrincipalRequested` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | The principal amount requested by the Borrower. | | `StartDate` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The timestamp of when the Loan starts [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time). | -| `PaymentTotal` | | `number` | `UINT32` | 1 | The total number of payments to be made against the Loan. | +| `PaymentTotal` | | `number` | `UINT32` | 1 | The total number of payments to be made against the Loan. | | `PaymentInterval` | | `number` | `UINT32` | 60 | Number of seconds between Loan payments. | | `GracePeriod` | | `number` | `UINT32` | 60 | The number of seconds after the Loan's Payment Due Date can be Defaulted. | | `Lender` | :heavy_check_mark: | `object` | `STObject` | `N/A` | An inner object that contains the signature of the Lender over the transaction. | @@ -756,7 +777,7 @@ The `LoanSet` transaction is a mutual agreement between the `Borrower` and the ` - `PaymentInterval` is less than `60` seconds. - `GracePeriod` is greater than the `PaymentInterval`. -- `Loan.StartDate < CurrentTime`. +- `Loan.StartDate < LastClosedLedger.CloseTime`. - Insufficient assets in the Vault: @@ -841,7 +862,7 @@ The transaction deletes an existing `Loan` object. | ---------------- | :--------: | :--------------------------------------------- | | `tfLoanDefault` | `0x0001` | Indicates that the Loan should be defaulted. | | `tfLoanImpair` | `0x0002` | Indicates that the Loan should be impaired. | -| `tfLoanUnimpair` | `0x0003` | Indicates that the Loan should be un-impaired. | +| `tfLoanUnimpair` | `0x0004` | Indicates that the Loan should be un-impaired. | ##### 3.2.3.1 Failure Conditions @@ -854,7 +875,7 @@ The transaction deletes an existing `Loan` object. - `Loan.PaymentRemaining == 0`. - The `tfDefault` flag is specified and: - - `CurrentTime` < `Loan.NextPaymentDueDate + Loan.GracePeriod`. + - `LastClosedLedger.CloseTime` < `Loan.NextPaymentDueDate + Loan.GracePeriod`. ##### 3.2.3.2 State Changes @@ -939,7 +960,7 @@ The Borrower submits a `LoanDraw` transaction to draw funds from the Loan. - A `Loan` object with the specified `LoanID` does not exist on the ledger. - The `AccountRoot.Account` of the submitter is not `Loan.Borrower`. - The Loan has not started: - - `Loan.StartDate > CurrentTime`. + - `Loan.StartDate > LastClosedLedger.CloseTime`. - There are insufficient assets: - `Loan.AssetAvailable` < `Amount`. @@ -958,7 +979,7 @@ The Borrower submits a `LoanDraw` transaction to draw funds from the Loan. - The `MPTokenIssuance` object of the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` has the `lsfMPTLocked` flag set. - The `Borrower` missed a payment: - - `CurrentTime > Loan.NextPaymentDueDate`. + - `LastClosedLedger.CloseTime > Loan.NextPaymentDueDate`. ##### 3.2.4.2 State Changes @@ -1294,7 +1315,7 @@ function make_payment(amount, current_time) -> (principal_paid, interest_paid, v - A `Loan` object with specified `LoanID` does not exist on the ledger. -- The Loan has not started yet: `Loan.StartDate > CurrentTime`. +- The Loan has not started yet: `Loan.StartDate > LastClosedLedger.CloseTime`. - The submitter `AccountRoot.Account` is not equal to `Loan.Borrower`. @@ -1311,9 +1332,9 @@ function make_payment(amount, current_time) -> (principal_paid, interest_paid, v - Has `lsfMPTLocked` flag set. - The `MPTokenIssuance` object of the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` has the `lsfMPTLocked` flag set. -- If `CurrentTime > Loan.NextPaymentDueDate` and `Amount` < `LatePaymentAmount()` +- If `LastClosedLedger.CloseTime > Loan.NextPaymentDueDate` and `Amount` < `LatePaymentAmount()` -- If `CurrentTime <= Loan.NextPaymentDueDate` and `Amount` < `PeriodicPaymentAmount()` +- If `LastClosedLedger.CloseTime <= Loan.NextPaymentDueDate` and `Amount` < `PeriodicPaymentAmount()` ##### 3.2.5.4 State Changes From f18e8f13379bff0f93460de982ebae73ade57937 Mon Sep 17 00:00:00 2001 From: Vito Date: Mon, 27 Jan 2025 16:46:54 +0000 Subject: [PATCH 05/77] removes CoverAvailable check from LoanBrokerDelete transaction --- XLS-0066d-lending-protocol/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index 87be5c2f..da2e6e3d 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -557,7 +557,6 @@ The transaction creates a new `LoanBroker` object or updates an existing one. - The submitter `AccountRoot.Account != LoanBroker(LoanBrokerID).Owner`. - The `OwnerCount` field is greater than zero. -- `CoverAvailable` is greater than zero. ##### 3.1.2.2 State Changes From 44e1b579323256e44c9f2a7dec91fc0a93414b68 Mon Sep 17 00:00:00 2001 From: Vito Tumas <5780819+Tapanito@users.noreply.github.com> Date: Mon, 27 Jan 2025 16:52:18 +0000 Subject: [PATCH 06/77] Apply suggestions from code review Co-authored-by: Ed Hennis --- XLS-0066d-lending-protocol/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index da2e6e3d..8b467871 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -740,9 +740,9 @@ An inner object that contains the signature of the Lender over the transaction. The final transaction must include `Signature` or `Signers`. -If the `Signers` field is necessary, then the total fee for the transaction will be increased due to the extra signatures that need to be processed, similar to the additional fees for multisigning. The minimum fee will be $(|signatures| + 1) \times base\_fee$ +If the `Signers` field is necessary, then the total fee for the transaction will be increased due to the extra signatures that need to be processed, similar to the additional fees for multisigning. The minimum fee will be $(|signatures| + 1) \times base_fee$ -The total fee calculation for signatures will now be $(1 + |tx.Signers| + |tx.Lender.Signers|) \times base\_fee$. +The total fee calculation for signatures will now be $(1 + |tx.Signers| + |tx.Lender.Signers|) \times base_fee$. This field is not a signing field (it will not be included in transaction signatures, though the `Signature` or `Signers` field will be included in the stored transaction). @@ -1012,7 +1012,7 @@ The Borrower submits a `LoanPay` transaction to make a Payment on the Loan. | Field Name | Required? | JSON Type | Internal Type | Default Value | Description | | ----------------- | :----------------: | :-------: | :-----------: | :-----------: | :------------------------------------------ | | `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | The transaction type. | -| `LoanID` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the Loan object to be drawn from. | +| `LoanID` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the Loan object to be paid to. | | `Amount` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | The amount of funds to pay. | ##### 3.2.5.1 Payment Types @@ -1177,7 +1177,7 @@ For the calculation, assume the following variables: Assume $f(\mathcal{v})$ is a Loan payment, $f(\mathcal{v}) = \mathcal{v'}$, the new outstanding loan value is equal to the application of the payment transaction to the current outstanding value. Furthermore, assume $\mathcal{V} \xrightarrow{f(\mathcal{v})} \mathcal{V'}$, is the change in the Loan total value as the result of applying $f(\mathcal{v})$. -we say that $\mathcal{V'} = \mathcal{P'} + \mathcal{I'}$. It's important to note that a payment transaction must never change the total principal. I.e. $\mathcal{P} = \mathcal{P'}$, the change in total value is caused by the change in total principal only. +we say that $\mathcal{V'} = \mathcal{P'} + \mathcal{I'}$. It's important to note that a payment transaction must never change the total principal. I.e. $\mathcal{P} = \mathcal{P'}$, the change in total value is caused by the change in total interest only. $\Delta_{\mathcal{V}} = \mathcal{I'} - \mathcal{I}$ is the total value change of the Loan. When $\Delta_{\mathcal{V}} > 0$ the total value of the Loan increased, when $\Delta_{\mathcal{V}} < 0$ the total value decreased, and if $\Delta_{\mathcal{V}} = 0$ the value remained the same. From 1a437ee6421e63659ad3191e8ae7a23626a56d78 Mon Sep 17 00:00:00 2001 From: Vito Date: Mon, 27 Jan 2025 16:53:05 +0000 Subject: [PATCH 07/77] cleans up latex code --- XLS-0066d-lending-protocol/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index 8b467871..f35be922 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -1175,7 +1175,7 @@ For the calculation, assume the following variables: - Let $\mathcal{V}$ and $\mathcal{v}$ represent the total and outstanding value of the Loan. $\mathcal{V} = \mathcal{P} + \mathcal{I}$ and $\mathcal{v} = \mathcal{p} + \mathcal{i}$. - Finally, let $\mathcal{m}$ represent the management fee rate of the Loan Broker. -Assume $f(\mathcal{v})$ is a Loan payment, $f(\mathcal{v}) = \mathcal{v'}$, the new outstanding loan value is equal to the application of the payment transaction to the current outstanding value. Furthermore, assume $\mathcal{V} \xrightarrow{f(\mathcal{v})} \mathcal{V'}$, is the change in the Loan total value as the result of applying $f(\mathcal{v})$. +Assume $f(\mathcal{v})$ is a Loan payment, $f(\mathcal{v}) = \mathcal{v'}$, the new outstanding loan value is equal to the application of the payment transaction to the current outstanding value. Furthermore, assume $\mathcal{V} \xrightarrow{f(\mathcal{v})}$ $\mathcal{V'}$, is the change in the Loan total value as the result of applying $f(\mathcal{v})$. we say that $\mathcal{V'} = \mathcal{P'} + \mathcal{I'}$. It's important to note that a payment transaction must never change the total principal. I.e. $\mathcal{P} = \mathcal{P'}$, the change in total value is caused by the change in total interest only. From 9f47ea2c4f30c29de098d0fb51cacf30195bc49e Mon Sep 17 00:00:00 2001 From: Vito Date: Tue, 28 Jan 2025 11:45:04 +0000 Subject: [PATCH 08/77] updates LoanDelete transaction to delete the LoanBroker DirectoryNode when the last Loan is delete --- XLS-0066d-lending-protocol/README.md | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index f35be922..5ae9b2c0 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -443,7 +443,7 @@ The `LoanID` is calculated as follows: | `PreviousTxnID` | `N/A` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the transaction that last modified this object. | | `PreviousTxnLgrSeq` | `N/A` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The ledger sequence containing the transaction that last modified this object. | | `Sequence` | `N/A` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The transaction sequence number that created the loan. | -| `OwnerNode` | `N/A` | :heavy_check_mark: | `number` | `UINT64` | `N/A` | Identifies the page where this item is referenced in the owner's directory. | +| `OwnerNode` | `N/A` | :heavy_check_mark: | `number` | `UINT64` | `N/A` | Identifies the page where this item is referenced in the `Borrower` owner's directory. | | `LoanBrokerNode` | `N/A` | :heavy_check_mark: | `number` | `UINT64` | `N/A` | Identifies the page where this item is referenced in the `LoanBroker`s owner directory. | | `LoanBrokerID` | `No` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the `LoanBroker` associated with this Loan Instance. | | `Borrower` | `No` | :heavy_check_mark: | `string` | `AccountID` | `N/A` | The address of the account that is the borrower. | @@ -477,9 +477,11 @@ The `Loan` object supports the following flags: #### 2.2.3 Ownership -The `Loan` objects are stored in the ledger and tracked in an [Owner Directory](https://xrpl.org/docs/references/protocol/ledger-data/ledger-entry-types/directorynode) owned by the `Borrower`. +The `Loan` objects are stored in the ledger and tracked in two [Owner Directories](https://xrpl.org/docs/references/protocol/ledger-data/ledger-entry-types/directorynode). + +- The `OwnerNode` is the `Owner Directory` of the `Borrower` who is the main `Owner` of the `Loan` object, and therefore is responsible for the owner reserve. +- The `LoanBroker` `_pseudo-account_` `Owner Directory` to track all loans associated with the same `LoanBroker` object. -Furthermore, to facilitate the `Loan` object lookup from the `LoanBroker`, the object is also tracked in the `OwnerDirectory` associated with the `LoanBroker` object. The `OwnerDirectory` page of the `LoanBroker` is captured by the `LoanBrokerNode` field. #### 2.2.4 Reserves @@ -562,7 +564,6 @@ The transaction creates a new `LoanBroker` object or updates an existing one. - Delete `LoanBrokerID` from the `OwnerDirectory` of the submitting account. - Delete `LoanBrokerID` from the `OwnerDirectory` of the Vault's `_pseudo-account_`. -- Delete the `OwnerDirectory` associated with the `LoanBroker` object. - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is `XRP`: @@ -579,10 +580,9 @@ The transaction creates a new `LoanBroker` object or updates an existing one. - Decrease the `MPToken.MPTAmount` by `CoverAvailable` of the _pseudo-account_ `MPToken` object for the `Vault.Asset`. - Increase the `MPToken.MPTAmount` by `CoverAvailable` of the submitter `MPToken` object for the `Vault.Asset`. - ##### 3.1.2.3 Invariants -**TBD** +- If `LoanBroker.OwnerCount = 0` the `DirectoryNode` for the `LoanBroker` does not exist [**Return to Index**](#index) @@ -602,6 +602,7 @@ The transaction deposits First Loss Capital into the `LoanBroker` object. - The submitter `AccountRoot.Account != LoanBroker(LoanBrokerID).Owner`. - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is `XRP`: + - `AccountRoot(LoanBroker.Owner).Balance < Amount` (LoanBroker does not have sufficient funds to deposit the First Loss Capital). - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `IOU`: @@ -844,6 +845,13 @@ The transaction deletes an existing `Loan` object. - `LoanBroker.OwnerCount -= 1` - Delete the `Loan` object. - Release reserve funds back to the Borrower. +- Remove `LoanID` from the `DirectoryNode.Indexes`. +- If `LoanBroker.OwnerCount = 0` + - Delete the `LoanBroker` `DirectoryNode`. + +##### 3.2.2.3 Invariants + +- If `Loan.PaymentRemaining = 0` then `Loan.PrincipalOutstanding = 0` [**Return to Index**](#index) @@ -1009,11 +1017,11 @@ The Borrower submits a `LoanDraw` transaction to draw funds from the Loan. The Borrower submits a `LoanPay` transaction to make a Payment on the Loan. -| Field Name | Required? | JSON Type | Internal Type | Default Value | Description | -| ----------------- | :----------------: | :-------: | :-----------: | :-----------: | :------------------------------------------ | -| `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | The transaction type. | +| Field Name | Required? | JSON Type | Internal Type | Default Value | Description | +| ----------------- | :----------------: | :-------: | :-----------: | :-----------: | :--------------------------------------- | +| `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | The transaction type. | | `LoanID` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the Loan object to be paid to. | -| `Amount` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | The amount of funds to pay. | +| `Amount` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | The amount of funds to pay. | ##### 3.2.5.1 Payment Types From 60a311ede00c5c428bed3f0642a1af083ba82914 Mon Sep 17 00:00:00 2001 From: Vito Date: Tue, 28 Jan 2025 14:42:25 +0000 Subject: [PATCH 09/77] in case insufficient first loss capital add loan broker fees to cover for the missing flc --- XLS-0066d-lending-protocol/README.md | 52 +++++++++++++++------------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index 5ae9b2c0..fc46d626 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -359,7 +359,7 @@ The First-Loss Capital is an optional mechanism to protect the Vault depositors Whenever the available cover falls below the minimum cover required, two consequences occur: - The Lender cannot issue new Loans. -- The Lender cannot receive fees. Borrower fees are ignored (i.e. the Borrower does not have to pay a Loan payment fee), and the Management Fee is instead deposited into a Vault. +- The Lender cannot directly receive fees. The fees are instead added to the First Loss Capital to cover the deficit. **Examples** @@ -482,7 +482,6 @@ The `Loan` objects are stored in the ledger and tracked in two [Owner Directorie - The `OwnerNode` is the `Owner Directory` of the `Borrower` who is the main `Owner` of the `Loan` object, and therefore is responsible for the owner reserve. - The `LoanBroker` `_pseudo-account_` `Owner Directory` to track all loans associated with the same `LoanBroker` object. - #### 2.2.4 Reserves The `Loan` object costs one owner reserve for the `Borrower`. @@ -582,7 +581,7 @@ The transaction creates a new `LoanBroker` object or updates an existing one. ##### 3.1.2.3 Invariants -- If `LoanBroker.OwnerCount = 0` the `DirectoryNode` for the `LoanBroker` does not exist +- If `LoanBroker.OwnerCount = 0` the `DirectoryNode` for the `LoanBroker` does not exist [**Return to Index**](#index) @@ -1378,29 +1377,27 @@ Furthermore, assume `full_periodic_payments` variable represents the number of p - `feeManagement = interest_paid x LoanBroker.ManagementFeeRate` - - If there is **not enough** first-loss capital: `LoanBroker.CoverAvailable < LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`: + - Decrease the management fee from totalPaid amount: - - Do not charge the management, add it to the total debt: - - `LoanBroker.DebtTotal += feeManagement` + - `totalPaid = totalPaid - feeManagement` + + - If there is **not enough** first-loss capital: `LoanBroker.CoverAvailable < LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`: - - If there is **enough** first-loss capital: `LoanBroker.CoverAvailable >= LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`: + - Add the fee to to First Loss Cover Pool: - - Decrease the management fee from totalPaid amount: - - `totalPaid = totalPaid - feeManagement` + - `LoanBroker.CoverAvailable = LoanBroker.CoverAvailable + (feeManagement + fee_paid)` - Decrease LoanBroker Debt by the amount paid: - - `LoanBroker.DebtTotal -= totalPaid` + - `LoanBroker.DebtTotal = LoanBroker.DebtTotal - totalPaid` - Update the LoanBroker Debt by the Loan value change: - - `LoanBroker.DebtTotal += valueChange` + - `LoanBroker.DebtTotal = LoanBroker.DebtTotal + valueChange` - Update the LoanBroker Debt by the change in the management fee: - - `LoanBroker.DebtTotal -= (valueChange x LoanBroker.ManagementFeeRate)` - - If `LoanPaymentRemaining == 0` and `LoanPrincipalOutstanding == 0`: - - Decrease active loans: - - `LoanBroker.OwnerCount = LoanBroker.OwnerCount - 1` + + - `LoanBroker.DebtTotal = LoanBroker.DebtTotal - (valueChange x LoanBroker.ManagementFeeRate)` - `Vault(LoanBroker(Loan.LoanBrokerID).VaultID)` state changes: @@ -1416,35 +1413,42 @@ Furthermore, assume `full_periodic_payments` variable represents the number of p - `Vault.AssetTotal = Vault.AssetTotal - (vaultChange x LoanBroker.managementFeeRate)` - - If there is **not enough** first-loss capital: `LoanBroker.CoverAvailable < LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`: - - The management fee was not charged; decrease Vault TotalValue: - - `Loan.AssetTotal = Loan.AssetTotal + feeManagement` - - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is `XRP`: - - Increase the `Balance` field of _pseudo-account_ `AccountRoot` by `principal_paid + (interest_paid - management_fee)`. - If `LoanBroker.CoverAvailable >= LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`: - Increase the `Balance` field of the `LoanBroker.Owner` `AccountRoot` by `fee_paid + management_fee`. + - Increase the `Balance` field of _pseudo-account_ `AccountRoot` by `principal_paid + (interest_paid - management_fee)`. + + - If `LoanBroker.CoverAvailable < LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`: + + - Increase the `Balance` field of _pseudo-account_ `AccountRoot` by `principal_paid + interest_paid + fee_paid` (the payment and management fee was added to First Loss Capital, and thus transfered to the _pseudo-account_). - Decrease the `Balance` field of the submitter `AccountRoot` by `principal_paid + interest_paid + fee_paid`. - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `IOU`: - - Increase the `RippleState` balance between the _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `principal_paid + (interest_paid - management_fee)`. - - If `LoanBroker.CoverAvailable >= LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum` : + - If `LoanBroker.CoverAvailable >= LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`: - Increase the `RippleState` balance between the `LoanBroker.Owner` `AccountRoot` and the `Issuer` `AccountRoot` by `fee_paid + management_fee`. + - Increase the `RippleState` balance between the _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `principal_paid + (interest_paid - management_fee)`. + + - If `LoanBroker.CoverAvailable < LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`: + + - Increase the `RippleState` balance between the _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `principal_paid + interest_paid + fee_paid` (the payment and management fee was added to First Loss Capital, and thus transfered to the _pseudo-account_). - Decrease the `RippleState` balance between the submitter `AccountRoot` and the `Issuer` `AccountRoot` by `principal_paid + interest_paid + fee_paid`. - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `MPT`: - - Increase the `MPToken.MPTAmount` by `principal_paid + (interest_paid - management_fee)` of the _pseudo-account_ `MPToken` object for the `Vault.Asset`. - If `LoanBroker.CoverAvailable >= LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`: - Increase the `MPToken.MPTAmount` by `fee_paid + management_fee` of the `LoanBroker.Owner` `MPToken` object for the `Vault.Asset`. - + - Increase the `MPToken.MPTAmount` by `principal_paid + (interest_paid - management_fee)` of the _pseudo-account_ `MPToken` object for the `Vault.Asset`. + + - If `LoanBroker.CoverAvailable < LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`: + - Increase the `MPToken.MPTAmount` by `principal_paid + interest_paid + fee_paid` of the _pseudo-account_ `MPToken` object for the `Vault.Asset`(the payment and management fee was added to First Loss Capital, and thus transfered to the _pseudo-account_) . + - Decrease the `MPToken.MPTAmount` by `principal_paid + interest_paid + fee_paid` of the submitter `MPToken` object for the `Vault.Asset`. [**Return to Index**](#index) From 9090bd31ea8cb1547e8990cfed54038fd40bb27f Mon Sep 17 00:00:00 2001 From: Vito Date: Tue, 28 Jan 2025 14:44:03 +0000 Subject: [PATCH 10/77] fixes loan object ownership --- XLS-0066d-lending-protocol/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index fc46d626..6b876337 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -480,7 +480,7 @@ The `Loan` object supports the following flags: The `Loan` objects are stored in the ledger and tracked in two [Owner Directories](https://xrpl.org/docs/references/protocol/ledger-data/ledger-entry-types/directorynode). - The `OwnerNode` is the `Owner Directory` of the `Borrower` who is the main `Owner` of the `Loan` object, and therefore is responsible for the owner reserve. -- The `LoanBroker` `_pseudo-account_` `Owner Directory` to track all loans associated with the same `LoanBroker` object. +- The `LoanBrokerNode` is the `Owner Directory` for the `LoanBroker` to track all loans associated with the same `LoanBroker` object. #### 2.2.4 Reserves From 885162053db967cca804747a36d33d53fda16598 Mon Sep 17 00:00:00 2001 From: Vito Date: Wed, 29 Jan 2025 13:33:09 +0000 Subject: [PATCH 11/77] adds pseudo-account to the LoanBroker --- XLS-0066d-lending-protocol/README.md | 190 ++++++++++++++++++--------- 1 file changed, 131 insertions(+), 59 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index 6b876337..b6be52e0 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -157,27 +157,27 @@ The lending protocol charges a number of fees that the Loan Broker can configure | Depositor | | LoanBroker | | Borrower | | AccountRoot | | AccountRoot | | AccountRoot | |-----------------| |-----------------| |-----------------| -| Owner Directory | | Owner Directory | | Owner Directory | -+-----------------+ +-----------------+ +-----------------+ - ^ | | | - | Reserve ____________Reserve____________ Reserve - Account | | | | - | V V V V +| Owner Directory | | Owner Directory | <-----OwnerNode | Owner Directory | <--------- ++-----------------+ +-----------------+ | +-----------------+ | + ^ | | | | | + | Reserve ____________Reserve____________ | Reserve | + Account | | | | | | + | V V V | V | ++-----------------+ +-----------------+ +-----------------+ +-----------------+ | +| | | |1 N| |1 N| | | +| MPToken | | Vault |--------->| LoanBroker |--------->| Loan |-OwnerNode- +| | | | | | | | +-----------------+ +-----------------+ +-----------------+ +-----------------+ -| | | |1 N| |1 N| | -| MPToken | | Vault |--------->| LoanBroker |--------->| Loan | -| | | | |-----------------| | | -+-----------------+ +-----------------+ | Owner Directory | +-----------------+ - | ^ +-----------------+ ^ - Issuance | ___________ ____________^ |_________Link_________| - | | Account | - V ^ | ^ -+-----------------+ | +-----------------+ | -| Share | | | Pseudo-Account | | -| MPTokenIssuance |<------Issuer------| -----| AccountRoot | | -| | |_Link_|-----------------|_Link_| -+-----------------+ | Owner Directory | - +-----------------+ + | ^ | ^ | + Issuance | | | | + | Account | Account | + V | -VaultNode- | | ++-----------------+ +-----------------+ | +-----------------+ | +| Share | | Pseudo-Account | | | Pseudo-Account | | +| MPTokenIssuance |<--Issuer-| AccountRoot | | | AccountRoot | | +| | |-----------------| | |-----------------| | ++-----------------+ | Owner Directory | <---- | Owner Directory | <- LoanBrokerNode--- + +-----------------+ +-----------------+ ``` [**Return to Index**](#index) @@ -211,7 +211,8 @@ The `LoanBroker` object has the following fields: | `OwnerNode` | `N/A` | :heavy_check_mark: | `number` | `UINT64` | `N/A` | Identifies the page where this item is referenced in the owner's directory. | | `VaultNode` | `N/A` | :heavy_check_mark: | `number` | `UINT64` | `N/A` | Identifies the page where this item is referenced in the Vault's _pseudo-account_ owner's directory. | | `VaultID` | `No` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the `Vault` object associated with this Lending Protocol Instance. | -| `Owner` | `No` | :heavy_check_mark: | `string` | `AccountID` | `N/A` | The address of the account that is the Loan Broker. | +| `Account` | `No` | :heavy_check_mark: | `string` | `AccountID` | `N/A` | The address of the `LoanBroker` _pseudo-account_. | +| `Owner` | `No` | :heavy_check_mark: | `string` | `AccountID` | `N/A` | The address of the Loan Broker account. | | `Data` | `Yes` | | `string` | `BLOB` | None | Arbitrary metadata about the `LoanBroker`. Limited to 256 bytes. | | `ManagementFeeRate` | `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` | `N/A` | :heavy_check_mark: | `number` | `UINT32` | 0 | The number of active Loans issued by the `LoanBroker`. | @@ -223,13 +224,11 @@ The `LoanBroker` object has the following fields: #### 2.1.3 `LoanBroker `_pseudo-account_` -The Lending Protocol uses the `_pseudo-account_` of the associated `Vault` object to hold the First-Loss Capital. +The `LoanBroker` _pseudo-account_ holds the First-Loss Capital deposited by the LoanBroker, as well as Loan funds. The _pseudo-account_ follows the XLS-64d specification for pseudo accounts. The `AccountRoot` object is created when creating the `Vault` object. #### 2.1.4 Ownership -The lending protocol object is stored in the ledger and tracked in an [Owner Directory](https://xrpl.org/docs/references/protocol/ledger-data/ledger-entry-types/directorynode) owned by the account submitting the `LoanBrokerSet` transaction. Furthermore, the object is also tracked in the `OwnerDirectory` of the _`pseudo-account`_. The `_pseudo_account_` `OwnerDirectory` page is captured by the `VaultNode` field. - -The `LoanBroker` requires tracking associated `Loan` objects to prevent the `LoanBroker` object from being deleted while loans are active as well as future RPC endpoints. Therefore, the `LoanBroker` has an associated [Owner Directory](https://xrpl.org/docs/references/protocol/ledger-data/ledger-entry-types/directorynode) object. +The lending protocol object is stored in the ledger and tracked in an [Owner Directory](https://xrpl.org/docs/references/protocol/ledger-data/ledger-entry-types/directorynode) owned by the account submitting the `LoanBrokerSet` transaction. Furthermore, the object is also tracked in the `OwnerDirectory` of the `Vault` _`pseudo-account`_. The `_pseudo_account_` `OwnerDirectory` page is captured by the `VaultNode` field. The `RootIndex` of the `DirectoryNode` object is the result of [`SHA512-Half`](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#hashes) of the following values concatenated in order: @@ -359,7 +358,7 @@ The First-Loss Capital is an optional mechanism to protect the Vault depositors Whenever the available cover falls below the minimum cover required, two consequences occur: - 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. +- The Lender cannot directly receive fees. The fees are instead added to the First Loss Capital to cover the deficit. **Examples** @@ -480,7 +479,7 @@ The `Loan` object supports the following flags: The `Loan` objects are stored in the ledger and tracked in two [Owner Directories](https://xrpl.org/docs/references/protocol/ledger-data/ledger-entry-types/directorynode). - The `OwnerNode` is the `Owner Directory` of the `Borrower` who is the main `Owner` of the `Loan` object, and therefore is responsible for the owner reserve. -- The `LoanBrokerNode` is the `Owner Directory` for the `LoanBroker` to track all loans associated with the same `LoanBroker` object. +- The `LoanBrokerNode` is the `Owner Directory` for the `LoanBroker` _pseudo-account_ to track all loans associated with the same `LoanBroker` object. #### 2.2.4 Reserves @@ -533,8 +532,20 @@ The transaction creates a new `LoanBroker` object or updates an existing one. - If `LoanBrokerID` is not specified: + - Create a new `LoanBroker` ledger object. + + - Create a new `AccountRoot` _pseudo-account_ object, setting the `AccountRoot.LoanBrokerID` to `LoanBrokerID`. + + - If the `Vault(VaultID).Asset` is an `IOU`: + + - Create a `Trustline` between the `Issuer` and the `LoanBroker` _pseudo-account_. + + - If the `Vault(VaultID).Asset` is an `MPT`: + + - Create an `MPToken` object for the `LoanBroker` _pseudo-account_. + - Add `LoanBrokerID` to the `OwnerDirectory` of the submitting account. - - Add `LoanBrokerID` to the `OwnerDirectory` of the Vault's `_pseudo-account_`. + - Add `LoanBrokerID` to the `OwnerDirectory` of the Vault's _pseudo-account_. - If `LoanBrokerID` is specified: - Update appropriate fields. @@ -562,23 +573,26 @@ The transaction creates a new `LoanBroker` object or updates an existing one. ##### 3.1.2.2 State Changes - Delete `LoanBrokerID` from the `OwnerDirectory` of the submitting account. -- Delete `LoanBrokerID` from the `OwnerDirectory` of the Vault's `_pseudo-account_`. +- Delete `LoanBrokerID` from the `OwnerDirectory` of the Vault's _pseudo-account_. - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is `XRP`: - - Decrease the `Balance` field of _pseudo-account_ `AccountRoot` by `CoverAvailable`. + - Decrease the `Balance` field of `LoanBroker` _pseudo-account_ `AccountRoot` by `CoverAvailable`. - Increase the `Balance` field of the submitter `AccountRoot` by `CoverAvailable`. - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `IOU`: - - Decrease the `RippleState` balance between the _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `CoverAvailable`. + - Decrease the `RippleState` balance between the `LoanBroker` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `CoverAvailable`. - Increase the `RippleState` balance between the submitter `AccountRoot` and the `Issuer` `AccountRoot` by `CoverAvailable`. - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `MPT`: - - Decrease the `MPToken.MPTAmount` by `CoverAvailable` of the _pseudo-account_ `MPToken` object for the `Vault.Asset`. + - Decrease the `MPToken.MPTAmount` by `CoverAvailable` of the `LoanBroker` _pseudo-account_ `MPToken` object for the `Vault.Asset`. - Increase the `MPToken.MPTAmount` by `CoverAvailable` of the submitter `MPToken` object for the `Vault.Asset`. +- Delete the `LoanBroker` _pseudo-account_ `AccountRoot` object. +- Delete the `LoanBroker` ledger object. + ##### 3.1.2.3 Invariants - If `LoanBroker.OwnerCount = 0` the `DirectoryNode` for the `LoanBroker` does not exist @@ -621,17 +635,17 @@ The transaction deposits First Loss Capital into the `LoanBroker` object. - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is `XRP`: - - Increase the `Balance` field of _pseudo-account_ `AccountRoot` by `Amount`. + - Increase the `Balance` field of `LoanBroker` _pseudo-account_ `AccountRoot` by `Amount`. - Decrease the `Balance` field of the submitter `AccountRoot` by `Amount`. - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `IOU`: - - Increase the `RippleState` balance between the _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `Amount`. + - Increase the `RippleState` balance between the `LoanBroker` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `Amount`. - Decrease the `RippleState` balance between the submitter `AccountRoot` and the `Issuer` `AccountRoot` by `Amount`. - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `MPT`: - - Increase the `MPToken.MPTAmount` by `Amount` of the _pseudo-account_ `MPToken` object for the `Vault.Asset`. + - Increase the `MPToken.MPTAmount` by `Amount` of the `LoanBroker` _pseudo-account_ `MPToken` object for the `Vault.Asset`. - Decrease the `MPToken.MPTAmount` by `Amount` of the submitter `MPToken` object for the `Vault.Asset`. - Increase `LoanBroker.CoverAvailable` by `Amount`. @@ -676,17 +690,17 @@ The `LoanBrokerCoverWithdraw` transaction withdraws the First-Loss Capital from - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is `XRP`: - - Decrease the `Balance` field of _pseudo-account_ `AccountRoot` by `Amount`. + - Decrease the `Balance` field of `LoanBroker` _pseudo-account_ `AccountRoot` by `Amount`. - Increase the `Balance` field of the submitter `AccountRoot` by `Amount`. - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `IOU`: - - Decrease the `RippleState` balance between the _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `Amount`. + - Decrease the `RippleState` balance between the `LoanBroker` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `Amount`. - Increase the `RippleState` balance between the submitter `AccountRoot` and the `Issuer` `AccountRoot` by `Amount`. - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `MPT`: - - Decrease the `MPToken.MPTAmount` by `Amount` of the _pseudo-account_ `MPToken` object for the `Vault.Asset`. + - Decrease the `MPToken.MPTAmount` by `Amount` of the `LoanBroker` _pseudo-account_ `MPToken` object for the `Vault.Asset`. - Increase the `MPToken.MPTAmount` by `Amount` of the submitter `MPToken` object for the `Vault.Asset`. - Decrease `LoanBroker.CoverAvailable` by `Amount`. @@ -791,14 +805,28 @@ The `LoanSet` transaction is a mutual agreement between the `Borrower` and the ` ##### 3.2.1.5 State Changes -- If the Loan Asset is an `IOU`: +- If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is `XRP`: + + - Decrease the `Balance` field of `Vault` _pseudo-account_ `AccountRoot` by `Loan.PrincipalRequested`. + - Increase the `Balance` field of `LoanBroker` _pseudo-account_ `AccountRoot` by `Loan.PrincipalRequested - Loan.LoanOriginationFee`. + - Increase the `Balance` field of `LoanBroker.Owner` `AccountRoot` by `Loan.LoanOriginationFee`. + +- If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `IOU`: - Create a `Trustline` between the `Issuer` and the `Borrower` if one does not exist. -- If the Loan Asset is an `MPT`: + - Decrease the `RippleState` balance between the `Vault` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `Loan.PrincipalRequested`. + - Increase the `RippleState` balance between the `LoanBroker` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `Loan.PrincipalRequested - Loan.LoanOriginationFee`. + - Increase the `RippleState` balance between the `LoanBroker.Owner` `AccountRoot` and the `Issuer` `AccountRoot` by `Loan.LoanOriginationFee`. + +- If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `MPT`: - Create an `MPToken` object for the `Borrower` if one does not exist. + - Decrease the `MPToken.MPTAmount` of the `Vault` _pseudo-account_ `MPToken` object for the `Vault.Asset` by `Loan.PrincipalRequested`. + - Increase the `MPToken.MPTAmount` of the `LoanBroker` _pseudo-account_ `MPToken` object for the `Vault.Asset` by `Loan.PrincipalRequested - Loan.LoanOriginationFee`. + - Increase the `MPToken.MPTAmount` of the `LoanBroker.Owner` `MPToken` object for the `Vault.Asset` by `Loan.LoanOriginationFee` + - `Vault(LoanBroker(LoanBrokerID).VaultID)` object state changes: - Decrease Asset Available in the Vault: @@ -813,8 +841,8 @@ The `LoanSet` transaction is a mutual agreement between the `Borrower` and the ` - `LoanBroker.DebtTotal += Loan.PrincipalRequested + (LoanInterest - (LoanInterest x LoanBroker.ManagementFeeRate)` - `LoanBroker.OwnerCount += 1` - - If the `DirectoryNode` for the `LoanBroker` does not exist, create one. - - Add `LoanID` to `DirectoryNode.Indexes`. + - Add `LoanID` to `DirectoryNode.Indexes` of the `LoanBroker` _pseudo-account_ `AccountRoot`. + - Add `LoanID` to `DirectoryNode.Indexes` of the `Borrower` `AccountRoot`. ##### 3.2.1.4 Invariants @@ -840,13 +868,33 @@ The transaction deletes an existing `Loan` object. ##### 3.2.2.2 State Changes -- Remove `LoanID` from the Owner Directory associated with the `LoanBroker`. -- `LoanBroker.OwnerCount -= 1` +- If `Loan(LoanID).AssetAvailable > 0` (transfer remaining funds to the borrower): + + - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is `XRP`: + + - Decrease the `Balance` field of `LoanBroker` _pseudo-account_ `AccountRoot` by `Loan(LoanID).AssetAvailable`. + - Increase the `Balance` field of `Loan(LoanID).Borrower` `AccountRoot` by `Loan(LoanID).AssetAvailable`. + +- If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `IOU`: + + - Decrease the `RippleState` balance between the `LoanBroker` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `Loan(LoanID).AssetAvailable`. + - Increase the `RippleState` balance between the `Loan(LoanID).Borrower` `AccountRoot` and the `Issuer` `AccountRoot` by `Loan(LoanID).AssetAvailable`. + +- If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `MPT`: + + - Decrease the `MPToken.MPTAmount` of the `LoanBroker` _pseudo-account_ `MPToken` object for the `Vault.Asset` by `Loan(LoanID).AssetAvailable`. + - Increase the `MPToken.MPTAmount` of the `Loan(LoanID).Borrower` `MPToken` object for the `Vault.Asset` by `Loan(LoanID).AssetAvailable` + - Delete the `Loan` object. -- Release reserve funds back to the Borrower. -- Remove `LoanID` from the `DirectoryNode.Indexes`. + +- Remove `LoanID` from `DirectoryNode.Indexes` of the `LoanBroker` _pseudo-account_ `AccountRoot`. - If `LoanBroker.OwnerCount = 0` - - Delete the `LoanBroker` `DirectoryNode`. + + - Delete the `LoanBroker` _pseudo-account_ `DirectoryNode`. + +- Remove `LoanID` from `DirectoryNode.Indexes` of the `Borrower` `AccountRoot`. + +- `LoanBroker.OwnerCount -= 1` ##### 3.2.2.3 Invariants @@ -919,6 +967,23 @@ The transaction deletes an existing `Loan` object. - `Loan(LoanID).AssetAvailable = 0` - `Loan(LoanID).PrincipalOutstanding = 0` +- Move the First-Loss Capital from the `LoanBroker` _pseudo-account_ to the `Vault` _pseudo-account_: + + - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is `XRP`: + + - Decrease the `Balance` field of `LoanBroker` _pseudo-account_ `AccountRoot` by `DefaultCovered`. + - Increase the `Balance` field of `Vault` _pseudo-account_ `AccountRoot` by `DefaultCovered`. + + - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `IOU`: + + - Decrease the `RippleState` balance between the `LoanBroker` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `DefaultCovered`. + - Increase the `RippleState` balance between the `Vault` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `DefaultCovered`. + + - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `MPT`: + + - Decrease the `MPToken.MPTAmount` of the `LoanBroker` _pseudo-account_ `MPToken` object for the `Vault.Asset` by `DefaultCovered`. + - Increase the `MPToken.MPTAmount` of the `Vault` _pseudo-account_ `MPToken` object for the `Vault.Asset` by `DefaultCovered`. + - If `tfLoanImpair` flag is specified: - Update the `Vault` object (set "paper loss"): @@ -967,7 +1032,7 @@ The Borrower submits a `LoanDraw` transaction to draw funds from the Loan. - The `AccountRoot.Account` of the submitter is not `Loan.Borrower`. - The Loan has not started: - `Loan.StartDate > LastClosedLedger.CloseTime`. -- There are insufficient assets: +- There are insufficient assets in the `Loan`: - `Loan.AssetAvailable` < `Amount`. @@ -991,17 +1056,17 @@ The Borrower submits a `LoanDraw` transaction to draw funds from the Loan. - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is `XRP`: - - Decrease the `Balance` field of _pseudo-account_ `AccountRoot` by `Amount`. + - Decrease the `Balance` field of `LoanBroker` _pseudo-account_ `AccountRoot` by `Amount`. - Increase the `Balance` field of the submitter `AccountRoot` by `Amount`. - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `IOU`: - - Decrease the `RippleState` balance between the _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `Amount`. + - Decrease the `RippleState` balance between the `LoanBroker` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `Amount`. - Increase the `RippleState` balance between the submitter `AccountRoot` and the `Issuer` `AccountRoot` by `Amount`. - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `MPT`: - - Decrease the `MPToken.MPTAmount` by `Amount` of the _pseudo-account_ `MPToken` object for the `Vault.Asset`. + - Decrease the `MPToken.MPTAmount` by `Amount` of the `LoanBroker` _pseudo-account_ `MPToken` object for the `Vault.Asset`. - Increase the `MPToken.MPTAmount` by `Amount` of the submitter `MPToken` object for the `Vault.Asset`. - Decrease `Loan.AssetAvailable` by `Amount`. @@ -1415,40 +1480,47 @@ Furthermore, assume `full_periodic_payments` variable represents the number of p - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is `XRP`: + - Increase the `Balance` field of `Vault` _pseudo-account_ `AccountRoot` by `principal_paid + (interest_paid - management_fee)`. + - If `LoanBroker.CoverAvailable >= LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`: - Increase the `Balance` field of the `LoanBroker.Owner` `AccountRoot` by `fee_paid + management_fee`. - - Increase the `Balance` field of _pseudo-account_ `AccountRoot` by `principal_paid + (interest_paid - management_fee)`. - If `LoanBroker.CoverAvailable < LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`: - - Increase the `Balance` field of _pseudo-account_ `AccountRoot` by `principal_paid + interest_paid + fee_paid` (the payment and management fee was added to First Loss Capital, and thus transfered to the _pseudo-account_). + - Increase the `Balance` field of the `LoanBroker` _pseudo-account_ `AccountRoot` by `fee_paid + management_fee`. (the payment and management fee was added to First Loss Capital, and thus transfered to the `LoanBroker` _pseudo-account_). + - Increase `LoanBroker.CoverAvailable` by `fee_paid + management_fee`. - Decrease the `Balance` field of the submitter `AccountRoot` by `principal_paid + interest_paid + fee_paid`. - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `IOU`: + - Increase the `RippleState` balance between the `Vault` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `principal_paid + (interest_paid - management_fee)`. + - If `LoanBroker.CoverAvailable >= LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`: - Increase the `RippleState` balance between the `LoanBroker.Owner` `AccountRoot` and the `Issuer` `AccountRoot` by `fee_paid + management_fee`. - - Increase the `RippleState` balance between the _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `principal_paid + (interest_paid - management_fee)`. - If `LoanBroker.CoverAvailable < LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`: - - Increase the `RippleState` balance between the _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `principal_paid + interest_paid + fee_paid` (the payment and management fee was added to First Loss Capital, and thus transfered to the _pseudo-account_). + - Increase the `RippleState` balance between the `LoanBroker` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `fee_paid + management_fee` (the payment and management fee was added to First Loss Capital, and thus transfered to the `LoanBroker` _pseudo-account_). + - Increase `LoanBroker.CoverAvailable` by `fee_paid + management_fee`. - Decrease the `RippleState` balance between the submitter `AccountRoot` and the `Issuer` `AccountRoot` by `principal_paid + interest_paid + fee_paid`. - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `MPT`: + - Increase the `MPToken.MPTAmount` by `principal_paid + (interest_paid - management_fee)` of the `Vault` _pseudo-account_ `MPToken` object for the `Vault.Asset`. + - If `LoanBroker.CoverAvailable >= LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`: - Increase the `MPToken.MPTAmount` by `fee_paid + management_fee` of the `LoanBroker.Owner` `MPToken` object for the `Vault.Asset`. - - Increase the `MPToken.MPTAmount` by `principal_paid + (interest_paid - management_fee)` of the _pseudo-account_ `MPToken` object for the `Vault.Asset`. - + - If `LoanBroker.CoverAvailable < LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`: - - Increase the `MPToken.MPTAmount` by `principal_paid + interest_paid + fee_paid` of the _pseudo-account_ `MPToken` object for the `Vault.Asset`(the payment and management fee was added to First Loss Capital, and thus transfered to the _pseudo-account_) . - + + - Increase the `MPToken.MPTAmount` by `fee_paid + management_fee` of the `LoanBroker` _pseudo-account_ `MPToken` object for the `Vault.Asset` (the payment and management fee was added to First Loss Capital, and thus transfered to the `LoanBroker` _pseudo-account_). + - Increase `LoanBroker.CoverAvailable` by `fee_paid + management_fee`. + - Decrease the `MPToken.MPTAmount` by `principal_paid + interest_paid + fee_paid` of the submitter `MPToken` object for the `Vault.Asset`. [**Return to Index**](#index) From 0f6644ce2e2bd91a27a6f332689c4aa95a76efa0 Mon Sep 17 00:00:00 2001 From: Vito Date: Fri, 31 Jan 2025 12:51:50 +0000 Subject: [PATCH 12/77] adds counterparty to LoanSet transaction --- XLS-0066d-lending-protocol/README.md | 87 ++++++++++++++++++---------- 1 file changed, 55 insertions(+), 32 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index b6be52e0..171ae766 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -715,26 +715,26 @@ In this section we specify transactions associated with the `Loan` ledger entry. The transaction creates a new `Loan` object. -| Field Name | Required? | JSON Type | Internal Type | Default Value | Description | -| -------------------- | :----------------: | :-------: | :-----------: | :-----------: | :-------------------------------------------------------------------------------------------------------------------------------------------- | -| `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | The transaction type. | -| `LoanBrokerID` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The Loan Broker ID associated with the loan. | -| `Flags` | | `string` | `UINT32` | 0 | Specifies the flags for the Loan. | -| `Data` | | `string` | `BLOB` | None | Arbitrary metadata in hex format. The field is limited to 256 bytes. | -| `Borrower` | :heavy_check_mark: | `string` | `AccountID` | `N/A` | The address of the account that is the borrower. | -| `LoanOriginationFee` | | `number` | `NUMBER` | 0 | A nominal funds amount paid to the `LoanBroker.Owner` when the Loan is created. | -| `LoanServiceFee` | | `number` | `NUMBER` | 0 | A nominal amount paid to the `LoanBroker.Owner` with every Loan payment. | -| `LatePaymentFee` | | `number` | `NUMBER` | 0 | A nominal funds amount paid to the `LoanBroker.Owner` when a payment is late. | -| `ClosePaymentFee` | | `number` | `NUMBER` | 0 | A nominal funds amount paid to the `LoanBroker.Owner` when an early full repayment is made. | -| `InterestRate` | | `number` | `UINT16` | 0 | Annualized interest rate of the Loan in basis points. | -| `LateInterestRate` | | `number` | `UINT16` | 0 | A premium added to the interest rate for late payments in basis points. Valid values are between 0 and 10000 inclusive. (0 - 100%) | -| `CloseInterestRate` | | `number` | `UINT16` | 0 | A Fee Rate charged for repaying the Loan early in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | -| `PrincipalRequested` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | The principal amount requested by the Borrower. | -| `StartDate` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The timestamp of when the Loan starts [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time). | -| `PaymentTotal` | | `number` | `UINT32` | 1 | The total number of payments to be made against the Loan. | -| `PaymentInterval` | | `number` | `UINT32` | 60 | Number of seconds between Loan payments. | -| `GracePeriod` | | `number` | `UINT32` | 60 | The number of seconds after the Loan's Payment Due Date can be Defaulted. | -| `Lender` | :heavy_check_mark: | `object` | `STObject` | `N/A` | An inner object that contains the signature of the Lender over the transaction. | +| Field Name | Required? | JSON Type | Internal Type | Default Value | Description | +| ----------------------- | :----------------: | :-------: | :-----------: | :-----------: | :-------------------------------------------------------------------------------------------------------------------------------------------- | +| `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | The transaction type. | +| `LoanBrokerID` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The Loan Broker ID associated with the loan. | +| `Flags` | | `string` | `UINT32` | 0 | Specifies the flags for the Loan. | +| `Data` | | `string` | `BLOB` | None | Arbitrary metadata in hex format. The field is limited to 256 bytes. | +| `Counterparty` | | `string` | `AccountID` | `N/A` | The address of the counterparty of the Loan. | +| `CounterpartySignature` | :heavy_check_mark: | `string` | `STObject` | `N/A` | The signature of the counterparty over the transaction. | +| `LoanOriginationFee` | | `number` | `NUMBER` | 0 | A nominal funds amount paid to the `LoanBroker.Owner` when the Loan is created. | +| `LoanServiceFee` | | `number` | `NUMBER` | 0 | A nominal amount paid to the `LoanBroker.Owner` with every Loan payment. | +| `LatePaymentFee` | | `number` | `NUMBER` | 0 | A nominal funds amount paid to the `LoanBroker.Owner` when a payment is late. | +| `ClosePaymentFee` | | `number` | `NUMBER` | 0 | A nominal funds amount paid to the `LoanBroker.Owner` when an early full repayment is made. | +| `InterestRate` | | `number` | `UINT16` | 0 | Annualized interest rate of the Loan in basis points. | +| `LateInterestRate` | | `number` | `UINT16` | 0 | A premium added to the interest rate for late payments in basis points. Valid values are between 0 and 10000 inclusive. (0 - 100%) | +| `CloseInterestRate` | | `number` | `UINT16` | 0 | A Fee Rate charged for repaying the Loan early in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | +| `PrincipalRequested` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | The principal amount requested by the Borrower. | +| `StartDate` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The timestamp of when the Loan starts [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time). | +| `PaymentTotal` | | `number` | `UINT32` | 1 | The total number of payments to be made against the Loan. | +| `PaymentInterval` | | `number` | `UINT32` | 60 | Number of seconds between Loan payments. | +| `GracePeriod` | | `number` | `UINT32` | 60 | The number of seconds after the Loan's Payment Due Date can be Defaulted. | ##### 3.2.1.1 `Flags` @@ -742,15 +742,15 @@ The transaction creates a new `Loan` object. | ------------------- | :--------: | :---------------------------------------------- | | `tfLoanOverpayment` | `0x0001` | Indicates that the vault supports overpayments. | -##### 3.2.1.2 `Lender` +##### 3.2.1.2 `CounterpartySignature` An inner object that contains the signature of the Lender over the transaction. The fields contained in this object are: -| Field Name | Required? | JSON Type | Internal Type | Default Value | Description | -| --------------- | :----------------: | :-------: | :-----------: | :-----------: | :--------------------------------------------------------------------------------------------------------------------- | -| `SigningPubKey` | :heavy_check_mark: | `string` | `STBlob` | `N/A` | The Public Key to be used to verify the validity of the signature. | -| `Signature` | :heavy_check_mark: | `string` | `STBlob` | `N/A` | The signature of over all signing fields, including the `Signature` of the Borrower. | -| `Signers` | :heavy_check_mark: | `list` | `STArray` | `N/A` | An array of transaction signatures from the `LoanBroker.Owner` signers to indicate their approval of this transaction. | +| Field Name | Required? | JSON Type | Internal Type | Default Value | Description | +| --------------- | :----------------: | :-------: | :-----------: | :-----------: | :----------------------------------------------------------------------------------------------------------------- | +| `SigningPubKey` | :heavy_check_mark: | `string` | `STBlob` | `N/A` | The Public Key to be used to verify the validity of the signature. | +| `Signature` | :heavy_check_mark: | `string` | `STBlob` | `N/A` | The signature of over all signing fields. | +| `Signers` | :heavy_check_mark: | `list` | `STArray` | `N/A` | An array of transaction signatures from the `Counterparty` signers to indicate their approval of this transaction. | The final transaction must include `Signature` or `Signers`. @@ -762,10 +762,33 @@ This field is not a signing field (it will not be included in transaction signat ##### 3.2.1.3 Multi-Signing -The `LoanSet` transaction is a mutual agreement between the `Borrower` and the `LoanBroke.Owner` to create a Loan. Therefore, the `LoanSet` transaction must be signed by both parties. The multi-signature flow is as follows: +The `LoanSet` transaction is a mutual agreement between the `Borrower` and the `LoanBroke.Owner` to create a Loan. Therefore, the `LoanSet` transaction must be signed by both parties. -1. The `Borrower` creates a new transaction with the pre-agreed terms of the Loan and signs the transaction. -2. The `Lender` signs over all signing fields, including the signature of the `Borrower`. +Either of the parties (Borrower or Loan Issuer) may initiate the transaction. The user flow is as follows: + +- `Borrower` initiates the transaction: + + 1. The `Borrower` creates the transaction from their account, setting the pre-agreed terms. + + - Optionally, the `Borrower` may set the `Counterparty` to `LoanBroker.Owner`. In case the `Counterparty` field is not set, it is assumed to be the `LoanBroker.Owner`. + + 2. The `Borrower` signs the transaction setting the `SigningPubKey`, `TxnSignature`, `Signers`, `Account`, `Fee`, `Sequence` fields. + 3. The `Borrower` sends the transaction to the `Loan Issuer`. + 4. The `Loan Issuer` verifies the loan-terms are as agreed upon and verifies the signature of the `Borrower`. + 5. The `Loan Issuer` signs the transaction, filling the `CounterpartySignature` field. + 6. The `Loan Issuer` submits the transaction. + +- `Loan Issuer` initiates the transaction: + + 1. The `Loan Issuer` creates the transaction from their account setting the pre-agreed terms. + + - The `Loan Issuer` must set the `Counterparty` to the `Borrower` account ID. + + 2. The `Loan Issuer` signs the transaction setting the `SigningPubKey`, `TxnSignature`, `Signers`, `Account`, `Fee`, `Sequence` fields. + 3. The `Loan Issuer` sends the transaction to the `Borrower`. + 4. The `Borrower` verifies the loan-terms are as agreed upon and verifies the signature of the `Loan Issuer`. + 5. The `Borrower` signs the transaction, filling the `CounterpartySignature` field. + 6. The `Borrower` submits the transaction. ##### 3.2.1.4 Failure Conditions @@ -1161,8 +1184,8 @@ $$ latePaymentInterest = principalOutstanding \times \frac{lateInterestRate \times secondsSinceLastPayment}{365 \times 24 \times 60 \times 60} $$ -A late payment pays more interest than calculated when increasing the Vault value in the `LoanSet` transaction. Therefore, the total Vault value captured by `Vault.AssetTotal` must be recalculate -d. +A late payment pays more interest than calculated when increasing the Vault value in the `LoanSet` transaction. Therefore, the total Vault value captured by `Vault.AssetTotal` must be recalculated. + Assume the function `PeriodicPayment()` returns the expected periodic payment, split into `principalPeriodic` and `interestPeriodic`. Furthermore, assume the function `LatePayment()` that implements the Late Payment formula. The function returns the late payment split into `principalLate` and `interestLate`, where `interestLate` is calculated using the formula above. Note that `principalPeriodic == principalLate` and `interestLate > interestPeriodic` are used only when the payment is late. Otherwise, `interestLate == interestPeriodic`. $$ @@ -1481,7 +1504,7 @@ Furthermore, assume `full_periodic_payments` variable represents the number of p - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is `XRP`: - Increase the `Balance` field of `Vault` _pseudo-account_ `AccountRoot` by `principal_paid + (interest_paid - management_fee)`. - + - If `LoanBroker.CoverAvailable >= LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`: - Increase the `Balance` field of the `LoanBroker.Owner` `AccountRoot` by `fee_paid + management_fee`. From 14a634f9fc34a72b650f93f135b6e267b3e8fdb5 Mon Sep 17 00:00:00 2001 From: Vito Date: Fri, 31 Jan 2025 13:07:00 +0000 Subject: [PATCH 13/77] updates failure conditions of LoanSet transaction --- XLS-0066d-lending-protocol/README.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index 171ae766..44b4c39d 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -790,11 +790,16 @@ Either of the parties (Borrower or Loan Issuer) may initiate the transaction. Th 5. The `Borrower` signs the transaction, filling the `CounterpartySignature` field. 6. The `Borrower` submits the transaction. -##### 3.2.1.4 Failure Conditions +##### 3.2.1.4 Fees + +The account specified in the `Account` field pays the transaction fee. + +##### 3.2.1.5 Failure Conditions - `LoanBroker` object with the specified `LoanBrokerID` does not exist on the ledger. -- The submitter `AccountRoot.Account != LoanBroker(LoanBrokerID).Owner`. -- `Lender. Signature` is invalid. +- If neither the `Account` or the `Counterparty` field are the `LoanBroker.Owner`. +- If the `Counterparty` field is not specified and the submitting account is not `LoanBroker.Owner`. +- If the `Counterparty.Signature` is invalid. - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `IOU`: @@ -826,7 +831,7 @@ Either of the parties (Borrower or Loan Issuer) may initiate the transaction. Th - Insufficient First-Loss Capital: - `LoanBroker(LoanBrokerID).CoverAvailable` < `(LoanBroker(LoanBrokerID).DebtTotal + Loan.PrincipalRequested) x LoanBroker(LoanBrokerID).CoverRateMinimum` -##### 3.2.1.5 State Changes +##### 3.2.1.6 State Changes - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is `XRP`: @@ -867,7 +872,7 @@ Either of the parties (Borrower or Loan Issuer) may initiate the transaction. Th - Add `LoanID` to `DirectoryNode.Indexes` of the `LoanBroker` _pseudo-account_ `AccountRoot`. - Add `LoanID` to `DirectoryNode.Indexes` of the `Borrower` `AccountRoot`. -##### 3.2.1.4 Invariants +##### 3.2.1.7 Invariants **TBD** From b77401b6508f220c874b4c0a1f7fd47d17664e42 Mon Sep 17 00:00:00 2001 From: Vito Tumas <5780819+Tapanito@users.noreply.github.com> Date: Thu, 13 Mar 2025 09:44:09 +0100 Subject: [PATCH 14/77] Update XLS-0066d-lending-protocol/README.md Co-authored-by: Ed Hennis --- XLS-0066d-lending-protocol/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index 44b4c39d..027b24c2 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -450,7 +450,7 @@ The `LoanID` is calculated as follows: | `LoanServiceFee` | `No` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | A nominal funds amount paid to the `LoanBroker.Owner` with every Loan payment. | | `LatePaymentFee` | `No` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | A nominal funds amount paid to the `LoanBroker.Owner` when a payment is late. | | `ClosePaymentFee` | `No` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | A nominal funds amount paid to the `LoanBroker.Owner` when a payment full payment is made. | -| `OveraymentFee` | `No` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | A fee charged on overpayments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | +| `OverpaymentFee` | `No` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | A fee charged on overpayments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | | `InterestRate` | `No` | :heavy_check_mark: | `number` | `UINT16` | `N/A` | Annualized interest rate of the Loan in 1/10th basis points. | | `LateInterestRate` | `No` | :heavy_check_mark: | `number` | `UINT16` | `N/A` | A premium is added to the interest rate for late payments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | | `CloseInterestRate` | `No` | :heavy_check_mark: | `number` | `UINT16` | `N/A` | An interest rate charged for repaying the Loan early in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | From aa50126a96d4064ff6a65bac76e878d345763d29 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Wed, 19 Mar 2025 11:30:05 +0100 Subject: [PATCH 15/77] addresses PR comments --- XLS-0066d-lending-protocol/README.md | 134 +++++++++++++-------------- 1 file changed, 67 insertions(+), 67 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index 027b24c2..a869ecd7 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -200,27 +200,27 @@ The key of the `LoanBroker` object is the result of [`SHA512-Half`](https://xrpl The `LoanBroker` object has the following fields: -| Field Name | Modifiable? | Required? | JSON Type | Internal Type | Default Value | Description | -| ---------------------- | :---------: | :----------------: | :-------: | :-----------: | :-----------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `LedgerEntryType` | `N/A` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | Ledger object type. | -| `LedgerIndex` | `N/A` | :heavy_check_mark: | `string` | `UINT16` | `N/A` | Ledger object identifier. | -| `Flags` | `Yes` | :heavy_check_mark: | `string` | `UINT32` | 0 | Ledger object flags. | -| `PreviousTxnID` | `N/A` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the transaction that last modified this object. | -| `PreviousTxnLgrSeq` | `N/A` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The sequence of the ledger containing the transaction that last modified this object. | -| `Sequence` | `N/A` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The transaction sequence number that created the `LoanBroker`. | -| `OwnerNode` | `N/A` | :heavy_check_mark: | `number` | `UINT64` | `N/A` | Identifies the page where this item is referenced in the owner's directory. | -| `VaultNode` | `N/A` | :heavy_check_mark: | `number` | `UINT64` | `N/A` | Identifies the page where this item is referenced in the Vault's _pseudo-account_ owner's directory. | -| `VaultID` | `No` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the `Vault` object associated with this Lending Protocol Instance. | -| `Account` | `No` | :heavy_check_mark: | `string` | `AccountID` | `N/A` | The address of the `LoanBroker` _pseudo-account_. | -| `Owner` | `No` | :heavy_check_mark: | `string` | `AccountID` | `N/A` | The address of the Loan Broker account. | -| `Data` | `Yes` | | `string` | `BLOB` | None | Arbitrary metadata about the `LoanBroker`. Limited to 256 bytes. | -| `ManagementFeeRate` | `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` | `N/A` | :heavy_check_mark: | `number` | `UINT32` | 0 | The number of active Loans issued by the `LoanBroker`. | -| `DebtTotal` | `N/A` | :heavy_check_mark: | `number` | `NUMBER` | 0 | The total asset amount the protocol owes the Vault, including interest. | -| `DebtMaximum` | `Yes` | :heavy_check_mark: | `number` | `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` | `N/A` | :heavy_check_mark: | `number` | `NUMBER` | 0 | The total amount of first-loss capital deposited into the Lending Protocol. | -| `CoverRateMinimum` | `No` | :heavy_check_mark: | `number` | `UINT16` | 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%. | -| `CoverRateLiquidation` | `No` | :heavy_check_mark: | `number` | `UINT16` | 0 | The 1/10th basis point of minimum required first loss capital that is liquidated to cover a Loan default. Valid values are between 0 and 100000 inclusive. A value of 1 is equivalent to 1/10 bps or 0.001%. | +| Field Name | User Modifiable? | Constant? | Required? | JSON Type | Internal Type | Default Value | Description | +| ---------------------- | :--------------: | :-------: | :----------------: | :-------: | :-----------: | :-----------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `LedgerEntryType` | `No` | `Yes` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | Ledger object type. | +| `LedgerIndex` | `No` | `Yes` | :heavy_check_mark: | `string` | `UINT16` | `N/A` | Ledger object identifier. | +| `Flags` | `Yes` | `No` | :heavy_check_mark: | `string` | `UINT32` | 0 | Ledger object flags. | +| `PreviousTxnID` | `No` | `No` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the transaction that last modified this object. | +| `PreviousTxnLgrSeq` | `No` | `No` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The sequence of the ledger containing the transaction that last modified this object. | +| `Sequence` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The transaction sequence number that created the `LoanBroker`. | +| `OwnerNode` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT64` | `N/A` | Identifies the page where this item is referenced in the owner's directory. | +| `VaultNode` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT64` | `N/A` | Identifies the page where this item is referenced in the Vault's _pseudo-account_ owner's directory. | +| `VaultID` | `No` | `Yes` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the `Vault` object associated with this Lending Protocol Instance. | +| `Account` | `No` | `Yes` | :heavy_check_mark: | `string` | `AccountID` | `N/A` | The address of the `LoanBroker` _pseudo-account_. | +| `Owner` | `No` | `Yes` | :heavy_check_mark: | `string` | `AccountID` | `N/A` | The address of the Loan Broker account. | +| `Data` | `Yes` | `No` | | `string` | `BLOB` | None | Arbitrary metadata about the `LoanBroker`. Limited to 256 bytes. | +| `ManagementFeeRate` | `No` | `Yes` | | `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` | `No` | :heavy_check_mark: | `number` | `UINT32` | 0 | The number of active Loans issued by the `LoanBroker`. | +| `DebtTotal` | `No` | `No` | :heavy_check_mark: | `number` | `NUMBER` | 0 | The total asset amount the protocol owes the Vault, including interest. | +| `DebtMaximum` | `Yes` | `No` | :heavy_check_mark: | `number` | `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` | `No` | :heavy_check_mark: | `number` | `NUMBER` | 0 | The total amount of first-loss capital deposited into the Lending Protocol. | +| `CoverRateMinimum` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT16` | 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%. | +| `CoverRateLiquidation` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT16` | 0 | The 1/10th basis point of minimum required first loss capital that is liquidated to cover a Loan default. Valid values are between 0 and 100000 inclusive. A value of 1 is equivalent to 1/10 bps or 0.001%. | #### 2.1.3 `LoanBroker `_pseudo-account_` @@ -434,45 +434,45 @@ The `LoanID` is calculated as follows: #### 2.2.2 Fields -| Field Name | Modifiable? | Required? | JSON Type | Internal Type | Default Value | Description | -| ------------------------- | :---------: | :----------------: | :-------: | :-----------: | :-------------------------------------------------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `LedgerEntryType` | `N/A` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | Ledger object type. | -| `LedgerIndex` | `N/A` | :heavy_check_mark: | `string` | `UINT16` | `N/A` | Ledger object identifier. | -| `Flags` | `Yes` | | `string` | `UINT32` | 0 | Ledger object flags. | -| `PreviousTxnID` | `N/A` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the transaction that last modified this object. | -| `PreviousTxnLgrSeq` | `N/A` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The ledger sequence containing the transaction that last modified this object. | -| `Sequence` | `N/A` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The transaction sequence number that created the loan. | -| `OwnerNode` | `N/A` | :heavy_check_mark: | `number` | `UINT64` | `N/A` | Identifies the page where this item is referenced in the `Borrower` owner's directory. | -| `LoanBrokerNode` | `N/A` | :heavy_check_mark: | `number` | `UINT64` | `N/A` | Identifies the page where this item is referenced in the `LoanBroker`s owner directory. | -| `LoanBrokerID` | `No` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the `LoanBroker` associated with this Loan Instance. | -| `Borrower` | `No` | :heavy_check_mark: | `string` | `AccountID` | `N/A` | The address of the account that is the borrower. | -| `LoanOriginationFee` | `No` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | A nominal funds amount paid to the `LoanBroker.Owner` when the Loan is created. | -| `LoanServiceFee` | `No` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | A nominal funds amount paid to the `LoanBroker.Owner` with every Loan payment. | -| `LatePaymentFee` | `No` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | A nominal funds amount paid to the `LoanBroker.Owner` when a payment is late. | -| `ClosePaymentFee` | `No` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | A nominal funds amount paid to the `LoanBroker.Owner` when a payment full payment is made. | -| `OverpaymentFee` | `No` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | A fee charged on overpayments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | -| `InterestRate` | `No` | :heavy_check_mark: | `number` | `UINT16` | `N/A` | Annualized interest rate of the Loan in 1/10th basis points. | -| `LateInterestRate` | `No` | :heavy_check_mark: | `number` | `UINT16` | `N/A` | A premium is added to the interest rate for late payments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | -| `CloseInterestRate` | `No` | :heavy_check_mark: | `number` | `UINT16` | `N/A` | An interest rate charged for repaying the Loan early in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | -| `OverpaymentInterestRate` | `No` | :heavy_check_mark: | `number` | `UINT16` | `N/A` | An interest rate charged on overpayments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | -| `StartDate` | `No` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The timestamp of when the Loan starts [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time). | -| `PaymentInterval` | `No` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | Number of seconds between Loan payments. | -| `GracePeriod` | `No` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The number of seconds after the Payment Due Date that the Loan can be Defaulted. | -| `PreviousPaymentDate` | `N/A` | :heavy_check_mark: | `number` | `UINT32` | `0` | The timestamp of when the previous payment was made in [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time). | -| `NextPaymentDueDate` | `N/A` | :heavy_check_mark: | `number` | `UINT32` | `LoanSet.StartDate + LoanSet.PaymentInterval` | The timestamp of when the next payment is due in [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time). | -| `PaymentRemaining` | `N/A` | :heavy_check_mark: | `number` | `UINT32` | `LoanSet.PaymentTotal` | The number of payments remaining on the Loan. | -| `AssetAvailable` | `N/A` | :heavy_check_mark: | `number` | `NUMBER` | `LoanSet.[PrincipalRequested - LoanOriginationFee]` | The asset amount that is available in the Loan. | -| `PrincipalOutstanding` | `No` | :heavy_check_mark: | `number` | `NUMBER` | `LoanSet.PrincipalRequested` | The principal amount requested by the Borrower. | +| Field Name | User Modifiable? | Constant? | Required? | JSON Type | Internal Type | Default Value | Description | +| ------------------------- | :--------------: | :-------: | :----------------: | :-------: | :-----------: | :-------------------------------------------------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `LedgerEntryType` | `No` | `Yes` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | Ledger object type. | +| `LedgerIndex` | `No` | `Yes` | :heavy_check_mark: | `string` | `UINT16` | `N/A` | Ledger object identifier. | +| `Flags` | `Yes` | `No` | | `string` | `UINT32` | 0 | Ledger object flags. | +| `PreviousTxnID` | `No` | `No` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the transaction that last modified this object. | +| `PreviousTxnLgrSeq` | `No` | `No` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The ledger sequence containing the transaction that last modified this object. | +| `Sequence` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The transaction sequence number that created the loan. | +| `OwnerNode` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT64` | `N/A` | Identifies the page where this item is referenced in the `Borrower` owner's directory. | +| `LoanBrokerNode` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT64` | `N/A` | Identifies the page where this item is referenced in the `LoanBroker`s owner directory. | +| `LoanBrokerID` | `No` | `Yes` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the `LoanBroker` associated with this Loan Instance. | +| `Borrower` | `No` | `Yes` | :heavy_check_mark: | `string` | `AccountID` | `N/A` | The address of the account that is the borrower. | +| `LoanOriginationFee` | `No` | `Yes` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | A nominal funds amount paid to the `LoanBroker.Owner` when the Loan is created. | +| `LoanServiceFee` | `No` | `Yes` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | A nominal funds amount paid to the `LoanBroker.Owner` with every Loan payment. | +| `LatePaymentFee` | `No` | `Yes` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | A nominal funds amount paid to the `LoanBroker.Owner` when a payment is late. | +| `ClosePaymentFee` | `No` | `Yes` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | A nominal funds amount paid to the `LoanBroker.Owner` when a payment full payment is made. | +| `OveraymentFee` | `No` | `Yes` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | A fee charged on overpayments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | +| `InterestRate` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT16` | `N/A` | Annualized interest rate of the Loan in 1/10th basis points. | +| `LateInterestRate` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT16` | `N/A` | A premium is added to the interest rate for late payments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | +| `CloseInterestRate` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT16` | `N/A` | An interest rate charged for repaying the Loan early in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | +| `OverpaymentInterestRate` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT16` | `N/A` | An interest rate charged on overpayments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | +| `StartDate` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The timestamp of when the Loan starts [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time). | +| `PaymentInterval` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | Number of seconds between Loan payments. | +| `GracePeriod` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The number of seconds after the Payment Due Date that the Loan can be Defaulted. | +| `PreviousPaymentDate` | `No` | `No` | :heavy_check_mark: | `number` | `UINT32` | `0` | The timestamp of when the previous payment was made in [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time). | +| `NextPaymentDueDate` | `No` | `No` | :heavy_check_mark: | `number` | `UINT32` | `LoanSet.StartDate + LoanSet.PaymentInterval` | The timestamp of when the next payment is due in [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time). | +| `PaymentRemaining` | `No` | `No` | :heavy_check_mark: | `number` | `UINT32` | `LoanSet.PaymentTotal` | The number of payments remaining on the Loan. | +| `AssetAvailable` | `No` | `No` | :heavy_check_mark: | `number` | `NUMBER` | `LoanSet.[PrincipalRequested - LoanOriginationFee]` | The asset amount that is available in the Loan. | +| `PrincipalOutstanding` | `No` | `No` | :heavy_check_mark: | `number` | `NUMBER` | `LoanSet.PrincipalRequested` | The principal amount requested by the Borrower. | ##### 2.2.2.1 Flags The `Loan` object supports the following flags: -| Flag Name | Flag Value | Modifiable? | Description | -| -------------------- | :--------: | :---------: | :----------------------------------------------------: | -| `lsfLoanDefault` | `0x0001` | `No` | If set, indicates that the Loan is defaulted. | -| `lsfLoanImpaired` | `0x0002` | `Yes` | If set, indicates that the Loan is impaired. | -| `lsfLoanOverpayment` | `0x0004` | `No` | If set, indicates that the Loan supports overpayments. | +| Flag Name | Flag Value | Modifiable? | Description | +| -------------------- | :----------: | :---------: | :----------------------------------------------------: | +| `lsfLoanDefault` | `0x00010000` | `No` | If set, indicates that the Loan is defaulted. | +| `lsfLoanImpaired` | `0x00020000` | `Yes` | If set, indicates that the Loan is impaired. | +| `lsfLoanOverpayment` | `0x00040000` | `No` | If set, indicates that the Loan supports overpayments. | #### 2.2.3 Ownership @@ -561,7 +561,7 @@ The transaction creates a new `LoanBroker` object or updates an existing one. | Field Name | Required? | JSON Type | Internal Type | Default Value | Description | | ----------------- | :----------------: | :-------: | :-----------: | :-----------: | :--------------------------------------------------- | | `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | The transaction type. | -| `LoanBrokerID` | | `string` | `HASH256` | `N/A` | The Loan Broker ID that the transaction is deleting. | +| `LoanBrokerID` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The Loan Broker ID that the transaction is deleting. | ##### 3.1.2.1 Failure Conditions @@ -738,9 +738,9 @@ The transaction creates a new `Loan` object. ##### 3.2.1.1 `Flags` -| Flag Name | Flag Value | Description | -| ------------------- | :--------: | :---------------------------------------------- | -| `tfLoanOverpayment` | `0x0001` | Indicates that the vault supports overpayments. | +| Flag Name | Flag Value | Description | +| ------------------- | :----------: | :---------------------------------------------- | +| `tfLoanOverpayment` | `0x00010000` | Indicates that the vault supports overpayments. | ##### 3.2.1.2 `CounterpartySignature` @@ -749,16 +749,16 @@ An inner object that contains the signature of the Lender over the transaction. | Field Name | Required? | JSON Type | Internal Type | Default Value | Description | | --------------- | :----------------: | :-------: | :-----------: | :-----------: | :----------------------------------------------------------------------------------------------------------------- | | `SigningPubKey` | :heavy_check_mark: | `string` | `STBlob` | `N/A` | The Public Key to be used to verify the validity of the signature. | -| `Signature` | :heavy_check_mark: | `string` | `STBlob` | `N/A` | The signature of over all signing fields. | +| `TxSignature` | :heavy_check_mark: | `string` | `STBlob` | `N/A` | The signature of over all signing fields. | | `Signers` | :heavy_check_mark: | `list` | `STArray` | `N/A` | An array of transaction signatures from the `Counterparty` signers to indicate their approval of this transaction. | -The final transaction must include `Signature` or `Signers`. +The final transaction must include `TxSignature` or `Signers` fields. If the `Signers` field is necessary, then the total fee for the transaction will be increased due to the extra signatures that need to be processed, similar to the additional fees for multisigning. The minimum fee will be $(|signatures| + 1) \times base_fee$ The total fee calculation for signatures will now be $(1 + |tx.Signers| + |tx.Lender.Signers|) \times base_fee$. -This field is not a signing field (it will not be included in transaction signatures, though the `Signature` or `Signers` field will be included in the stored transaction). +This field is not a signing field (it will not be included in transaction signatures, though the `TxSignature` or `Signers` field will be included in the stored transaction). ##### 3.2.1.3 Multi-Signing @@ -940,11 +940,11 @@ The transaction deletes an existing `Loan` object. ##### 3.2.3.1 `Flags` -| Flag Name | Flag Value | Description | -| ---------------- | :--------: | :--------------------------------------------- | -| `tfLoanDefault` | `0x0001` | Indicates that the Loan should be defaulted. | -| `tfLoanImpair` | `0x0002` | Indicates that the Loan should be impaired. | -| `tfLoanUnimpair` | `0x0004` | Indicates that the Loan should be un-impaired. | +| Flag Name | Flag Value | Description | +| ---------------- | :----------: | :--------------------------------------------- | +| `tfLoanDefault` | `0x00010000` | Indicates that the Loan should be defaulted. | +| `tfLoanImpair` | `0x00020000` | Indicates that the Loan should be impaired. | +| `tfLoanUnimpair` | `0x00040000` | Indicates that the Loan should be un-impaired. | ##### 3.2.3.1 Failure Conditions From 9acddc13041366dfcae1fbda44a7727859733c8f Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Wed, 19 Mar 2025 11:31:50 +0100 Subject: [PATCH 16/77] fixes signature field name --- XLS-0066d-lending-protocol/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index a869ecd7..51c259a0 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -749,16 +749,16 @@ An inner object that contains the signature of the Lender over the transaction. | Field Name | Required? | JSON Type | Internal Type | Default Value | Description | | --------------- | :----------------: | :-------: | :-----------: | :-----------: | :----------------------------------------------------------------------------------------------------------------- | | `SigningPubKey` | :heavy_check_mark: | `string` | `STBlob` | `N/A` | The Public Key to be used to verify the validity of the signature. | -| `TxSignature` | :heavy_check_mark: | `string` | `STBlob` | `N/A` | The signature of over all signing fields. | +| `TxnSignature` | :heavy_check_mark: | `string` | `STBlob` | `N/A` | The signature of over all signing fields. | | `Signers` | :heavy_check_mark: | `list` | `STArray` | `N/A` | An array of transaction signatures from the `Counterparty` signers to indicate their approval of this transaction. | -The final transaction must include `TxSignature` or `Signers` fields. +The final transaction must include `TxnSignature` or `Signers` fields. If the `Signers` field is necessary, then the total fee for the transaction will be increased due to the extra signatures that need to be processed, similar to the additional fees for multisigning. The minimum fee will be $(|signatures| + 1) \times base_fee$ The total fee calculation for signatures will now be $(1 + |tx.Signers| + |tx.Lender.Signers|) \times base_fee$. -This field is not a signing field (it will not be included in transaction signatures, though the `TxSignature` or `Signers` field will be included in the stored transaction). +This field is not a signing field (it will not be included in transaction signatures, though the `TxnSignature` or `Signers` field will be included in the stored transaction). ##### 3.2.1.3 Multi-Signing @@ -799,7 +799,7 @@ The account specified in the `Account` field pays the transaction fee. - `LoanBroker` object with the specified `LoanBrokerID` does not exist on the ledger. - If neither the `Account` or the `Counterparty` field are the `LoanBroker.Owner`. - If the `Counterparty` field is not specified and the submitting account is not `LoanBroker.Owner`. -- If the `Counterparty.Signature` is invalid. +- If the `Counterparty.TxnSignature` is invalid. - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `IOU`: From 294f58280e92cb905df29b5351f7a736122e565c Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Fri, 4 Apr 2025 12:42:47 +0200 Subject: [PATCH 17/77] changes internal type of cover rate variables --- XLS-0066d-lending-protocol/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index 51c259a0..1e68126f 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -219,8 +219,8 @@ The `LoanBroker` object has the following fields: | `DebtTotal` | `No` | `No` | :heavy_check_mark: | `number` | `NUMBER` | 0 | The total asset amount the protocol owes the Vault, including interest. | | `DebtMaximum` | `Yes` | `No` | :heavy_check_mark: | `number` | `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` | `No` | :heavy_check_mark: | `number` | `NUMBER` | 0 | The total amount of first-loss capital deposited into the Lending Protocol. | -| `CoverRateMinimum` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT16` | 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%. | -| `CoverRateLiquidation` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT16` | 0 | The 1/10th basis point of minimum required first loss capital that is liquidated to cover a Loan default. Valid values are between 0 and 100000 inclusive. A value of 1 is equivalent to 1/10 bps or 0.001%. | +| `CoverRateMinimum` | `No` | `Yes` | :heavy_check_mark: | `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%. | +| `CoverRateLiquidation` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | 0 | The 1/10th basis point of minimum required first loss capital that is liquidated to cover a Loan default. Valid values are between 0 and 100000 inclusive. A value of 1 is equivalent to 1/10 bps or 0.001%. | #### 2.1.3 `LoanBroker `_pseudo-account_` @@ -510,8 +510,8 @@ The transaction creates a new `LoanBroker` object or updates an existing one. | `Data` | | `string` | `BLOB` | None | Arbitrary metadata in hex format. The field is limited to 256 bytes. | | `ManagementFeeRate` | | `number` | `UINT16` | 0 | The 1/10th basis point fee charged by the Lending Protocol Owner. Valid values are between 0 and 10000 inclusive. | | `DebtMaximum` | | `number` | `NUMBER` | 0 | The maximum amount the protocol can owe the Vault. The default value of 0 means there is no limit to the debt. | -| `CoverRateMinimum` | | `number` | `UINT16` | 0 | The 1/10th basis point `DebtTotal` that the first loss capital must cover. Valid values are between 0 and 100000 inclusive. | -| `CoverRateLiquidation` | | `number` | `UINT16` | 0 | The 1/10th basis point of minimum required first loss capital liquidated to cover a Loan default. Valid values are between 0 and 100000 inclusive. | +| `CoverRateMinimum` | | `number` | `UINT32` | 0 | The 1/10th basis point `DebtTotal` that the first loss capital must cover. Valid values are between 0 and 100000 inclusive. | +| `CoverRateLiquidation` | | `number` | `UINT32` | 0 | The 1/10th basis point of minimum required first loss capital liquidated to cover a Loan default. Valid values are between 0 and 100000 inclusive. | ##### 3.1.1.1 Failure Conditions From 8740e2f01afc05c8740d6f110ea58cd24487ba23 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Fri, 4 Apr 2025 12:43:26 +0200 Subject: [PATCH 18/77] renames singular vault attributes to plural --- XLS-0066d-lending-protocol/README.md | 82 ++++++++++++++-------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index 1e68126f..2c0ba596 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -251,9 +251,9 @@ Example 1: # Issuing a Loan # ** Initial States ** -- Vault -- -AssetTotal = 100,000 Tokens -AssetAvailable = 100,000 Tokens -ShareTotal = 100,000 Shares +AssetsTotal = 100,000 Tokens +AssetsAvailable = 100,000 Tokens +SharesTotal = 100,000 Shares -- Lending Protocol -- DebtTotal = 0 @@ -274,16 +274,16 @@ LoanInterest = LoanPrincipal x LoanInterestRate -- Vault -- # Increase the potential value of the Vault -AssetTotal = AssetTotal + ((LoanInterest - (LoanInterest x ManagementFeeRate))) +AssetsTotal = AssetsTotal + ((LoanInterest - (LoanInterest x ManagementFeeRate))) = 100,000 + (100 - (100 x 0.1)) = 100,000 + 90 = 100,090 Tokens # Decrease Asset Available in the Vault -AssetAvailable = AssetAvailable - LoanPrincipal +AssetsAvailable = AssetsAvailable - LoanPrincipal = 100,000 - 1,000 = 99,000 Tokens -ShareTotal = (UNCHANGED) +SharesTotal = (UNCHANGED) -- Lending Protocol -- # Increase Lending Protocol Debt @@ -299,9 +299,9 @@ Example 2: # Loan Payment # ** Initial States ** -- Vault -- -AssetTotal = 100,090 Tokens -AssetAvailable = 99,000 Tokens -ShareTotal = 100,000 Shares +AssetsTotal = 100,090 Tokens +AssetsAvailable = 99,000 Tokens +SharesTotal = 100,000 Shares -- Lending Protocol -- DebtTotal = 1,090 Tokens @@ -329,14 +329,14 @@ PaymentInterestPortion = 50 Tokens ** State Changes ** -- Vault -- -AssetTotal = (UNCHANGED) +AssetsTotal = (UNCHANGED) # Increase Asset Available in the Vault -AssetAvailable = AssetAvailable + PaymentPrincipalPortion + (PaymentInterestPortion - (PaymentInterestPortion x ManagementFeeRate) +AssetsAvailable = AssetsAvailable + PaymentPrincipalPortion + (PaymentInterestPortion - (PaymentInterestPortion x ManagementFeeRate) = 99,000 + 500 + (50 - (50 x 0.1)) = 99,545 Tokens -ShareTotal = (UNCHANGED) +SharesTotal = (UNCHANGED) -- Lending Protocol -- @@ -368,9 +368,9 @@ Example 1: Loan Default ** Initial States ** -- Vault -- -AssetTotal = 100,090 Tokens -AssetAvailable = 99,000 Tokens -ShareTotal = 100,000 Tokens +AssetsTotal = 100,090 Tokens +AssetsAvailable = 99,000 Tokens +SharesTotal = 100,000 Tokens -- Lending Protocol -- DebtTotal = 1,090 Tokens @@ -396,15 +396,15 @@ DefaultRemaining = DefaultAmount - DefaultCovered ** State Changes ** -- Vault -- -AssetTotal = AssetTotal - DefaultRemaining +AssetsTotal = AssetsTotal - DefaultRemaining = 100,090 - 1,079.1 = 99,010.9 Tokens -AssetAvailable = AssetAvailable + DefaultCovered +AssetsAvailable = AssetsAvailable + DefaultCovered = 99,000 + 10.9 = 99,010.9 Tokens -ShareTotal = (UNCHANGED) +SharesTotal = (UNCHANGED) -- Lending Protocol -- DebtTotal = DebtTotal - DefaultAmount @@ -461,7 +461,7 @@ The `LoanID` is calculated as follows: | `PreviousPaymentDate` | `No` | `No` | :heavy_check_mark: | `number` | `UINT32` | `0` | The timestamp of when the previous payment was made in [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time). | | `NextPaymentDueDate` | `No` | `No` | :heavy_check_mark: | `number` | `UINT32` | `LoanSet.StartDate + LoanSet.PaymentInterval` | The timestamp of when the next payment is due in [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time). | | `PaymentRemaining` | `No` | `No` | :heavy_check_mark: | `number` | `UINT32` | `LoanSet.PaymentTotal` | The number of payments remaining on the Loan. | -| `AssetAvailable` | `No` | `No` | :heavy_check_mark: | `number` | `NUMBER` | `LoanSet.[PrincipalRequested - LoanOriginationFee]` | The asset amount that is available in the Loan. | +| `AssetsAvailable` | `No` | `No` | :heavy_check_mark: | `number` | `NUMBER` | `LoanSet.[PrincipalRequested - LoanOriginationFee]` | The asset amount that is available in the Loan. | | `PrincipalOutstanding` | `No` | `No` | :heavy_check_mark: | `number` | `NUMBER` | `LoanSet.PrincipalRequested` | The principal amount requested by the Borrower. | ##### 2.2.2.1 Flags @@ -822,7 +822,7 @@ The account specified in the `Account` field pays the transaction fee. - Insufficient assets in the Vault: - - `Vault(LoanBroker(LoanBrokerID).VaultID).AssetAvailable` < `Loan.PrincipalRequested`. + - `Vault(LoanBroker(LoanBrokerID).VaultID).AssetsAvailable` < `Loan.PrincipalRequested`. - Exceeds maximum Debt of the LoanBroker: @@ -859,10 +859,10 @@ The account specified in the `Account` field pays the transaction fee. - Decrease Asset Available in the Vault: - - `Vault.AssetAvailable -= Loan.PrincipalRequested`. + - `Vault.AssetsAvailable -= Loan.PrincipalRequested`. - Increase the Total Value of the Vault: - - `Vault.AssetTotal += LoanInterest - (LoanInterest x LoanBroker.ManagementFeeRate)` where `LoanInterest` is the Loan's total interest. + - `Vault.AssetsTotal += LoanInterest - (LoanInterest x LoanBroker.ManagementFeeRate)` where `LoanInterest` is the Loan's total interest. - `LoanBroker(LoanBrokerID)` object changes: @@ -896,22 +896,22 @@ The transaction deletes an existing `Loan` object. ##### 3.2.2.2 State Changes -- If `Loan(LoanID).AssetAvailable > 0` (transfer remaining funds to the borrower): +- If `Loan(LoanID).AssetsAvailable > 0` (transfer remaining funds to the borrower): - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is `XRP`: - - Decrease the `Balance` field of `LoanBroker` _pseudo-account_ `AccountRoot` by `Loan(LoanID).AssetAvailable`. - - Increase the `Balance` field of `Loan(LoanID).Borrower` `AccountRoot` by `Loan(LoanID).AssetAvailable`. + - Decrease the `Balance` field of `LoanBroker` _pseudo-account_ `AccountRoot` by `Loan(LoanID).AssetsAvailable`. + - Increase the `Balance` field of `Loan(LoanID).Borrower` `AccountRoot` by `Loan(LoanID).AssetsAvailable`. - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `IOU`: - - Decrease the `RippleState` balance between the `LoanBroker` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `Loan(LoanID).AssetAvailable`. - - Increase the `RippleState` balance between the `Loan(LoanID).Borrower` `AccountRoot` and the `Issuer` `AccountRoot` by `Loan(LoanID).AssetAvailable`. + - Decrease the `RippleState` balance between the `LoanBroker` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `Loan(LoanID).AssetsAvailable`. + - Increase the `RippleState` balance between the `Loan(LoanID).Borrower` `AccountRoot` and the `Issuer` `AccountRoot` by `Loan(LoanID).AssetsAvailable`. - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `MPT`: - - Decrease the `MPToken.MPTAmount` of the `LoanBroker` _pseudo-account_ `MPToken` object for the `Vault.Asset` by `Loan(LoanID).AssetAvailable`. - - Increase the `MPToken.MPTAmount` of the `Loan(LoanID).Borrower` `MPToken` object for the `Vault.Asset` by `Loan(LoanID).AssetAvailable` + - Decrease the `MPToken.MPTAmount` of the `LoanBroker` _pseudo-account_ `MPToken` object for the `Vault.Asset` by `Loan(LoanID).AssetsAvailable`. + - Increase the `MPToken.MPTAmount` of the `Loan(LoanID).Borrower` `MPToken` object for the `Vault.Asset` by `Loan(LoanID).AssetsAvailable` - Delete the `Loan` object. @@ -966,7 +966,7 @@ The transaction deletes an existing `Loan` object. - Calculate the amount of the Default that First-Loss Capital covers: - The default Amount equals the outstanding principal and interest, excluding any funds unclaimed by the Borrower. - - `DefaultAmount = (Loan.PrincipalOutstanding + Loan.InterestOutstanding) - Loan.AssetAvailable`. + - `DefaultAmount = (Loan.PrincipalOutstanding + Loan.InterestOutstanding) - Loan.AssetsAvailable`. - Apply the First-Loss Capital to the Default Amount - `DefaultCovered = min((LoanBroker(Loan.LoanBrokerID).DebtTotal x LoanBroker(Loan.LoanBrokerID).CoverRateMinimum) x LoanBroker(Loan.LoanBrokerID).CoverRateLiquidation, DefaultAmount)` - `DefaultAmount -= DefaultCovered` @@ -974,15 +974,15 @@ The transaction deletes an existing `Loan` object. - Update the `Vault` object: - Decrease the Total Value of the Vault: - - `Vault(LoanBroker(LoanBrokerID).VaultID).AssetTotal -= DefaultAmount`. + - `Vault(LoanBroker(LoanBrokerID).VaultID).AssetsTotal -= DefaultAmount`. - Increase the Asset Available of the Vault by liquidated First-Loss Capital and any unclaimed funds amount: - - `Vault(LoanBroker(LoanBrokerID).VaultID).AssetAvailable += DefaultCovered + Loan.AssetAvailable`. + - `Vault(LoanBroker(LoanBrokerID).VaultID).AssetsAvailable += DefaultCovered + Loan.AssetsAvailable`. - Update the `LoanBroker` object: - Decrease the Debt of the LoanBroker: - `LoanBroker(LoanBrokerID).DebtTotal -= ` - - `Loan.PrincipalOutstanding + Loan.InterestOutstanding + Loan.AssetAvailable` + - `Loan.PrincipalOutstanding + Loan.InterestOutstanding + Loan.AssetsAvailable` - Decrease the First-Loss Capital Cover Available: - `LoanBroker(LoanBrokerID).CoverAvailable -= DefaultCovered` - Decrease the number of active Loans: @@ -992,7 +992,7 @@ The transaction deletes an existing `Loan` object. - `Loan(LoanID).Flags = lsfLoanDefault` - `Loan(LoanID).PaymentRemaining = 0` - - `Loan(LoanID).AssetAvailable = 0` + - `Loan(LoanID).AssetsAvailable = 0` - `Loan(LoanID).PrincipalOutstanding = 0` - Move the First-Loss Capital from the `LoanBroker` _pseudo-account_ to the `Vault` _pseudo-account_: @@ -1062,7 +1062,7 @@ The Borrower submits a `LoanDraw` transaction to draw funds from the Loan. - `Loan.StartDate > LastClosedLedger.CloseTime`. - There are insufficient assets in the `Loan`: - - `Loan.AssetAvailable` < `Amount`. + - `Loan.AssetsAvailable` < `Amount`. - The `Loan` has `lsfLoanImpaired` or `lsfLoanDefault` flags set. @@ -1097,7 +1097,7 @@ The Borrower submits a `LoanDraw` transaction to draw funds from the Loan. - Decrease the `MPToken.MPTAmount` by `Amount` of the `LoanBroker` _pseudo-account_ `MPToken` object for the `Vault.Asset`. - Increase the `MPToken.MPTAmount` by `Amount` of the submitter `MPToken` object for the `Vault.Asset`. -- Decrease `Loan.AssetAvailable` by `Amount`. +- Decrease `Loan.AssetsAvailable` by `Amount`. ##### 3.2.4.3 Invariants @@ -1189,7 +1189,7 @@ $$ latePaymentInterest = principalOutstanding \times \frac{lateInterestRate \times secondsSinceLastPayment}{365 \times 24 \times 60 \times 60} $$ -A late payment pays more interest than calculated when increasing the Vault value in the `LoanSet` transaction. Therefore, the total Vault value captured by `Vault.AssetTotal` must be recalculated. +A late payment pays more interest than calculated when increasing the Vault value in the `LoanSet` transaction. Therefore, the total Vault value captured by `Vault.AssetsTotal` must be recalculated. Assume the function `PeriodicPayment()` returns the expected periodic payment, split into `principalPeriodic` and `interestPeriodic`. Furthermore, assume the function `LatePayment()` that implements the Late Payment formula. The function returns the late payment split into `principalLate` and `interestLate`, where `interestLate` is calculated using the formula above. Note that `principalPeriodic == principalLate` and `interestLate > interestPeriodic` are used only when the payment is late. Otherwise, `interestLate == interestPeriodic`. @@ -1250,7 +1250,7 @@ $$ prepaymentPenalty = principalOutstanding \times closeInterestRate $$ -An early payment pays less interest than calculated when increasing the Vault value in the `LoanSet` transaction. Therefore, the Vault value (captured by `Vault.AssetTotal`) must be recalculated after an early payment. +An early payment pays less interest than calculated when increasing the Vault value in the `LoanSet` transaction. Therefore, the Vault value (captured by `Vault.AssetsTotal`) must be recalculated after an early payment. Assume a function `CurrentValue()` that returns `principalOutstanding` and `interestOutstanding` of the Loan. Furthermore, assume a function `ClosePayment()` that implements the Full Payment calculation. The function returns the total full payment due split into `principal` and `interest`. @@ -1496,15 +1496,15 @@ Furthermore, assume `full_periodic_payments` variable represents the number of p - Increase available assets in the Vault by the amount paid: - - `Vault.AssetAvailable = Vault.AssetAvailable + totalPaid` + - `Vault.AssetsAvailable = Vault.AssetsAvailable + totalPaid` - Update the Vault total value by the change in the Loan total value: - - `Vault.AssetTotal = Vault.AssetTotal + valueChange` + - `Vault.AssetsTotal = Vault.AssetsTotal + valueChange` - Update the Vault total value by the change in the management fee: - - `Vault.AssetTotal = Vault.AssetTotal - (vaultChange x LoanBroker.managementFeeRate)` + - `Vault.AssetsTotal = Vault.AssetsTotal - (vaultChange x LoanBroker.managementFeeRate)` - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is `XRP`: From b1754024b33d91874ff1de67f9c515ed8aff1713 Mon Sep 17 00:00:00 2001 From: Vito Tumas <5780819+Tapanito@users.noreply.github.com> Date: Thu, 24 Apr 2025 17:50:42 +0200 Subject: [PATCH 19/77] Apply suggestions from code review Co-authored-by: Ed Hennis --- XLS-0066d-lending-protocol/README.md | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index 2c0ba596..990ca44c 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -509,7 +509,7 @@ The transaction creates a new `LoanBroker` object or updates an existing one. | `Flags` | | `string` | `UINT32` | 0 | Specifies the flags for the Lending Protocol. | | `Data` | | `string` | `BLOB` | None | Arbitrary metadata in hex format. The field is limited to 256 bytes. | | `ManagementFeeRate` | | `number` | `UINT16` | 0 | The 1/10th basis point fee charged by the Lending Protocol Owner. Valid values are between 0 and 10000 inclusive. | -| `DebtMaximum` | | `number` | `NUMBER` | 0 | The maximum amount the protocol can owe the Vault. The default value of 0 means there is no limit to the debt. | +| `DebtMaximum` | | `number` | `NUMBER` | 0 | The maximum amount the protocol can owe the Vault. The default value of 0 means there is no limit to the debt. Must not be negative. | | `CoverRateMinimum` | | `number` | `UINT32` | 0 | The 1/10th basis point `DebtTotal` that the first loss capital must cover. Valid values are between 0 and 100000 inclusive. | | `CoverRateLiquidation` | | `number` | `UINT32` | 0 | The 1/10th basis point of minimum required first loss capital liquidated to cover a Loan default. Valid values are between 0 and 100000 inclusive. | @@ -595,7 +595,7 @@ The transaction creates a new `LoanBroker` object or updates an existing one. ##### 3.1.2.3 Invariants -- If `LoanBroker.OwnerCount = 0` the `DirectoryNode` for the `LoanBroker` does not exist +- If `LoanBroker.OwnerCount = 0` the `DirectoryNode` will have at most one node (the root), which will only hold entries for `RippleState` or `MPToken` objects. [**Return to Index**](#index) @@ -616,7 +616,7 @@ The transaction deposits First Loss Capital into the `LoanBroker` object. - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is `XRP`: - - `AccountRoot(LoanBroker.Owner).Balance < Amount` (LoanBroker does not have sufficient funds to deposit the First Loss Capital). + - `AccountRoot(LoanBroker.Owner).Balance - Reserve(AccountRoot(LoanBroker.Owner).OwnerCount) < Amount` (LoanBroker does not have sufficient funds to deposit the First Loss Capital). - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `IOU`: @@ -666,7 +666,7 @@ The `LoanBrokerCoverWithdraw` transaction withdraws the First-Loss Capital from | `LoanBrokerID` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The Loan Broker ID from which to withdraw First-Loss Capital. | | `Amount` | :heavy_check_mark: | `object` | `NUMBER` | 0 | The amount of Vault asset to withdraw. | -##### 3.2.2.1 Failure conditions +##### 3.1.4.1 Failure conditions - `LoanBroker` object with the specified `LoanBrokerID` does not exist on the ledger. - The submitter `AccountRoot.Account != LoanBroker(LoanBrokerID).Owner`. @@ -686,7 +686,7 @@ The `LoanBrokerCoverWithdraw` transaction withdraws the First-Loss Capital from - `LoanBroker.CoverAvailable - Amount` < `LoanBroker.DebtTotal * LoanBroker.CoverRateMinimum` -##### 3.2.2.2 State Changes +##### 3.1.4.2 State Changes - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is `XRP`: @@ -748,15 +748,17 @@ An inner object that contains the signature of the Lender over the transaction. | Field Name | Required? | JSON Type | Internal Type | Default Value | Description | | --------------- | :----------------: | :-------: | :-----------: | :-----------: | :----------------------------------------------------------------------------------------------------------------- | -| `SigningPubKey` | :heavy_check_mark: | `string` | `STBlob` | `N/A` | The Public Key to be used to verify the validity of the signature. | -| `TxnSignature` | :heavy_check_mark: | `string` | `STBlob` | `N/A` | The signature of over all signing fields. | -| `Signers` | :heavy_check_mark: | `list` | `STArray` | `N/A` | An array of transaction signatures from the `Counterparty` signers to indicate their approval of this transaction. | +| `SigningPubKey` | | `string` | `STBlob` | `N/A` | The Public Key to be used to verify the validity of the signature. | +| `TxnSignature` | | `string` | `STBlob` | `N/A` | The signature of over all signing fields. | +| `Signers` | | `list` | `STArray` | `N/A` | An array of transaction signatures from the `Counterparty` signers to indicate their approval of this transaction. | -The final transaction must include `TxnSignature` or `Signers` fields. +The final transaction must include exactly one of +1. The `SigningPubKey` and `TxnSignature` fields, or +2. The `Signers` field. -If the `Signers` field is necessary, then the total fee for the transaction will be increased due to the extra signatures that need to be processed, similar to the additional fees for multisigning. The minimum fee will be $(|signatures| + 1) \times base_fee$ +The total fee for the transaction will be increased due to the extra signatures that need to be processed, similar to the additional fees for multisigning. The minimum fee will be $(|signatures| + 1) \times base_fee$ where $|signatures| == max(1, |tx.CounterPartySignature.Signers|)$ -The total fee calculation for signatures will now be $(1 + |tx.Signers| + |tx.Lender.Signers|) \times base_fee$. +The total fee calculation for signatures will now be $(1 + |tx.Signers| + |signatures|) \times base_fee$. In other words, even without a `tx.Signers` list, the minimum fee will be $2 \times base_fee$. This field is not a signing field (it will not be included in transaction signatures, though the `TxnSignature` or `Signers` field will be included in the stored transaction). From 36ec7cb78414760032a6cbf5c12bbb14531a6ad9 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Thu, 24 Apr 2025 21:07:02 +0200 Subject: [PATCH 20/77] changes Amount type to AMOUNT --- XLS-0066d-lending-protocol/README.md | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index 990ca44c..37016f06 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -461,7 +461,7 @@ The `LoanID` is calculated as follows: | `PreviousPaymentDate` | `No` | `No` | :heavy_check_mark: | `number` | `UINT32` | `0` | The timestamp of when the previous payment was made in [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time). | | `NextPaymentDueDate` | `No` | `No` | :heavy_check_mark: | `number` | `UINT32` | `LoanSet.StartDate + LoanSet.PaymentInterval` | The timestamp of when the next payment is due in [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time). | | `PaymentRemaining` | `No` | `No` | :heavy_check_mark: | `number` | `UINT32` | `LoanSet.PaymentTotal` | The number of payments remaining on the Loan. | -| `AssetsAvailable` | `No` | `No` | :heavy_check_mark: | `number` | `NUMBER` | `LoanSet.[PrincipalRequested - LoanOriginationFee]` | The asset amount that is available in the Loan. | +| `AssetsAvailable` | `No` | `No` | :heavy_check_mark: | `number` | `NUMBER` | `LoanSet.[PrincipalRequested - LoanOriginationFee]` | The asset amount that is available in the Loan. | | `PrincipalOutstanding` | `No` | `No` | :heavy_check_mark: | `number` | `NUMBER` | `LoanSet.PrincipalRequested` | The principal amount requested by the Borrower. | ##### 2.2.2.1 Flags @@ -509,7 +509,7 @@ The transaction creates a new `LoanBroker` object or updates an existing one. | `Flags` | | `string` | `UINT32` | 0 | Specifies the flags for the Lending Protocol. | | `Data` | | `string` | `BLOB` | None | Arbitrary metadata in hex format. The field is limited to 256 bytes. | | `ManagementFeeRate` | | `number` | `UINT16` | 0 | The 1/10th basis point fee charged by the Lending Protocol Owner. Valid values are between 0 and 10000 inclusive. | -| `DebtMaximum` | | `number` | `NUMBER` | 0 | The maximum amount the protocol can owe the Vault. The default value of 0 means there is no limit to the debt. Must not be negative. | +| `DebtMaximum` | | `number` | `NUMBER` | 0 | The maximum amount the protocol can owe the Vault. The default value of 0 means there is no limit to the debt. Must not be negative. | | `CoverRateMinimum` | | `number` | `UINT32` | 0 | The 1/10th basis point `DebtTotal` that the first loss capital must cover. Valid values are between 0 and 100000 inclusive. | | `CoverRateLiquidation` | | `number` | `UINT32` | 0 | The 1/10th basis point of minimum required first loss capital liquidated to cover a Loan default. Valid values are between 0 and 100000 inclusive. | @@ -607,7 +607,7 @@ The transaction deposits First Loss Capital into the `LoanBroker` object. | ----------------- | :----------------: | :-------: | :-----------: | :-----------: | :------------------------------------------------ | | `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | The transaction type. | | `LoanBrokerID` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The Loan Broker ID to deposit First-Loss Capital. | -| `Amount` | :heavy_check_mark: | `object` | `NUMBER` | 0 | The Fist-Loss Capital amount to deposit. | +| `Amount` | :heavy_check_mark: | `object` | `AMOUNT` | 0 | The Fist-Loss Capital amount to deposit. | ##### 3.1.3.1 Failure Conditions @@ -664,7 +664,7 @@ The `LoanBrokerCoverWithdraw` transaction withdraws the First-Loss Capital from | ----------------- | :----------------: | :-------: | :-----------: | :-----------: | :------------------------------------------------------------ | | `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | Transaction type. | | `LoanBrokerID` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The Loan Broker ID from which to withdraw First-Loss Capital. | -| `Amount` | :heavy_check_mark: | `object` | `NUMBER` | 0 | The amount of Vault asset to withdraw. | +| `Amount` | :heavy_check_mark: | `object` | `AMOUNT` | 0 | The amount of Vault asset to withdraw. | ##### 3.1.4.1 Failure conditions @@ -746,13 +746,14 @@ The transaction creates a new `Loan` object. An inner object that contains the signature of the Lender over the transaction. The fields contained in this object are: -| Field Name | Required? | JSON Type | Internal Type | Default Value | Description | -| --------------- | :----------------: | :-------: | :-----------: | :-----------: | :----------------------------------------------------------------------------------------------------------------- | -| `SigningPubKey` | | `string` | `STBlob` | `N/A` | The Public Key to be used to verify the validity of the signature. | -| `TxnSignature` | | `string` | `STBlob` | `N/A` | The signature of over all signing fields. | -| `Signers` | | `list` | `STArray` | `N/A` | An array of transaction signatures from the `Counterparty` signers to indicate their approval of this transaction. | +| Field Name | Required? | JSON Type | Internal Type | Default Value | Description | +| --------------- | :-------: | :-------: | :-----------: | :-----------: | :----------------------------------------------------------------------------------------------------------------- | +| `SigningPubKey` | | `string` | `STBlob` | `N/A` | The Public Key to be used to verify the validity of the signature. | +| `TxnSignature` | | `string` | `STBlob` | `N/A` | The signature of over all signing fields. | +| `Signers` | | `list` | `STArray` | `N/A` | An array of transaction signatures from the `Counterparty` signers to indicate their approval of this transaction. | + +The final transaction must include exactly one of -The final transaction must include exactly one of 1. The `SigningPubKey` and `TxnSignature` fields, or 2. The `Signers` field. @@ -1054,7 +1055,7 @@ The Borrower submits a `LoanDraw` transaction to draw funds from the Loan. | ----------------- | :----------------: | :-------: | :-----------: | :-----------: | :------------------------------------------ | | `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | The transaction type. | | `LoanID` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the Loan object to be drawn from. | -| `Amount` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | The amount of funds to drawdown. | +| `Amount` | :heavy_check_mark: | `number` | `AMOUNT` | `N/A` | The amount of funds to drawdown. | ##### 3.2.4.1 Failure Conditions @@ -1115,7 +1116,7 @@ The Borrower submits a `LoanPay` transaction to make a Payment on the Loan. | ----------------- | :----------------: | :-------: | :-----------: | :-----------: | :--------------------------------------- | | `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | The transaction type. | | `LoanID` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the Loan object to be paid to. | -| `Amount` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | The amount of funds to pay. | +| `Amount` | :heavy_check_mark: | `number` | `AMOUNT` | `N/A` | The amount of funds to pay. | ##### 3.2.5.1 Payment Types From 6c9a815033dcd733a0c45ecb5a2c00f900301fe4 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Mon, 28 Apr 2025 16:07:24 +0200 Subject: [PATCH 21/77] adds owner reserve chages to LoanSet --- XLS-0066d-lending-protocol/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index 37016f06..e50dfe71 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -836,6 +836,9 @@ The account specified in the `Account` field pays the transaction fee. ##### 3.2.1.6 State Changes +- Create the `Loan` object. +- Increment `AccountRoot(Borrower).OwnerCount` by `1` + - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is `XRP`: - Decrease the `Balance` field of `Vault` _pseudo-account_ `AccountRoot` by `Loan.PrincipalRequested`. From ea0f369e51dd143d0c5d43f22c6c0af7c08c06ca Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Mon, 28 Apr 2025 16:10:55 +0200 Subject: [PATCH 22/77] changes InterestRate type to uint32 --- XLS-0066d-lending-protocol/README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index e50dfe71..c821b9f2 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -451,10 +451,10 @@ The `LoanID` is calculated as follows: | `LatePaymentFee` | `No` | `Yes` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | A nominal funds amount paid to the `LoanBroker.Owner` when a payment is late. | | `ClosePaymentFee` | `No` | `Yes` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | A nominal funds amount paid to the `LoanBroker.Owner` when a payment full payment is made. | | `OveraymentFee` | `No` | `Yes` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | A fee charged on overpayments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | -| `InterestRate` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT16` | `N/A` | Annualized interest rate of the Loan in 1/10th basis points. | -| `LateInterestRate` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT16` | `N/A` | A premium is added to the interest rate for late payments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | -| `CloseInterestRate` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT16` | `N/A` | An interest rate charged for repaying the Loan early in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | -| `OverpaymentInterestRate` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT16` | `N/A` | An interest rate charged on overpayments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | +| `InterestRate` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | Annualized interest rate of the Loan in 1/10th basis points. | +| `LateInterestRate` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | A premium is added to the interest rate for late payments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | +| `CloseInterestRate` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | An interest rate charged for repaying the Loan early in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | +| `OverpaymentInterestRate` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | An interest rate charged on overpayments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | | `StartDate` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The timestamp of when the Loan starts [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time). | | `PaymentInterval` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | Number of seconds between Loan payments. | | `GracePeriod` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The number of seconds after the Payment Due Date that the Loan can be Defaulted. | @@ -727,9 +727,9 @@ The transaction creates a new `Loan` object. | `LoanServiceFee` | | `number` | `NUMBER` | 0 | A nominal amount paid to the `LoanBroker.Owner` with every Loan payment. | | `LatePaymentFee` | | `number` | `NUMBER` | 0 | A nominal funds amount paid to the `LoanBroker.Owner` when a payment is late. | | `ClosePaymentFee` | | `number` | `NUMBER` | 0 | A nominal funds amount paid to the `LoanBroker.Owner` when an early full repayment is made. | -| `InterestRate` | | `number` | `UINT16` | 0 | Annualized interest rate of the Loan in basis points. | -| `LateInterestRate` | | `number` | `UINT16` | 0 | A premium added to the interest rate for late payments in basis points. Valid values are between 0 and 10000 inclusive. (0 - 100%) | -| `CloseInterestRate` | | `number` | `UINT16` | 0 | A Fee Rate charged for repaying the Loan early in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | +| `InterestRate` | | `number` | `UINT32` | 0 | Annualized interest rate of the Loan in basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | +| `LateInterestRate` | | `number` | `UINT32` | 0 | A premium added to the interest rate for late payments in basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | +| `CloseInterestRate` | | `number` | `UINT32` | 0 | A Fee Rate charged for repaying the Loan early in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | | `PrincipalRequested` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | The principal amount requested by the Borrower. | | `StartDate` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The timestamp of when the Loan starts [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time). | | `PaymentTotal` | | `number` | `UINT32` | 1 | The total number of payments to be made against the Loan. | From 0b136098edb160d9efff34c3eff42ae75bd7d8ba Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Mon, 28 Apr 2025 16:21:09 +0200 Subject: [PATCH 23/77] adds missing overpayment fields to LoanSet transaction --- XLS-0066d-lending-protocol/README.md | 46 +++++++++++++++------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index c821b9f2..989e7f88 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -450,7 +450,7 @@ The `LoanID` is calculated as follows: | `LoanServiceFee` | `No` | `Yes` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | A nominal funds amount paid to the `LoanBroker.Owner` with every Loan payment. | | `LatePaymentFee` | `No` | `Yes` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | A nominal funds amount paid to the `LoanBroker.Owner` when a payment is late. | | `ClosePaymentFee` | `No` | `Yes` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | A nominal funds amount paid to the `LoanBroker.Owner` when a payment full payment is made. | -| `OveraymentFee` | `No` | `Yes` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | A fee charged on overpayments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | +| `OverpaymentFee` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | A fee charged on overpayments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | | `InterestRate` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | Annualized interest rate of the Loan in 1/10th basis points. | | `LateInterestRate` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | A premium is added to the interest rate for late payments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | | `CloseInterestRate` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | An interest rate charged for repaying the Loan early in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | @@ -715,26 +715,28 @@ In this section we specify transactions associated with the `Loan` ledger entry. The transaction creates a new `Loan` object. -| Field Name | Required? | JSON Type | Internal Type | Default Value | Description | -| ----------------------- | :----------------: | :-------: | :-----------: | :-----------: | :-------------------------------------------------------------------------------------------------------------------------------------------- | -| `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | The transaction type. | -| `LoanBrokerID` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The Loan Broker ID associated with the loan. | -| `Flags` | | `string` | `UINT32` | 0 | Specifies the flags for the Loan. | -| `Data` | | `string` | `BLOB` | None | Arbitrary metadata in hex format. The field is limited to 256 bytes. | -| `Counterparty` | | `string` | `AccountID` | `N/A` | The address of the counterparty of the Loan. | -| `CounterpartySignature` | :heavy_check_mark: | `string` | `STObject` | `N/A` | The signature of the counterparty over the transaction. | -| `LoanOriginationFee` | | `number` | `NUMBER` | 0 | A nominal funds amount paid to the `LoanBroker.Owner` when the Loan is created. | -| `LoanServiceFee` | | `number` | `NUMBER` | 0 | A nominal amount paid to the `LoanBroker.Owner` with every Loan payment. | -| `LatePaymentFee` | | `number` | `NUMBER` | 0 | A nominal funds amount paid to the `LoanBroker.Owner` when a payment is late. | -| `ClosePaymentFee` | | `number` | `NUMBER` | 0 | A nominal funds amount paid to the `LoanBroker.Owner` when an early full repayment is made. | -| `InterestRate` | | `number` | `UINT32` | 0 | Annualized interest rate of the Loan in basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | -| `LateInterestRate` | | `number` | `UINT32` | 0 | A premium added to the interest rate for late payments in basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | -| `CloseInterestRate` | | `number` | `UINT32` | 0 | A Fee Rate charged for repaying the Loan early in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | -| `PrincipalRequested` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | The principal amount requested by the Borrower. | -| `StartDate` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The timestamp of when the Loan starts [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time). | -| `PaymentTotal` | | `number` | `UINT32` | 1 | The total number of payments to be made against the Loan. | -| `PaymentInterval` | | `number` | `UINT32` | 60 | Number of seconds between Loan payments. | -| `GracePeriod` | | `number` | `UINT32` | 60 | The number of seconds after the Loan's Payment Due Date can be Defaulted. | +| Field Name | Required? | JSON Type | Internal Type | Default Value | Description | +| ------------------------- | :----------------: | :-------: | :-----------: | :-----------: | :-------------------------------------------------------------------------------------------------------------------------------------------- | +| `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | The transaction type. | +| `LoanBrokerID` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The Loan Broker ID associated with the loan. | +| `Flags` | | `string` | `UINT32` | 0 | Specifies the flags for the Loan. | +| `Data` | | `string` | `BLOB` | None | Arbitrary metadata in hex format. The field is limited to 256 bytes. | +| `Counterparty` | | `string` | `AccountID` | `N/A` | The address of the counterparty of the Loan. | +| `CounterpartySignature` | :heavy_check_mark: | `string` | `STObject` | `N/A` | The signature of the counterparty over the transaction. | +| `LoanOriginationFee` | | `number` | `NUMBER` | 0 | A nominal funds amount paid to the `LoanBroker.Owner` when the Loan is created. | +| `LoanServiceFee` | | `number` | `NUMBER` | 0 | A nominal amount paid to the `LoanBroker.Owner` with every Loan payment. | +| `LatePaymentFee` | | `number` | `NUMBER` | 0 | A nominal funds amount paid to the `LoanBroker.Owner` when a payment is late. | +| `ClosePaymentFee` | | `number` | `NUMBER` | 0 | A nominal funds amount paid to the `LoanBroker.Owner` when an early full repayment is made. | +| `OverpaymentFee` | | `number` | `UINT32` | 0 | A fee charged on overpayments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | +| `InterestRate` | | `number` | `UINT32` | 0 | Annualized interest rate of the Loan in basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | +| `LateInterestRate` | | `number` | `UINT32` | 0 | A premium added to the interest rate for late payments in basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | +| `CloseInterestRate` | | `number` | `UINT32` | 0 | A Fee Rate charged for repaying the Loan early in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | +| `OverpaymentInterestRate` | | `number` | `UINT32` | 0 | An interest rate charged on overpayments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | +| `PrincipalRequested` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | The principal amount requested by the Borrower. | +| `StartDate` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The timestamp of when the Loan starts [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time). | +| `PaymentTotal` | | `number` | `UINT32` | 1 | The total number of payments to be made against the Loan. | +| `PaymentInterval` | | `number` | `UINT32` | 60 | Number of seconds between Loan payments. | +| `GracePeriod` | | `number` | `UINT32` | 60 | The number of seconds after the Loan's Payment Due Date can be Defaulted. | ##### 3.2.1.1 `Flags` @@ -1397,7 +1399,7 @@ function make_payment(amount, current_time) -> (principal_paid, interest_paid, v loan.principal_outstanding -= total_principal_paid let overpayment = min(loan.principal_outstanding, amount % periodic_payment) - if overpayment > 0 && is_set(lsfOverayment) { + if overpayment > 0 && is_set(lsfOverpayment) { let interest_portion = overpayment * loan.overpayment_interest_rate let fee_portion = overpayment * loan.overpayment_fee let remainder = overpayment - interest_portion - fee_portion From b631e671077952d015f04f056d365a5418fc7f3a Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Tue, 29 Apr 2025 15:20:07 +0200 Subject: [PATCH 24/77] adds pseudo-account locked/frozen checks to LoanBrokerCoverWithdraw --- XLS-0066d-lending-protocol/README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index 989e7f88..eba50fbc 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -630,6 +630,7 @@ The transaction deposits First Loss Capital into the `LoanBroker` object. - Has `lsfMPTLocked` flag set. - `MPTAmount` < `Amount`. - The `MPTokenIssuance` object of the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` has the `lsfMPTLocked` flag set. + - The `MPTokenIssuance` object of the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` does not have the `lsfMPTCanTransfer` flag set (the asset is not transferable). ##### 3.1.3.2 State Changes @@ -673,15 +674,17 @@ The `LoanBrokerCoverWithdraw` transaction withdraws the First-Loss Capital from - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `IOU`: - - The trustline between the submitter account and the `Issuer` of the asset is frozen. + - The `RippleState` object between the submitter account and the `Issuer` of the asset has the `lsfLowFreeze` or `lsfHighFreeze` flag set. - The `AccountRoot` object of the `Issuer` has the `lsfGlobalFreeze` flag set. + - The `RippleState` between the `LoanBroker.Account` and the `Issuer` has the `lsfLowFreeze` or `lsfHighFreeze` flag set. - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `MPT`: - - The `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the submitter `AccountRoot`: - - Has `lsfMPTLocked` flag set. + - The `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the submitter `AccountRoot` has `lsfMPTLocked` flag set. - The `MPTokenIssuance` object of the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` has the `lsfMPTLocked` flag set. - + - The `MPTokenIssuance` object of the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` does not have the `lsfMPTCanTransfer` flag set (the asset is not transferable). + - The `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the `LoanBroker.Account` `AccountRoot` has `lsfMPTLocked` flag set. (The Loan Broker _pseudo-account_ is locked). + - The `LoanBroker.CoverAvailable` < `Amount`. - `LoanBroker.CoverAvailable - Amount` < `LoanBroker.DebtTotal * LoanBroker.CoverRateMinimum` From ff058bfc615f5601acea0662f76b2f29c31a9392 Mon Sep 17 00:00:00 2001 From: Vito Tumas <5780819+Tapanito@users.noreply.github.com> Date: Tue, 29 Apr 2025 16:06:54 +0200 Subject: [PATCH 25/77] Update XLS-0066d-lending-protocol/README.md Co-authored-by: Ed Hennis --- XLS-0066d-lending-protocol/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index eba50fbc..d758e256 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -806,7 +806,7 @@ The account specified in the `Account` field pays the transaction fee. - `LoanBroker` object with the specified `LoanBrokerID` does not exist on the ledger. - If neither the `Account` or the `Counterparty` field are the `LoanBroker.Owner`. -- If the `Counterparty` field is not specified and the submitting account is not `LoanBroker.Owner`. +- If the `Counterparty` field is not specified and the `CounterpartySignature` is not from the `LoanBroker.Owner`. - If the `Counterparty.TxnSignature` is invalid. - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `IOU`: From 13987664c50540b55a2a1fcd29f0ec1973309574 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Thu, 1 May 2025 11:48:49 +0200 Subject: [PATCH 26/77] Improves how Loan ID is calculated by introducing a new LoanSequence field --- XLS-0066d-lending-protocol/README.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index d758e256..ba7f4821 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -208,6 +208,7 @@ The `LoanBroker` object has the following fields: | `PreviousTxnID` | `No` | `No` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the transaction that last modified this object. | | `PreviousTxnLgrSeq` | `No` | `No` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The sequence of the ledger containing the transaction that last modified this object. | | `Sequence` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The transaction sequence number that created the `LoanBroker`. | +| `LoanSequence` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | 0 | A sequential identifier for Loan objects, incremented each time a new Loan is created by this LoanBroker instance. | | `OwnerNode` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT64` | `N/A` | Identifies the page where this item is referenced in the owner's directory. | | `VaultNode` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT64` | `N/A` | Identifies the page where this item is referenced in the Vault's _pseudo-account_ owner's directory. | | `VaultID` | `No` | `Yes` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the `Vault` object associated with this Lending Protocol Instance. | @@ -430,7 +431,7 @@ The `LoanID` is calculated as follows: - The `Loan` space key `0x004C` (capital L) - The [`AccountID`](https://xrpl.org/docs/references/protocol/binary-format/#accountid-fields) of the Borrower account. - The `LoanBrokerID` of the associated `LoanBroker` object. - - The `Sequence` number of the **`LoanSet`** transaction. If the transaction used a [Ticket](https://xrpl.org/docs/concepts/accounts/tickets/), use the `TicketSequence` value. + - The `LoanSequence` of the `LoanBroker` object. #### 2.2.2 Fields @@ -442,6 +443,7 @@ The `LoanID` is calculated as follows: | `PreviousTxnID` | `No` | `No` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the transaction that last modified this object. | | `PreviousTxnLgrSeq` | `No` | `No` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The ledger sequence containing the transaction that last modified this object. | | `Sequence` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The transaction sequence number that created the loan. | +| `LoanSequence` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The sequence number of the Loan. | | `OwnerNode` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT64` | `N/A` | Identifies the page where this item is referenced in the `Borrower` owner's directory. | | `LoanBrokerNode` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT64` | `N/A` | Identifies the page where this item is referenced in the `LoanBroker`s owner directory. | | `LoanBrokerID` | `No` | `Yes` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the `LoanBroker` associated with this Loan Instance. | @@ -842,7 +844,8 @@ The account specified in the `Account` field pays the transaction fee. ##### 3.2.1.6 State Changes - Create the `Loan` object. -- Increment `AccountRoot(Borrower).OwnerCount` by `1` +- Increment `AccountRoot(Borrower).OwnerCount` by `1`. +- Increment `LoanBroker.LoanSequence` by `1`. - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is `XRP`: @@ -1569,3 +1572,9 @@ Furthermore, assume `full_periodic_payments` variable represents the number of p **TBD** # Appendix + +## A-1 F.A.Q. + +### A-1.1 What is the `LoanBroker.LoanSequence` field? + +A sequential identifier for Loans associated with a LoanBroker object. This value increments with each new Loan created by the broker. Unlike `LoanBroker.OwnerCount`, which tracks the number of currently active Loans, `LoanBroker.LoanSequence` reflects the total number of Loans ever created. \ No newline at end of file From 01d89816658ea7e17d06fb5bdf9231625f2b047b Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Thu, 1 May 2025 17:50:38 +0200 Subject: [PATCH 27/77] removes sequence from the Loan object --- XLS-0066d-lending-protocol/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index ba7f4821..6c41bfc2 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -442,7 +442,6 @@ The `LoanID` is calculated as follows: | `Flags` | `Yes` | `No` | | `string` | `UINT32` | 0 | Ledger object flags. | | `PreviousTxnID` | `No` | `No` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the transaction that last modified this object. | | `PreviousTxnLgrSeq` | `No` | `No` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The ledger sequence containing the transaction that last modified this object. | -| `Sequence` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The transaction sequence number that created the loan. | | `LoanSequence` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The sequence number of the Loan. | | `OwnerNode` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT64` | `N/A` | Identifies the page where this item is referenced in the `Borrower` owner's directory. | | `LoanBrokerNode` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT64` | `N/A` | Identifies the page where this item is referenced in the `LoanBroker`s owner directory. | From 7e923c2449c229c136cecd19c1907a3e92e6b271 Mon Sep 17 00:00:00 2001 From: Vito Tumas <5780819+Tapanito@users.noreply.github.com> Date: Mon, 5 May 2025 14:37:57 +0200 Subject: [PATCH 28/77] Apply suggestions from code review Co-authored-by: Ed Hennis --- XLS-0066d-lending-protocol/README.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index 6c41bfc2..df2a9916 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -974,7 +974,7 @@ The transaction deletes an existing `Loan` object. ##### 3.2.3.2 State Changes -- If the `tfDefault` flag is specified: +- If the `tfLoanDefault` flag is specified: - Calculate the amount of the Default that First-Loss Capital covers: @@ -998,12 +998,10 @@ The transaction deletes an existing `Loan` object. - `Loan.PrincipalOutstanding + Loan.InterestOutstanding + Loan.AssetsAvailable` - Decrease the First-Loss Capital Cover Available: - `LoanBroker(LoanBrokerID).CoverAvailable -= DefaultCovered` - - Decrease the number of active Loans: - - `LoanBroker(LoanBrokerID).OwnerCount -= 1` - Update the `Loan` object: - - `Loan(LoanID).Flags = lsfLoanDefault` + - `Loan(LoanID).Flags |= lsfLoanDefault` - `Loan(LoanID).PaymentRemaining = 0` - `Loan(LoanID).AssetsAvailable = 0` - `Loan(LoanID).PrincipalOutstanding = 0` @@ -1032,7 +1030,7 @@ The transaction deletes an existing `Loan` object. - `Vault(LoanBroker(LoanBrokerID).VaultID).LossUnrealized += Loan.PrincipalOutstanding + TotalInterestOutstanding()` (Please refer to section [**3.2.5.1. Payment Types**](#3251-payment-types), which outlines how to calculate total interest outstanding) - Update the `Loan` object: - - `Loan(LoanID).Flags = lsfLoanImpaired` + - `Loan(LoanID).Flags |= lsfLoanImpaired` - If `currentTime < Loan(LoanID).NextPaymentDueDate` (if the loan payment is not yet late): - `Loan(LoanID).NextPaymentDueDate = currentTime` (move the next payment due date to now) @@ -1043,7 +1041,7 @@ The transaction deletes an existing `Loan` object. - Update the `Loan` object: - - `Loan(LoanID).Flags = 0` + - `Loan(LoanID).Flags &= ~lsfLoanImpaired` - If `Loan(LoanID).PreviousPaymentDate + Loan(LoanID).PaymentInterval > currentTime` (the loan was unimpaired within the payment interval): - `Loan(LoanID).NextPaymentDueDate = Loan(LoanID).PreviousPaymentDate + Loan(LoanID).PaymentInterval` From 5609061006734ae2cb46fec24b8ae37c9d78b2a2 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Tue, 6 May 2025 15:31:24 +0200 Subject: [PATCH 29/77] addresses PR comments --- XLS-0066d-lending-protocol/README.md | 114 ++++++++++++++++++--------- 1 file changed, 78 insertions(+), 36 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index df2a9916..6424e427 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -238,7 +238,7 @@ The `RootIndex` of the `DirectoryNode` object is the result of [`SHA512-Half`](h #### 2.1.5 Reserves -The `LoanBroker` object costs one owner reserve for the account creating it. +The `LoanBroker` object costs two owner reserve for the account creating it. #### 2.1.6 Accounting @@ -380,36 +380,48 @@ CoverRateLiquidation = 0.1 (10%) CoverAvailable = 1,000 Tokens -- Loan -- -DefaultAmount = 1,090 Tokens +AssetsAvailable = 500 Tokens +PrincipleOutstanding = 1,000 Tokens +InterestOutstanding = 90 Tokens # First-Loss Capital liquidation maths +DefaultAmount = PrincipleOutstanding + InterestOutstanding - AssetsAvailable + = 1,000 + 90 - 500 + = 590 + # The amount of the default that the first-loss capital scheme will cover -DefaultCovered = min((DebtTotal x CoverRateMinimum) x CoverRateLiquidation, DefaultAmount) - = min((1,090 * 0.1) * 0.1, 1,090) = min(10.9, 1,090) +DefaultCovered = min((DebtTotal x CoverRateMinimum) x CoverRateLiquidation, DefaultAmount) + = min((1,090 * 0.1) * 0.1, 1,090) = min(10.9, 590) = 10.9 Tokens -DefaultRemaining = DefaultAmount - DefaultCovered - = 1,090 - 10.9 - = 1,079.1 Tokens +Loss = DefaultAmount - DefaultCovered + = 590 - 10.9 + = 579.1 Tokens + +FundsReturned = DefaultCovered + AssetsAvailable + = 10.9 + 500 + = 510.9 + +# Note, Loss + FundsReturned MUST be equal to PrincipleOutstanding + InterestOutstanding ** State Changes ** -- Vault -- -AssetsTotal = AssetsTotal - DefaultRemaining - = 100,090 - 1,079.1 - = 99,010.9 Tokens +AssetsTotal = AssetsTotal - Loss + = 100,090 - 579.1 + = 99,510.9 Tokens -AssetsAvailable = AssetsAvailable + DefaultCovered - = 99,000 + 10.9 - = 99,010.9 Tokens +AssetsAvailable = AssetsAvailable + FundsReturned + = 99,000 + 510.9 + = 99,510.9 Tokens SharesTotal = (UNCHANGED) -- Lending Protocol -- -DebtTotal = DebtTotal - DefaultAmount - = 1,090 - 1,090 +DebtTotal = DebtTotal - PrincipleOutstanding + InterestOutstanding + = 1,090 - (1,000 + 90) = 0 Tokens CoverAvailable = CoverAvailable - DefaultCovered @@ -447,7 +459,7 @@ The `LoanID` is calculated as follows: | `LoanBrokerNode` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT64` | `N/A` | Identifies the page where this item is referenced in the `LoanBroker`s owner directory. | | `LoanBrokerID` | `No` | `Yes` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the `LoanBroker` associated with this Loan Instance. | | `Borrower` | `No` | `Yes` | :heavy_check_mark: | `string` | `AccountID` | `N/A` | The address of the account that is the borrower. | -| `LoanOriginationFee` | `No` | `Yes` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | A nominal funds amount paid to the `LoanBroker.Owner` when the Loan is created. | +| `LoanOriginationFee` | `No` | `Yes` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | A nominal nds amount paid to the `LoanBroker.Owner` when the Loan is created. | | `LoanServiceFee` | `No` | `Yes` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | A nominal funds amount paid to the `LoanBroker.Owner` with every Loan payment. | | `LatePaymentFee` | `No` | `Yes` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | A nominal funds amount paid to the `LoanBroker.Owner` when a payment is late. | | `ClosePaymentFee` | `No` | `Yes` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | A nominal funds amount paid to the `LoanBroker.Owner` when a payment full payment is made. | @@ -685,7 +697,7 @@ The `LoanBrokerCoverWithdraw` transaction withdraws the First-Loss Capital from - The `MPTokenIssuance` object of the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` has the `lsfMPTLocked` flag set. - The `MPTokenIssuance` object of the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` does not have the `lsfMPTCanTransfer` flag set (the asset is not transferable). - The `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the `LoanBroker.Account` `AccountRoot` has `lsfMPTLocked` flag set. (The Loan Broker _pseudo-account_ is locked). - + - The `LoanBroker.CoverAvailable` < `Amount`. - `LoanBroker.CoverAvailable - Amount` < `LoanBroker.DebtTotal * LoanBroker.CoverRateMinimum` @@ -913,18 +925,18 @@ The transaction deletes an existing `Loan` object. - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is `XRP`: - - Decrease the `Balance` field of `LoanBroker` _pseudo-account_ `AccountRoot` by `Loan(LoanID).AssetsAvailable`. - - Increase the `Balance` field of `Loan(LoanID).Borrower` `AccountRoot` by `Loan(LoanID).AssetsAvailable`. + - Decrease the `Balance` field of `LoanBroker` _pseudo-account_ `AccountRoot` by `Loan(LoanID).AssetsAvailable`. + - Increase the `Balance` field of `Loan(LoanID).Borrower` `AccountRoot` by `Loan(LoanID).AssetsAvailable`. -- If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `IOU`: + - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `IOU`: - - Decrease the `RippleState` balance between the `LoanBroker` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `Loan(LoanID).AssetsAvailable`. - - Increase the `RippleState` balance between the `Loan(LoanID).Borrower` `AccountRoot` and the `Issuer` `AccountRoot` by `Loan(LoanID).AssetsAvailable`. + - Decrease the `RippleState` balance between the `LoanBroker` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `Loan(LoanID).AssetsAvailable`. + - Increase the `RippleState` balance between the `Loan(LoanID).Borrower` `AccountRoot` and the `Issuer` `AccountRoot` by `Loan(LoanID).AssetsAvailable`. -- If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `MPT`: + - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `MPT`: - - Decrease the `MPToken.MPTAmount` of the `LoanBroker` _pseudo-account_ `MPToken` object for the `Vault.Asset` by `Loan(LoanID).AssetsAvailable`. - - Increase the `MPToken.MPTAmount` of the `Loan(LoanID).Borrower` `MPToken` object for the `Vault.Asset` by `Loan(LoanID).AssetsAvailable` + - Decrease the `MPToken.MPTAmount` of the `LoanBroker` _pseudo-account_ `MPToken` object for the `Vault.Asset` by `Loan(LoanID).AssetsAvailable`. + - Increase the `MPToken.MPTAmount` of the `Loan(LoanID).Borrower` `MPToken` object for the `Vault.Asset` by `Loan(LoanID).AssetsAvailable` - Delete the `Loan` object. @@ -953,6 +965,8 @@ The transaction deletes an existing `Loan` object. ##### 3.2.3.1 `Flags` +`LoanManage` transaction `Flags` are mutually exclusive. + | Flag Name | Flag Value | Description | | ---------------- | :----------: | :--------------------------------------------- | | `tfLoanDefault` | `0x00010000` | Indicates that the Loan should be defaulted. | @@ -965,11 +979,12 @@ The transaction deletes an existing `Loan` object. - The `Account` submitting the transaction is not the `LoanBroker.Owner`. - The `lsfLoanDefault` flag is set on the Loan object. Once a Loan is defaulted, it cannot be modified. -- If `Loan(LoanID).Flags == lsfLoanImpaired` AND `tfLoanImpair` flag is provided. +- If `Loan(LoanID).Flags == lsfLoanImpaired` AND `tfLoanImpair` flag is provided (impairing an already impaired loan). +- If `Loan(LoanID).Flags == 0` AND `tfLoanUnimpair` flag is provided (clearning impairment for an uninpaired loan). - `Loan.PaymentRemaining == 0`. -- The `tfDefault` flag is specified and: +- The `tfLoanDefault` flag is specified and: - `LastClosedLedger.CloseTime` < `Loan.NextPaymentDueDate + Loan.GracePeriod`. ##### 3.2.3.2 State Changes @@ -990,12 +1005,14 @@ The transaction deletes an existing `Loan` object. - `Vault(LoanBroker(LoanBrokerID).VaultID).AssetsTotal -= DefaultAmount`. - Increase the Asset Available of the Vault by liquidated First-Loss Capital and any unclaimed funds amount: - `Vault(LoanBroker(LoanBrokerID).VaultID).AssetsAvailable += DefaultCovered + Loan.AssetsAvailable`. + - If `Loan.lsfLoanImpaired` flag is set: + - `Vault(LoanBroker(LoanBrokerID).VaultID).LossUnrealized -= Loan.PrincipalOutstanding + TotalInterestOutstanding()` (Please refer to section [**3.2.5.1.5 Total Value Calculation**](#3252-total-loan-value-calculation), which outlines how to calculate total interest outstanding). - Update the `LoanBroker` object: - Decrease the Debt of the LoanBroker: - `LoanBroker(LoanBrokerID).DebtTotal -= ` - - `Loan.PrincipalOutstanding + Loan.InterestOutstanding + Loan.AssetsAvailable` + - `Loan.PrincipalOutstanding + Loan.InterestOutstanding` - Decrease the First-Loss Capital Cover Available: - `LoanBroker(LoanBrokerID).CoverAvailable -= DefaultCovered` @@ -1027,7 +1044,7 @@ The transaction deletes an existing `Loan` object. - Update the `Vault` object (set "paper loss"): - - `Vault(LoanBroker(LoanBrokerID).VaultID).LossUnrealized += Loan.PrincipalOutstanding + TotalInterestOutstanding()` (Please refer to section [**3.2.5.1. Payment Types**](#3251-payment-types), which outlines how to calculate total interest outstanding) + - `Vault(LoanBroker(LoanBrokerID).VaultID).LossUnrealized += Loan.PrincipalOutstanding + TotalInterestOutstanding()` (Please refer to section [**3.2.5.1.5 Total Value Calculation**](#3252-total-loan-value-calculation), which outlines how to calculate total interest outstanding) - Update the `Loan` object: - `Loan(LoanID).Flags |= lsfLoanImpaired` @@ -1037,11 +1054,11 @@ The transaction deletes an existing `Loan` object. - If the `tfLoanUnimpair` flag is specified: - Update the `Vault` object (clear "paper loss"): - - `Vault(LoanBroker(LoanBrokerID).VaultID).LossUnrealized -= Loan.PrincipalOutstanding + TotalInterestOutstanding()` (Please refer to section [**3.2.5.1. Payment Types**](#3251-payment-types), which outlines how to calculate total interest outstanding) - + - `Vault(LoanBroker(LoanBrokerID).VaultID).LossUnrealized -= Loan.PrincipalOutstanding + TotalInterestOutstanding()` (Please refer to section [**3.2.5.1.5 Total Value Calculation**](#3252-total-loan-value-calculation), which outlines how to calculate total interest outstanding) - Update the `Loan` object: - `Loan(LoanID).Flags &= ~lsfLoanImpaired` + - If `Loan(LoanID).PreviousPaymentDate + Loan(LoanID).PaymentInterval > currentTime` (the loan was unimpaired within the payment interval): - `Loan(LoanID).NextPaymentDueDate = Loan(LoanID).PreviousPaymentDate + Loan(LoanID).PaymentInterval` @@ -1332,7 +1349,32 @@ $$ LoanBroker.DebtTotal = LoanBroker.DebtTotal - managementFeeChange $$ -##### 3.2.5.2 Transaction Pseudo-code +##### 3.2.5.2 Total Loan Value Calculation + +At any point in time the following forumula can be used to calculate the total remaining value of the loan: + +$$ +totalValueOutstanding = periodicPayment \times paymentsRemaining +$$ + +We calculate the total interest outstanding as follows: + +$$ +totalInterestOutstanding = totalValueOutstanding - principalOutstanding +$$ + +$$ +periodicPayment = principalOutstanding \times \frac{periodicRate \times (1 + periodicRate)^{PaymentRemaining}}{(1 + periodicRate)^{PaymentRemaining} - 1} +$$ + +where the periodic interest rate is the interest rate charged per payment period: + +$$ +periodicRate = \frac{interestRate \times paymentInterval}{365 \times 24 \times 60 \times 60} +$$ + + +##### 3.2.5.3 Transaction Pseudo-code The following is the pseudo-code for handling a Loan payment transaction. @@ -1421,7 +1463,7 @@ function make_payment(amount, current_time) -> (principal_paid, interest_paid, v return (total_principal_paid, total_interest_paid, loan_value_change, total_fee_paid) ``` -##### 3.2.5.3 Failure Conditions +##### 3.2.5.4 Failure Conditions - A `Loan` object with specified `LoanID` does not exist on the ledger. @@ -1446,7 +1488,7 @@ function make_payment(amount, current_time) -> (principal_paid, interest_paid, v - If `LastClosedLedger.CloseTime <= Loan.NextPaymentDueDate` and `Amount` < `PeriodicPaymentAmount()` -##### 3.2.5.4 State Changes +##### 3.2.5.5 State Changes Assume the payment is split into `principal`, `interest` and `fee`, and `totalDue = principal + interest + fee`. @@ -1564,7 +1606,7 @@ Furthermore, assume `full_periodic_payments` variable represents the number of p [**Return to Index**](#index) -##### 3.2.5.4 Invariants +##### 3.2.5.6 Invariants **TBD** @@ -1574,4 +1616,4 @@ Furthermore, assume `full_periodic_payments` variable represents the number of p ### A-1.1 What is the `LoanBroker.LoanSequence` field? -A sequential identifier for Loans associated with a LoanBroker object. This value increments with each new Loan created by the broker. Unlike `LoanBroker.OwnerCount`, which tracks the number of currently active Loans, `LoanBroker.LoanSequence` reflects the total number of Loans ever created. \ No newline at end of file +A sequential identifier for Loans associated with a LoanBroker object. This value increments with each new Loan created by the broker. Unlike `LoanBroker.OwnerCount`, which tracks the number of currently active Loans, `LoanBroker.LoanSequence` reflects the total number of Loans ever created. From b16dad1cfcdf45efd20315e5979102b972295dd4 Mon Sep 17 00:00:00 2001 From: Vito Tumas <5780819+Tapanito@users.noreply.github.com> Date: Wed, 7 May 2025 15:34:38 +0200 Subject: [PATCH 30/77] Apply suggestions from code review Co-authored-by: Ed Hennis --- XLS-0066d-lending-protocol/README.md | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index 6424e427..6abcdc4a 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -1351,26 +1351,30 @@ $$ ##### 3.2.5.2 Total Loan Value Calculation -At any point in time the following forumula can be used to calculate the total remaining value of the loan: +At any point in time the following formulae can be used to calculate the total remaining value of the loan. + +The periodic interest rate is the interest rate charged per payment period. $$ -totalValueOutstanding = periodicPayment \times paymentsRemaining +periodicRate = \frac{interestRate \times paymentInterval}{365 \times 24 \times 60 \times 60} $$ -We calculate the total interest outstanding as follows: +The payment is computed based on the periodic rate, principal outstanding, and number of payments remaining. (This means the payment amount can decrease if the borrow pays principal early.) $$ -totalInterestOutstanding = totalValueOutstanding - principalOutstanding +periodicPayment = principalOutstanding \times \frac{periodicRate \times (1 + periodicRate)^{PaymentRemaining}}{(1 + periodicRate)^{PaymentRemaining} - 1} $$ +The total loan value is simply: + $$ -periodicPayment = principalOutstanding \times \frac{periodicRate \times (1 + periodicRate)^{PaymentRemaining}}{(1 + periodicRate)^{PaymentRemaining} - 1} +totalValueOutstanding = periodicPayment \times paymentsRemaining $$ -where the periodic interest rate is the interest rate charged per payment period: +We calculate the total interest outstanding as follows: $$ -periodicRate = \frac{interestRate \times paymentInterval}{365 \times 24 \times 60 \times 60} +totalInterestOutstanding = totalValueOutstanding - principalOutstanding $$ From 65cc318eb0795579511915e4a969383fb55a222e Mon Sep 17 00:00:00 2001 From: Vito Tumas <5780819+Tapanito@users.noreply.github.com> Date: Mon, 12 May 2025 13:39:06 +0200 Subject: [PATCH 31/77] Apply suggestions from code review Co-authored-by: Ed Hennis --- XLS-0066d-lending-protocol/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index 6abcdc4a..4fe43933 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -1488,9 +1488,9 @@ function make_payment(amount, current_time) -> (principal_paid, interest_paid, v - Has `lsfMPTLocked` flag set. - The `MPTokenIssuance` object of the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` has the `lsfMPTLocked` flag set. -- If `LastClosedLedger.CloseTime > Loan.NextPaymentDueDate` and `Amount` < `LatePaymentAmount()` +- If `LastClosedLedger.CloseTime >= Loan.NextPaymentDueDate` and `Amount` < `LatePaymentAmount()` -- If `LastClosedLedger.CloseTime <= Loan.NextPaymentDueDate` and `Amount` < `PeriodicPaymentAmount()` +- If `LastClosedLedger.CloseTime < Loan.NextPaymentDueDate` and `Amount` < `PeriodicPaymentAmount()` ##### 3.2.5.5 State Changes From 4ff2165ae01301122f9cabbe970b308cba2c4ab7 Mon Sep 17 00:00:00 2001 From: Vito Tumas <5780819+Tapanito@users.noreply.github.com> Date: Mon, 12 May 2025 14:08:13 +0200 Subject: [PATCH 32/77] Update XLS-0066d-lending-protocol/README.md Co-authored-by: Ed Hennis --- XLS-0066d-lending-protocol/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index 4fe43933..b5f78db6 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -1057,13 +1057,13 @@ The transaction deletes an existing `Loan` object. - `Vault(LoanBroker(LoanBrokerID).VaultID).LossUnrealized -= Loan.PrincipalOutstanding + TotalInterestOutstanding()` (Please refer to section [**3.2.5.1.5 Total Value Calculation**](#3252-total-loan-value-calculation), which outlines how to calculate total interest outstanding) - Update the `Loan` object: - - `Loan(LoanID).Flags &= ~lsfLoanImpaired` + - `CandidateDueDate = max(Loan.PreviousPaymentDate, Loan.StartDate) + Loan.PaymentInterval` - - If `Loan(LoanID).PreviousPaymentDate + Loan(LoanID).PaymentInterval > currentTime` (the loan was unimpaired within the payment interval): + - If `CandidateDueDate > currentTime` (the loan was unimpaired within the payment interval): - - `Loan(LoanID).NextPaymentDueDate = Loan(LoanID).PreviousPaymentDate + Loan(LoanID).PaymentInterval` + - `Loan(LoanID).NextPaymentDueDate = CandidateDueDate` - - If `Loan(LoanID).PreviousPaymentDate + Loan(LoanID).PaymentInterval < currentTime` (the loan was unimpaired after the original payment due date): + - If `CandidateDueDate <= currentTime` (the loan was unimpaired after the original payment due date): - `Loan(LoanID).NextPaymentDueDate = currentTime + Loan(LoanID).PaymentInterval` ##### 3.2.3.3 Invariants From 33908afd4efd55711149c6e55c8637158b31294b Mon Sep 17 00:00:00 2001 From: Vito Tumas <5780819+Tapanito@users.noreply.github.com> Date: Mon, 12 May 2025 14:12:30 +0200 Subject: [PATCH 33/77] Update XLS-0066d-lending-protocol/README.md Co-authored-by: Ed Hennis --- XLS-0066d-lending-protocol/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index b5f78db6..779f4d63 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -1428,7 +1428,6 @@ function make_payment(amount, current_time) -> (principal_paid, interest_paid, v return "insufficient amount paid" error } - loan.payments_remaining -= full_periodic_payments loan.next_payment_due_date = loan.next_payment_due_date + loan.payment_interval * full_periodic_payments loan.last_payment_date = loan.next_payment_due_date - loan.payment_interval @@ -1441,12 +1440,13 @@ function make_payment(amount, current_time) -> (principal_paid, interest_paid, v while full_periodic_payments > 0 { total_principal_paid += periodic_payment.principal total_interest_paid += periodic_payment.interest + loan.payments_remaining -= full_periodic_payments + loan.principal_outstanding -= periodic_payment.principal + periodic_payment = loan.compute_periodic_payment() full_periodic_payments -= 1 } - loan.principal_outstanding -= total_principal_paid - let overpayment = min(loan.principal_outstanding, amount % periodic_payment) if overpayment > 0 && is_set(lsfOverpayment) { let interest_portion = overpayment * loan.overpayment_interest_rate From b8f8b6fbeebfc8792d1c7db4bf46491d7d40336f Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Mon, 12 May 2025 14:54:39 +0200 Subject: [PATCH 34/77] adds checks for frozen LoanBroker pseudo-account --- XLS-0066d-lending-protocol/README.md | 35 +++++++++++++++++----------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index 779f4d63..b8b6bf3e 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -551,7 +551,7 @@ The transaction creates a new `LoanBroker` object or updates an existing one. - If the `Vault(VaultID).Asset` is an `IOU`: - - Create a `Trustline` between the `Issuer` and the `LoanBroker` _pseudo-account_. + - Create a `RippleState` object between the `Issuer` and the `LoanBroker` _pseudo-account_. - If the `Vault(VaultID).Asset` is an `MPT`: @@ -634,14 +634,16 @@ The transaction deposits First Loss Capital into the `LoanBroker` object. - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `IOU`: - The `RippleState` object between the submitter account and the `Issuer` of the asset has the `lsfLowFreeze` or `lsfHighFreeze` flag set. + - The `RippleState` between the `LoanBroker.Account` and the `Issuer` has the `lsfLowFreeze` or `lsfHighFreeze` flag set. (The Loan Broker _pseudo-account_ is frozen). - The `AccountRoot` object of the `Issuer` has the `lsfGlobalFreeze` flag set. - - The trustline `Balance` < `Amount`. + - The `RippleState` object `Balance` < `Amount` (Depositor has insufficient funds). - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `MPT`: - The `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the submitter `AccountRoot`: - Has `lsfMPTLocked` flag set. - `MPTAmount` < `Amount`. + - The `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the `LoanBroker.Account` `AccountRoot` has `lsfMPTLocked` flag set. (The Loan Broker _pseudo-account_ is locked). - The `MPTokenIssuance` object of the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` has the `lsfMPTLocked` flag set. - The `MPTokenIssuance` object of the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` does not have the `lsfMPTCanTransfer` flag set (the asset is not transferable). @@ -689,7 +691,7 @@ The `LoanBrokerCoverWithdraw` transaction withdraws the First-Loss Capital from - The `RippleState` object between the submitter account and the `Issuer` of the asset has the `lsfLowFreeze` or `lsfHighFreeze` flag set. - The `AccountRoot` object of the `Issuer` has the `lsfGlobalFreeze` flag set. - - The `RippleState` between the `LoanBroker.Account` and the `Issuer` has the `lsfLowFreeze` or `lsfHighFreeze` flag set. + - The `RippleState` between the `LoanBroker.Account` and the `Issuer` has the `lsfLowFreeze` or `lsfHighFreeze` flag set. (The Loan Broker _pseudo-account_ is frozen). - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `MPT`: @@ -824,13 +826,14 @@ The account specified in the `Account` field pays the transaction fee. - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `IOU`: - - The trustline between the submitter account and the `Issuer` of the asset is frozen. + - The `RippleState` object between the submitter account and the `Issuer` of the asset has the `lsfLowFreeze` or `lsfHighFreeze` flag set. + - The `RippleState` between the `LoanBroker.Account` and the `Issuer` has the `lsfLowFreeze` or `lsfHighFreeze` flag set. (The Loan Broker _pseudo-account_ is frozen). - The `AccountRoot` object of the `Issuer` has the `lsfGlobalFreeze` flag set. - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `MPT`: - - The `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the submitter `AccountRoot`: - - Has `lsfMPTLocked` flag set. + - The `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the submitter `AccountRoot` has `lsfMPTLocked` flag set. + - The `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the `LoanBroker.Account` `AccountRoot` has `lsfMPTLocked` flag set. (The Loan Broker _pseudo-account_ is locked). - The `MPTokenIssuance` object of the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` has the `lsfMPTLocked` flag set. - Either of the `tfDefault`, `tfImpair` or `tfUnimpair` flags are set. @@ -866,7 +869,7 @@ The account specified in the `Account` field pays the transaction fee. - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `IOU`: - - Create a `Trustline` between the `Issuer` and the `Borrower` if one does not exist. + - Create a `RippleState` object between the `Issuer` and the `Borrower` if one does not exist. - Decrease the `RippleState` balance between the `Vault` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `Loan.PrincipalRequested`. - Increase the `RippleState` balance between the `LoanBroker` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `Loan.PrincipalRequested - Loan.LoanOriginationFee`. @@ -1096,13 +1099,14 @@ The Borrower submits a `LoanDraw` transaction to draw funds from the Loan. - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `IOU`: - - The trustline between the submitter account and the `Issuer` of the asset is frozen. + - The `RippleState` object between the submitter account and the `Issuer` of the asset has the `lsfLowFreeze` or `lsfHighFreeze` flag set. + - The `RippleState` between the `LoanBroker.Account` and the `Issuer` has the `lsfLowFreeze` or `lsfHighFreeze` flag set. (The Loan Broker _pseudo-account_ is frozen). - The `AccountRoot` object of the `Issuer` has the `lsfGlobalFreeze` flag set. - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `MPT`: - - The `MPToken` object for the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` of the submitter `AccountRoot`: - - Has `lsfMPTLocked` flag set. + - The `MPToken` object for the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` of the submitter `AccountRoot` has `lsfMPTLocked` flag set. + - The `MPToken` object for the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` of the `LoanBroker.Account` `AccountRoot` has `lsfMPTLocked` flag set. (The Loan Broker _pseudo-account_ is locked). - The `MPTokenIssuance` object of the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` has the `lsfMPTLocked` flag set. - The `Borrower` missed a payment: @@ -1477,16 +1481,19 @@ function make_payment(amount, current_time) -> (principal_paid, interest_paid, v - `Loan.PaymentRemaining` or `Loan.PrincipalOutstanding` is `0`. +- The Borrower paid insufficient amount: `full_periodic_payments < 0`. + - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `IOU`: - - The trustline between the submitter account and the `Issuer` of the asset is frozen. + - The `RippleState` object between the submitter account and the `Issuer` of the asset has the `lsfLowFreeze` or `lsfHighFreeze` flag set. + - The `RippleState` between the `LoanBroker.Account` and the `Issuer` has the `lsfLowFreeze` or `lsfHighFreeze` flag set. (The Loan Broker _pseudo-account_ is frozen). - The `AccountRoot` object of the `Issuer` has the `lsfGlobalFreeze` flag set. - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `MPT`: - - The `MPToken` object for the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` of the submitter `AccountRoot`: - - Has `lsfMPTLocked` flag set. - - The `MPTokenIssuance` object of the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` has the `lsfMPTLocked` flag set. + - The `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the submitter `AccountRoot` has `lsfMPTLocked` flag set. + - The `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the `LoanBroker.Account` `AccountRoot` has `lsfMPTLocked` flag set. (The Loan Broker _pseudo-account_ is locked). + - The `MPTokenIssuance` object of the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` has the `lsfMPTLocked` flag set. - If `LastClosedLedger.CloseTime >= Loan.NextPaymentDueDate` and `Amount` < `LatePaymentAmount()` From fe723a2c50652888a5bfab9518adf541f824a0b8 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Mon, 12 May 2025 14:55:13 +0200 Subject: [PATCH 35/77] clarifies LoanPay totalDue and totalPaid --- XLS-0066d-lending-protocol/README.md | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index b8b6bf3e..0cef704a 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -1473,6 +1473,20 @@ function make_payment(amount, current_time) -> (principal_paid, interest_paid, v ##### 3.2.5.4 Failure Conditions +Assume the payment is split into `principal`, `interest` and `fee`, and `totalDue = principal + interest + fee`. `totalDue` is the minimum payment due by the borrower. + +Assume the payment is handled by a function that implements the [Pseudo-Code](#3252-transaction-pseudo-code) that returns `principal_paid`, `interest_paid`, `value_change` and `fee_paid`, where: + +- `principal_paid` is the amount of principal that the payment covered. +- `interest_paid` is the amount of interest that the payment covered. +- `fee_paid` is the amount of fee that the payment covered. +- `totalPaid = principal_paid + interest_paid + fee_paid` is the total amount the borrower paid. +- `value_change` is the amount by which the total value of the Loan changed. + - If `value_change` < `0`, Loan value decreased. + - If `value_change` > `0`, Loan value increased. + +Furthermore, assume `full_periodic_payments` variable represents the number of payment intervals that the payment covered. + - A `Loan` object with specified `LoanID` does not exist on the ledger. - The Loan has not started yet: `Loan.StartDate > LastClosedLedger.CloseTime`. @@ -1501,19 +1515,6 @@ function make_payment(amount, current_time) -> (principal_paid, interest_paid, v ##### 3.2.5.5 State Changes -Assume the payment is split into `principal`, `interest` and `fee`, and `totalDue = principal + interest + fee`. - -Assume the payment is handled by a function that implements the [Pseudo-Code](#3252-transaction-pseudo-code) that returns `principal_paid`, `interest_paid`, `value_change` and `fee_paid`, where: - -- `principal_paid` is the amount of principal that the payment covered. -- `interest_paid` is the amount of interest that the payment covered. -- `fee_paid` is the amount of fee that the payment covered. -- `value_change` is the amount by which the total value of the Loan changed. - - If `value_change` < `0`, Loan value decreased. - - If `value_change` > `0`, Loan value increased. - -Furthermore, assume `full_periodic_payments` variable represents the number of payment intervals that the payment covered. - - `Loan` object state changes: - If `Loan(LoanID).Flags == lsfLoanImpaired`: From 389dd2eb25c0217e703421cd4a102ded8162038b Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Mon, 12 May 2025 14:55:57 +0200 Subject: [PATCH 36/77] corrects LoanSet debtMaximum and coverAvailable checks --- XLS-0066d-lending-protocol/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index 0cef704a..690b9f0d 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -850,10 +850,10 @@ The account specified in the `Account` field pays the transaction fee. - Exceeds maximum Debt of the LoanBroker: - - `LoanBroker(LoanBrokerID).DebtMaximum` < `LoanBroker(LoanBrokerID).DebtTotal + Loan.PrincipalRequested` + - `LoanBroker(LoanBrokerID).DebtMaximum` < `Loan.PrincipalRequested + (LoanInterest - (LoanInterest x LoanBroker.ManagementFeeRate)` - Insufficient First-Loss Capital: - - `LoanBroker(LoanBrokerID).CoverAvailable` < `(LoanBroker(LoanBrokerID).DebtTotal + Loan.PrincipalRequested) x LoanBroker(LoanBrokerID).CoverRateMinimum` + - `LoanBroker(LoanBrokerID).CoverAvailable` < `(LoanBroker(LoanBrokerID).DebtTotal + Loan.PrincipalRequested + (LoanInterest - (LoanInterest x LoanBroker.ManagementFeeRate)) x LoanBroker(LoanBrokerID).CoverRateMinimum` ##### 3.2.1.6 State Changes From fd20def2da0fa4983bf175d93bef15a862bbdd3c Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Mon, 12 May 2025 14:56:15 +0200 Subject: [PATCH 37/77] improves LoanManage readability with ReturnToVault variable --- XLS-0066d-lending-protocol/README.md | 46 +++++++++++++++------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index 690b9f0d..b95596af 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -1001,13 +1001,14 @@ The transaction deletes an existing `Loan` object. - Apply the First-Loss Capital to the Default Amount - `DefaultCovered = min((LoanBroker(Loan.LoanBrokerID).DebtTotal x LoanBroker(Loan.LoanBrokerID).CoverRateMinimum) x LoanBroker(Loan.LoanBrokerID).CoverRateLiquidation, DefaultAmount)` - `DefaultAmount -= DefaultCovered` + - `ReturnToVault = DefaultCovered + Loan.AssetsAvailable` - Update the `Vault` object: - Decrease the Total Value of the Vault: - `Vault(LoanBroker(LoanBrokerID).VaultID).AssetsTotal -= DefaultAmount`. - Increase the Asset Available of the Vault by liquidated First-Loss Capital and any unclaimed funds amount: - - `Vault(LoanBroker(LoanBrokerID).VaultID).AssetsAvailable += DefaultCovered + Loan.AssetsAvailable`. + - `Vault(LoanBroker(LoanBrokerID).VaultID).AssetsAvailable += ReturnToVault`. - If `Loan.lsfLoanImpaired` flag is set: - `Vault(LoanBroker(LoanBrokerID).VaultID).LossUnrealized -= Loan.PrincipalOutstanding + TotalInterestOutstanding()` (Please refer to section [**3.2.5.1.5 Total Value Calculation**](#3252-total-loan-value-calculation), which outlines how to calculate total interest outstanding). @@ -1026,39 +1027,40 @@ The transaction deletes an existing `Loan` object. - `Loan(LoanID).AssetsAvailable = 0` - `Loan(LoanID).PrincipalOutstanding = 0` -- Move the First-Loss Capital from the `LoanBroker` _pseudo-account_ to the `Vault` _pseudo-account_: + - Move the First-Loss Capital from the `LoanBroker` _pseudo-account_ to the `Vault` _pseudo-account_: - - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is `XRP`: + - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is `XRP`: - - Decrease the `Balance` field of `LoanBroker` _pseudo-account_ `AccountRoot` by `DefaultCovered`. - - Increase the `Balance` field of `Vault` _pseudo-account_ `AccountRoot` by `DefaultCovered`. + - Decrease the `Balance` field of `LoanBroker` _pseudo-account_ `AccountRoot` by `ReturnToVault`. + - Increase the `Balance` field of `Vault` _pseudo-account_ `AccountRoot` by `ReturnToVault`. - - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `IOU`: + - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `IOU`: - - Decrease the `RippleState` balance between the `LoanBroker` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `DefaultCovered`. - - Increase the `RippleState` balance between the `Vault` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `DefaultCovered`. + - Decrease the `RippleState` balance between the `LoanBroker` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `ReturnToVault`. + - Increase the `RippleState` balance between the `Vault` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `ReturnToVault`. - - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `MPT`: + - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `MPT`: - - Decrease the `MPToken.MPTAmount` of the `LoanBroker` _pseudo-account_ `MPToken` object for the `Vault.Asset` by `DefaultCovered`. - - Increase the `MPToken.MPTAmount` of the `Vault` _pseudo-account_ `MPToken` object for the `Vault.Asset` by `DefaultCovered`. + - Decrease the `MPToken.MPTAmount` of the `LoanBroker` _pseudo-account_ `MPToken` object for the `Vault.Asset` by `ReturnToVault`. + - Increase the `MPToken.MPTAmount` of the `Vault` _pseudo-account_ `MPToken` object for the `Vault.Asset` by `ReturnToVault`. -- If `tfLoanImpair` flag is specified: + - If `tfLoanImpair` flag is specified: - - Update the `Vault` object (set "paper loss"): + - Update the `Vault` object (set "paper loss"): - - `Vault(LoanBroker(LoanBrokerID).VaultID).LossUnrealized += Loan.PrincipalOutstanding + TotalInterestOutstanding()` (Please refer to section [**3.2.5.1.5 Total Value Calculation**](#3252-total-loan-value-calculation), which outlines how to calculate total interest outstanding) + - `Vault(LoanBroker(LoanBrokerID).VaultID).LossUnrealized += Loan.PrincipalOutstanding + TotalInterestOutstanding()` (Please refer to section [**3.2.5.1.5 Total Value Calculation**](#3252-total-loan-value-calculation), which outlines how to calculate total interest outstanding) - - Update the `Loan` object: - - `Loan(LoanID).Flags |= lsfLoanImpaired` - - If `currentTime < Loan(LoanID).NextPaymentDueDate` (if the loan payment is not yet late): - - `Loan(LoanID).NextPaymentDueDate = currentTime` (move the next payment due date to now) + - Update the `Loan` object: + - `Loan(LoanID).Flags |= lsfLoanImpaired` + - If `currentTime < Loan(LoanID).NextPaymentDueDate` (if the loan payment is not yet late): + - `Loan(LoanID).NextPaymentDueDate = currentTime` (move the next payment due date to now) -- If the `tfLoanUnimpair` flag is specified: + - If the `tfLoanUnimpair` flag is specified: - - Update the `Vault` object (clear "paper loss"): - - `Vault(LoanBroker(LoanBrokerID).VaultID).LossUnrealized -= Loan.PrincipalOutstanding + TotalInterestOutstanding()` (Please refer to section [**3.2.5.1.5 Total Value Calculation**](#3252-total-loan-value-calculation), which outlines how to calculate total interest outstanding) - - Update the `Loan` object: + - Update the `Vault` object (clear "paper loss"): + - `Vault(LoanBroker(LoanBrokerID).VaultID).LossUnrealized -= Loan.PrincipalOutstanding + TotalInterestOutstanding()` (Please refer to section [**3.2.5.1.5 Total Value Calculation**](#3252-total-loan-value-calculation), which outlines how to calculate total interest outstanding) + + - Update the `Loan` object: - `CandidateDueDate = max(Loan.PreviousPaymentDate, Loan.StartDate) + Loan.PaymentInterval` From 321319e772f7f43b40b2173e71ace7ea506bb6aa Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Mon, 12 May 2025 15:02:07 +0200 Subject: [PATCH 38/77] adds checks to LoanPay to ensure both Vault pseudo-account and LoanbRoker pseudo-accounts are not funded --- XLS-0066d-lending-protocol/README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index b95596af..ba848971 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -1503,13 +1503,15 @@ Furthermore, assume `full_periodic_payments` variable represents the number of p - The `RippleState` object between the submitter account and the `Issuer` of the asset has the `lsfLowFreeze` or `lsfHighFreeze` flag set. - The `RippleState` between the `LoanBroker.Account` and the `Issuer` has the `lsfLowFreeze` or `lsfHighFreeze` flag set. (The Loan Broker _pseudo-account_ is frozen). + - The `RippleState` between the `Vault(LoanBroker(Loan.LoanBrokerID).VaultID).Account` and the `Issuer` has the `lsfLowFreeze` or `lsfHighFreeze` flag set. (The Vault _pseudo-account_ is frozen). - The `AccountRoot` object of the `Issuer` has the `lsfGlobalFreeze` flag set. - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `MPT`: - - The `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the submitter `AccountRoot` has `lsfMPTLocked` flag set. - - The `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the `LoanBroker.Account` `AccountRoot` has `lsfMPTLocked` flag set. (The Loan Broker _pseudo-account_ is locked). - - The `MPTokenIssuance` object of the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` has the `lsfMPTLocked` flag set. + - The `MPToken` object for the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` of the submitter `AccountRoot` has `lsfMPTLocked` flag set. + - The `MPToken` object for the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` of the `LoanBroker.Account` `AccountRoot` has `lsfMPTLocked` flag set. (The Loan Broker _pseudo-account_ is locked). + - The `MPToken` object for the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` of the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Account` `AccountRoot` has `lsfMPTLocked` flag set. (The Vault _pseudo-account_ is locked). + - The `MPTokenIssuance` object of the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` has the `lsfMPTLocked` flag set. - If `LastClosedLedger.CloseTime >= Loan.NextPaymentDueDate` and `Amount` < `LatePaymentAmount()` From d45aeb5a6c6d3eb3f3c19b4c38c33a9acfd7ebaa Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Tue, 20 May 2025 10:26:52 +0200 Subject: [PATCH 39/77] adds calculations for secondsSinceLastPayment --- XLS-0066d-lending-protocol/README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index ba848971..52fc93bb 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -1217,6 +1217,10 @@ $$ totalDue = periodicPayment + latePaymentFee + latePaymentInterest $$ +$$ +secondsSinceLastPayment = lastLedgerCloseTime - max(Loan.previousPaymentDate, Loan.startDate) +$$ + A special, late payment interest rate is applied for the over-due period: $$ @@ -1272,6 +1276,10 @@ $$ totalDue = principalOutstanding + accruedInterest + prepaymentPenalty + ClosePaymentFee $$ +$$ +secondsSinceLastPayment = lastLedgerCloseTime - max(Loan.previousPaymentDate, Loan.startDate) +$$ + Accrued interest up to the point of early closure is calculated as follows: $$ @@ -1294,8 +1302,6 @@ $$ valueChange = (prepaymentPenalty) - (interestOutstanding - accruedInterest) $$ -Note that `valueChange <= 0` as an early repayment reduces the total value of the Loan. - ###### 3.2.5.1.5 Management Fee Calculations The `LoanBroker` Management fee is charged against the interest portion of the Loan and subtracted from the total Loan value at Loan creation. However, the fee is charged only during Loan payments. Early and Late payments change the total value of the Loan by decreasing or increasing the value of total interest. Therefore, when an early, late or an overpayment payment is made, the management fee must be updated. From 81e2ff8954763fa572ba0bb937333391327dca63 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Tue, 20 May 2025 11:20:13 +0200 Subject: [PATCH 40/77] adds destination field to the LoanBrokerCoverWithdraw transaction --- XLS-0066d-lending-protocol/README.md | 69 +++++++++++++++++++--------- 1 file changed, 47 insertions(+), 22 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index 52fc93bb..1ad2e7aa 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -1,8 +1,8 @@ -
    
+
 Title:        Lending Protocol
 Revision:     1 (2024-10-18)
 
-
Authors: +
Authors: Vytautas Vito Tumas Aanchal Malhotra @@ -193,7 +193,7 @@ The `LoanBroker` object captures attributes of the Lending Protocol. The key of the `LoanBroker` object is the result of [`SHA512-Half`](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#hashes) of the following values concatenated in order: - The `LoanBroker` space key `0x006C` (Lower-case `l`). -- The `AccountID`(https://xrpl.org/docs/references/protocol/binary-format/#accountid-fields) of the account submitting the `LoanBrokerSet` transaction, i.e. `Lender`. +- The `AccountID`() of the account submitting the `LoanBrokerSet` transaction, i.e. `Lender`. - The transaction `Sequence` number. If the transaction used a [Ticket](https://xrpl.org/docs/concepts/accounts/tickets/), use the `TicketSequence` value. #### 2.1.2 Fields @@ -223,7 +223,7 @@ The `LoanBroker` object has the following fields: | `CoverRateMinimum` | `No` | `Yes` | :heavy_check_mark: | `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%. | | `CoverRateLiquidation` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | 0 | The 1/10th basis point of minimum required first loss capital that is liquidated to cover a Loan default. Valid values are between 0 and 100000 inclusive. A value of 1 is equivalent to 1/10 bps or 0.001%. | -#### 2.1.3 `LoanBroker `_pseudo-account_` +#### 2.1.3 `LoanBroker`_pseudo-account_` The `LoanBroker` _pseudo-account_ holds the First-Loss Capital deposited by the LoanBroker, as well as Loan funds. The _pseudo-account_ follows the XLS-64d specification for pseudo accounts. The `AccountRoot` object is created when creating the `Vault` object. @@ -676,28 +676,41 @@ The transaction deposits First Loss Capital into the `LoanBroker` object. The `LoanBrokerCoverWithdraw` transaction withdraws the First-Loss Capital from the `LoanBroker`. -| Field Name | Required? | JSON Type | Internal Type | Default Value | Description | -| ----------------- | :----------------: | :-------: | :-----------: | :-----------: | :------------------------------------------------------------ | -| `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | Transaction type. | -| `LoanBrokerID` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The Loan Broker ID from which to withdraw First-Loss Capital. | -| `Amount` | :heavy_check_mark: | `object` | `AMOUNT` | 0 | The amount of Vault asset to withdraw. | +| Field Name | Required? | JSON Type | Internal Type | Default Value | Description | +| ----------------- | :----------------: | :-------: | :-----------: | :-----------: | :---------------------------------------------------------------------- | +| `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | Transaction type. | +| `LoanBrokerID` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The Loan Broker ID from which to withdraw First-Loss Capital. | +| `Amount` | :heavy_check_mark: | `object` | `AMOUNT` | 0 | The amount of Vault asset to withdraw. | +| `Destination` | | `string` | `AccountID` | Empty | An account to receive the assets. It must be able to receive the asset. | ##### 3.1.4.1 Failure conditions - `LoanBroker` object with the specified `LoanBrokerID` does not exist on the ledger. - The submitter `AccountRoot.Account != LoanBroker(LoanBrokerID).Owner`. +- The `Destination` account is specified and it does not have permission to receive the asset. + - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `IOU`: - - The `RippleState` object between the submitter account and the `Issuer` of the asset has the `lsfLowFreeze` or `lsfHighFreeze` flag set. - - The `AccountRoot` object of the `Issuer` has the `lsfGlobalFreeze` flag set. + - If the `Destination` field is not specified: + - The `RippleState` object between the submitter account and the `Issuer` of the asset has the `lsfLowFreeze` or `lsfHighFreeze` flag set. + + - If the `Destination` field is specified: + - If `Destination` is not the `Issuer` and the `RippleState` object between the `Destination` account and the `Issuer` of the asset has the `lsfLowFreeze` or `lsfHighFreeze` flag set. + + - The `AccountRoot` object of the `Issuer` has the `lsfGlobalFreeze` flag set and `Destination` is not the `Issuer` of the asset. - The `RippleState` between the `LoanBroker.Account` and the `Issuer` has the `lsfLowFreeze` or `lsfHighFreeze` flag set. (The Loan Broker _pseudo-account_ is frozen). - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `MPT`: - - The `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the submitter `AccountRoot` has `lsfMPTLocked` flag set. - - The `MPTokenIssuance` object of the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` has the `lsfMPTLocked` flag set. - - The `MPTokenIssuance` object of the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` does not have the `lsfMPTCanTransfer` flag set (the asset is not transferable). + - If the `Destination` field is not specified: + - The `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the submitter `AccountRoot` has `lsfMPTLocked` flag set. + + - If the `Destination` field specified: + - If the `Destination` is not the `Issuer` and the `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the `Destination` `AccountRoot` has `lsfMPTLocked` flag set. + + - The `MPTokenIssuance` object of the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` has the `lsfMPTLocked` flag set and `Destination` is not the `Issuer` of the asset. + - The `MPTokenIssuance` object of the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` does not have the `lsfMPTCanTransfer` flag set and `Destination` is not the `Issuer` of the asset (the asset is not transferable). - The `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the `LoanBroker.Account` `AccountRoot` has `lsfMPTLocked` flag set. (The Loan Broker _pseudo-account_ is locked). - The `LoanBroker.CoverAvailable` < `Amount`. @@ -709,17 +722,31 @@ The `LoanBrokerCoverWithdraw` transaction withdraws the First-Loss Capital from - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is `XRP`: - Decrease the `Balance` field of `LoanBroker` _pseudo-account_ `AccountRoot` by `Amount`. - - Increase the `Balance` field of the submitter `AccountRoot` by `Amount`. + - If `Destination` field is not specified: + - Increase the `Balance` field of the submitter `AccountRoot` by `Amount`. + - If `Destination` field is specified: + - Increase the `Balance` field of the `Destination` `AccountRoot` by `Amount`. + - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `IOU`: - Decrease the `RippleState` balance between the `LoanBroker` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `Amount`. - - Increase the `RippleState` balance between the submitter `AccountRoot` and the `Issuer` `AccountRoot` by `Amount`. + - If `Destination` field is not specified: + - Increase the `RippleState` balance between the submitter `AccountRoot` and the `Issuer` `AccountRoot` by `Amount`. + - If `Destination` field is specified: + - Create a `RippleState` object if it does not exist between the `Destination` `AccountRoot` and the `Issuer` `AccountRoot`. + - Increase the `RippleState` balance between the `Destination` `AccountRoot` and the `Issuer` `AccountRoot` by `Amount`. + - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `MPT`: - Decrease the `MPToken.MPTAmount` by `Amount` of the `LoanBroker` _pseudo-account_ `MPToken` object for the `Vault.Asset`. - - Increase the `MPToken.MPTAmount` by `Amount` of the submitter `MPToken` object for the `Vault.Asset`. + - If `Destination` field is not specified: + - Increase the `MPToken.MPTAmount` by `Amount` of the submitter `MPToken` object for the `Vault.Asset`. + + - If `Destination` field is specified: + - Create a `MPToken` object for the `Destination` `AccountRoot` if it does not exist. + - Increase the `MPToken.MPTAmount` by `Amount` of the `Destination` `MPToken` object for the `Vault.Asset`. - Decrease `LoanBroker.CoverAvailable` by `Amount`. @@ -1015,8 +1042,7 @@ The transaction deletes an existing `Loan` object. - Update the `LoanBroker` object: - Decrease the Debt of the LoanBroker: - - `LoanBroker(LoanBrokerID).DebtTotal -= ` - - `Loan.PrincipalOutstanding + Loan.InterestOutstanding` + - `LoanBroker(LoanBrokerID).DebtTotal -= Loan.PrincipalOutstanding + Loan.InterestOutstanding` - Decrease the First-Loss Capital Cover Available: - `LoanBroker(LoanBrokerID).CoverAvailable -= DefaultCovered` @@ -1059,7 +1085,7 @@ The transaction deletes an existing `Loan` object. - Update the `Vault` object (clear "paper loss"): - `Vault(LoanBroker(LoanBrokerID).VaultID).LossUnrealized -= Loan.PrincipalOutstanding + TotalInterestOutstanding()` (Please refer to section [**3.2.5.1.5 Total Value Calculation**](#3252-total-loan-value-calculation), which outlines how to calculate total interest outstanding) - + - Update the `Loan` object: - `CandidateDueDate = max(Loan.PreviousPaymentDate, Loan.StartDate) + Loan.PaymentInterval` @@ -1389,7 +1415,6 @@ $$ totalInterestOutstanding = totalValueOutstanding - principalOutstanding $$ - ##### 3.2.5.3 Transaction Pseudo-code The following is the pseudo-code for handling a Loan payment transaction. @@ -1634,7 +1659,7 @@ Furthermore, assume `full_periodic_payments` variable represents the number of p # Appendix -## A-1 F.A.Q. +## A-1 F.A.Q ### A-1.1 What is the `LoanBroker.LoanSequence` field? From 3eee2bd4a0f4471689cc989e203d7635b0d91db3 Mon Sep 17 00:00:00 2001 From: Vito Tumas <5780819+Tapanito@users.noreply.github.com> Date: Fri, 30 May 2025 11:40:19 +0200 Subject: [PATCH 41/77] Update XLS-0066d-lending-protocol/README.md Co-authored-by: Mayukha Vadari --- XLS-0066d-lending-protocol/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index 1ad2e7aa..d3cbdd1f 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -49,7 +49,7 @@ This version intentionally skips the complex mechanisms of automated on-chain co - [**3.1. LoanBroker Transactions**](#31-loanbroker-transactions) - [**3.1.1. LoanBrokerSet Transaction**](#311-loanbrokerset) - [**3.1.2. LoanBrokerDelete Transaction**](#312-loanbrokerdelete) - - [**3.1.3. LoanBrokerCovereposit Transaction**](#313-loanbrokercoverdeposit) + - [**3.1.3. LoanBrokerCoverDeposit Transaction**](#313-loanbrokercoverdeposit) - [**3.1.4. LoanBrokerCoverWithdraw Transaction**](#314-loanbrokercoverwithdraw) - [**3.2 Loan Transactions**](#32-loan-transactions) - [**3.2.1. LoanSet Transaction**](#321-loanset-transaction) From ed5a62dad5c785f51b5595c9ef4fd8c772ba1ac7 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Fri, 13 Jun 2025 07:01:00 +0200 Subject: [PATCH 42/77] adds LoanBrokerCoverClawback transaction --- XLS-0066d-lending-protocol/README.md | 59 ++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 3 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index d3cbdd1f..19beebbd 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -51,6 +51,7 @@ This version intentionally skips the complex mechanisms of automated on-chain co - [**3.1.2. LoanBrokerDelete Transaction**](#312-loanbrokerdelete) - [**3.1.3. LoanBrokerCoverDeposit Transaction**](#313-loanbrokercoverdeposit) - [**3.1.4. LoanBrokerCoverWithdraw Transaction**](#314-loanbrokercoverwithdraw) + - [**3.1.5. LoanBrokerCoverClawback Transaction**](#315-loanbrokercoverclawback) - [**3.2 Loan Transactions**](#32-loan-transactions) - [**3.2.1. LoanSet Transaction**](#321-loanset-transaction) - [**3.2.2. LoanDelete Transaction**](#322-loandelete-transaction) @@ -73,6 +74,7 @@ The specification introduces the following transactions: - **`LoanBrokerDelete`**: A transaction to delete an existing `LoanBroker` object. - **`LoanBrokerCoverDeposit`**: A transaction to deposit First-Loss Capital. - **`LoanBrokerCoverWithdraw`**: A transaction to withdraw First-Loss Capital. +- **`LoanBrokerCoverClawback`**: A transaction to clawback the First-Loss Capital. This transaction can only be submitted by the Issuer of the asset. - **`LoanSet`**: A transaction to create a new `Loan` object. - **`LoanDelete`**: A transaction to delete an existing `Loan` object. - **`LoanManage`**: A transaction to manage an existing `Loan`. @@ -680,7 +682,7 @@ The `LoanBrokerCoverWithdraw` transaction withdraws the First-Loss Capital from | ----------------- | :----------------: | :-------: | :-----------: | :-----------: | :---------------------------------------------------------------------- | | `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | Transaction type. | | `LoanBrokerID` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The Loan Broker ID from which to withdraw First-Loss Capital. | -| `Amount` | :heavy_check_mark: | `object` | `AMOUNT` | 0 | The amount of Vault asset to withdraw. | +| `Amount` | :heavy_check_mark: | `object` | `AMOUNT` | 0 | The Fist-Loss Capital amount to withdraw. | | `Destination` | | `string` | `AccountID` | Empty | An account to receive the assets. It must be able to receive the asset. | ##### 3.1.4.1 Failure conditions @@ -752,6 +754,53 @@ The `LoanBrokerCoverWithdraw` transaction withdraws the First-Loss Capital from [**Return to Index**](#index) +#### 3.1.5 `LoanBrokerCoverClawback` + +The `LoanBrokerCoverClawback` transaction claws back the First-Loss Capital from the `LoanBroker`. The transaction can only be submitted by the Issuer of the Loan asset. Furthermore, the transaction can only clawback funds up to the minimum cover required for the current loans. + +| Field Name | Required? | JSON Type | Internal Type | Default Value | Description | +| ----------------- | :----------------: | :-------: | :-----------: | :-----------: | :---------------------------------------------------------------------- | +| `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | Transaction type. | +| `LoanBrokerID` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The Loan Broker ID from which to withdraw First-Loss Capital. | +| `Amount` | :heavy_check_mark: | `object` | `AMOUNT` | 0 | The Fist-Loss Capital amount to clawback. If the amount is `0` clawback funds up to `LoanBroker.DebtTotal * LoanBroker.CoverRateMinimum` | + +##### 3.1.5.1 Failure conditions + +- `LoanBroker` object with the specified `LoanBrokerID` does not exist on the ledger. +- The submitter `AccountRoot.Account != LoanBroker(LoanBrokerID).Owner`. + +- If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is `XRP`. + +- If `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `IOU` and: + + - The Issuer account is not the submitter of the transaction. + - If the `AccountRoot(Issuer)` object does not have lsfAllowTrustLineClawback flag set (the asset does not support clawback). + - If the `AccountRoot(Issuer)` has the lsfNoFreeze flag set (the asset cannot be frozen). + +- If `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `MPT` and: + + - MPTokenIssuance.Issuer is not the submitter of the transaction. + - MPTokenIssuance.lsfMPTCanClawback flag is not set (the asset does not support clawback). + - If the MPTokenIssuance.lsfMPTCanLock flag is NOT set (the asset cannot be locked). + +- The `Amount` > `0` and `LoanBroker.CoverAvailable` < `Amount`. + +- `LoanBroker.CoverAvailable - Amount` < `LoanBroker.DebtTotal * LoanBroker.CoverRateMinimum` + +##### 3.1.5.2 State Changes + +- If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `IOU`: + + - Decrease the `RippleState` balance between the `LoanBroker` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `Amount`. + +- If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `MPT`: + + - Decrease the `MPToken.MPTAmount` by `Amount` of the `LoanBroker` _pseudo-account_ `MPToken` object for the `Vault.Asset`. + +- Decrease `LoanBroker.CoverAvailable` by `Amount`. + +[**Return to Index**](#index) + ### 3.2. `Loan` Transactions In this section we specify transactions associated with the `Loan` ledger entry. @@ -1659,8 +1708,12 @@ Furthermore, assume `full_periodic_payments` variable represents the number of p # Appendix -## A-1 F.A.Q +## A-1 F.A.Q. -### A-1.1 What is the `LoanBroker.LoanSequence` field? +### A-1.1. What is the `LoanBroker.LoanSequence` field? A sequential identifier for Loans associated with a LoanBroker object. This value increments with each new Loan created by the broker. Unlike `LoanBroker.OwnerCount`, which tracks the number of currently active Loans, `LoanBroker.LoanSequence` reflects the total number of Loans ever created. + +### A-1-2. Why the `LoanBrokerCoverClawback` cannot clawback the full LoanBroker.CoverAvailable amount? + +The `LoanBrokerCoverClawback` transaction allows the Issuer to clawback the `LoanBroker` First-Loss Capital, specifically the `LoanBroker.CoverAvailable` amount. The transaction cannot claw back the full CoverAvailable amount because the LoanBroker must maintain a minimum level of first-loss capital to protect depositors. This minimum is calculated as `LoanBroker.DebtTotal * LoanBroker.CoverRateMinimum`. When a `LoanBroker` has active loans, a complete clawback would leave depositors vulnerable to unexpected losses. Therefore, the system ensures that a minimum amount of first-loss capital is always maintained. From f992b00e6292a7b2dd53f0864c39fe93e38e6de3 Mon Sep 17 00:00:00 2001 From: Vito Tumas <5780819+Tapanito@users.noreply.github.com> Date: Wed, 16 Jul 2025 16:24:14 +0200 Subject: [PATCH 43/77] Apply suggestions from code review Co-authored-by: Ed Hennis --- XLS-0066d-lending-protocol/README.md | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index 19beebbd..0eebf90a 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -225,7 +225,7 @@ The `LoanBroker` object has the following fields: | `CoverRateMinimum` | `No` | `Yes` | :heavy_check_mark: | `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%. | | `CoverRateLiquidation` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | 0 | The 1/10th basis point of minimum required first loss capital that is liquidated to cover a Loan default. Valid values are between 0 and 100000 inclusive. A value of 1 is equivalent to 1/10 bps or 0.001%. | -#### 2.1.3 `LoanBroker`_pseudo-account_` +#### 2.1.3 `LoanBroker` _pseudo-account_ The `LoanBroker` _pseudo-account_ holds the First-Loss Capital deposited by the LoanBroker, as well as Loan funds. The _pseudo-account_ follows the XLS-64d specification for pseudo accounts. The `AccountRoot` object is created when creating the `Vault` object. @@ -1137,6 +1137,7 @@ The transaction deletes an existing `Loan` object. - Update the `Loan` object: + - `Loan(LoanID).Flags &= ~lsfLoanImpaired` - `CandidateDueDate = max(Loan.PreviousPaymentDate, Loan.StartDate) + Loan.PaymentInterval` - If `CandidateDueDate > currentTime` (the loan was unimpaired within the payment interval): @@ -1619,19 +1620,26 @@ Furthermore, assume `full_periodic_payments` variable represents the number of p - `feeManagement = interest_paid x LoanBroker.ManagementFeeRate` - - Decrease the management fee from totalPaid amount: + - Total paid, and what portion goes to the vault: + + - `totalPaid = principal_paid + interest_paid + fee_paid` + - `totalPaidToVault = principal_paid + interest_paid` + - `totalPaidToBroker = fee_paid` + + - Adjust the totals for the management fee: - - `totalPaid = totalPaid - feeManagement` + - `totalPaidToVault = totalPaidToVault - feeManagement` + - `totalPaidToBroker = totalPaidToBroker + feeManagement` - If there is **not enough** first-loss capital: `LoanBroker.CoverAvailable < LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`: - Add the fee to to First Loss Cover Pool: - - `LoanBroker.CoverAvailable = LoanBroker.CoverAvailable + (feeManagement + fee_paid)` + - `LoanBroker.CoverAvailable = LoanBroker.CoverAvailable + (totalPaidToBroker)` - Decrease LoanBroker Debt by the amount paid: - - `LoanBroker.DebtTotal = LoanBroker.DebtTotal - totalPaid` + - `LoanBroker.DebtTotal = LoanBroker.DebtTotal - (totalPaid - fee_paid)` - Update the LoanBroker Debt by the Loan value change: @@ -1645,7 +1653,7 @@ Furthermore, assume `full_periodic_payments` variable represents the number of p - Increase available assets in the Vault by the amount paid: - - `Vault.AssetsAvailable = Vault.AssetsAvailable + totalPaid` + - `Vault.AssetsAvailable = Vault.AssetsAvailable + totalPaidToVault` - Update the Vault total value by the change in the Loan total value: From e4930c6d7c84e5b771a1ccb91dbee2290cfb4fb5 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Wed, 16 Jul 2025 16:42:45 +0200 Subject: [PATCH 44/77] adds deepfreeze flag checks where appropriate --- XLS-0066d-lending-protocol/README.md | 30 ++++++++++++++-------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index 0eebf90a..aaaeb7a0 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -99,7 +99,7 @@ The flow of the lending protocol is as follows: ### 1.2.1 Clawback -Clawback is a mechanism by which an asset Issuer (IOU or MPT, not XRP) claws back the funds. It can be performed on the Vault, not the Lending Protocol. +Clawback is a mechanism by which an asset Issuer (IOU or MPT, not XRP) claws back the funds. The Issuer may clawback funds from the First-Loss Capital. ### 1.2.2 Freeze @@ -636,7 +636,7 @@ The transaction deposits First Loss Capital into the `LoanBroker` object. - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `IOU`: - The `RippleState` object between the submitter account and the `Issuer` of the asset has the `lsfLowFreeze` or `lsfHighFreeze` flag set. - - The `RippleState` between the `LoanBroker.Account` and the `Issuer` has the `lsfLowFreeze` or `lsfHighFreeze` flag set. (The Loan Broker _pseudo-account_ is frozen). + - The `RippleState` between the `LoanBroker.Account` and the `Issuer` has the `lsfLowDeepFreeze` or `lsfHighDeepFreeze` flag set. (The Loan Broker _pseudo-account_ is frozen). - The `AccountRoot` object of the `Issuer` has the `lsfGlobalFreeze` flag set. - The `RippleState` object `Balance` < `Amount` (Depositor has insufficient funds). @@ -695,9 +695,10 @@ The `LoanBrokerCoverWithdraw` transaction withdraws the First-Loss Capital from - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `IOU`: - If the `Destination` field is not specified: - - The `RippleState` object between the submitter account and the `Issuer` of the asset has the `lsfLowFreeze` or `lsfHighFreeze` flag set. + - The `RippleState` object between the submitter account and the `Issuer` of the asset has the `lsfLowDeepFreeze` or `lsfHighDeepFreeze` flag set. - If the `Destination` field is specified: + - The `RippleState` object between the `Destination` account and the `Issuer` of the asset does not exist. - If `Destination` is not the `Issuer` and the `RippleState` object between the `Destination` account and the `Issuer` of the asset has the `lsfLowFreeze` or `lsfHighFreeze` flag set. - The `AccountRoot` object of the `Issuer` has the `lsfGlobalFreeze` flag set and `Destination` is not the `Issuer` of the asset. @@ -709,8 +710,9 @@ The `LoanBrokerCoverWithdraw` transaction withdraws the First-Loss Capital from - The `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the submitter `AccountRoot` has `lsfMPTLocked` flag set. - If the `Destination` field specified: + - The `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the `Destination` `AccountRoot` does not exist. - If the `Destination` is not the `Issuer` and the `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the `Destination` `AccountRoot` has `lsfMPTLocked` flag set. - + - The `MPTokenIssuance` object of the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` has the `lsfMPTLocked` flag set and `Destination` is not the `Issuer` of the asset. - The `MPTokenIssuance` object of the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` does not have the `lsfMPTCanTransfer` flag set and `Destination` is not the `Issuer` of the asset (the asset is not transferable). - The `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the `LoanBroker.Account` `AccountRoot` has `lsfMPTLocked` flag set. (The Loan Broker _pseudo-account_ is locked). @@ -737,7 +739,6 @@ The `LoanBrokerCoverWithdraw` transaction withdraws the First-Loss Capital from - Increase the `RippleState` balance between the submitter `AccountRoot` and the `Issuer` `AccountRoot` by `Amount`. - If `Destination` field is specified: - - Create a `RippleState` object if it does not exist between the `Destination` `AccountRoot` and the `Issuer` `AccountRoot`. - Increase the `RippleState` balance between the `Destination` `AccountRoot` and the `Issuer` `AccountRoot` by `Amount`. - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `MPT`: @@ -747,7 +748,6 @@ The `LoanBrokerCoverWithdraw` transaction withdraws the First-Loss Capital from - Increase the `MPToken.MPTAmount` by `Amount` of the submitter `MPToken` object for the `Vault.Asset`. - If `Destination` field is specified: - - Create a `MPToken` object for the `Destination` `AccountRoot` if it does not exist. - Increase the `MPToken.MPTAmount` by `Amount` of the `Destination` `MPToken` object for the `Vault.Asset`. - Decrease `LoanBroker.CoverAvailable` by `Amount`. @@ -767,7 +767,6 @@ The `LoanBrokerCoverClawback` transaction claws back the First-Loss Capital from ##### 3.1.5.1 Failure conditions - `LoanBroker` object with the specified `LoanBrokerID` does not exist on the ledger. -- The submitter `AccountRoot.Account != LoanBroker(LoanBrokerID).Owner`. - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is `XRP`. @@ -868,8 +867,7 @@ Either of the parties (Borrower or Loan Issuer) may initiate the transaction. Th - `Borrower` initiates the transaction: 1. The `Borrower` creates the transaction from their account, setting the pre-agreed terms. - - - Optionally, the `Borrower` may set the `Counterparty` to `LoanBroker.Owner`. In case the `Counterparty` field is not set, it is assumed to be the `LoanBroker.Owner`. + - Optionally, the `Borrower` may set the `Counterparty` to `LoanBroker.Owner`. In case the `Counterparty` field is not set, it is assumed to be the `LoanBroker.Owner`. 2. The `Borrower` signs the transaction setting the `SigningPubKey`, `TxnSignature`, `Signers`, `Account`, `Fee`, `Sequence` fields. 3. The `Borrower` sends the transaction to the `Loan Issuer`. @@ -881,7 +879,7 @@ Either of the parties (Borrower or Loan Issuer) may initiate the transaction. Th 1. The `Loan Issuer` creates the transaction from their account setting the pre-agreed terms. - - The `Loan Issuer` must set the `Counterparty` to the `Borrower` account ID. + - The `Loan Issuer` must set the `Counterparty` to the `Borrower` account ID. 2. The `Loan Issuer` signs the transaction setting the `SigningPubKey`, `TxnSignature`, `Signers`, `Account`, `Fee`, `Sequence` fields. 3. The `Loan Issuer` sends the transaction to the `Borrower`. @@ -903,7 +901,9 @@ The account specified in the `Account` field pays the transaction fee. - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `IOU`: - The `RippleState` object between the submitter account and the `Issuer` of the asset has the `lsfLowFreeze` or `lsfHighFreeze` flag set. - - The `RippleState` between the `LoanBroker.Account` and the `Issuer` has the `lsfLowFreeze` or `lsfHighFreeze` flag set. (The Loan Broker _pseudo-account_ is frozen). + - The `RippleState` between the `LoanBroker.Account` and the `Issuer` has the `lsfLowDeepFreeze` or `lsfHighDeepFreeze` flag set. (The Loan Broker _pseudo-account_ is frozen). + + - The `RippleState` between the `Vault(LoanBroker(LoanBrokerID).VaultID).Account` and the `Issuer` has the `lsfLowFreeze` or `lsfHighFreeze` flag set. (The Vault _pseudo-account_ is frozen). - The `AccountRoot` object of the `Issuer` has the `lsfGlobalFreeze` flag set. - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `MPT`: @@ -1177,7 +1177,7 @@ The Borrower submits a `LoanDraw` transaction to draw funds from the Loan. - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `IOU`: - - The `RippleState` object between the submitter account and the `Issuer` of the asset has the `lsfLowFreeze` or `lsfHighFreeze` flag set. + - The `RippleState` object between the submitter account and the `Issuer` of the asset has the `lsfLowDeepFreeze` or `lsfHighDeepFreeze` flag set. - The `RippleState` between the `LoanBroker.Account` and the `Issuer` has the `lsfLowFreeze` or `lsfHighFreeze` flag set. (The Loan Broker _pseudo-account_ is frozen). - The `AccountRoot` object of the `Issuer` has the `lsfGlobalFreeze` flag set. @@ -1510,7 +1510,7 @@ function make_payment(amount, current_time) -> (principal_paid, interest_paid, v let periodic_payment = loan.compute_periodic_payment() - let full_periodic_payments = floor(amount / periodic_payment) + let full_periodic_payments = floor(amount / (periodic_payment + loan.service_fee)) if full_periodic_payments < 1 { return "insufficient amount paid" error } @@ -1583,8 +1583,8 @@ Furthermore, assume `full_periodic_payments` variable represents the number of p - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `IOU`: - The `RippleState` object between the submitter account and the `Issuer` of the asset has the `lsfLowFreeze` or `lsfHighFreeze` flag set. - - The `RippleState` between the `LoanBroker.Account` and the `Issuer` has the `lsfLowFreeze` or `lsfHighFreeze` flag set. (The Loan Broker _pseudo-account_ is frozen). - - The `RippleState` between the `Vault(LoanBroker(Loan.LoanBrokerID).VaultID).Account` and the `Issuer` has the `lsfLowFreeze` or `lsfHighFreeze` flag set. (The Vault _pseudo-account_ is frozen). + - The `RippleState` between the `LoanBroker.Account` and the `Issuer` has the `lsfLowDeepFreeze` or `lsfHighDeepFreeze` flag set. (The Loan Broker _pseudo-account_ is frozen). + - The `RippleState` between the `Vault(LoanBroker(Loan.LoanBrokerID).VaultID).Account` and the `Issuer` has the `lsfLowDeepFreeze` or `lsfHighDeepFreeze` flag set. (The Vault _pseudo-account_ is frozen). - The `AccountRoot` object of the `Issuer` has the `lsfGlobalFreeze` flag set. - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `MPT`: From 71b9e36c327d726a3e63badc5262789d4629fc44 Mon Sep 17 00:00:00 2001 From: Vito Tumas <5780819+Tapanito@users.noreply.github.com> Date: Fri, 8 Aug 2025 15:36:24 +0200 Subject: [PATCH 45/77] Apply suggestions from code review Co-authored-by: Ed Hennis --- XLS-0066d-lending-protocol/README.md | 30 +++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index aaaeb7a0..1ccb4b8a 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -761,33 +761,49 @@ The `LoanBrokerCoverClawback` transaction claws back the First-Loss Capital from | Field Name | Required? | JSON Type | Internal Type | Default Value | Description | | ----------------- | :----------------: | :-------: | :-----------: | :-----------: | :---------------------------------------------------------------------- | | `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | Transaction type. | -| `LoanBrokerID` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The Loan Broker ID from which to withdraw First-Loss Capital. | -| `Amount` | :heavy_check_mark: | `object` | `AMOUNT` | 0 | The Fist-Loss Capital amount to clawback. If the amount is `0` clawback funds up to `LoanBroker.DebtTotal * LoanBroker.CoverRateMinimum` | +| `LoanBrokerID` | | `string` | `HASH256` | `N/A` | The Loan Broker ID from which to withdraw First-Loss Capital. Must be provided if the `Amount` is an MPT, or `Amount` is an IOU and `issuer` is specified as the `Account` submitting the transaction. | +| `Amount` | | `object` | `AMOUNT` | 0 | The First-Loss Capital amount to clawback. If the amount is `0` or not provided, clawback funds up to `LoanBroker.DebtTotal * LoanBroker.CoverRateMinimum`. | ##### 3.1.5.1 Failure conditions -- `LoanBroker` object with the specified `LoanBrokerID` does not exist on the ledger. +- Neither `LoanBrokerID` nor `Amount` are specified. +- `Amount` is specified and `Amount < 0` +- `Amount` specifies an XRP amount. +- If the `LoanBrokerID` is specified, the `LoanBroker` object with that ID does not exist on the ledger. +- If the `LoanBrokerID` is not specified, and can not be determined from `Amount`. + - `Amount` specifies an MPT. + - `Amount` specifies an IOU, and the `issuer` value is *not* a pseudo-account with `Account(Amount.issuer).LoanBrokerID` set. If it is set, treat `LoanBrokerID` as `Account(Amount.issuer).LoanBrokerID` for the rest of this transaction. +- If both the `LoanBrokerID` and `Amount` are specified, and: + + - The `Amount.issuer` value does not match the submitter `Account` of the transaction or `LoanBroker(LoanBrokerID).Account` (the pseudo-account of the LoanBroker). + - The `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is not the same asset type as `Amount`, allowing for an IOU `Amount.issuer` to specify `LoanBroker(LoanBrokerID).Account` instead of `Vault(LoanBroker(LoanBrokerID).VaultID).Asset`. - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is `XRP`. - If `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `IOU` and: - The Issuer account is not the submitter of the transaction. + - `Amount.issuer` value is not one of + - The submitter of the transaction + - `LoanBroker(LoanBrokerID).Account` - If the `AccountRoot(Issuer)` object does not have lsfAllowTrustLineClawback flag set (the asset does not support clawback). - If the `AccountRoot(Issuer)` has the lsfNoFreeze flag set (the asset cannot be frozen). - If `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `MPT` and: - MPTokenIssuance.Issuer is not the submitter of the transaction. + - If the `LoanBrokerID` is not specified. - MPTokenIssuance.lsfMPTCanClawback flag is not set (the asset does not support clawback). - If the MPTokenIssuance.lsfMPTCanLock flag is NOT set (the asset cannot be locked). -- The `Amount` > `0` and `LoanBroker.CoverAvailable` < `Amount`. - -- `LoanBroker.CoverAvailable - Amount` < `LoanBroker.DebtTotal * LoanBroker.CoverRateMinimum` +- `LoanBroker.CoverAvailable` <= `LoanBroker.DebtTotal * LoanBroker.CoverRateMinimum` ##### 3.1.5.2 State Changes +- If `Amount` is 0 or unset, set `Amount` to `LoanBroker.CoverAvailable - LoanBroker.DebtTotal * LoanBroker.CoverRateMinimum`. +- Otherwise set `Amount` to `min(Amount, `LoanBroker.CoverAvailable - LoanBroker.DebtTotal * LoanBroker.CoverRateMinimum`). + + - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `IOU`: - Decrease the `RippleState` balance between the `LoanBroker` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `Amount`. @@ -835,7 +851,7 @@ The transaction creates a new `Loan` object. | Flag Name | Flag Value | Description | | ------------------- | :----------: | :---------------------------------------------- | -| `tfLoanOverpayment` | `0x00010000` | Indicates that the vault supports overpayments. | +| `tfLoanOverpayment` | `0x00010000` | Indicates that the loan supports overpayments. | ##### 3.2.1.2 `CounterpartySignature` From 47ece0f5a54fb3992ebcee7a78433e8de35d4eb8 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Thu, 14 Aug 2025 14:28:23 +0200 Subject: [PATCH 46/77] improves freeze documentation --- XLS-0066d-lending-protocol/README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index 1ccb4b8a..a217b1f5 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -103,12 +103,14 @@ Clawback is a mechanism by which an asset Issuer (IOU or MPT, not XRP) claws bac ### 1.2.2 Freeze -Freeze is a mechanism by which an asset Issuer (IOUT or MPT, not XRP) freezes an `Account`, preventing that account from sending or receiving the Asset. Furthermore, an Issuer may enact a global freeze, which prevents everyone from sending or receiving the Asset. Note that in both single-account and global freezes, the Asset can be sent to the Issuer. +Freeze is a mechanism by which an asset Issuer (IOUT or MPT, not XRP) freezes an `Account`, preventing that account from sending the Asset. Deep Freeze is a mechanism by which an asset Issuer prevents and `Account` from both sending and receiving and Asset. Finally, an Issuer may enact a global freeze, which prevents everyone from sending or receiving the Asset. Note that in both single-account and global freezes, the Asset can be sent to the Issuer. -If the Issuer freezes a Borrower's account, the Borrower cannot make loan payments or draw down funds. A frozen account does not lift the obligation to repay a Loan. -If a Loan Broker's account is frozen, the Broker will not receive any Loan fees. They will be able to create new loans, and existing loans will not be affected. However, the Loan Broker cannot deposit or withdraw First-Loss Capital. +If the Issuer freezes a Borrower's account, the Borrower cannot make loan payments. However, a frozen account does not lift the obligation to repay a Loan. If the Issuer Deep Freezes a Borrower's account, the Brrower cannot make loan payments and they cannot draw down Loan funds. + +A Deep Freeze does not affect the Loan Broker's functions. However, a Deep Freeze will prevent the Loan Broker from receing any Lending Protocol Fees. + +The Issuer may also Freeze of Deep Freeze the `_pseudo-account_` of the Loan Broker. A Freeze on the `_pseudo-account_` will prevent the Loan Broker from creating new Loans as well as prevent Borrowers from drawing down their Loans. However existing Loans will not be affected. In contrast, a Deep Freeze, will also prevent the Loans from being paid. -Finally, the exact behaviour has yet to be defined in a global freeze. **TBD** ### 1.3 Risk Management From 43eeab3eb7092fa9082f859fdf916cdfd45608c6 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Fri, 5 Sep 2025 13:23:55 +0200 Subject: [PATCH 47/77] removes LoanDraw transaction, as a result removes Loan.AssetsAvailable and LoanSet.StartDate fields --- XLS-0066d-lending-protocol/README.md | 159 ++++++--------------------- 1 file changed, 35 insertions(+), 124 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index a217b1f5..8ddec386 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -56,8 +56,7 @@ This version intentionally skips the complex mechanisms of automated on-chain co - [**3.2.1. LoanSet Transaction**](#321-loanset-transaction) - [**3.2.2. LoanDelete Transaction**](#322-loandelete-transaction) - [**3.2.3. LoanManage Transaction**](#323-loanmanage-transaction) - - [**3.2.4. LoanDraw Transaction**](#324-loandraw-transaction) - - [**3.2.5 LoanPay Transaction**](#325-loanpay-transaction) + - [**3.2.4. LoanPay Transaction**](#324-loanpay-transaction) - [**Appendix**](#appendix) ## 1. Introduction @@ -78,7 +77,6 @@ The specification introduces the following transactions: - **`LoanSet`**: A transaction to create a new `Loan` object. - **`LoanDelete`**: A transaction to delete an existing `Loan` object. - **`LoanManage`**: A transaction to manage an existing `Loan`. -- **`LoanDraw`**: A transaction to drawdown `Loan` funds. - **`LoanPay`**: A transaction to make a `Loan` payment. The flow of the lending protocol is as follows: @@ -87,13 +85,12 @@ The flow of the lending protocol is as follows: 2. The Loan Broker creates a `LoanBroker` ledger entry with a `LoanBrokerSet` transaction. 3. The Depositors deposit assets into the `Vault`. 4. Optionally, the Loan Broker deposits First-Loss Capital into the `LoanBroker` with the `LoanBrokerCoverDeposit` transaction. -5. The Loan Broker and Borrower create a `Loan` object with a `LoanSet` transaction. -6. The Borrower can draw funds with the `LoanDraw` transaction and make payments with the `LoanPay`. -7. If the Borrower fails to pay the Loan, the Loan Broker can default the `Loan` using the `LoanManage` transaction. -8. Once the Loan has matured (or defaulted), the Borrower or the Loan Broker can delete it using a `LoanDelete` transaction. -9. Optionally, the Loan Broker can withdraw the First-Loss Capital using the `LoanBrokerCoverWithdraw` transaction. -10. When all `Loan` objects are deleted, the Loan Broker can delete the `LoanBroker` object with a `LoanBrokerDelete` transaction. -11. When all `LoanBroker` objects are deleted, the Loan Broker can delete the `Vault` object. +5. The Loan Broker and Borrower create a `Loan` object with a `LoanSet` transaction and the requested principal (excluding fees) is transered to the Borrower. +6. If the Borrower fails to pay the Loan, the Loan Broker can default the `Loan` using the `LoanManage` transaction. +7. Once the Loan has matured (or defaulted), the Borrower or the Loan Broker can delete it using a `LoanDelete` transaction. +8. Optionally, the Loan Broker can withdraw the First-Loss Capital using the `LoanBrokerCoverWithdraw` transaction. +9. When all `Loan` objects are deleted, the Loan Broker can delete the `LoanBroker` object with a `LoanBrokerDelete` transaction. +10. When all `LoanBroker` objects are deleted, the Loan Broker can delete the `Vault` object. ### 1.2 Compliance Features @@ -105,12 +102,11 @@ Clawback is a mechanism by which an asset Issuer (IOU or MPT, not XRP) claws bac Freeze is a mechanism by which an asset Issuer (IOUT or MPT, not XRP) freezes an `Account`, preventing that account from sending the Asset. Deep Freeze is a mechanism by which an asset Issuer prevents and `Account` from both sending and receiving and Asset. Finally, an Issuer may enact a global freeze, which prevents everyone from sending or receiving the Asset. Note that in both single-account and global freezes, the Asset can be sent to the Issuer. -If the Issuer freezes a Borrower's account, the Borrower cannot make loan payments. However, a frozen account does not lift the obligation to repay a Loan. If the Issuer Deep Freezes a Borrower's account, the Brrower cannot make loan payments and they cannot draw down Loan funds. +If the Issuer freezes a Borrower's account, the Borrower cannot make loan payments. However, a frozen account does not lift the obligation to repay a Loan. If the Issuer Deep Freezes a Borrower's account, the Brrower cannot make loan payments. A Deep Freeze does not affect the Loan Broker's functions. However, a Deep Freeze will prevent the Loan Broker from receing any Lending Protocol Fees. -The Issuer may also Freeze of Deep Freeze the `_pseudo-account_` of the Loan Broker. A Freeze on the `_pseudo-account_` will prevent the Loan Broker from creating new Loans as well as prevent Borrowers from drawing down their Loans. However existing Loans will not be affected. In contrast, a Deep Freeze, will also prevent the Loans from being paid. - +The Issuer may also Freeze of Deep Freeze the `_pseudo-account_` of the Loan Broker. A Freeze on the `_pseudo-account_` will prevent the Loan Broker from creating new Loans. However existing Loans will not be affected. In contrast, a Deep Freeze, will also prevent the Loans from being paid. ### 1.3 Risk Management @@ -141,7 +137,6 @@ The lending protocol charges a number of fees that the Loan Broker can configure - **`Fixed-Term Loan`**: A type of Loan with a known end date and a constant periodic payment schedule. - **`Principal`**: The original sum of money borrowed that must be repaid, excluding interest or other fees. - **`Interest`**: The cost of borrowing the Asset, calculated as a percentage of the loan principal, which the Borrower pays to the Lender over time. -- **`Drawdown`**: The process where a borrower accesses part or all of the loan funds after the Loan has been created. - **`Default`**: The failure by the Borrower to meet the obligations of a loan, such as missing payments. - **`First-Loss Capital`**: The portion of capital that absorbs initial losses in case of a Default, protecting the Vault from loss. - **`Term`**: The period over which a Borrower must repay the Loan. @@ -472,13 +467,12 @@ The `LoanID` is calculated as follows: | `LateInterestRate` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | A premium is added to the interest rate for late payments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | | `CloseInterestRate` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | An interest rate charged for repaying the Loan early in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | | `OverpaymentInterestRate` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | An interest rate charged on overpayments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | -| `StartDate` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The timestamp of when the Loan starts [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time). | +| `StartDate` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `CurrentLedgerTimestamp` | The timestamp of when the Loan started [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time). | | `PaymentInterval` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | Number of seconds between Loan payments. | | `GracePeriod` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The number of seconds after the Payment Due Date that the Loan can be Defaulted. | | `PreviousPaymentDate` | `No` | `No` | :heavy_check_mark: | `number` | `UINT32` | `0` | The timestamp of when the previous payment was made in [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time). | | `NextPaymentDueDate` | `No` | `No` | :heavy_check_mark: | `number` | `UINT32` | `LoanSet.StartDate + LoanSet.PaymentInterval` | The timestamp of when the next payment is due in [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time). | | `PaymentRemaining` | `No` | `No` | :heavy_check_mark: | `number` | `UINT32` | `LoanSet.PaymentTotal` | The number of payments remaining on the Loan. | -| `AssetsAvailable` | `No` | `No` | :heavy_check_mark: | `number` | `NUMBER` | `LoanSet.[PrincipalRequested - LoanOriginationFee]` | The asset amount that is available in the Loan. | | `PrincipalOutstanding` | `No` | `No` | :heavy_check_mark: | `number` | `NUMBER` | `LoanSet.PrincipalRequested` | The principal amount requested by the Borrower. | ##### 2.2.2.1 Flags @@ -844,7 +838,6 @@ The transaction creates a new `Loan` object. | `CloseInterestRate` | | `number` | `UINT32` | 0 | A Fee Rate charged for repaying the Loan early in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | | `OverpaymentInterestRate` | | `number` | `UINT32` | 0 | An interest rate charged on overpayments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | | `PrincipalRequested` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | The principal amount requested by the Borrower. | -| `StartDate` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The timestamp of when the Loan starts [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time). | | `PaymentTotal` | | `number` | `UINT32` | 1 | The total number of payments to be made against the Loan. | | `PaymentInterval` | | `number` | `UINT32` | 60 | Number of seconds between Loan payments. | | `GracePeriod` | | `number` | `UINT32` | 60 | The number of seconds after the Loan's Payment Due Date can be Defaulted. | @@ -918,15 +911,16 @@ The account specified in the `Account` field pays the transaction fee. - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `IOU`: - - The `RippleState` object between the submitter account and the `Issuer` of the asset has the `lsfLowFreeze` or `lsfHighFreeze` flag set. + - The `RippleState` object between the Borrower account and the `Issuer` of the asset has the `lsfLowFreeze` or `lsfHighFreeze` flag set. + - The `RippleState` object between the LoanBroker account and the `Issuer` of the asset has the `lsfLowFreeze` or `lsfHighFreeze` flag set. - The `RippleState` between the `LoanBroker.Account` and the `Issuer` has the `lsfLowDeepFreeze` or `lsfHighDeepFreeze` flag set. (The Loan Broker _pseudo-account_ is frozen). - - The `RippleState` between the `Vault(LoanBroker(LoanBrokerID).VaultID).Account` and the `Issuer` has the `lsfLowFreeze` or `lsfHighFreeze` flag set. (The Vault _pseudo-account_ is frozen). - The `AccountRoot` object of the `Issuer` has the `lsfGlobalFreeze` flag set. - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `MPT`: - - The `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the submitter `AccountRoot` has `lsfMPTLocked` flag set. + - The `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the LoanBroker `AccountRoot` has `lsfMPTLocked` flag set. + - The `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the Borrower `AccountRoot` has `lsfMPTLocked` flag set. - The `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the `LoanBroker.Account` `AccountRoot` has `lsfMPTLocked` flag set. (The Loan Broker _pseudo-account_ is locked). - The `MPTokenIssuance` object of the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` has the `lsfMPTLocked` flag set. @@ -936,7 +930,6 @@ The account specified in the `Account` field pays the transaction fee. - `PaymentInterval` is less than `60` seconds. - `GracePeriod` is greater than the `PaymentInterval`. -- `Loan.StartDate < LastClosedLedger.CloseTime`. - Insufficient assets in the Vault: @@ -958,7 +951,7 @@ The account specified in the `Account` field pays the transaction fee. - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is `XRP`: - Decrease the `Balance` field of `Vault` _pseudo-account_ `AccountRoot` by `Loan.PrincipalRequested`. - - Increase the `Balance` field of `LoanBroker` _pseudo-account_ `AccountRoot` by `Loan.PrincipalRequested - Loan.LoanOriginationFee`. + - Increase the `Balance` field of `Borrower` `AccountRoot` by `Loan.PrincipalRequested - Loan.LoanOriginationFee`. - Increase the `Balance` field of `LoanBroker.Owner` `AccountRoot` by `Loan.LoanOriginationFee`. - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `IOU`: @@ -966,7 +959,7 @@ The account specified in the `Account` field pays the transaction fee. - Create a `RippleState` object between the `Issuer` and the `Borrower` if one does not exist. - Decrease the `RippleState` balance between the `Vault` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `Loan.PrincipalRequested`. - - Increase the `RippleState` balance between the `LoanBroker` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `Loan.PrincipalRequested - Loan.LoanOriginationFee`. + - Increase the `RippleState` balance between the `Borrower` `AccountRoot` and the `Issuer` `AccountRoot` by `Loan.PrincipalRequested - Loan.LoanOriginationFee`. - Increase the `RippleState` balance between the `LoanBroker.Owner` `AccountRoot` and the `Issuer` `AccountRoot` by `Loan.LoanOriginationFee`. - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `MPT`: @@ -974,7 +967,7 @@ The account specified in the `Account` field pays the transaction fee. - Create an `MPToken` object for the `Borrower` if one does not exist. - Decrease the `MPToken.MPTAmount` of the `Vault` _pseudo-account_ `MPToken` object for the `Vault.Asset` by `Loan.PrincipalRequested`. - - Increase the `MPToken.MPTAmount` of the `LoanBroker` _pseudo-account_ `MPToken` object for the `Vault.Asset` by `Loan.PrincipalRequested - Loan.LoanOriginationFee`. + - Increase the `MPToken.MPTAmount` of the `Borrower` `MPToken` object for the `Vault.Asset` by `Loan.PrincipalRequested - Loan.LoanOriginationFee`. - Increase the `MPToken.MPTAmount` of the `LoanBroker.Owner` `MPToken` object for the `Vault.Asset` by `Loan.LoanOriginationFee` - `Vault(LoanBroker(LoanBrokerID).VaultID)` object state changes: @@ -1018,23 +1011,6 @@ The transaction deletes an existing `Loan` object. ##### 3.2.2.2 State Changes -- If `Loan(LoanID).AssetsAvailable > 0` (transfer remaining funds to the borrower): - - - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is `XRP`: - - - Decrease the `Balance` field of `LoanBroker` _pseudo-account_ `AccountRoot` by `Loan(LoanID).AssetsAvailable`. - - Increase the `Balance` field of `Loan(LoanID).Borrower` `AccountRoot` by `Loan(LoanID).AssetsAvailable`. - - - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `IOU`: - - - Decrease the `RippleState` balance between the `LoanBroker` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `Loan(LoanID).AssetsAvailable`. - - Increase the `RippleState` balance between the `Loan(LoanID).Borrower` `AccountRoot` and the `Issuer` `AccountRoot` by `Loan(LoanID).AssetsAvailable`. - - - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `MPT`: - - - Decrease the `MPToken.MPTAmount` of the `LoanBroker` _pseudo-account_ `MPToken` object for the `Vault.Asset` by `Loan(LoanID).AssetsAvailable`. - - Increase the `MPToken.MPTAmount` of the `Loan(LoanID).Borrower` `MPToken` object for the `Vault.Asset` by `Loan(LoanID).AssetsAvailable` - - Delete the `Loan` object. - Remove `LoanID` from `DirectoryNode.Indexes` of the `LoanBroker` _pseudo-account_ `AccountRoot`. @@ -1091,11 +1067,11 @@ The transaction deletes an existing `Loan` object. - Calculate the amount of the Default that First-Loss Capital covers: - The default Amount equals the outstanding principal and interest, excluding any funds unclaimed by the Borrower. - - `DefaultAmount = (Loan.PrincipalOutstanding + Loan.InterestOutstanding) - Loan.AssetsAvailable`. + - `DefaultAmount = (Loan.PrincipalOutstanding + Loan.InterestOutstanding)`. - Apply the First-Loss Capital to the Default Amount - `DefaultCovered = min((LoanBroker(Loan.LoanBrokerID).DebtTotal x LoanBroker(Loan.LoanBrokerID).CoverRateMinimum) x LoanBroker(Loan.LoanBrokerID).CoverRateLiquidation, DefaultAmount)` - `DefaultAmount -= DefaultCovered` - - `ReturnToVault = DefaultCovered + Loan.AssetsAvailable` + - `ReturnToVault = DefaultCovered` - Update the `Vault` object: @@ -1104,7 +1080,7 @@ The transaction deletes an existing `Loan` object. - Increase the Asset Available of the Vault by liquidated First-Loss Capital and any unclaimed funds amount: - `Vault(LoanBroker(LoanBrokerID).VaultID).AssetsAvailable += ReturnToVault`. - If `Loan.lsfLoanImpaired` flag is set: - - `Vault(LoanBroker(LoanBrokerID).VaultID).LossUnrealized -= Loan.PrincipalOutstanding + TotalInterestOutstanding()` (Please refer to section [**3.2.5.1.5 Total Value Calculation**](#3252-total-loan-value-calculation), which outlines how to calculate total interest outstanding). + - `Vault(LoanBroker(LoanBrokerID).VaultID).LossUnrealized -= Loan.PrincipalOutstanding + TotalInterestOutstanding()` (Please refer to section [**3.2.4.1.5 Total Value Calculation**](#3242-total-loan-value-calculation), which outlines how to calculate total interest outstanding). - Update the `LoanBroker` object: @@ -1117,7 +1093,6 @@ The transaction deletes an existing `Loan` object. - `Loan(LoanID).Flags |= lsfLoanDefault` - `Loan(LoanID).PaymentRemaining = 0` - - `Loan(LoanID).AssetsAvailable = 0` - `Loan(LoanID).PrincipalOutstanding = 0` - Move the First-Loss Capital from the `LoanBroker` _pseudo-account_ to the `Vault` _pseudo-account_: @@ -1141,7 +1116,7 @@ The transaction deletes an existing `Loan` object. - Update the `Vault` object (set "paper loss"): - - `Vault(LoanBroker(LoanBrokerID).VaultID).LossUnrealized += Loan.PrincipalOutstanding + TotalInterestOutstanding()` (Please refer to section [**3.2.5.1.5 Total Value Calculation**](#3252-total-loan-value-calculation), which outlines how to calculate total interest outstanding) + - `Vault(LoanBroker(LoanBrokerID).VaultID).LossUnrealized += Loan.PrincipalOutstanding + TotalInterestOutstanding()` (Please refer to section [**3.2.4.1.5 Total Value Calculation**](#3242-total-loan-value-calculation), which outlines how to calculate total interest outstanding) - Update the `Loan` object: - `Loan(LoanID).Flags |= lsfLoanImpaired` @@ -1151,7 +1126,7 @@ The transaction deletes an existing `Loan` object. - If the `tfLoanUnimpair` flag is specified: - Update the `Vault` object (clear "paper loss"): - - `Vault(LoanBroker(LoanBrokerID).VaultID).LossUnrealized -= Loan.PrincipalOutstanding + TotalInterestOutstanding()` (Please refer to section [**3.2.5.1.5 Total Value Calculation**](#3252-total-loan-value-calculation), which outlines how to calculate total interest outstanding) + - `Vault(LoanBroker(LoanBrokerID).VaultID).LossUnrealized -= Loan.PrincipalOutstanding + TotalInterestOutstanding()` (Please refer to section [**3.2.4.1.5 Total Value Calculation**](#3242-total-loan-value-calculation), which outlines how to calculate total interest outstanding) - Update the `Loan` object: @@ -1171,69 +1146,7 @@ The transaction deletes an existing `Loan` object. [**Return to Index**](#index) -#### 3.2.4 `LoanDraw` Transaction - -The Borrower submits a `LoanDraw` transaction to draw funds from the Loan. - -| Field Name | Required? | JSON Type | Internal Type | Default Value | Description | -| ----------------- | :----------------: | :-------: | :-----------: | :-----------: | :------------------------------------------ | -| `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | The transaction type. | -| `LoanID` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the Loan object to be drawn from. | -| `Amount` | :heavy_check_mark: | `number` | `AMOUNT` | `N/A` | The amount of funds to drawdown. | - -##### 3.2.4.1 Failure Conditions - -- A `Loan` object with the specified `LoanID` does not exist on the ledger. -- The `AccountRoot.Account` of the submitter is not `Loan.Borrower`. -- The Loan has not started: - - `Loan.StartDate > LastClosedLedger.CloseTime`. -- There are insufficient assets in the `Loan`: - - - `Loan.AssetsAvailable` < `Amount`. - -- The `Loan` has `lsfLoanImpaired` or `lsfLoanDefault` flags set. - -- If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `IOU`: - - - The `RippleState` object between the submitter account and the `Issuer` of the asset has the `lsfLowDeepFreeze` or `lsfHighDeepFreeze` flag set. - - The `RippleState` between the `LoanBroker.Account` and the `Issuer` has the `lsfLowFreeze` or `lsfHighFreeze` flag set. (The Loan Broker _pseudo-account_ is frozen). - - The `AccountRoot` object of the `Issuer` has the `lsfGlobalFreeze` flag set. - -- If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `MPT`: - - - The `MPToken` object for the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` of the submitter `AccountRoot` has `lsfMPTLocked` flag set. - - The `MPToken` object for the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` of the `LoanBroker.Account` `AccountRoot` has `lsfMPTLocked` flag set. (The Loan Broker _pseudo-account_ is locked). - - The `MPTokenIssuance` object of the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` has the `lsfMPTLocked` flag set. - -- The `Borrower` missed a payment: - - `LastClosedLedger.CloseTime > Loan.NextPaymentDueDate`. - -##### 3.2.4.2 State Changes - -- If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is `XRP`: - - - Decrease the `Balance` field of `LoanBroker` _pseudo-account_ `AccountRoot` by `Amount`. - - Increase the `Balance` field of the submitter `AccountRoot` by `Amount`. - -- If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `IOU`: - - - Decrease the `RippleState` balance between the `LoanBroker` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `Amount`. - - Increase the `RippleState` balance between the submitter `AccountRoot` and the `Issuer` `AccountRoot` by `Amount`. - -- If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `MPT`: - - - Decrease the `MPToken.MPTAmount` by `Amount` of the `LoanBroker` _pseudo-account_ `MPToken` object for the `Vault.Asset`. - - Increase the `MPToken.MPTAmount` by `Amount` of the submitter `MPToken` object for the `Vault.Asset`. - -- Decrease `Loan.AssetsAvailable` by `Amount`. - -##### 3.2.4.3 Invariants - -**TBD** - -[**Return to Index**](#index) - -#### 3.2.5 `LoanPay` Transaction +#### 3.2.4 `LoanPay` Transaction The Borrower submits a `LoanPay` transaction to make a Payment on the Loan. @@ -1243,7 +1156,7 @@ The Borrower submits a `LoanPay` transaction to make a Payment on the Loan. | `LoanID` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the Loan object to be paid to. | | `Amount` | :heavy_check_mark: | `number` | `AMOUNT` | `N/A` | The amount of funds to pay. | -##### 3.2.5.1 Payment Types +##### 3.2.4.1 Payment Types A Loan payment has four types: @@ -1275,7 +1188,7 @@ If the Loan Broker and the borrower have agreed to allow overpayments, any amoun Each payment comprises three parts, `principal`, `interest` and `fee`. The `principal` is an amount paid against the principal of the Loan, `interest` is the interest portion of the Loan, and `fee` is the fee part paid by the Borrower on top of `principal` and `interest`. -###### 3.2.5.1.1 Regular Payment +###### 3.2.4.1.1 Regular Payment A periodic payment amount is calculated using the amortization payment formula: @@ -1303,7 +1216,7 @@ $$ principal = periodicPayment - interest $$ -###### 3.2.5.1.2 Late Payment +###### 3.2.4.1.2 Late Payment When a Borrower makes a payment after `NextPaymentDueDate`, they must pay a nominal late payment fee and an additional interest rate charged on the overdue amount for the unpaid period. The formula is as follows: @@ -1331,7 +1244,7 @@ $$ Note that `valueChange >= 0`. -###### 3.2.5.1.3 Loan Overpayment +###### 3.2.4.1.3 Loan Overpayment - Let $\mathcal{P}$ and $\mathcal{p}$ represent the total and outstanding Loan principal. - Let $\mathcal{I}$ and $\mathcal{i}$ represent the total and outstanding Loan interest computed from $\mathcal{P}$ and $\mathcal{p}$ respectively. @@ -1362,7 +1275,7 @@ $$ valueChange = \mathcal{i} - \mathcal{i'} $$ -###### 3.2.5.1.4 Early Full Repayment +###### 3.2.4.1.4 Early Full Repayment A Borrower can close a Loan early by submitting the total amount needed to do so. This amount is the sum of the remaining balance, any accrued interest, a prepayment penalty, and a prepayment fee. @@ -1396,7 +1309,7 @@ $$ valueChange = (prepaymentPenalty) - (interestOutstanding - accruedInterest) $$ -###### 3.2.5.1.5 Management Fee Calculations +###### 3.2.4.1.5 Management Fee Calculations The `LoanBroker` Management fee is charged against the interest portion of the Loan and subtracted from the total Loan value at Loan creation. However, the fee is charged only during Loan payments. Early and Late payments change the total value of the Loan by decreasing or increasing the value of total interest. Therefore, when an early, late or an overpayment payment is made, the management fee must be updated. @@ -1455,7 +1368,7 @@ $$ LoanBroker.DebtTotal = LoanBroker.DebtTotal - managementFeeChange $$ -##### 3.2.5.2 Total Loan Value Calculation +##### 3.2.4.2 Total Loan Value Calculation At any point in time the following formulae can be used to calculate the total remaining value of the loan. @@ -1483,7 +1396,7 @@ $$ totalInterestOutstanding = totalValueOutstanding - principalOutstanding $$ -##### 3.2.5.3 Transaction Pseudo-code +##### 3.2.4.3 Transaction Pseudo-code The following is the pseudo-code for handling a Loan payment transaction. @@ -1572,11 +1485,11 @@ function make_payment(amount, current_time) -> (principal_paid, interest_paid, v return (total_principal_paid, total_interest_paid, loan_value_change, total_fee_paid) ``` -##### 3.2.5.4 Failure Conditions +##### 3.2.4.4 Failure Conditions Assume the payment is split into `principal`, `interest` and `fee`, and `totalDue = principal + interest + fee`. `totalDue` is the minimum payment due by the borrower. -Assume the payment is handled by a function that implements the [Pseudo-Code](#3252-transaction-pseudo-code) that returns `principal_paid`, `interest_paid`, `value_change` and `fee_paid`, where: +Assume the payment is handled by a function that implements the [Pseudo-Code](#3242-transaction-pseudo-code) that returns `principal_paid`, `interest_paid`, `value_change` and `fee_paid`, where: - `principal_paid` is the amount of principal that the payment covered. - `interest_paid` is the amount of interest that the payment covered. @@ -1590,8 +1503,6 @@ Furthermore, assume `full_periodic_payments` variable represents the number of p - A `Loan` object with specified `LoanID` does not exist on the ledger. -- The Loan has not started yet: `Loan.StartDate > LastClosedLedger.CloseTime`. - - The submitter `AccountRoot.Account` is not equal to `Loan.Borrower`. - `Loan.PaymentRemaining` or `Loan.PrincipalOutstanding` is `0`. @@ -1616,7 +1527,7 @@ Furthermore, assume `full_periodic_payments` variable represents the number of p - If `LastClosedLedger.CloseTime < Loan.NextPaymentDueDate` and `Amount` < `PeriodicPaymentAmount()` -##### 3.2.5.5 State Changes +##### 3.2.4.5 State Changes - `Loan` object state changes: @@ -1728,7 +1639,7 @@ Furthermore, assume `full_periodic_payments` variable represents the number of p [**Return to Index**](#index) -##### 3.2.5.6 Invariants +##### 3.2.4.6 Invariants **TBD** From ca55bb1b853649ca7a38e73922df019f19827a2a Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Fri, 5 Sep 2025 13:26:38 +0200 Subject: [PATCH 48/77] adds LedgerEntryType and TransactionType values --- XLS-0066d-lending-protocol/README.md | 31 ++++++++++++++-------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index 8ddec386..05aa9458 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -201,7 +201,8 @@ The `LoanBroker` object has the following fields: | Field Name | User Modifiable? | Constant? | Required? | JSON Type | Internal Type | Default Value | Description | | ---------------------- | :--------------: | :-------: | :----------------: | :-------: | :-----------: | :-----------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `LedgerEntryType` | `No` | `Yes` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | Ledger object type. | + +| `LedgerEntryType` | `No` | `Yes` | :heavy_check_mark: | `string` | `UINT16` | `0x0088` | Ledger object type. | | `LedgerIndex` | `No` | `Yes` | :heavy_check_mark: | `string` | `UINT16` | `N/A` | Ledger object identifier. | | `Flags` | `Yes` | `No` | :heavy_check_mark: | `string` | `UINT32` | 0 | Ledger object flags. | | `PreviousTxnID` | `No` | `No` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the transaction that last modified this object. | @@ -448,7 +449,7 @@ The `LoanID` is calculated as follows: | Field Name | User Modifiable? | Constant? | Required? | JSON Type | Internal Type | Default Value | Description | | ------------------------- | :--------------: | :-------: | :----------------: | :-------: | :-----------: | :-------------------------------------------------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `LedgerEntryType` | `No` | `Yes` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | Ledger object type. | +| `LedgerEntryType` | `No` | `Yes` | :heavy_check_mark: | `string` | `UINT16` | `0x0089` | Ledger object type. | | `LedgerIndex` | `No` | `Yes` | :heavy_check_mark: | `string` | `UINT16` | `N/A` | Ledger object identifier. | | `Flags` | `Yes` | `No` | | `string` | `UINT32` | 0 | Ledger object flags. | | `PreviousTxnID` | `No` | `No` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the transaction that last modified this object. | @@ -514,7 +515,7 @@ The transaction creates a new `LoanBroker` object or updates an existing one. | Field Name | Required? | JSON Type | Internal Type | Default Value | Description | | ---------------------- | :----------------: | :-------: | :-----------: | :-----------: | :------------------------------------------------------------------------------------------------------------------------------------------------- | -| `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | The transaction type. | +| `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | `74` | The transaction type. | | `VaultID` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The Vault ID that the Lending Protocol will use to access liquidity. | | `LoanBrokerID` | | `string` | `HASH256` | `N/A` | The Loan Broker ID that the transaction is modifying. | | `Flags` | | `string` | `UINT32` | 0 | Specifies the flags for the Lending Protocol. | @@ -571,7 +572,7 @@ The transaction creates a new `LoanBroker` object or updates an existing one. | Field Name | Required? | JSON Type | Internal Type | Default Value | Description | | ----------------- | :----------------: | :-------: | :-----------: | :-----------: | :--------------------------------------------------- | -| `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | The transaction type. | +| `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | `75` | The transaction type. | | `LoanBrokerID` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The Loan Broker ID that the transaction is deleting. | ##### 3.1.2.1 Failure Conditions @@ -616,7 +617,7 @@ The transaction deposits First Loss Capital into the `LoanBroker` object. | Field Name | Required? | JSON Type | Internal Type | Default Value | Description | | ----------------- | :----------------: | :-------: | :-----------: | :-----------: | :------------------------------------------------ | -| `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | The transaction type. | +| `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | `76` | The transaction type. | | `LoanBrokerID` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The Loan Broker ID to deposit First-Loss Capital. | | `Amount` | :heavy_check_mark: | `object` | `AMOUNT` | 0 | The Fist-Loss Capital amount to deposit. | @@ -676,7 +677,7 @@ The `LoanBrokerCoverWithdraw` transaction withdraws the First-Loss Capital from | Field Name | Required? | JSON Type | Internal Type | Default Value | Description | | ----------------- | :----------------: | :-------: | :-----------: | :-----------: | :---------------------------------------------------------------------- | -| `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | Transaction type. | +| `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | `77` | Transaction type. | | `LoanBrokerID` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The Loan Broker ID from which to withdraw First-Loss Capital. | | `Amount` | :heavy_check_mark: | `object` | `AMOUNT` | 0 | The Fist-Loss Capital amount to withdraw. | | `Destination` | | `string` | `AccountID` | Empty | An account to receive the assets. It must be able to receive the asset. | @@ -754,11 +755,11 @@ The `LoanBrokerCoverWithdraw` transaction withdraws the First-Loss Capital from The `LoanBrokerCoverClawback` transaction claws back the First-Loss Capital from the `LoanBroker`. The transaction can only be submitted by the Issuer of the Loan asset. Furthermore, the transaction can only clawback funds up to the minimum cover required for the current loans. -| Field Name | Required? | JSON Type | Internal Type | Default Value | Description | -| ----------------- | :----------------: | :-------: | :-----------: | :-----------: | :---------------------------------------------------------------------- | -| `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | Transaction type. | -| `LoanBrokerID` | | `string` | `HASH256` | `N/A` | The Loan Broker ID from which to withdraw First-Loss Capital. Must be provided if the `Amount` is an MPT, or `Amount` is an IOU and `issuer` is specified as the `Account` submitting the transaction. | -| `Amount` | | `object` | `AMOUNT` | 0 | The First-Loss Capital amount to clawback. If the amount is `0` or not provided, clawback funds up to `LoanBroker.DebtTotal * LoanBroker.CoverRateMinimum`. | +| Field Name | Required? | JSON Type | Internal Type | Default Value | Description | +| ----------------- | :----------------: | :-------: | :-----------: | :-----------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | `78` | Transaction type. | +| `LoanBrokerID` | | `string` | `HASH256` | `N/A` | The Loan Broker ID from which to withdraw First-Loss Capital. Must be provided if the `Amount` is an MPT, or `Amount` is an IOU and `issuer` is specified as the `Account` submitting the transaction. | +| `Amount` | | `object` | `AMOUNT` | 0 | The First-Loss Capital amount to clawback. If the amount is `0` or not provided, clawback funds up to `LoanBroker.DebtTotal * LoanBroker.CoverRateMinimum`. | ##### 3.1.5.1 Failure conditions @@ -822,7 +823,7 @@ The transaction creates a new `Loan` object. | Field Name | Required? | JSON Type | Internal Type | Default Value | Description | | ------------------------- | :----------------: | :-------: | :-----------: | :-----------: | :-------------------------------------------------------------------------------------------------------------------------------------------- | -| `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | The transaction type. | +| `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | `80` | The transaction type. | | `LoanBrokerID` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The Loan Broker ID associated with the loan. | | `Flags` | | `string` | `UINT32` | 0 | Specifies the flags for the Loan. | | `Data` | | `string` | `BLOB` | None | Arbitrary metadata in hex format. The field is limited to 256 bytes. | @@ -999,7 +1000,7 @@ The transaction deletes an existing `Loan` object. | Field Name | Required? | JSON Type | Internal Type | Default Value | Description | | ----------------- | :----------------: | :-------: | :-----------: | :-----------: | :--------------------------------------- | -| `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | The transaction type. | +| `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | `81` | The transaction type. | | `LoanID` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the Loan object to be deleted. | ##### 3.2.2.1 Failure Conditions @@ -1032,7 +1033,7 @@ The transaction deletes an existing `Loan` object. | Field Name | Required? | JSON Type | Internal Type | Default Value | Description | | ----------------- | :----------------: | :-------: | :-----------: | :-----------: | :--------------------------------------- | -| `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | The transaction type. | +| `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | `82` | The transaction type. | | `LoanID` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the Loan object to be updated. | | `Flags` | | `string` | `UINT32` | 0 | Specifies the flags for the Loan. | @@ -1152,7 +1153,7 @@ The Borrower submits a `LoanPay` transaction to make a Payment on the Loan. | Field Name | Required? | JSON Type | Internal Type | Default Value | Description | | ----------------- | :----------------: | :-------: | :-----------: | :-----------: | :--------------------------------------- | -| `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | **TODO** | The transaction type. | +| `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | `83` | The transaction type. | | `LoanID` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the Loan object to be paid to. | | `Amount` | :heavy_check_mark: | `number` | `AMOUNT` | `N/A` | The amount of funds to pay. | From 1ca33894741a6378111bd2c0eea81168a1707187 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Fri, 5 Sep 2025 13:29:40 +0200 Subject: [PATCH 49/77] fixes basis point inconcisitencies in LoanSet transaction --- XLS-0066d-lending-protocol/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index 05aa9458..e309f413 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -834,8 +834,8 @@ The transaction creates a new `Loan` object. | `LatePaymentFee` | | `number` | `NUMBER` | 0 | A nominal funds amount paid to the `LoanBroker.Owner` when a payment is late. | | `ClosePaymentFee` | | `number` | `NUMBER` | 0 | A nominal funds amount paid to the `LoanBroker.Owner` when an early full repayment is made. | | `OverpaymentFee` | | `number` | `UINT32` | 0 | A fee charged on overpayments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | -| `InterestRate` | | `number` | `UINT32` | 0 | Annualized interest rate of the Loan in basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | -| `LateInterestRate` | | `number` | `UINT32` | 0 | A premium added to the interest rate for late payments in basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | +| `InterestRate` | | `number` | `UINT32` | 0 | Annualized interest rate of the Loan in in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | +| `LateInterestRate` | | `number` | `UINT32` | 0 | A premium added to the interest rate for late payments in in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | | `CloseInterestRate` | | `number` | `UINT32` | 0 | A Fee Rate charged for repaying the Loan early in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | | `OverpaymentInterestRate` | | `number` | `UINT32` | 0 | An interest rate charged on overpayments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | | `PrincipalRequested` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | The principal amount requested by the Borrower. | From cdc45a693c0f19db5b139cb1491306c4727b7e39 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Fri, 5 Sep 2025 13:29:58 +0200 Subject: [PATCH 50/77] lints the markdown using pretier --- XLS-0066d-lending-protocol/README.md | 46 ++++++++++++++++------------ 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index e309f413..a126f97a 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -679,8 +679,8 @@ The `LoanBrokerCoverWithdraw` transaction withdraws the First-Loss Capital from | ----------------- | :----------------: | :-------: | :-----------: | :-----------: | :---------------------------------------------------------------------- | | `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | `77` | Transaction type. | | `LoanBrokerID` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The Loan Broker ID from which to withdraw First-Loss Capital. | -| `Amount` | :heavy_check_mark: | `object` | `AMOUNT` | 0 | The Fist-Loss Capital amount to withdraw. | -| `Destination` | | `string` | `AccountID` | Empty | An account to receive the assets. It must be able to receive the asset. | +| `Amount` | :heavy_check_mark: | `object` | `AMOUNT` | 0 | The Fist-Loss Capital amount to withdraw. | +| `Destination` | | `string` | `AccountID` | Empty | An account to receive the assets. It must be able to receive the asset. | ##### 3.1.4.1 Failure conditions @@ -692,21 +692,25 @@ The `LoanBrokerCoverWithdraw` transaction withdraws the First-Loss Capital from - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `IOU`: - If the `Destination` field is not specified: + - The `RippleState` object between the submitter account and the `Issuer` of the asset has the `lsfLowDeepFreeze` or `lsfHighDeepFreeze` flag set. - + - If the `Destination` field is specified: + - The `RippleState` object between the `Destination` account and the `Issuer` of the asset does not exist. - If `Destination` is not the `Issuer` and the `RippleState` object between the `Destination` account and the `Issuer` of the asset has the `lsfLowFreeze` or `lsfHighFreeze` flag set. - + - The `AccountRoot` object of the `Issuer` has the `lsfGlobalFreeze` flag set and `Destination` is not the `Issuer` of the asset. - The `RippleState` between the `LoanBroker.Account` and the `Issuer` has the `lsfLowFreeze` or `lsfHighFreeze` flag set. (The Loan Broker _pseudo-account_ is frozen). - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `MPT`: - If the `Destination` field is not specified: + - The `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the submitter `AccountRoot` has `lsfMPTLocked` flag set. - If the `Destination` field specified: + - The `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the `Destination` `AccountRoot` does not exist. - If the `Destination` is not the `Issuer` and the `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the `Destination` `AccountRoot` has `lsfMPTLocked` flag set. @@ -724,24 +728,27 @@ The `LoanBrokerCoverWithdraw` transaction withdraws the First-Loss Capital from - Decrease the `Balance` field of `LoanBroker` _pseudo-account_ `AccountRoot` by `Amount`. - If `Destination` field is not specified: + - Increase the `Balance` field of the submitter `AccountRoot` by `Amount`. - If `Destination` field is specified: - Increase the `Balance` field of the `Destination` `AccountRoot` by `Amount`. - + - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `IOU`: - Decrease the `RippleState` balance between the `LoanBroker` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `Amount`. - If `Destination` field is not specified: + - Increase the `RippleState` balance between the submitter `AccountRoot` and the `Issuer` `AccountRoot` by `Amount`. - If `Destination` field is specified: - Increase the `RippleState` balance between the `Destination` `AccountRoot` and the `Issuer` `AccountRoot` by `Amount`. - + - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `MPT`: - Decrease the `MPToken.MPTAmount` by `Amount` of the `LoanBroker` _pseudo-account_ `MPToken` object for the `Vault.Asset`. - If `Destination` field is not specified: + - Increase the `MPToken.MPTAmount` by `Amount` of the submitter `MPToken` object for the `Vault.Asset`. - If `Destination` field is specified: @@ -769,10 +776,10 @@ The `LoanBrokerCoverClawback` transaction claws back the First-Loss Capital from - If the `LoanBrokerID` is specified, the `LoanBroker` object with that ID does not exist on the ledger. - If the `LoanBrokerID` is not specified, and can not be determined from `Amount`. - `Amount` specifies an MPT. - - `Amount` specifies an IOU, and the `issuer` value is *not* a pseudo-account with `Account(Amount.issuer).LoanBrokerID` set. If it is set, treat `LoanBrokerID` as `Account(Amount.issuer).LoanBrokerID` for the rest of this transaction. + - `Amount` specifies an IOU, and the `issuer` value is _not_ a pseudo-account with `Account(Amount.issuer).LoanBrokerID` set. If it is set, treat `LoanBrokerID` as `Account(Amount.issuer).LoanBrokerID` for the rest of this transaction. - If both the `LoanBrokerID` and `Amount` are specified, and: - - The `Amount.issuer` value does not match the submitter `Account` of the transaction or `LoanBroker(LoanBrokerID).Account` (the pseudo-account of the LoanBroker). + - The `Amount.issuer` value does not match the submitter `Account` of the transaction or `LoanBroker(LoanBrokerID).Account` (the pseudo-account of the LoanBroker). - The `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is not the same asset type as `Amount`, allowing for an IOU `Amount.issuer` to specify `LoanBroker(LoanBrokerID).Account` instead of `Vault(LoanBroker(LoanBrokerID).VaultID).Asset`. - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is `XRP`. @@ -796,19 +803,18 @@ The `LoanBrokerCoverClawback` transaction claws back the First-Loss Capital from - `LoanBroker.CoverAvailable` <= `LoanBroker.DebtTotal * LoanBroker.CoverRateMinimum` ##### 3.1.5.2 State Changes - -- If `Amount` is 0 or unset, set `Amount` to `LoanBroker.CoverAvailable - LoanBroker.DebtTotal * LoanBroker.CoverRateMinimum`. -- Otherwise set `Amount` to `min(Amount, `LoanBroker.CoverAvailable - LoanBroker.DebtTotal * LoanBroker.CoverRateMinimum`). +- If `Amount` is 0 or unset, set `Amount` to `LoanBroker.CoverAvailable - LoanBroker.DebtTotal * LoanBroker.CoverRateMinimum`. +- Otherwise set `Amount` to `min(Amount,`LoanBroker.CoverAvailable - LoanBroker.DebtTotal * LoanBroker.CoverRateMinimum`). - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `IOU`: - Decrease the `RippleState` balance between the `LoanBroker` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `Amount`. - + - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `MPT`: - Decrease the `MPToken.MPTAmount` by `Amount` of the `LoanBroker` _pseudo-account_ `MPToken` object for the `Vault.Asset`. - + - Decrease `LoanBroker.CoverAvailable` by `Amount`. [**Return to Index**](#index) @@ -845,8 +851,8 @@ The transaction creates a new `Loan` object. ##### 3.2.1.1 `Flags` -| Flag Name | Flag Value | Description | -| ------------------- | :----------: | :---------------------------------------------- | +| Flag Name | Flag Value | Description | +| ------------------- | :----------: | :--------------------------------------------- | | `tfLoanOverpayment` | `0x00010000` | Indicates that the loan supports overpayments. | ##### 3.2.1.2 `CounterpartySignature` @@ -879,7 +885,8 @@ Either of the parties (Borrower or Loan Issuer) may initiate the transaction. Th - `Borrower` initiates the transaction: 1. The `Borrower` creates the transaction from their account, setting the pre-agreed terms. - - Optionally, the `Borrower` may set the `Counterparty` to `LoanBroker.Owner`. In case the `Counterparty` field is not set, it is assumed to be the `LoanBroker.Owner`. + + - Optionally, the `Borrower` may set the `Counterparty` to `LoanBroker.Owner`. In case the `Counterparty` field is not set, it is assumed to be the `LoanBroker.Owner`. 2. The `Borrower` signs the transaction setting the `SigningPubKey`, `TxnSignature`, `Signers`, `Account`, `Fee`, `Sequence` fields. 3. The `Borrower` sends the transaction to the `Loan Issuer`. @@ -891,7 +898,7 @@ Either of the parties (Borrower or Loan Issuer) may initiate the transaction. Th 1. The `Loan Issuer` creates the transaction from their account setting the pre-agreed terms. - - The `Loan Issuer` must set the `Counterparty` to the `Borrower` account ID. + - The `Loan Issuer` must set the `Counterparty` to the `Borrower` account ID. 2. The `Loan Issuer` signs the transaction setting the `SigningPubKey`, `TxnSignature`, `Signers`, `Account`, `Fee`, `Sequence` fields. 3. The `Loan Issuer` sends the transaction to the `Borrower`. @@ -941,6 +948,7 @@ The account specified in the `Account` field pays the transaction fee. - `LoanBroker(LoanBrokerID).DebtMaximum` < `Loan.PrincipalRequested + (LoanInterest - (LoanInterest x LoanBroker.ManagementFeeRate)` - Insufficient First-Loss Capital: + - `LoanBroker(LoanBrokerID).CoverAvailable` < `(LoanBroker(LoanBrokerID).DebtTotal + Loan.PrincipalRequested + (LoanInterest - (LoanInterest x LoanBroker.ManagementFeeRate)) x LoanBroker(LoanBrokerID).CoverRateMinimum` ##### 3.2.1.6 State Changes @@ -1551,11 +1559,11 @@ Furthermore, assume `full_periodic_payments` variable represents the number of p - `feeManagement = interest_paid x LoanBroker.ManagementFeeRate` - Total paid, and what portion goes to the vault: - + - `totalPaid = principal_paid + interest_paid + fee_paid` - `totalPaidToVault = principal_paid + interest_paid` - `totalPaidToBroker = fee_paid` - + - Adjust the totals for the management fee: - `totalPaidToVault = totalPaidToVault - feeManagement` From c1411aa811e4ed55923fd06bda0c094820080375 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Fri, 5 Sep 2025 13:33:32 +0200 Subject: [PATCH 51/77] updates preamble to follow the new format --- XLS-0066d-lending-protocol/README.md | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index a126f97a..44efcf21 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -1,17 +1,12 @@
-Title:        Lending Protocol
-Revision:     1 (2024-10-18)
-
-
Authors: - Vytautas Vito Tumas - Aanchal Malhotra - -Affiliation: - Ripple + xls: 66 + title: Lending Protocol + description: XRP Ledger-native protocol for issuing uncollateralized, fixed-term loans using pooled funds, enabling on-chain credit origination. + author: Vytautas Vito Tumas (@Tapanito) Aanchal Malhotra + status: Draft + category: Amendment + created: 2024-10-18
- -# Lending Protocol - ## _Abstract_ Decentralized Finance (DeFi) lending represents a transformative force within the blockchain ecosystem. It revolutionizes traditional financial services by offering a peer-to-peer alternative without intermediaries like banks or financial institutions. At its core, DeFi lending platforms empower users to borrow and lend digital assets directly, fostering financial inclusion, transparency, and efficiency. From 069a91027abc57fc1ef1fce09d8f30de758b0b8e Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Fri, 5 Sep 2025 13:59:56 +0200 Subject: [PATCH 52/77] clarifies which fields are modifiable with the LoanBrokerSet transaction --- XLS-0066d-lending-protocol/README.md | 128 +++++++++++++-------------- 1 file changed, 64 insertions(+), 64 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index 44efcf21..b608bcaf 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -1,4 +1,4 @@ -
+  
   xls: 66
   title: Lending Protocol
   description: XRP Ledger-native protocol for issuing uncollateralized, fixed-term loans using pooled funds, enabling on-chain credit origination.
@@ -7,6 +7,7 @@
   category: Amendment
   created: 2024-10-18
 
+ ## _Abstract_ Decentralized Finance (DeFi) lending represents a transformative force within the blockchain ecosystem. It revolutionizes traditional financial services by offering a peer-to-peer alternative without intermediaries like banks or financial institutions. At its core, DeFi lending platforms empower users to borrow and lend digital assets directly, fostering financial inclusion, transparency, and efficiency. @@ -194,29 +195,28 @@ The key of the `LoanBroker` object is the result of [`SHA512-Half`](https://xrpl The `LoanBroker` object has the following fields: -| Field Name | User Modifiable? | Constant? | Required? | JSON Type | Internal Type | Default Value | Description | -| ---------------------- | :--------------: | :-------: | :----------------: | :-------: | :-----------: | :-----------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | - -| `LedgerEntryType` | `No` | `Yes` | :heavy_check_mark: | `string` | `UINT16` | `0x0088` | Ledger object type. | -| `LedgerIndex` | `No` | `Yes` | :heavy_check_mark: | `string` | `UINT16` | `N/A` | Ledger object identifier. | -| `Flags` | `Yes` | `No` | :heavy_check_mark: | `string` | `UINT32` | 0 | Ledger object flags. | -| `PreviousTxnID` | `No` | `No` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the transaction that last modified this object. | -| `PreviousTxnLgrSeq` | `No` | `No` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The sequence of the ledger containing the transaction that last modified this object. | -| `Sequence` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The transaction sequence number that created the `LoanBroker`. | -| `LoanSequence` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | 0 | A sequential identifier for Loan objects, incremented each time a new Loan is created by this LoanBroker instance. | -| `OwnerNode` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT64` | `N/A` | Identifies the page where this item is referenced in the owner's directory. | -| `VaultNode` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT64` | `N/A` | Identifies the page where this item is referenced in the Vault's _pseudo-account_ owner's directory. | -| `VaultID` | `No` | `Yes` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the `Vault` object associated with this Lending Protocol Instance. | -| `Account` | `No` | `Yes` | :heavy_check_mark: | `string` | `AccountID` | `N/A` | The address of the `LoanBroker` _pseudo-account_. | -| `Owner` | `No` | `Yes` | :heavy_check_mark: | `string` | `AccountID` | `N/A` | The address of the Loan Broker account. | -| `Data` | `Yes` | `No` | | `string` | `BLOB` | None | Arbitrary metadata about the `LoanBroker`. Limited to 256 bytes. | -| `ManagementFeeRate` | `No` | `Yes` | | `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` | `No` | :heavy_check_mark: | `number` | `UINT32` | 0 | The number of active Loans issued by the `LoanBroker`. | -| `DebtTotal` | `No` | `No` | :heavy_check_mark: | `number` | `NUMBER` | 0 | The total asset amount the protocol owes the Vault, including interest. | -| `DebtMaximum` | `Yes` | `No` | :heavy_check_mark: | `number` | `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` | `No` | :heavy_check_mark: | `number` | `NUMBER` | 0 | The total amount of first-loss capital deposited into the Lending Protocol. | -| `CoverRateMinimum` | `No` | `Yes` | :heavy_check_mark: | `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%. | -| `CoverRateLiquidation` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | 0 | The 1/10th basis point of minimum required first loss capital that is liquidated to cover a Loan default. Valid values are between 0 and 100000 inclusive. A value of 1 is equivalent to 1/10 bps or 0.001%. | +| Field Name | User Modifiable? | Constant? | Required? | JSON Type | Internal Type | Default Value | Description | + | ---------------------- | :--------------: | :-------: | :-----------------: | :-------: | :-----------: | :-----------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `LedgerEntryType` | `No` | `Yes` | :heavy\*check_mark: | `string` | `UINT16` | `0x0088` | Ledger object type. | +| `LedgerIndex` | `No` | `Yes` | :heavy_check_mark: | `string` | `UINT16` | `N/A` | Ledger object identifier. | +| `Flags` | `Yes` | `No` | :heavy_check_mark: | `string` | `UINT32` | 0 | Ledger object flags. | +| `PreviousTxnID` | `No` | `No` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the transaction that last modified this object. | +| `PreviousTxnLgrSeq` | `No` | `No` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The sequence of the ledger containing the transaction that last modified this object. | +| `Sequence` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The transaction sequence number that created the `LoanBroker`. | +| `LoanSequence` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | 0 | A sequential identifier for Loan objects, incremented each time a new Loan is created by this LoanBroker instance. | +| `OwnerNode` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT64` | `N/A` | Identifies the page where this item is referenced in the owner's directory. | +| `VaultNode` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT64` | `N/A` | Identifies the page where this item is referenced in the Vault's \_pseudo-account\* owner's directory. | +| `VaultID` | `No` | `Yes` | :heavy\*check_mark: | `string` | `HASH256` | `N/A` | The ID of the `Vault` object associated with this Lending Protocol Instance. | +| `Account` | `No` | `Yes` | :heavy_check_mark: | `string` | `AccountID` | `N/A` | The address of the `LoanBroker` _pseudo-account_. | +| `Owner` | `No` | `Yes` | :heavy_check_mark: | `string` | `AccountID` | `N/A` | The address of the Loan Broker account. | +| `Data` | `Yes` | `No` | | `string` | `BLOB` | None | Arbitrary metadata about the `LoanBroker`. Limited to 256 bytes. | +| `ManagementFeeRate` | `No` | `Yes` | | `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` | `No` | :heavy_check_mark: | `number` | `UINT32` | 0 | The number of active Loans issued by the `LoanBroker`. | +| `DebtTotal` | `No` | `No` | :heavy_check_mark: | `number` | `NUMBER` | 0 | The total asset amount the protocol owes the Vault, including interest. | +| `DebtMaximum` | `Yes` | `No` | :heavy_check_mark: | `number` | `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` | `No` | :heavy_check_mark: | `number` | `NUMBER` | 0 | The total amount of first-loss capital deposited into the Lending Protocol. | +| `CoverRateMinimum` | `No` | `Yes` | :heavy_check_mark: | `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%. | +| `CoverRateLiquidation` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | 0 | The 1/10th basis point of minimum required first loss capital that is liquidated to cover a Loan default. Valid values are between 0 and 100000 inclusive. A value of 1 is equivalent to 1/10 bps or 0.001%. | #### 2.1.3 `LoanBroker` _pseudo-account_ @@ -442,34 +442,34 @@ The `LoanID` is calculated as follows: #### 2.2.2 Fields -| Field Name | User Modifiable? | Constant? | Required? | JSON Type | Internal Type | Default Value | Description | -| ------------------------- | :--------------: | :-------: | :----------------: | :-------: | :-----------: | :-------------------------------------------------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `LedgerEntryType` | `No` | `Yes` | :heavy_check_mark: | `string` | `UINT16` | `0x0089` | Ledger object type. | -| `LedgerIndex` | `No` | `Yes` | :heavy_check_mark: | `string` | `UINT16` | `N/A` | Ledger object identifier. | -| `Flags` | `Yes` | `No` | | `string` | `UINT32` | 0 | Ledger object flags. | -| `PreviousTxnID` | `No` | `No` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the transaction that last modified this object. | -| `PreviousTxnLgrSeq` | `No` | `No` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The ledger sequence containing the transaction that last modified this object. | -| `LoanSequence` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The sequence number of the Loan. | -| `OwnerNode` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT64` | `N/A` | Identifies the page where this item is referenced in the `Borrower` owner's directory. | -| `LoanBrokerNode` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT64` | `N/A` | Identifies the page where this item is referenced in the `LoanBroker`s owner directory. | -| `LoanBrokerID` | `No` | `Yes` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the `LoanBroker` associated with this Loan Instance. | -| `Borrower` | `No` | `Yes` | :heavy_check_mark: | `string` | `AccountID` | `N/A` | The address of the account that is the borrower. | -| `LoanOriginationFee` | `No` | `Yes` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | A nominal nds amount paid to the `LoanBroker.Owner` when the Loan is created. | -| `LoanServiceFee` | `No` | `Yes` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | A nominal funds amount paid to the `LoanBroker.Owner` with every Loan payment. | -| `LatePaymentFee` | `No` | `Yes` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | A nominal funds amount paid to the `LoanBroker.Owner` when a payment is late. | -| `ClosePaymentFee` | `No` | `Yes` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | A nominal funds amount paid to the `LoanBroker.Owner` when a payment full payment is made. | -| `OverpaymentFee` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | A fee charged on overpayments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | -| `InterestRate` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | Annualized interest rate of the Loan in 1/10th basis points. | -| `LateInterestRate` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | A premium is added to the interest rate for late payments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | -| `CloseInterestRate` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | An interest rate charged for repaying the Loan early in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | -| `OverpaymentInterestRate` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | An interest rate charged on overpayments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | -| `StartDate` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `CurrentLedgerTimestamp` | The timestamp of when the Loan started [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time). | -| `PaymentInterval` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | Number of seconds between Loan payments. | -| `GracePeriod` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The number of seconds after the Payment Due Date that the Loan can be Defaulted. | -| `PreviousPaymentDate` | `No` | `No` | :heavy_check_mark: | `number` | `UINT32` | `0` | The timestamp of when the previous payment was made in [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time). | -| `NextPaymentDueDate` | `No` | `No` | :heavy_check_mark: | `number` | `UINT32` | `LoanSet.StartDate + LoanSet.PaymentInterval` | The timestamp of when the next payment is due in [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time). | -| `PaymentRemaining` | `No` | `No` | :heavy_check_mark: | `number` | `UINT32` | `LoanSet.PaymentTotal` | The number of payments remaining on the Loan. | -| `PrincipalOutstanding` | `No` | `No` | :heavy_check_mark: | `number` | `NUMBER` | `LoanSet.PrincipalRequested` | The principal amount requested by the Borrower. | +| Field Name | User Modifiable? | Constant? | Required? | JSON Type | Internal Type | Default Value | Description | +| ------------------------- | :--------------: | :-------: | :----------------: | :-------: | :-----------: | :-------------------------------------------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `LedgerEntryType` | `No` | `Yes` | :heavy_check_mark: | `string` | `UINT16` | `0x0089` | Ledger object type. | +| `LedgerIndex` | `No` | `Yes` | :heavy_check_mark: | `string` | `UINT16` | `N/A` | Ledger object identifier. | +| `Flags` | `Yes` | `No` | | `string` | `UINT32` | 0 | Ledger object flags. | +| `PreviousTxnID` | `No` | `No` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the transaction that last modified this object. | +| `PreviousTxnLgrSeq` | `No` | `No` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The ledger sequence containing the transaction that last modified this object. | +| `LoanSequence` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The sequence number of the Loan. | +| `OwnerNode` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT64` | `N/A` | Identifies the page where this item is referenced in the `Borrower` owner's directory. | +| `LoanBrokerNode` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT64` | `N/A` | Identifies the page where this item is referenced in the `LoanBroker`s owner directory. | +| `LoanBrokerID` | `No` | `Yes` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The ID of the `LoanBroker` associated with this Loan Instance. | +| `Borrower` | `No` | `Yes` | :heavy_check_mark: | `string` | `AccountID` | `N/A` | The address of the account that is the borrower. | +| `LoanOriginationFee` | `No` | `Yes` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | A nominal nds amount paid to the `LoanBroker.Owner` when the Loan is created. | +| `LoanServiceFee` | `No` | `Yes` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | A nominal funds amount paid to the `LoanBroker.Owner` with every Loan payment. | +| `LatePaymentFee` | `No` | `Yes` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | A nominal funds amount paid to the `LoanBroker.Owner` when a payment is late. | +| `ClosePaymentFee` | `No` | `Yes` | :heavy_check_mark: | `number` | `NUMBER` | `N/A` | A nominal funds amount paid to the `LoanBroker.Owner` when a payment full payment is made. | +| `OverpaymentFee` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | A fee charged on overpayments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | +| `InterestRate` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | Annualized interest rate of the Loan in 1/10th basis points. | +| `LateInterestRate` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | A premium is added to the interest rate for late payments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | +| `CloseInterestRate` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | An interest rate charged for repaying the Loan early in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | +| `OverpaymentInterestRate` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | An interest rate charged on overpayments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%) | +| `StartDate` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `CurrentLedgerTimestamp` | The timestamp of when the Loan started [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time). | +| `PaymentInterval` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | Number of seconds between Loan payments. | +| `GracePeriod` | `No` | `Yes` | :heavy_check_mark: | `number` | `UINT32` | `N/A` | The number of seconds after the Payment Due Date that the Loan can be Defaulted. | +| `PreviousPaymentDate` | `No` | `No` | :heavy_check_mark: | `number` | `UINT32` | `0` | The timestamp of when the previous payment was made in [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time). | +| `NextPaymentDueDate` | `No` | `No` | :heavy_check_mark: | `number` | `UINT32` | `LoanSet.StartDate + LoanSet.PaymentInterval` | The timestamp of when the next payment is due in [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time). | +| `PaymentRemaining` | `No` | `No` | :heavy_check_mark: | `number` | `UINT32` | `LoanSet.PaymentTotal` | The number of payments remaining on the Loan. | +| `PrincipalOutstanding` | `No` | `No` | :heavy_check_mark: | `number` | `NUMBER` | `LoanSet.PrincipalRequested` | The principal amount requested by the Borrower. | ##### 2.2.2.1 Flags @@ -508,17 +508,17 @@ In this section we specify the transactions associated with the `LoanBroker` led The transaction creates a new `LoanBroker` object or updates an existing one. -| Field Name | Required? | JSON Type | Internal Type | Default Value | Description | -| ---------------------- | :----------------: | :-------: | :-----------: | :-----------: | :------------------------------------------------------------------------------------------------------------------------------------------------- | -| `TransactionType` | :heavy_check_mark: | `string` | `UINT16` | `74` | The transaction type. | -| `VaultID` | :heavy_check_mark: | `string` | `HASH256` | `N/A` | The Vault ID that the Lending Protocol will use to access liquidity. | -| `LoanBrokerID` | | `string` | `HASH256` | `N/A` | The Loan Broker ID that the transaction is modifying. | -| `Flags` | | `string` | `UINT32` | 0 | Specifies the flags for the Lending Protocol. | -| `Data` | | `string` | `BLOB` | None | Arbitrary metadata in hex format. The field is limited to 256 bytes. | -| `ManagementFeeRate` | | `number` | `UINT16` | 0 | The 1/10th basis point fee charged by the Lending Protocol Owner. Valid values are between 0 and 10000 inclusive. | -| `DebtMaximum` | | `number` | `NUMBER` | 0 | The maximum amount the protocol can owe the Vault. The default value of 0 means there is no limit to the debt. Must not be negative. | -| `CoverRateMinimum` | | `number` | `UINT32` | 0 | The 1/10th basis point `DebtTotal` that the first loss capital must cover. Valid values are between 0 and 100000 inclusive. | -| `CoverRateLiquidation` | | `number` | `UINT32` | 0 | The 1/10th basis point of minimum required first loss capital liquidated to cover a Loan default. Valid values are between 0 and 100000 inclusive. | +| Field Name | Required? | Modifiable? | JSON Type | Internal Type | Default Value | Description | +| ---------------------- | :----------------: | :---------: | :-------: | :-----------: | :-----------: | :------------------------------------------------------------------------------------------------------------------------------------------------- | +| `TransactionType` | :heavy_check_mark: | `No` | `string` | `UINT16` | `74` | The transaction type. | +| `VaultID` | :heavy_check_mark: | `No` | `string` | `HASH256` | `N/A` | The Vault ID that the Lending Protocol will use to access liquidity. | +| `LoanBrokerID` | | `No` | `string` | `HASH256` | `N/A` | The Loan Broker ID that the transaction is modifying. | +| `Flags` | | `Yes` | `string` | `UINT32` | 0 | Specifies the flags for the Lending Protocol. | +| `Data` | | `Yes` | `string` | `BLOB` | None | Arbitrary metadata in hex format. The field is limited to 256 bytes. | +| `ManagementFeeRate` | | `No` | `number` | `UINT16` | 0 | The 1/10th basis point fee charged by the Lending Protocol Owner. Valid values are between 0 and 10000 inclusive. | +| `DebtMaximum` | | `Yes` | `number` | `NUMBER` | 0 | The maximum amount the protocol can owe the Vault. The default value of 0 means there is no limit to the debt. Must not be negative. | +| `CoverRateMinimum` | | `No` | `number` | `UINT32` | 0 | The 1/10th basis point `DebtTotal` that the first loss capital must cover. Valid values are between 0 and 100000 inclusive. | +| `CoverRateLiquidation` | | `No` | `number` | `UINT32` | 0 | The 1/10th basis point of minimum required first loss capital liquidated to cover a Loan default. Valid values are between 0 and 100000 inclusive. | ##### 3.1.1.1 Failure Conditions @@ -800,7 +800,7 @@ The `LoanBrokerCoverClawback` transaction claws back the First-Loss Capital from ##### 3.1.5.2 State Changes - If `Amount` is 0 or unset, set `Amount` to `LoanBroker.CoverAvailable - LoanBroker.DebtTotal * LoanBroker.CoverRateMinimum`. -- Otherwise set `Amount` to `min(Amount,`LoanBroker.CoverAvailable - LoanBroker.DebtTotal * LoanBroker.CoverRateMinimum`). +- Otherwise set `Amount` to `min(Amount,`LoanBroker.CoverAvailable - LoanBroker.DebtTotal \* LoanBroker.CoverRateMinimum`). - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `IOU`: From 02a27d95bf4c936ee9cd5b50c16cb28b0c0d942d Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Tue, 16 Sep 2025 15:45:12 +0200 Subject: [PATCH 53/77] fixes flag names --- XLS-0066d-lending-protocol/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index b608bcaf..bba9b0d9 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -927,7 +927,7 @@ The account specified in the `Account` field pays the transaction fee. - The `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the `LoanBroker.Account` `AccountRoot` has `lsfMPTLocked` flag set. (The Loan Broker _pseudo-account_ is locked). - The `MPTokenIssuance` object of the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` has the `lsfMPTLocked` flag set. -- Either of the `tfDefault`, `tfImpair` or `tfUnimpair` flags are set. +- Either of the `tfLoanDefault`, `tfLoanImpair` or `tfLoanUnimpair` flags are set. - The `Borrower` `AccountRoot` object does not exist. From 2c2524e6889de28775e9703750bee823e038d856 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Tue, 23 Sep 2025 12:41:50 +0200 Subject: [PATCH 54/77] improves variable name consistency --- XLS-0066d-lending-protocol/README.md | 66 ++++++++++++++-------------- 1 file changed, 32 insertions(+), 34 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index bba9b0d9..493425f5 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -259,23 +259,23 @@ ManagementFeeRate = 0.1 (10%) # The Lender issues the following Loan -- Loan -- -LoanPrincipal = 1,000 Tokens -LoanInterestRate = 0.1 (10%) +PrincipalRequested = 1,000 Tokens +InterestRate = 0.1 (10%) # SIMPLIfIED -LoanInterest = LoanPrincipal x LoanInterestRate +TotalInterestOutstanding = PrincipalRequested x InterestRate = 100 Tokens ** State Changes ** -- Vault -- # Increase the potential value of the Vault -AssetsTotal = AssetsTotal + ((LoanInterest - (LoanInterest x ManagementFeeRate))) +AssetsTotal = AssetsTotal + ((TotalInterestOutstanding - (TotalInterestOutstanding x ManagementFeeRate))) = 100,000 + (100 - (100 x 0.1)) = 100,000 + 90 = 100,090 Tokens # Decrease Asset Available in the Vault -AssetsAvailable = AssetsAvailable - LoanPrincipal +AssetsAvailable = AssetsAvailable - PrincipalRequested = 100,000 - 1,000 = 99,000 Tokens @@ -283,7 +283,7 @@ SharesTotal = (UNCHANGED) -- Lending Protocol -- # Increase Lending Protocol Debt -DebtTotal = DebtTotal + LoanPrincipal + (LoanInterest - (LoanInterest x ManagementFeeRate)) +DebtTotal = DebtTotal + PrincipalRequested + (TotalInterestOutstanding - (TotalInterestOutstanding x ManagementFeeRate)) = 0 + 1,000 + (100 - (100 x 0.1)) = 1,000 + 90 = 1,090 Tokens @@ -305,14 +305,14 @@ DebtTotal = 1,090 Tokens ManagementFeeRate = 0.1 (10%) -- Loan -- -LoanPrincipal = 1,000 Tokens -LoanInterestRate = 0.1 (10%) -# SIMPLIfIED -LoanPayments = 2 +PrincipalRequested = 1,000 Tokens +InterestRate = 0.1 (10%) +# SIMPLIFIED +PaymentRemaining = 2 -# SIMPLIfIED -LoanInterest = LoanPrincipal x LoanInterestRate - = 100 Tokens +# SIMPLIFIED +TotalInterestOutstanding = PrincipalRequested x InterestRate + = 100 Tokens # The Borrower makes a single payment @@ -328,7 +328,7 @@ PaymentInterestPortion = 50 Tokens AssetsTotal = (UNCHANGED) # Increase Asset Available in the Vault -AssetsAvailable = AssetsAvailable + PaymentPrincipalPortion + (PaymentInterestPortion - (PaymentInterestPortion x ManagementFeeRate) +AssetsAvailable = AssetsAvailable + PaymentPrincipalPortion + (PaymentInterestPortion - (PaymentInterestPortion x ManagementFeeRate)) = 99,000 + 500 + (50 - (50 x 0.1)) = 99,545 Tokens @@ -375,29 +375,27 @@ CoverRateLiquidation = 0.1 (10%) CoverAvailable = 1,000 Tokens -- Loan -- -AssetsAvailable = 500 Tokens PrincipleOutstanding = 1,000 Tokens InterestOutstanding = 90 Tokens # First-Loss Capital liquidation maths -DefaultAmount = PrincipleOutstanding + InterestOutstanding - AssetsAvailable - = 1,000 + 90 - 500 - = 590 +DefaultAmount = PrincipleOutstanding + InterestOutstanding + = 1,000 + 90 + = 1,090 # The amount of the default that the first-loss capital scheme will cover DefaultCovered = min((DebtTotal x CoverRateMinimum) x CoverRateLiquidation, DefaultAmount) - = min((1,090 * 0.1) * 0.1, 1,090) = min(10.9, 590) + = min((1,090 * 0.1) * 0.1, 1,090) = min(10.9, 1,090) = 10.9 Tokens Loss = DefaultAmount - DefaultCovered - = 590 - 10.9 - = 579.1 Tokens + = 1,090 - 10.9 + = 1,079.1 Tokens -FundsReturned = DefaultCovered + AssetsAvailable - = 10.9 + 500 - = 510.9 +FundsReturned = DefaultCovered + = 10.9 # Note, Loss + FundsReturned MUST be equal to PrincipleOutstanding + InterestOutstanding @@ -405,12 +403,12 @@ FundsReturned = DefaultCovered + AssetsAvailable -- Vault -- AssetsTotal = AssetsTotal - Loss - = 100,090 - 579.1 - = 99,510.9 Tokens + = 100,090 - 1,079.1 + = 99,010.9 Tokens AssetsAvailable = AssetsAvailable + FundsReturned - = 99,000 + 510.9 - = 99,510.9 Tokens + = 99,000 + 10.9 + = 99,010.9 Tokens SharesTotal = (UNCHANGED) @@ -940,11 +938,11 @@ The account specified in the `Account` field pays the transaction fee. - Exceeds maximum Debt of the LoanBroker: - - `LoanBroker(LoanBrokerID).DebtMaximum` < `Loan.PrincipalRequested + (LoanInterest - (LoanInterest x LoanBroker.ManagementFeeRate)` + - `LoanBroker(LoanBrokerID).DebtMaximum` < `Loan.PrincipalRequested + (TotalInterestOutstanding() - (TotalInterestOutstanding() x LoanBroker.ManagementFeeRate)` - Insufficient First-Loss Capital: - - `LoanBroker(LoanBrokerID).CoverAvailable` < `(LoanBroker(LoanBrokerID).DebtTotal + Loan.PrincipalRequested + (LoanInterest - (LoanInterest x LoanBroker.ManagementFeeRate)) x LoanBroker(LoanBrokerID).CoverRateMinimum` + - `LoanBroker(LoanBrokerID).CoverAvailable` < `(LoanBroker(LoanBrokerID).DebtTotal + Loan.PrincipalRequested + (TotalInterestOutstanding() - (TotalInterestOutstanding() x LoanBroker.ManagementFeeRate)) x LoanBroker(LoanBrokerID).CoverRateMinimum` ##### 3.2.1.6 State Changes @@ -981,11 +979,11 @@ The account specified in the `Account` field pays the transaction fee. - `Vault.AssetsAvailable -= Loan.PrincipalRequested`. - Increase the Total Value of the Vault: - - `Vault.AssetsTotal += LoanInterest - (LoanInterest x LoanBroker.ManagementFeeRate)` where `LoanInterest` is the Loan's total interest. + - `Vault.AssetsTotal += TotalInterestOutstanding() - (TotalInterestOutstanding() x LoanBroker.ManagementFeeRate)`. - `LoanBroker(LoanBrokerID)` object changes: - - `LoanBroker.DebtTotal += Loan.PrincipalRequested + (LoanInterest - (LoanInterest x LoanBroker.ManagementFeeRate)` + - `LoanBroker.DebtTotal += Loan.PrincipalRequested + (TotalInterestOutstanding() - (TotalInterestOutstanding() x LoanBroker.ManagementFeeRate)` - `LoanBroker.OwnerCount += 1` - Add `LoanID` to `DirectoryNode.Indexes` of the `LoanBroker` _pseudo-account_ `AccountRoot`. @@ -1391,7 +1389,7 @@ $$ The total loan value is simply: $$ -totalValueOutstanding = periodicPayment \times paymentsRemaining +totalValueOutstanding = periodicPayment \times PaymentRemaining $$ We calculate the total interest outstanding as follows: @@ -1542,7 +1540,7 @@ Furthermore, assume `full_periodic_payments` variable represents the number of p - Decrease `Loan.PaymentRemaining` by `full_periodic_payments`. - Decrease `Loan.PrincipalOutstanding` by `principal_paid`. - - If `Loan.PaymentRemaining > 0` and `LoanPrincipalOutstanding > 0`: + - If `Loan.PaymentRemaining > 0` and `Loan.PrincipalOutstanding > 0`: - Set the next payment date: `Loan.NextPaymentDueDate += Loan.PaymentInterval * full_periodic_payments`. - Set the previous payment date: `Loan.PreviousPaymentDate = Loan.NextPaymentDueDate - Loan.PaymentInterval`. From 4ba07c6633251026743ddb89cff140be5f39ba41 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Fri, 26 Sep 2025 11:22:20 +0200 Subject: [PATCH 55/77] adds DebtTotal > 0 condition to LoanBrokerDelete --- XLS-0066d-lending-protocol/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index 493425f5..3f168525 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -573,7 +573,8 @@ The transaction creates a new `LoanBroker` object or updates an existing one. - `LoanBroker` object with the specified `LoanBrokerID` does not exist on the ledger. - The submitter `AccountRoot.Account != LoanBroker(LoanBrokerID).Owner`. -- The `OwnerCount` field is greater than zero. +- The `OwnerCount > 0` there are loan objects. +- The `DebtTotal > 0` there are unpaid loans. ##### 3.1.2.2 State Changes From 4b02ae4c387855c233bc700046dbf5544a0fc679 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Fri, 26 Sep 2025 18:05:12 +0200 Subject: [PATCH 56/77] adds Loan.TotalValueOutstanding --- XLS-0066d-lending-protocol/README.md | 151 ++++++++++++++++++++------- 1 file changed, 114 insertions(+), 37 deletions(-) diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md index 3f168525..1f5d1cb8 100644 --- a/XLS-0066d-lending-protocol/README.md +++ b/XLS-0066d-lending-protocol/README.md @@ -1,4 +1,4 @@ -
+
   xls: 66
   title: Lending Protocol
   description: XRP Ledger-native protocol for issuing uncollateralized, fixed-term loans using pooled funds, enabling on-chain credit origination.
@@ -140,7 +140,7 @@ The lending protocol charges a number of fees that the Loan Broker can configure
 - **`Repayment Schedule`**: A detailed plan that outlines when and how much a borrower must pay to repay the Loan fLoan.
 - **`Grace Period`**: A set period after the Loan's due date after which the Loan Broker can default the Loan
 
-### 1.6.2 Actors
+#### 1.6.2 Actors
 
 - **`LoanBroker`**: The entity issuing the Loan.
 - **`Borrower`**: The account that is borrowing funds.
@@ -196,7 +196,7 @@ The key of the `LoanBroker` object is the result of [`SHA512-Half`](https://xrpl
 The `LoanBroker` object has the following fields:
 
 | Field Name             | User Modifiable? | Constant? |      Required?      | JSON Type | Internal Type | Default Value | Description                                                                                                                                                                                                  |
-  | ---------------------- | :--------------: | :-------: | :-----------------: | :-------: | :-----------: | :-----------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| ---------------------- | :--------------: | :-------: | :-----------------: | :-------: | :-----------: | :-----------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
 | `LedgerEntryType`      |       `No`       |   `Yes`   | :heavy\*check_mark: | `string`  |   `UINT16`    |   `0x0088`    | Ledger object type.                                                                                                                                                                                          |
 | `LedgerIndex`          |       `No`       |   `Yes`   | :heavy_check_mark:  | `string`  |   `UINT16`    |     `N/A`     | Ledger object identifier.                                                                                                                                                                                    |
 | `Flags`                |      `Yes`       |   `No`    | :heavy_check_mark:  | `string`  |   `UINT32`    |       0       | Ledger object flags.                                                                                                                                                                                         |
@@ -463,11 +463,12 @@ The `LoanID` is calculated as follows:
 | `OverpaymentInterestRate` |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                     `N/A`                     | An interest rate charged on overpayments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%)                                   |
 | `StartDate`               |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |           `CurrentLedgerTimestamp`            | The timestamp of when the Loan started [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time).                 |
 | `PaymentInterval`         |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                     `N/A`                     | Number of seconds between Loan payments.                                                                                                                       |
-| `GracePeriod`             |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                     `N/A`                     | The number of seconds after the Payment Due Date that the Loan can be Defaulted.                                                                               |
+| `GracePeriod`             |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                     `N/A`                     | The number of seconds after the Loan's Payment Due Date that the Loan can be Defaulted.                                                                        |
 | `PreviousPaymentDate`     |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `UINT32`    |                      `0`                      | The timestamp of when the previous payment was made in [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time). |
 | `NextPaymentDueDate`      |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `UINT32`    | `LoanSet.StartDate + LoanSet.PaymentInterval` | The timestamp of when the next payment is due in [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time).       |
 | `PaymentRemaining`        |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `UINT32`    |            `LoanSet.PaymentTotal`             | The number of payments remaining on the Loan.                                                                                                                  |
-| `PrincipalOutstanding`    |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `NUMBER`    |         `LoanSet.PrincipalRequested`          | The principal amount requested by the Borrower.                                                                                                                |
+| `PrincipalOutstanding`    |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `NUMBER`    |         `LoanSet.PrincipalRequested`          | The principal amount due to be paid by the Borrower.                                                                                                           |
+| `TotalValueOutstanding`   |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `NUMBER`    |           `TotalValueOutstanding()`           | THe total outstanding value of the Loan.                                                                                                                       |
 
 ##### 2.2.2.1 Flags
 
@@ -490,7 +491,19 @@ The `Loan` objects are stored in the ledger and tracked in two [Owner Directorie
 
 The `Loan` object costs one owner reserve for the `Borrower`.
 
-#### 2.2.5 Impairment
+#### 2.2.5 Loan Total Value
+
+The loan's financial state is tracked through three key components:
+
+- **PrincipalOutstanding**: Represents the remaining principal balance that the borrower must repay to satisfy the original loan amount.
+- **TotalValueOutstanding**: Encompasses the complete remaining loan obligation, comprising both the outstanding principal and all scheduled interest payments based on the original amortization schedule. This value excludes any additional interest charges resulting from late payments.
+- **InterestOutstanding**: The total scheduled interest remaining on the loan, derived as `TotalValueOutstanding - PrincipalOutstanding`.
+
+**Asset-Specific Precision Handling**: For discrete asset types (MPTs and XRP denominated in drops), both `TotalValueOutstanding` and `PrincipalOutstanding` values are truncated to whole numbers to ensure compatibility with the underlying asset precision requirements.
+
+**Late Payment Interest Treatment**: Late payment penalties and additional interest charges are calculated and collected separately from the core loan value. These charges do not modify the `TotalValueOutstanding` calculation, which remains anchored to the original scheduled payment terms.
+
+#### 2.2.6 Impairment
 
 When the Loan Broker discovers that the Borower cannot make an upcoming payment, impairment allows the Loan Broker to register a "paper loss" with the Vault. The impairment mechanism moves the Next Payment Due Date to the time the Loan was impaired, allowing to default the Loan more quickly. However, if the Borrower makes a payment, the impairment status is automatically cleared.
 
@@ -511,10 +524,10 @@ The transaction creates a new `LoanBroker` object or updates an existing one.
 | `TransactionType`      | :heavy_check_mark: |    `No`     | `string`  |   `UINT16`    |     `74`      | The transaction type.                                                                                                                              |
 | `VaultID`              | :heavy_check_mark: |    `No`     | `string`  |   `HASH256`   |     `N/A`     | The Vault ID that the Lending Protocol will use to access liquidity.                                                                               |
 | `LoanBrokerID`         |                    |    `No`     | `string`  |   `HASH256`   |     `N/A`     | The Loan Broker ID that the transaction is modifying.                                                                                              |
-| `Flags`                |                    |    `Yes`     | `string`  |   `UINT32`    |       0       | Specifies the flags for the Lending Protocol.                                                                                                      |
-| `Data`                 |                    |    `Yes`     | `string`  |    `BLOB`     |     None      | Arbitrary metadata in hex format. The field is limited to 256 bytes.                                                                               |
+| `Flags`                |                    |    `Yes`    | `string`  |   `UINT32`    |       0       | Specifies the flags for the LoanBroker.                                                                                                            |
+| `Data`                 |                    |    `Yes`    | `string`  |    `BLOB`     |     None      | Arbitrary metadata in hex format. The field is limited to 256 bytes.                                                                               |
 | `ManagementFeeRate`    |                    |    `No`     | `number`  |   `UINT16`    |       0       | The 1/10th basis point fee charged by the Lending Protocol Owner. Valid values are between 0 and 10000 inclusive.                                  |
-| `DebtMaximum`          |                    |    `Yes`     | `number`  |   `NUMBER`    |       0       | The maximum amount the protocol can owe the Vault. The default value of 0 means there is no limit to the debt. Must not be negative.               |
+| `DebtMaximum`          |                    |    `Yes`    | `number`  |   `NUMBER`    |       0       | The maximum amount the protocol can owe the Vault. The default value of 0 means there is no limit to the debt. Must not be negative.               |
 | `CoverRateMinimum`     |                    |    `No`     | `number`  |   `UINT32`    |       0       | The 1/10th basis point `DebtTotal` that the first loss capital must cover. Valid values are between 0 and 100000 inclusive.                        |
 | `CoverRateLiquidation` |                    |    `No`     | `number`  |   `UINT32`    |       0       | The 1/10th basis point of minimum required first loss capital liquidated to cover a Loan default. Valid values are between 0 and 100000 inclusive. |
 
@@ -750,6 +763,10 @@ The `LoanBrokerCoverWithdraw` transaction withdraws the First-Loss Capital from
 
 - Decrease `LoanBroker.CoverAvailable` by `Amount`.
 
+##### 3.1.4.3 Invariants
+
+**TBD**
+
 [**Return to Index**](#index)
 
 #### 3.1.5 `LoanBrokerCoverClawback`
@@ -1011,6 +1028,7 @@ The transaction deletes an existing `Loan` object.
 - The Account submitting the `LoanDelete` is not the `LoanBroker.Owner` or the `Loan.Borrower`.
 - The Loan is active:
   - `Loan.PaymentRemaining > 0`
+  - `Loan.TotalValueOutstanding > 0`
 
 ##### 3.2.2.2 State Changes
 
@@ -1027,7 +1045,7 @@ The transaction deletes an existing `Loan` object.
 
 ##### 3.2.2.3 Invariants
 
-- If `Loan.PaymentRemaining = 0` then `Loan.PrincipalOutstanding = 0`
+- If `Loan.PaymentRemaining = 0` then `Loan.PrincipalOutstanding = 0 && Loan.TotalValueOutstanding = 0`
 
 [**Return to Index**](#index)
 
@@ -1056,7 +1074,7 @@ The transaction deletes an existing `Loan` object.
 - The `lsfLoanDefault` flag is set on the Loan object. Once a Loan is defaulted, it cannot be modified.
 
 - If `Loan(LoanID).Flags == lsfLoanImpaired` AND `tfLoanImpair` flag is provided (impairing an already impaired loan).
-- If `Loan(LoanID).Flags == 0` AND `tfLoanUnimpair` flag is provided (clearning impairment for an uninpaired loan).
+- If `Loan(LoanID).Flags == 0` AND `tfLoanUnimpair` flag is provided (clearing impairment for an unimpaired loan).
 
 - `Loan.PaymentRemaining == 0`.
 
@@ -1085,18 +1103,18 @@ The transaction deletes an existing `Loan` object.
     - If `Loan.lsfLoanImpaired` flag is set:
       - `Vault(LoanBroker(LoanBrokerID).VaultID).LossUnrealized -= Loan.PrincipalOutstanding + TotalInterestOutstanding()` (Please refer to section [**3.2.4.1.5 Total Value Calculation**](#3242-total-loan-value-calculation), which outlines how to calculate total interest outstanding).
 
-- Update the `LoanBroker` object:
+  - Update the `LoanBroker` object:
 
-  - Decrease the Debt of the LoanBroker:
-    - `LoanBroker(LoanBrokerID).DebtTotal -= Loan.PrincipalOutstanding + Loan.InterestOutstanding`
-  - Decrease the First-Loss Capital Cover Available:
-    - `LoanBroker(LoanBrokerID).CoverAvailable -= DefaultCovered`
+    - Decrease the Debt of the LoanBroker:
+      - `LoanBroker(LoanBrokerID).DebtTotal -= Loan.PrincipalOutstanding + Loan.InterestOutstanding`
+    - Decrease the First-Loss Capital Cover Available:
+      - `LoanBroker(LoanBrokerID).CoverAvailable -= DefaultCovered`
 
-- Update the `Loan` object:
+  - Update the `Loan` object:
 
-  - `Loan(LoanID).Flags |= lsfLoanDefault`
-  - `Loan(LoanID).PaymentRemaining = 0`
-  - `Loan(LoanID).PrincipalOutstanding = 0`
+    - `Loan(LoanID).Flags |= lsfLoanDefault`
+    - `Loan(LoanID).PaymentRemaining = 0`
+    - `Loan(LoanID).PrincipalOutstanding = 0`
 
   - Move the First-Loss Capital from the `LoanBroker` _pseudo-account_ to the `Vault` _pseudo-account_:
 
@@ -1185,7 +1203,7 @@ The following diagram depicts how a payment is handled based on the amount paid.
                           Payment Amount
 ```
 
-The minimum payment required is determined by whether the borrower makes the payment before or on the `NextPaymentDueDate` or if it is late. Any payment below the minimum amount required is rejected. With a single `LoanPay` transaction, the Borrower can make multiple loan payments. For example, if the periodic payment amount is 400 Tokens and the Borrower makes a payment of 900 Tokens, the payment will be treated as two periodic payments, moving the NextPaymentDueDate forward by two payment intervals, and the remaining 100 Tokens will be an overpayment.
+The minimum payment required is determined by whether the borrower makes the payment before or on the `NextPaymentDueDate` or if it is late. Any payment below the minimum amount required is rejected. With a single `LoanPay` transaction, the Borrower can make multiple loan payments. For example, if the periodic payment amount is 400 Tokens and the Borrower makes a payment of 900 Tokens, the payment will be treated as two periodic payments, moving the NextPaymentDueDate forward to two payment intervals, and the remaining 100 Tokens will be an overpayment.
 
 If the Loan Broker and the borrower have agreed to allow overpayments, any amount above the periodic payment is treated as an overpayment. However, if overpayments are not supported, the excess amount will not be charged and will remain with the borrower.
 
@@ -1219,6 +1237,34 @@ $$
 principal = periodicPayment - interest
 $$
 
+When only a single payment remains (PaymentRemaining = 1) the periodic payment MUST be set equal to the current TotalValueOutstanding (i.e. principalOutstanding + interestOutstanding before any asset-specific rounding/truncation). This overrides the standard amortization formula for the last installment. The purpose is to eliminate residual dust created by iterative rounding (e.g. integer truncation for XRP drops or whole‑unit MPTs) that could otherwise make the loan impossible to fully extinguish.
+
+Formally:
+If PaymentRemaining > 1:
+periodicPayment = formula result (rounded per asset rules)
+If PaymentRemaining = 1:
+periodicPayment = TotalValueOutstanding (rounded per asset rules; this sets TotalValueOutstanding to 0 after payment)
+
+Rationale:
+Repeated downward rounding (truncate / floor) of each scheduled payment can accumulate underpayment relative to the unrounded amortization schedule. Without this adjustment, the final formula-derived payment (after rounding) might be smaller than the remaining outstanding value, leaving a non-zero remainder that can never be cleared by any subsequent scheduled payment (because no payments remain).
+
+Example (integer-only MPT):
+
+- Initial TotalValueOutstanding = 11 MPT
+- PaymentTotal = 10
+- Amortization produces periodicPayment = 1.1 MPT
+- Asset supports only whole units ⇒ each scheduled payment is truncated to 1 MPT
+  Progress:
+  After 9 payments: Paid = 9 MPT, Remaining TotalValueOutstanding = 2 MPT
+  If we applied the formula again for the 10th payment:
+  periodicPayment (unrounded) = 1.1 MPT → truncated to 1 MPT
+  Remaining after payment = 1 MPT (cannot be repaid; no payments left)
+  Adjustment:
+  Because PaymentRemaining = 1, set periodicPayment = TotalValueOutstanding = 2 MPT
+  Final payment = 2 MPT clears the loan exactly (PrincipalOutstanding = 0, TotalValueOutstanding = 0).
+
+Therefore, implementations MUST detect the single remaining payment case and substitute the outstanding value to guarantee full extinguishment of the debt.
+
 ###### 3.2.4.1.2 Late Payment
 
 When a Borrower makes a payment after `NextPaymentDueDate`, they must pay a nominal late payment fee and an additional interest rate charged on the overdue amount for the unpaid period. The formula is as follows:
@@ -1245,7 +1291,7 @@ $$
 valueChange = interestLate - interestPeriodic
 $$
 
-Note that `valueChange >= 0`.
+Note that `valueChange >= 0` for late payments, i.e. a late payment increases the value of the loan.
 
 ###### 3.2.4.1.3 Loan Overpayment
 
@@ -1435,56 +1481,86 @@ function make_payment(amount, current_time) -> (principal_paid, interest_paid, v
     if amount >= full_payment && loan.payments_remaining > 1 {
         loan.payments_remaining = 0
         loan.principal_outstanding = 0
-
+        let total_interest_outstanding = loan.lotal_value_outstanding - full_payment.principal
         // A full payment decreases the value of the loan by the difference between the interest paid and the expected outstanding interest
-        return (full_payment.principal, full_payment.interest, full_payment.interest - loan.compute_current_value().interest, full_payment.fee)
+        return (full_payment.principal, full_payment.interest, full_payment.interest - total_interest_outstanding, full_payment.fee)
     }
 
-    // if the payment is not late nor if it's a full payment, then it must be a periodic once
-
+    // PERIODIC (ON‑TIME) FLOW
+    // Compute scheduled periodic payment. Override if this is the final installment to eliminate rounding dust.
     let periodic_payment = loan.compute_periodic_payment()
+    if loan.payments_remaining == 1 {
+        // Final scheduled payment: pay exactly all remaining value (principal + interest) before rounding.
+        periodic_payment.principal = loan.principal_outstanding
+        periodic_payment.interest  = loan.total_value_outstanding - loan.principal_outstanding
+    }
 
-    let full_periodic_payments = floor(amount / (periodic_payment + loan.service_fee))
+    let periodic_payment_total = periodic_payment.principal + periodic_payment.interest
+
+    // Determine how many full periodic installments this single Amount can cover (cannot exceed remaining)
+    let full_periodic_payments = floor(amount / (periodic_payment_total + loan.service_fee))
     if full_periodic_payments < 1 {
         return "insufficient amount paid" error
     }
+    if full_periodic_payments > loan.payments_remaining {
+        full_periodic_payments = loan.payments_remaining
+    }
 
     loan.next_payment_due_date = loan.next_payment_due_date + loan.payment_interval * full_periodic_payments
     loan.last_payment_date = loan.next_payment_due_date - loan.payment_interval
 
-
     let total_principal_paid = 0
     let total_interest_paid = 0
     let loan_value_change = 0
     let total_fee_paid = loan.service_fee * full_periodic_payments
-
+    let loan_value_change = 0
+  
     while full_periodic_payments > 0 {
         total_principal_paid += periodic_payment.principal
-        total_interest_paid += periodic_payment.interest
-        loan.payments_remaining -= full_periodic_payments
+        total_interest_paid  += periodic_payment.interest
+        loan.payments_remaining -= 1
         loan.principal_outstanding -= periodic_payment.principal
 
+        if loan.payments_remaining == 0 {
+            // All done; no further recomputation needed.
+            break
+        }
+
+        // Recompute next periodic payment (may change after principal reduction).
         periodic_payment = loan.compute_periodic_payment()
+
+        // If after recomputation only one payment remains, force final payoff to avoid residual dust.
+        if loan.payments_remaining == 1 {
+          periodic_payment.principal = loan.principal_outstanding
+          periodic_payment.interest  = loan.total_value_outstanding - loan.principal_outstanding
+      }
+
         full_periodic_payments -= 1
     }
 
-    let overpayment = min(loan.principal_outstanding, amount % periodic_payment)
+    let overpayment = min(loan.principal_outstanding, amount % (periodic_payment + loan.service_fee))
     if overpayment > 0 && is_set(lsfOverpayment) {
         let interest_portion = overpayment * loan.overpayment_interest_rate
         let fee_portion = overpayment * loan.overpayment_fee
         let remainder = overpayment - interest_portion - fee_portion
 
         total_principal_paid += remainder
-        total_interest_paid += interest_portion
-        total_fee_paid += fee_portion
+        total_interest_paid  += interest_portion
+        total_fee_paid       += fee_portion
 
         let current_value = loan.compute_current_value()
         loan.principal_outstanding -= remainder
         let new_value = loan.compute_current_value()
 
+        // loan_value_change: change in future interest due to principal reduction + interest just paid
         loan_value_change = (new_value.interest - current_value.interest) + interest_portion
     }
 
+    // If final installment just executed, ensure outstanding principal hits zero (guard against residual 1-unit dust)
+    if loan.payments_remaining == 0 {
+        loan.principal_outstanding = 0
+    }
+
     return (total_principal_paid, total_interest_paid, loan_value_change, total_fee_paid)
 ```
 
@@ -1500,7 +1576,7 @@ Assume the payment is handled by a function that implements the [Pseudo-Code](#3
 - `totalPaid = principal_paid + interest_paid + fee_paid` is the total amount the borrower paid.
 - `value_change` is the amount by which the total value of the Loan changed.
   - If `value_change` < `0`, Loan value decreased.
-  - If `value_change` > `0`, Loan value increased.
+  - If `value_change` > `0`, Loan value increased, and if `value_change` = `0` the value remained the same.
 
 Furthermore, assume `full_periodic_payments` variable represents the number of payment intervals that the payment covered.
 
@@ -1508,7 +1584,7 @@ Furthermore, assume `full_periodic_payments` variable represents the number of p
 
 - The submitter `AccountRoot.Account` is not equal to `Loan.Borrower`.
 
-- `Loan.PaymentRemaining` or `Loan.PrincipalOutstanding` is `0`.
+- `Loan.PaymentRemaining` or `Loan.TotalValueOutstanding` is `0`.
 
 - The Borrower paid insufficient amount: `full_periodic_payments < 0`.
 
@@ -1516,7 +1592,7 @@ Furthermore, assume `full_periodic_payments` variable represents the number of p
 
   - The `RippleState` object between the submitter account and the `Issuer` of the asset has the `lsfLowFreeze` or `lsfHighFreeze` flag set.
   - The `RippleState` between the `LoanBroker.Account` and the `Issuer` has the `lsfLowDeepFreeze` or `lsfHighDeepFreeze` flag set. (The Loan Broker _pseudo-account_ is frozen).
-  - The `RippleState` between the `Vault(LoanBroker(Loan.LoanBrokerID).VaultID).Account` and the `Issuer` has the `lsfLowDeepFreeze` or `lsfHighDeepFreeze` flag set. (The Vault _pseudo-account_ is frozen).
+  - The `RippleState` between the `Vault(LoanBroker(Loan.LoanBrokerID).VaultID).Account` and the `Issuer` has the `lsfLowFreeze` or `lsfHighFreeze` flag set. (The Vault _pseudo-account_ is frozen).
   - The `AccountRoot` object of the `Issuer` has the `lsfGlobalFreeze` flag set.
 
 - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `MPT`:
@@ -1540,6 +1616,7 @@ Furthermore, assume `full_periodic_payments` variable represents the number of p
 
   - Decrease `Loan.PaymentRemaining` by `full_periodic_payments`.
   - Decrease `Loan.PrincipalOutstanding` by `principal_paid`.
+  - Update `Loan.TotalValueOutstanding` = `(Loan.TotalValueOutstanding + value_change) - (principal_paid + interest_paid)`. 
 
   - If `Loan.PaymentRemaining > 0` and `Loan.PrincipalOutstanding > 0`:
 

From a43426ed47af9852d922c2640f6eb8f572e7c5a4 Mon Sep 17 00:00:00 2001
From: Vito <5780819+Tapanito@users.noreply.github.com>
Date: Fri, 26 Sep 2025 18:12:02 +0200
Subject: [PATCH 57/77] adds Loan.PrincipalRequested

---
 XLS-0066d-lending-protocol/README.md | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md
index 1f5d1cb8..573d1bc3 100644
--- a/XLS-0066d-lending-protocol/README.md
+++ b/XLS-0066d-lending-protocol/README.md
@@ -468,7 +468,8 @@ The `LoanID` is calculated as follows:
 | `NextPaymentDueDate`      |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `UINT32`    | `LoanSet.StartDate + LoanSet.PaymentInterval` | The timestamp of when the next payment is due in [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time).       |
 | `PaymentRemaining`        |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `UINT32`    |            `LoanSet.PaymentTotal`             | The number of payments remaining on the Loan.                                                                                                                  |
 | `PrincipalOutstanding`    |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `NUMBER`    |         `LoanSet.PrincipalRequested`          | The principal amount due to be paid by the Borrower.                                                                                                           |
-| `TotalValueOutstanding`   |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `NUMBER`    |           `TotalValueOutstanding()`           | THe total outstanding value of the Loan.                                                                                                                       |
+| `TotalValueOutstanding`   |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `NUMBER`    |           `TotalValueOutstanding()`           | The total outstanding value of the Loan.                                                                                                                       |
+| `PrincipalRequested`      |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `NUMBER`    |         `LoanSet.PrincipalRequested`          | The amount of principal requested for the Loan.                                                                                                                |
 
 ##### 2.2.2.1 Flags
 
@@ -1514,7 +1515,7 @@ function make_payment(amount, current_time) -> (principal_paid, interest_paid, v
     let loan_value_change = 0
     let total_fee_paid = loan.service_fee * full_periodic_payments
     let loan_value_change = 0
-  
+
     while full_periodic_payments > 0 {
         total_principal_paid += periodic_payment.principal
         total_interest_paid  += periodic_payment.interest
@@ -1616,7 +1617,7 @@ Furthermore, assume `full_periodic_payments` variable represents the number of p
 
   - Decrease `Loan.PaymentRemaining` by `full_periodic_payments`.
   - Decrease `Loan.PrincipalOutstanding` by `principal_paid`.
-  - Update `Loan.TotalValueOutstanding` = `(Loan.TotalValueOutstanding + value_change) - (principal_paid + interest_paid)`. 
+  - Update `Loan.TotalValueOutstanding` = `(Loan.TotalValueOutstanding + value_change) - (principal_paid + interest_paid)`.
 
   - If `Loan.PaymentRemaining > 0` and `Loan.PrincipalOutstanding > 0`:
 

From 70221302f69de61b644437ca1952ccc67092802d Mon Sep 17 00:00:00 2001
From: Vito <5780819+Tapanito@users.noreply.github.com>
Date: Mon, 29 Sep 2025 20:04:41 +0200
Subject: [PATCH 58/77] adds authentication requirements to the loanset
 transaction

---
 XLS-0066d-lending-protocol/README.md | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md
index 573d1bc3..be839a76 100644
--- a/XLS-0066d-lending-protocol/README.md
+++ b/XLS-0066d-lending-protocol/README.md
@@ -880,7 +880,7 @@ An inner object that contains the signature of the Lender over the transaction.
 The final transaction must include exactly one of
 
 1. The `SigningPubKey` and `TxnSignature` fields, or
-2. The `Signers` field.
+2. The `Signers` field and, optionally, an empty `SigningPubKey`.
 
 The total fee for the transaction will be increased due to the extra signatures that need to be processed, similar to the additional fees for multisigning. The minimum fee will be $(|signatures| + 1) \times base_fee$ where $|signatures| == max(1, |tx.CounterPartySignature.Signers|)$
 
@@ -936,6 +936,7 @@ The account specified in the `Account` field pays the transaction fee.
   - The `RippleState` between the `LoanBroker.Account` and the `Issuer` has the `lsfLowDeepFreeze` or `lsfHighDeepFreeze` flag set. (The Loan Broker _pseudo-account_ is frozen).
   - The `RippleState` between the `Vault(LoanBroker(LoanBrokerID).VaultID).Account` and the `Issuer` has the `lsfLowFreeze` or `lsfHighFreeze` flag set. (The Vault _pseudo-account_ is frozen).
   - The `AccountRoot` object of the `Issuer` has the `lsfGlobalFreeze` flag set.
+  - The `AccountRoot` object of the `Issuer` has the `lsfRequireAuth` flag set, and the `RippleState` object between the `Issuer` and the Borrower does not have the `lsfLowAuth` and `lsfHighAuth` flags set. 
 
 - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `MPT`:
 
@@ -943,7 +944,8 @@ The account specified in the `Account` field pays the transaction fee.
   - The `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the Borrower `AccountRoot` has `lsfMPTLocked` flag set.
   - The `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the `LoanBroker.Account` `AccountRoot` has `lsfMPTLocked` flag set. (The Loan Broker _pseudo-account_ is locked).
   - The `MPTokenIssuance` object of the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` has the `lsfMPTLocked` flag set.
-
+- The `MPTokenIssuance` object of the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` has the `lsfMPTRequireAuth` flag set and the `MPToken`of the Borrower `AccountRoot` does not have the `lsfMPTAuthorized` flag set.
+  
 - Either of the `tfLoanDefault`, `tfLoanImpair` or `tfLoanUnimpair` flags are set.
 
 - The `Borrower` `AccountRoot` object does not exist.

From 2ef8f5a4b736c1a5174e0c051db3f9948367c144 Mon Sep 17 00:00:00 2001
From: Vito <5780819+Tapanito@users.noreply.github.com>
Date: Tue, 30 Sep 2025 13:23:52 +0200
Subject: [PATCH 59/77] fixes debt maximum calculation

---
 XLS-0066d-lending-protocol/README.md | 17 +++++++++--------
 1 file changed, 9 insertions(+), 8 deletions(-)

diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md
index be839a76..e8c284fe 100644
--- a/XLS-0066d-lending-protocol/README.md
+++ b/XLS-0066d-lending-protocol/README.md
@@ -959,7 +959,7 @@ The account specified in the `Account` field pays the transaction fee.
 
 - Exceeds maximum Debt of the LoanBroker:
 
-  - `LoanBroker(LoanBrokerID).DebtMaximum` < `Loan.PrincipalRequested + (TotalInterestOutstanding() - (TotalInterestOutstanding() x LoanBroker.ManagementFeeRate)`
+  - `LoanBroker(LoanBrokerID).DebtMaximum` < `LoanBroker(LoanBrokerID).DebtTotal + Loan.PrincipalRequested + (TotalInterestOutstanding() - (TotalInterestOutstanding() x LoanBroker.ManagementFeeRate)`
 
 - Insufficient First-Loss Capital:
 
@@ -1240,16 +1240,17 @@ $$
 principal = periodicPayment - interest
 $$
 
-When only a single payment remains (PaymentRemaining = 1) the periodic payment MUST be set equal to the current TotalValueOutstanding (i.e. principalOutstanding + interestOutstanding before any asset-specific rounding/truncation). This overrides the standard amortization formula for the last installment. The purpose is to eliminate residual dust created by iterative rounding (e.g. integer truncation for XRP drops or whole‑unit MPTs) that could otherwise make the loan impossible to fully extinguish.
+When only a single payment remains (PaymentRemaining = 1) the periodic payment is set equal to the current TotalValueOutstanding (i.e. principalOutstanding + interestOutstanding before any asset-specific rounding/truncation). This overrides the standard amortization formula for the last installment. The purpose is to eliminate residual dust created by iterative rounding (e.g. integer truncation for XRP drops or whole‑unit MPTs) that could otherwise make the loan impossible to fully repay. Repeated rounding of each scheduled payment can accumulate relative to the unrounded amortization schedule. Without this adjustment, the final formula-derived payment might be smaller or greater than the remaining outstanding value, leaving a non-zero or negative remainder that can never be cleared by any subsequent scheduled payment.
+
 
 Formally:
+
+```
 If PaymentRemaining > 1:
-periodicPayment = formula result (rounded per asset rules)
+  periodicPayment = formula result (rounded per asset rules)
 If PaymentRemaining = 1:
-periodicPayment = TotalValueOutstanding (rounded per asset rules; this sets TotalValueOutstanding to 0 after payment)
-
-Rationale:
-Repeated downward rounding (truncate / floor) of each scheduled payment can accumulate underpayment relative to the unrounded amortization schedule. Without this adjustment, the final formula-derived payment (after rounding) might be smaller than the remaining outstanding value, leaving a non-zero remainder that can never be cleared by any subsequent scheduled payment (because no payments remain).
+  periodicPayment = TotalValueOutstanding (rounded per asset rules; this sets TotalValueOutstanding to 0 after payment)
+```
 
 Example (integer-only MPT):
 
@@ -1266,7 +1267,7 @@ Example (integer-only MPT):
   Because PaymentRemaining = 1, set periodicPayment = TotalValueOutstanding = 2 MPT
   Final payment = 2 MPT clears the loan exactly (PrincipalOutstanding = 0, TotalValueOutstanding = 0).
 
-Therefore, implementations MUST detect the single remaining payment case and substitute the outstanding value to guarantee full extinguishment of the debt.
+Therefore, implementations must detect the single remaining payment case and substitute the outstanding value to guarantee full extinguishment of the debt.
 
 ###### 3.2.4.1.2 Late Payment
 

From 0a05584c896bb91378fb0d9faf03ab7c46b8de74 Mon Sep 17 00:00:00 2001
From: Vito <5780819+Tapanito@users.noreply.github.com>
Date: Tue, 30 Sep 2025 15:14:18 +0200
Subject: [PATCH 60/77] improves the handling of fees when loanbroker account
 is frozen

---
 XLS-0066d-lending-protocol/README.md | 66 ++++++++++++++++++++--------
 1 file changed, 47 insertions(+), 19 deletions(-)

diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md
index e8c284fe..95d9bb6a 100644
--- a/XLS-0066d-lending-protocol/README.md
+++ b/XLS-0066d-lending-protocol/README.md
@@ -931,21 +931,25 @@ The account specified in the `Account` field pays the transaction fee.
 
 - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `IOU`:
 
-  - The `RippleState` object between the Borrower account and the `Issuer` of the asset has the `lsfLowFreeze` or `lsfHighFreeze` flag set.
-  - The `RippleState` object between the LoanBroker account and the `Issuer` of the asset has the `lsfLowFreeze` or `lsfHighFreeze` flag set.
+  - The `AccountRoot` object of the `Issuer` has the `lsfGlobalFreeze` flag set.
+  - The `AccountRoot` object of the `Issuer` has the `lsfRequireAuth` flag set, and the `RippleState` object between the `Issuer` and the Borrower does not have the `lsfLowAuth` and `lsfHighAuth` flags set.
+
   - The `RippleState` between the `LoanBroker.Account` and the `Issuer` has the `lsfLowDeepFreeze` or `lsfHighDeepFreeze` flag set. (The Loan Broker _pseudo-account_ is frozen).
   - The `RippleState` between the `Vault(LoanBroker(LoanBrokerID).VaultID).Account` and the `Issuer` has the `lsfLowFreeze` or `lsfHighFreeze` flag set. (The Vault _pseudo-account_ is frozen).
-  - The `AccountRoot` object of the `Issuer` has the `lsfGlobalFreeze` flag set.
-  - The `AccountRoot` object of the `Issuer` has the `lsfRequireAuth` flag set, and the `RippleState` object between the `Issuer` and the Borrower does not have the `lsfLowAuth` and `lsfHighAuth` flags set. 
+
+  - The `RippleState` object between the Borrower account and the `Issuer` of the asset has the `lsfLowDeepFreeze` or `lsfHighDeepFreeze` flag set (The borrower cannot send and receive funds).
+  - The `RippleState` object between the Borrower account and the `Issuer` of the asset has the `lsfLowFreeze` or `lsfHighFreeze` flag set (The borrower cannot send funds).
 
 - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `MPT`:
 
-  - The `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the LoanBroker `AccountRoot` has `lsfMPTLocked` flag set.
-  - The `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the Borrower `AccountRoot` has `lsfMPTLocked` flag set.
-  - The `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the `LoanBroker.Account` `AccountRoot` has `lsfMPTLocked` flag set. (The Loan Broker _pseudo-account_ is locked).
   - The `MPTokenIssuance` object of the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` has the `lsfMPTLocked` flag set.
-- The `MPTokenIssuance` object of the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` has the `lsfMPTRequireAuth` flag set and the `MPToken`of the Borrower `AccountRoot` does not have the `lsfMPTAuthorized` flag set.
-  
+  - The `MPTokenIssuance` object of the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` has the `lsfMPTRequireAuth` flag set and the `MPToken`of the Borrower `AccountRoot` does not have the `lsfMPTAuthorized` flag set.
+
+  - The `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the `LoanBroker.Account` `AccountRoot` has `lsfMPTLocked` flag set. (The Loan Broker _pseudo-account_ is locked).
+  - The `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the `Vault(LoanBroker(LoanBrokerID).VaultID).Account` `AccountRoot` has `lsfMPTLocked` flag set. (The Vault _pseudo-account_ is locked).
+
+  - The `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the Borrower `AccountRoot` has `lsfMPTLocked` flag set (The Borrower MPToken is locked).
+
 - Either of the `tfLoanDefault`, `tfLoanImpair` or `tfLoanUnimpair` flags are set.
 
 - The `Borrower` `AccountRoot` object does not exist.
@@ -983,7 +987,15 @@ The account specified in the `Account` field pays the transaction fee.
 
   - Decrease the `RippleState` balance between the `Vault` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `Loan.PrincipalRequested`.
   - Increase the `RippleState` balance between the `Borrower` `AccountRoot` and the `Issuer` `AccountRoot` by `Loan.PrincipalRequested - Loan.LoanOriginationFee`.
-  - Increase the `RippleState` balance between the `LoanBroker.Owner` `AccountRoot` and the `Issuer` `AccountRoot` by `Loan.LoanOriginationFee`.
+
+  - If the `RippleState` object between the `LoanBroker.Owner` `AccountRoot` and the `Issuer` of the asset has the `lsfLowDeepFreeze` or `lsfHighDeepFreeze` flag set (The LoanBroker cannot receive funds):
+
+    - Increase the `RippleState` balance between the `LoanBroker` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `Loan.LoanOriginationFee` (the loan origination fee was added to First Loss Capital, and thus transfered to the `LoanBroker` _pseudo-account_).
+    - Increase `LoanBroker.CoverAvailable` by `Loan.LoanOriginationFee`.
+
+  - Otherwise:
+
+    - Increase the `RippleState` balance between the `LoanBroker.Owner` `AccountRoot` and the `Issuer` `AccountRoot` by `Loan.LoanOriginationFee`.
 
 - If the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` is an `MPT`:
 
@@ -991,7 +1003,14 @@ The account specified in the `Account` field pays the transaction fee.
 
   - Decrease the `MPToken.MPTAmount` of the `Vault` _pseudo-account_ `MPToken` object for the `Vault.Asset` by `Loan.PrincipalRequested`.
   - Increase the `MPToken.MPTAmount` of the `Borrower` `MPToken` object for the `Vault.Asset` by `Loan.PrincipalRequested - Loan.LoanOriginationFee`.
-  - Increase the `MPToken.MPTAmount` of the `LoanBroker.Owner` `MPToken` object for the `Vault.Asset` by `Loan.LoanOriginationFee`
+
+  - The `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the `LoanBroker.Owner` `AccountRoot` has `lsfMPTLocked` flag set (The LoanBroker cannot receive funds):
+
+    - Increase the `MPToken.MPTAmount` by `Loan.LoanOriginationFee` of the `LoanBroker` _pseudo-account_ `MPToken` object for the `Vault.Asset` (the loan origination fee was added to First Loss Capital, and thus transfered to the `LoanBroker` _pseudo-account_).
+    - Increase `LoanBroker.CoverAvailable` by `Loan.LoanOriginationFee`.
+
+  - Otherwise:
+    - Increase the `MPToken.MPTAmount` of the `LoanBroker.Owner` `MPToken` object for the `Vault.Asset` by `Loan.LoanOriginationFee`
 
 - `Vault(LoanBroker(LoanBrokerID).VaultID)` object state changes:
 
@@ -1242,7 +1261,6 @@ $$
 
 When only a single payment remains (PaymentRemaining = 1) the periodic payment is set equal to the current TotalValueOutstanding (i.e. principalOutstanding + interestOutstanding before any asset-specific rounding/truncation). This overrides the standard amortization formula for the last installment. The purpose is to eliminate residual dust created by iterative rounding (e.g. integer truncation for XRP drops or whole‑unit MPTs) that could otherwise make the loan impossible to fully repay. Repeated rounding of each scheduled payment can accumulate relative to the unrounded amortization schedule. Without this adjustment, the final formula-derived payment might be smaller or greater than the remaining outstanding value, leaving a non-zero or negative remainder that can never be cleared by any subsequent scheduled payment.
 
-
 Formally:
 
 ```
@@ -1695,31 +1713,41 @@ Furthermore, assume `full_periodic_payments` variable represents the number of p
 
   - Increase the `RippleState` balance between the `Vault` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `principal_paid + (interest_paid - management_fee)`.
 
-  - If `LoanBroker.CoverAvailable >= LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`:
+  - If `LoanBroker.CoverAvailable < LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`:
 
-    - Increase the `RippleState` balance between the `LoanBroker.Owner` `AccountRoot` and the `Issuer` `AccountRoot` by `fee_paid + management_fee`.
+    - Increase the `RippleState` balance between the `LoanBroker` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `fee_paid + management_fee` (the payment and management fee was added to First Loss Capital, and thus transfered to the `LoanBroker` _pseudo-account_).
+    - Increase `LoanBroker.CoverAvailable` by `fee_paid + management_fee`.
 
-  - If `LoanBroker.CoverAvailable < LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`:
+  - If the `RippleState` object between the `LoanBroker.Owner` `AccountRoot` and the `Issuer` of the asset has the `lsfLowDeepFreeze` or `lsfHighDeepFreeze` flag set (The LoanBroker cannot receive funds):
 
     - Increase the `RippleState` balance between the `LoanBroker` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `fee_paid + management_fee` (the payment and management fee was added to First Loss Capital, and thus transfered to the `LoanBroker` _pseudo-account_).
     - Increase `LoanBroker.CoverAvailable` by `fee_paid + management_fee`.
 
+  - If `LoanBroker.CoverAvailable >= LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`:
+
+    - Increase the `RippleState` balance between the `LoanBroker.Owner` `AccountRoot` and the `Issuer` `AccountRoot` by `fee_paid + management_fee`.
+
   - Decrease the `RippleState` balance between the submitter `AccountRoot` and the `Issuer` `AccountRoot` by `principal_paid + interest_paid + fee_paid`.
 
 - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `MPT`:
 
   - Increase the `MPToken.MPTAmount` by `principal_paid + (interest_paid - management_fee)` of the `Vault` _pseudo-account_ `MPToken` object for the `Vault.Asset`.
 
-  - If `LoanBroker.CoverAvailable >= LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`:
+  - If `LoanBroker.CoverAvailable < LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`:
 
-    - Increase the `MPToken.MPTAmount` by `fee_paid + management_fee` of the `LoanBroker.Owner` `MPToken` object for the `Vault.Asset`.
+    - Increase the `MPToken.MPTAmount` by `fee_paid + management_fee` of the `LoanBroker` _pseudo-account_ `MPToken` object for the `Vault.Asset` (the payment and management fee was added to First Loss Capital, and thus transfered to the `LoanBroker` _pseudo-account_).
+    - Increase `LoanBroker.CoverAvailable` by `fee_paid + management_fee`.
 
-  - If `LoanBroker.CoverAvailable < LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`:
+  - The `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the `LoanBroker.Owner` `AccountRoot` has `lsfMPTLocked` flag set (The LoanBroker cannot receive funds):
 
     - Increase the `MPToken.MPTAmount` by `fee_paid + management_fee` of the `LoanBroker` _pseudo-account_ `MPToken` object for the `Vault.Asset` (the payment and management fee was added to First Loss Capital, and thus transfered to the `LoanBroker` _pseudo-account_).
     - Increase `LoanBroker.CoverAvailable` by `fee_paid + management_fee`.
 
-  - Decrease the `MPToken.MPTAmount` by `principal_paid + interest_paid + fee_paid` of the submitter `MPToken` object for the `Vault.Asset`.
+  - If `LoanBroker.CoverAvailable >= LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`:
+
+    - Increase the `MPToken.MPTAmount` by `fee_paid + management_fee` of the `LoanBroker.Owner` `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset`.
+
+  - Decrease the `MPToken.MPTAmount` by `principal_paid + interest_paid + fee_paid` of the submitter `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset`.
 
 [**Return to Index**](#index)
 

From 63730e59bab19950a820503ebd67033859931c44 Mon Sep 17 00:00:00 2001
From: Vito <5780819+Tapanito@users.noreply.github.com>
Date: Wed, 1 Oct 2025 16:33:47 +0200
Subject: [PATCH 61/77] improves the pseudo-code accuracy and removes bugs in
 late payment computation

---
 XLS-0066d-lending-protocol/README.md | 272 ++++++++++++++++-----------
 1 file changed, 159 insertions(+), 113 deletions(-)

diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md
index 95d9bb6a..5476197b 100644
--- a/XLS-0066d-lending-protocol/README.md
+++ b/XLS-0066d-lending-protocol/README.md
@@ -1296,24 +1296,26 @@ totalDue = periodicPayment + latePaymentFee + latePaymentInterest
 $$
 
 $$
-secondsSinceLastPayment = lastLedgerCloseTime - max(Loan.previousPaymentDate, Loan.startDate)
+secondsOverdue = lastLedgerCloseTime - Loan.NextPaymentDueDate
 $$
 
 A special, late payment interest rate is applied for the over-due period:
 
 $$
-latePaymentInterest = principalOutstanding \times \frac{lateInterestRate \times secondsSinceLastPayment}{365 \times 24 \times 60 \times 60}
+latePeriodicRate = \frac{lateInterestRate \times secondsOverdue}{365 \times 24 \times 60 \times 60}
 $$
 
-A late payment pays more interest than calculated when increasing the Vault value in the `LoanSet` transaction. Therefore, the total Vault value captured by `Vault.AssetsTotal` must be recalculated.
+$$
+latePaymentInterest = principalOutstanding \times latePeriodicRate
+$$
 
-Assume the function `PeriodicPayment()` returns the expected periodic payment, split into `principalPeriodic` and `interestPeriodic`. Furthermore, assume the function `LatePayment()` that implements the Late Payment formula. The function returns the late payment split into `principalLate` and `interestLate`, where `interestLate` is calculated using the formula above. Note that `principalPeriodic == principalLate` and `interestLate > interestPeriodic` are used only when the payment is late. Otherwise, `interestLate == interestPeriodic`.
+A late payment pays more interest than calculated when increasing the Vault value in the `LoanSet` transaction. Therefore, the total Vault value captured by `Vault.AssetsTotal` must be recalculated. The increase in the `Vault.AssetsTotal` value is simply the `latePaymentInterest`.
 
 $$
-valueChange = interestLate - interestPeriodic
+valueChange = latePaymentInterest
 $$
 
-Note that `valueChange >= 0` for late payments, i.e. a late payment increases the value of the loan.
+Note that `valueChange > 0` for late payments, i.e. a late payment increases the value of the loan.
 
 ###### 3.2.4.1.3 Loan Overpayment
 
@@ -1472,133 +1474,177 @@ $$
 The following is the pseudo-code for handling a Loan payment transaction.
 
 ```
-function make_payment(amount, current_time) -> (principal_paid, interest_paid, value_change, fee_paid):
-    if loan.payments_remaining is 0 || loan.principal_outstanding is 0 {
+function compute_periodic_payment() -> (principal, interest):
+  let periodicRate = (loan.interestRate x loan.paymentInterval) / (365 * 24 * 60 * 60)
+
+  let raisedRate = (1 + periodicRate)^loan.paymentsRemaining
+  let periodicPayment = loan.principalOutstanding x (periodicRate x raisedRate) / (raisedRate - 1)
+
+  let interest = loan.principalOutstanding x periodicRate
+  let principal = periodicPayment - interest
+
+  return (principal, interest)
+
+function compute_late_payment_interest(currentTime) -> (lateInterest):
+
+    let secondsOverdue = lastLedgerClostTime() - loan.nextPaymentDueDate
+    let latePeriodicRate = (Loan.LateInterestRate x secondsOverdue) / (365 * 24 * 60 * 60)
+    let latePaymentInterest = Loan.PrincipalOutstanding x latePeriodicRate
+
+    return latePaymentInterest
+
+function compute_full_payment(currentTime) -> (principal, interest):
+  let periodicRate = (loan.interestRate x loan.paymentInterval) / (365 * 24 * 60 * 60)
+
+  let secondsSinceLastPayment = lastLedgerCloseTime() - max(loan.previousPaymentDate, loan.startDate)
+
+  let accruedInterest = loan.principalOutstanding x periodicRate x (secondsSinceLastPayment / loan.paymentInterval)
+  let prepaymentPenalty = loan.principalOutstanding x loan.closeInterestRate
+
+  return loan.principalOutstanding, accruedInterest + prepaymentPenalty
+
+function make_payment(amo unt, currentTime) -> (principalPaid, interestPaid, valueChange, feePaid):
+    if loan.paymentRemaining is 0 || loan.principalOutstanding is 0 {
         return "loan complete" error
     }
 
     // the payment is late
-    if loan.next_payment_due_date < current_time {
-        let late_payment = loan.compute_late_payment(current_time)
-        if amount < late_payment {
+    if loan.nextPaymentDueDate < currentTime {
+
+        let periodicPayment = compute_periodic_payment()
+        let lateInterest = compute_late_payment_interest(currentTime)
+
+        // insufficient funds
+        if amount < (periodicPayment + loan.serviceFee + lateInterest + loan.latePaymentFee) {
             return "insufficient amount paid" error
         }
 
-        loan.payments_remaining -= 1
-        loan.principal_outstanding -= late_payment.principal
+        loan.paymentRemaining -= 1
+        loan.previousPaymentDate = loan.nextPaymentDueDate
+        loan.nextPaymentDueDate = loan.nextPaymentDueDate + loan.paymentInterval
 
-        loan.last_payment_date = loan.next_payment_due_date
-        loan.next_payment_due_date = loan.next_payment_due_date + loan.payment_interval
+        loan.principalOutstanding -= periodicPayment.principal
 
-        let periodic_payment = loan.compute_periodic_payment()
-
-        // A late payment increases the value of the loan by the difference between periodic and late payment interest
-        return (late_payment.principal, late_payment.interest, late_payment.interest - periodic_payment.interest, loan.late_payment_fee)
+        return (
+          periodicPayment.principal,                // a late payment does not affect the principal portion due
+          periodicPayment.interest + lateInterest,  // a late payment incorporates both periodic interest and the late interest
+          lateInterest,                             // the value of the loan increases by the lateInterest amount
+          loan.serviceFee + loan.latePaymentFee     // the fee paid for a loan payment includes both: the regular service fee and the late payment fee
+          )
     }
 
-    let full_payment = loan.compute_full_payment(current_time)
+    let fullPayment = compute_full_payment(currentTime)
 
     // if the payment is equal or higher than full payment amount
     // and there is more than one payment remaining, make a full payment
-    if amount >= full_payment && loan.payments_remaining > 1 {
-        loan.payments_remaining = 0
-        loan.principal_outstanding = 0
-        let total_interest_outstanding = loan.lotal_value_outstanding - full_payment.principal
-        // A full payment decreases the value of the loan by the difference between the interest paid and the expected outstanding interest
-        return (full_payment.principal, full_payment.interest, full_payment.interest - total_interest_outstanding, full_payment.fee)
+    if amount >= fullPayment && loan.paymentRemaining > 1 {
+        let totalInterestOutstanding = loan.totalValueOutstanding - loan.principalOutstanding
+        let loanValueChange = fullPayment.interest - totalInterestOutstanding
+
+        loan.paymentRemaining = 0
+        loan.principalOutstanding = 0
+        return (
+          fullPayment.principal, // full payment repays the entire outstnading principal, i.e. equivalent to loan.principalOutstanding
+          fullPayment.interest,  // full payment repays any accrued interest since the last payment and additional full payment interest 
+          loanValueChange,       // a full payment changes the total value of the loan
+          loan.closePaymentFee   // an early payment pays a specific closePaymentFee 
+        )
     }
 
-    // PERIODIC (ON‑TIME) FLOW
     // Compute scheduled periodic payment. Override if this is the final installment to eliminate rounding dust.
-    let periodic_payment = loan.compute_periodic_payment()
-    if loan.payments_remaining == 1 {
+    let periodicPayment = compute_periodic_payment()
+    if loan.paymentRemaining == 1 {
         // Final scheduled payment: pay exactly all remaining value (principal + interest) before rounding.
-        periodic_payment.principal = loan.principal_outstanding
-        periodic_payment.interest  = loan.total_value_outstanding - loan.principal_outstanding
+        periodicPayment.principal = loan.principalOutstanding
+        periodicPayment.interest  = loan.totalValueOutstanding - loan.principalOutstanding
     }
 
-    let periodic_payment_total = periodic_payment.principal + periodic_payment.interest
+    let periodicPaymentTotal = periodicPayment.principal + periodicPayment.interest
 
     // Determine how many full periodic installments this single Amount can cover (cannot exceed remaining)
-    let full_periodic_payments = floor(amount / (periodic_payment_total + loan.service_fee))
-    if full_periodic_payments < 1 {
+    let fullPeriodicPayments = floor(amount / periodicPaymentTotal)
+    if fullPeriodicPayments < 1 {
         return "insufficient amount paid" error
     }
-    if full_periodic_payments > loan.payments_remaining {
-        full_periodic_payments = loan.payments_remaining
-    }
 
-    loan.next_payment_due_date = loan.next_payment_due_date + loan.payment_interval * full_periodic_payments
-    loan.last_payment_date = loan.next_payment_due_date - loan.payment_interval
-
-    let total_principal_paid = 0
-    let total_interest_paid = 0
-    let loan_value_change = 0
-    let total_fee_paid = loan.service_fee * full_periodic_payments
-    let loan_value_change = 0
-
-    while full_periodic_payments > 0 {
-        total_principal_paid += periodic_payment.principal
-        total_interest_paid  += periodic_payment.interest
-        loan.payments_remaining -= 1
-        loan.principal_outstanding -= periodic_payment.principal
+    if fullPeriodicPayments > loan.paymentRemaining {
+        fullPeriodicPayments = loan.paymentRemaining
+    }
 
-        if loan.payments_remaining == 0 {
-            // All done; no further recomputation needed.
-            break
-        }
+    let totalPrincipalPaid = 0
+    let totalInterestPaid = 0
+    let loanValueChange = 0
 
-        // Recompute next periodic payment (may change after principal reduction).
-        periodic_payment = loan.compute_periodic_payment()
+    while fullPeriodicPayments > 0 {
+        let periodicPayment = compute_periodic_payment()
 
         // If after recomputation only one payment remains, force final payoff to avoid residual dust.
-        if loan.payments_remaining == 1 {
-          periodic_payment.principal = loan.principal_outstanding
-          periodic_payment.interest  = loan.total_value_outstanding - loan.principal_outstanding
-      }
+        if loan.paymentRemaining == 1 {
+          periodicPayment.principal = loan.principalOutstanding
+          periodicPayment.interest  = loan.totalValueOutstanding - loan.principalOutstanding
+        }
 
-        full_periodic_payments -= 1
+        totalPrincipalPaid += periodicPayment.principal
+        totalInterestPaid  += periodicPayment.interest
+        loan.paymentRemaining -= 1
+        loan.principalOutstanding -= periodicPayment.principal
     }
 
-    let overpayment = min(loan.principal_outstanding, amount % (periodic_payment + loan.service_fee))
-    if overpayment > 0 && is_set(lsfOverpayment) {
-        let interest_portion = overpayment * loan.overpayment_interest_rate
-        let fee_portion = overpayment * loan.overpayment_fee
-        let remainder = overpayment - interest_portion - fee_portion
+    let totalFeePaid = loan.serviceFee * fullPeriodicPayments
+    loan.nextPaymentDueDate = loan.nextPaymentDueDate + loan.paymentInterval * fullPeriodicPayments
+    loan.previousPaymentDate = loan.nextPaymentDueDate - loan.paymentInterval
 
-        total_principal_paid += remainder
-        total_interest_paid  += interest_portion
-        total_fee_paid       += fee_portion
+    let newTotalValue = periodicPayment x loan.paymentRemaining
+    let overpayment = min(loan.principalOutstanding, amount - (totalPrincipalPaid + totalInterestPaid + totalFeePaid));
+    let overpaymentInterestPortion = 0
+  
+    if overpayment > 0 && is_set(lsfOverpayment) {
+        overpaymentInterestPortion = overpayment * loan.overpaymentInterestRate
+        let feePortion = overpayment * loan.overpaymentFee
+        let remainder = overpayment - overpaymentInterestPortion - feePortion
+
+        if remainder > 0 {
+          totalPrincipalPaid += remainder
+          totalInterestPaid  += overpaymentInterestPortion
+          totalFeePaid       += feePortion
+          loan.principalOutstanding -= remainder
+        }
+    }
 
-        let current_value = loan.compute_current_value()
-        loan.principal_outstanding -= remainder
-        let new_value = loan.compute_current_value()
+    // we are paying off principal early, we have to recompute the periodic payment
+    let totalValueAfterOverpayment = compute_periodic_payment() x loan.paymentRemaining
 
-        // loan_value_change: change in future interest due to principal reduction + interest just paid
-        loan_value_change = (new_value.interest - current_value.interest) + interest_portion
-    }
+    // loan overpayment decreases future interest, and thus decreases the overall value of the loan
+    // no assumptions should be made about the sign of loanValueChange
+    loanValueChange = (totalVaultAfterOverpayment - newTotalValue) + overpaymentInterestPortion
 
     // If final installment just executed, ensure outstanding principal hits zero (guard against residual 1-unit dust)
-    if loan.payments_remaining == 0 {
-        loan.principal_outstanding = 0
+    if loan.paymentRemaining == 0 {
+        loan.principalOutstanding = 0
+        loan.totalValueOutstanding = 0
     }
 
-    return (total_principal_paid, total_interest_paid, loan_value_change, total_fee_paid)
+    return (
+      totalPrincipalPaid, // this will include the periodicPayment principal and any overpayment
+      totalInterestPaid,  // this will include the periodicPayment interest and any overpayment
+      loanValueChange,    // valueChange in loal total value by overpayment
+      totalFeePaid        // the total fee
+    )
 ```
 
 ##### 3.2.4.4 Failure Conditions
 
 Assume the payment is split into `principal`, `interest` and `fee`, and `totalDue = principal + interest + fee`. `totalDue` is the minimum payment due by the borrower.
 
-Assume the payment is handled by a function that implements the [Pseudo-Code](#3242-transaction-pseudo-code) that returns `principal_paid`, `interest_paid`, `value_change` and `fee_paid`, where:
+Assume the payment is handled by a function that implements the [Pseudo-Code](#3242-transaction-pseudo-code) that returns `principalPaid`, `interestPaid`, `valueChange` and `feePaid`, where:
 
-- `principal_paid` is the amount of principal that the payment covered.
-- `interest_paid` is the amount of interest that the payment covered.
-- `fee_paid` is the amount of fee that the payment covered.
-- `totalPaid = principal_paid + interest_paid + fee_paid` is the total amount the borrower paid.
-- `value_change` is the amount by which the total value of the Loan changed.
-  - If `value_change` < `0`, Loan value decreased.
-  - If `value_change` > `0`, Loan value increased, and if `value_change` = `0` the value remained the same.
+- `principalPaid` is the amount of principal that the payment covered.
+- `interestPaid` is the amount of interest that the payment covered.
+- `feePaid` is the amount of fee that the payment covered.
+- `totalPaid = principalPaid + interestPaid + feePaid` is the total amount the borrower paid.
+- `valueChange` is the amount by which the total value of the Loan changed.
+  - If `valueChange` < `0`, Loan value decreased.
+  - If `valueChange` > `0`, Loan value increased, and if `valueChange` = `0` the value remained the same.
 
 Furthermore, assume `full_periodic_payments` variable represents the number of payment intervals that the payment covered.
 
@@ -1637,8 +1683,8 @@ Furthermore, assume `full_periodic_payments` variable represents the number of p
     - `Loan(LoanID).Flags = 0`
 
   - Decrease `Loan.PaymentRemaining` by `full_periodic_payments`.
-  - Decrease `Loan.PrincipalOutstanding` by `principal_paid`.
-  - Update `Loan.TotalValueOutstanding` = `(Loan.TotalValueOutstanding + value_change) - (principal_paid + interest_paid)`.
+  - Decrease `Loan.PrincipalOutstanding` by `principalPaid`.
+  - Update `Loan.TotalValueOutstanding` = `(Loan.TotalValueOutstanding + valueChange) - (principalPaid + interestPaid)`.
 
   - If `Loan.PaymentRemaining > 0` and `Loan.PrincipalOutstanding > 0`:
 
@@ -1649,13 +1695,13 @@ Furthermore, assume `full_periodic_payments` variable represents the number of p
 
   - Compute the management fee:
 
-    - `feeManagement = interest_paid x LoanBroker.ManagementFeeRate`
+    - `feeManagement = interestPaid x LoanBroker.ManagementFeeRate`
 
   - Total paid, and what portion goes to the vault:
 
-    - `totalPaid = principal_paid + interest_paid + fee_paid`
-    - `totalPaidToVault = principal_paid + interest_paid`
-    - `totalPaidToBroker = fee_paid`
+    - `totalPaid = principalPaid + interestPaid + feePaid`
+    - `totalPaidToVault = principalPaid + interestPaid`
+    - `totalPaidToBroker = feePaid`
 
   - Adjust the totals for the management fee:
 
@@ -1670,7 +1716,7 @@ Furthermore, assume `full_periodic_payments` variable represents the number of p
 
   - Decrease LoanBroker Debt by the amount paid:
 
-    - `LoanBroker.DebtTotal = LoanBroker.DebtTotal - (totalPaid - fee_paid)`
+    - `LoanBroker.DebtTotal = LoanBroker.DebtTotal - (totalPaid - feePaid)`
 
   - Update the LoanBroker Debt by the Loan value change:
 
@@ -1696,58 +1742,58 @@ Furthermore, assume `full_periodic_payments` variable represents the number of p
 
 - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is `XRP`:
 
-  - Increase the `Balance` field of `Vault` _pseudo-account_ `AccountRoot` by `principal_paid + (interest_paid - management_fee)`.
+  - Increase the `Balance` field of `Vault` _pseudo-account_ `AccountRoot` by `principalPaid + (interestPaid - management_fee)`.
 
   - If `LoanBroker.CoverAvailable >= LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`:
 
-    - Increase the `Balance` field of the `LoanBroker.Owner` `AccountRoot` by `fee_paid + management_fee`.
+    - Increase the `Balance` field of the `LoanBroker.Owner` `AccountRoot` by `feePaid + management_fee`.
 
   - If `LoanBroker.CoverAvailable < LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`:
 
-    - Increase the `Balance` field of the `LoanBroker` _pseudo-account_ `AccountRoot` by `fee_paid + management_fee`. (the payment and management fee was added to First Loss Capital, and thus transfered to the `LoanBroker` _pseudo-account_).
-    - Increase `LoanBroker.CoverAvailable` by `fee_paid + management_fee`.
+    - Increase the `Balance` field of the `LoanBroker` _pseudo-account_ `AccountRoot` by `feePaid + management_fee`. (the payment and management fee was added to First Loss Capital, and thus transfered to the `LoanBroker` _pseudo-account_).
+    - Increase `LoanBroker.CoverAvailable` by `feePaid + management_fee`.
 
-  - Decrease the `Balance` field of the submitter `AccountRoot` by `principal_paid + interest_paid + fee_paid`.
+  - Decrease the `Balance` field of the submitter `AccountRoot` by `principalPaid + interestPaid + feePaid`.
 
 - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `IOU`:
 
-  - Increase the `RippleState` balance between the `Vault` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `principal_paid + (interest_paid - management_fee)`.
+  - Increase the `RippleState` balance between the `Vault` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `principalPaid + (interestPaid - management_fee)`.
 
   - If `LoanBroker.CoverAvailable < LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`:
 
-    - Increase the `RippleState` balance between the `LoanBroker` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `fee_paid + management_fee` (the payment and management fee was added to First Loss Capital, and thus transfered to the `LoanBroker` _pseudo-account_).
-    - Increase `LoanBroker.CoverAvailable` by `fee_paid + management_fee`.
+    - Increase the `RippleState` balance between the `LoanBroker` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `feePaid + management_fee` (the payment and management fee was added to First Loss Capital, and thus transfered to the `LoanBroker` _pseudo-account_).
+    - Increase `LoanBroker.CoverAvailable` by `feePaid + management_fee`.
 
   - If the `RippleState` object between the `LoanBroker.Owner` `AccountRoot` and the `Issuer` of the asset has the `lsfLowDeepFreeze` or `lsfHighDeepFreeze` flag set (The LoanBroker cannot receive funds):
 
-    - Increase the `RippleState` balance between the `LoanBroker` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `fee_paid + management_fee` (the payment and management fee was added to First Loss Capital, and thus transfered to the `LoanBroker` _pseudo-account_).
-    - Increase `LoanBroker.CoverAvailable` by `fee_paid + management_fee`.
+    - Increase the `RippleState` balance between the `LoanBroker` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `feePaid + management_fee` (the payment and management fee was added to First Loss Capital, and thus transfered to the `LoanBroker` _pseudo-account_).
+    - Increase `LoanBroker.CoverAvailable` by `feePaid + management_fee`.
 
   - If `LoanBroker.CoverAvailable >= LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`:
 
-    - Increase the `RippleState` balance between the `LoanBroker.Owner` `AccountRoot` and the `Issuer` `AccountRoot` by `fee_paid + management_fee`.
+    - Increase the `RippleState` balance between the `LoanBroker.Owner` `AccountRoot` and the `Issuer` `AccountRoot` by `feePaid + management_fee`.
 
-  - Decrease the `RippleState` balance between the submitter `AccountRoot` and the `Issuer` `AccountRoot` by `principal_paid + interest_paid + fee_paid`.
+  - Decrease the `RippleState` balance between the submitter `AccountRoot` and the `Issuer` `AccountRoot` by `principalPaid + interestPaid + feePaid`.
 
 - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `MPT`:
 
-  - Increase the `MPToken.MPTAmount` by `principal_paid + (interest_paid - management_fee)` of the `Vault` _pseudo-account_ `MPToken` object for the `Vault.Asset`.
+  - Increase the `MPToken.MPTAmount` by `principalPaid + (interestPaid - management_fee)` of the `Vault` _pseudo-account_ `MPToken` object for the `Vault.Asset`.
 
   - If `LoanBroker.CoverAvailable < LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`:
 
-    - Increase the `MPToken.MPTAmount` by `fee_paid + management_fee` of the `LoanBroker` _pseudo-account_ `MPToken` object for the `Vault.Asset` (the payment and management fee was added to First Loss Capital, and thus transfered to the `LoanBroker` _pseudo-account_).
-    - Increase `LoanBroker.CoverAvailable` by `fee_paid + management_fee`.
+    - Increase the `MPToken.MPTAmount` by `feePaid + management_fee` of the `LoanBroker` _pseudo-account_ `MPToken` object for the `Vault.Asset` (the payment and management fee was added to First Loss Capital, and thus transfered to the `LoanBroker` _pseudo-account_).
+    - Increase `LoanBroker.CoverAvailable` by `feePaid + management_fee`.
 
   - The `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the `LoanBroker.Owner` `AccountRoot` has `lsfMPTLocked` flag set (The LoanBroker cannot receive funds):
 
-    - Increase the `MPToken.MPTAmount` by `fee_paid + management_fee` of the `LoanBroker` _pseudo-account_ `MPToken` object for the `Vault.Asset` (the payment and management fee was added to First Loss Capital, and thus transfered to the `LoanBroker` _pseudo-account_).
-    - Increase `LoanBroker.CoverAvailable` by `fee_paid + management_fee`.
+    - Increase the `MPToken.MPTAmount` by `feePaid + management_fee` of the `LoanBroker` _pseudo-account_ `MPToken` object for the `Vault.Asset` (the payment and management fee was added to First Loss Capital, and thus transfered to the `LoanBroker` _pseudo-account_).
+    - Increase `LoanBroker.CoverAvailable` by `feePaid + management_fee`.
 
   - If `LoanBroker.CoverAvailable >= LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`:
 
-    - Increase the `MPToken.MPTAmount` by `fee_paid + management_fee` of the `LoanBroker.Owner` `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset`.
+    - Increase the `MPToken.MPTAmount` by `feePaid + management_fee` of the `LoanBroker.Owner` `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset`.
 
-  - Decrease the `MPToken.MPTAmount` by `principal_paid + interest_paid + fee_paid` of the submitter `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset`.
+  - Decrease the `MPToken.MPTAmount` by `principalPaid + interestPaid + feePaid` of the submitter `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset`.
 
 [**Return to Index**](#index)
 

From ead6cec3d048c66a4cfce4eb683f649f196cb0c4 Mon Sep 17 00:00:00 2001
From: Vito <5780819+Tapanito@users.noreply.github.com>
Date: Tue, 21 Oct 2025 14:27:43 +0200
Subject: [PATCH 62/77] adds loan overpayment flag to LoanPay transaction

---
 XLS-0066d-lending-protocol/README.md | 47 +++++++++++++++++-----------
 1 file changed, 28 insertions(+), 19 deletions(-)

diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md
index 5476197b..d621737f 100644
--- a/XLS-0066d-lending-protocol/README.md
+++ b/XLS-0066d-lending-protocol/README.md
@@ -1193,13 +1193,20 @@ The transaction deletes an existing `Loan` object.
 
 The Borrower submits a `LoanPay` transaction to make a Payment on the Loan.
 
-| Field Name        |     Required?      | JSON Type | Internal Type | Default Value | Description                              |
-| ----------------- | :----------------: | :-------: | :-----------: | :-----------: | :--------------------------------------- |
-| `TransactionType` | :heavy_check_mark: | `string`  |   `UINT16`    |     `83`      | The transaction type.                    |
-| `LoanID`          | :heavy_check_mark: | `string`  |   `HASH256`   |     `N/A`     | The ID of the Loan object to be paid to. |
-| `Amount`          | :heavy_check_mark: | `number`  |   `AMOUNT`    |     `N/A`     | The amount of funds to pay.              |
+| Field Name        |     Required?      | JSON Type | Internal Type | Default Value | Description                               |
+| ----------------- | :----------------: | :-------: | :-----------: | :-----------: | :---------------------------------------- |
+| `TransactionType` | :heavy_check_mark: | `string`  |   `UINT16`    |     `83`      | The transaction type.                     |
+| `LoanID`          | :heavy_check_mark: | `string`  |   `HASH256`   |     `N/A`     | The ID of the Loan object to be paid to.  |
+| `Amount`          | :heavy_check_mark: | `number`  |   `AMOUNT`    |     `N/A`     | The amount of funds to pay.               |
+| `Flags`           |                    | `string`  |   `UINT32`    |       0       | Specifies the flags for the Loan Payment. |
+
+##### 3.2.4.1 `Flags`
 
-##### 3.2.4.1 Payment Types
+| Flag Name           |  Flag Value  | Description                                                                  |
+| ------------------- | :----------: | :--------------------------------------------------------------------------- |
+| `tfLoanOverpayment` | `0x00010000` | Indicates that remaining payment amount should be treated as an overpayment. |
+
+##### 3.2.4.2 Payment Types
 
 A Loan payment has four types:
 
@@ -1231,7 +1238,7 @@ If the Loan Broker and the borrower have agreed to allow overpayments, any amoun
 
 Each payment comprises three parts, `principal`, `interest` and `fee`. The `principal` is an amount paid against the principal of the Loan, `interest` is the interest portion of the Loan, and `fee` is the fee part paid by the Borrower on top of `principal` and `interest`.
 
-###### 3.2.4.1.1 Regular Payment
+###### 3.2.4.2.1 Regular Payment
 
 A periodic payment amount is calculated using the amortization payment formula:
 
@@ -1287,7 +1294,7 @@ Example (integer-only MPT):
 
 Therefore, implementations must detect the single remaining payment case and substitute the outstanding value to guarantee full extinguishment of the debt.
 
-###### 3.2.4.1.2 Late Payment
+###### 3.2.4.2.2 Late Payment
 
 When a Borrower makes a payment after `NextPaymentDueDate`, they must pay a nominal late payment fee and an additional interest rate charged on the overdue amount for the unpaid period. The formula is as follows:
 
@@ -1317,7 +1324,7 @@ $$
 
 Note that `valueChange > 0` for late payments, i.e. a late payment increases the value of the loan.
 
-###### 3.2.4.1.3 Loan Overpayment
+###### 3.2.4.2.3 Loan Overpayment
 
 - Let $\mathcal{P}$ and $\mathcal{p}$ represent the total and outstanding Loan principal.
 - Let $\mathcal{I}$ and $\mathcal{i}$ represent the total and outstanding Loan interest computed from $\mathcal{P}$ and $\mathcal{p}$ respectively.
@@ -1348,7 +1355,7 @@ $$
 valueChange =  \mathcal{i} - \mathcal{i'}
 $$
 
-###### 3.2.4.1.4 Early Full Repayment
+###### 3.2.4.2.4 Early Full Repayment
 
 A Borrower can close a Loan early by submitting the total amount needed to do so. This amount is the sum of the remaining balance, any accrued interest, a prepayment penalty, and a prepayment fee.
 
@@ -1382,7 +1389,7 @@ $$
 valueChange = (prepaymentPenalty) - (interestOutstanding - accruedInterest)
 $$
 
-###### 3.2.4.1.5 Management Fee Calculations
+###### 3.2.4.2.5 Management Fee Calculations
 
 The `LoanBroker` Management fee is charged against the interest portion of the Loan and subtracted from the total Loan value at Loan creation. However, the fee is charged only during Loan payments. Early and Late payments change the total value of the Loan by decreasing or increasing the value of total interest. Therefore, when an early, late or an overpayment payment is made, the management fee must be updated.
 
@@ -1441,7 +1448,7 @@ $$
 LoanBroker.DebtTotal = LoanBroker.DebtTotal - managementFeeChange
 $$
 
-##### 3.2.4.2 Total Loan Value Calculation
+##### 3.2.4.3 Total Loan Value Calculation
 
 At any point in time the following formulae can be used to calculate the total remaining value of the loan.
 
@@ -1469,7 +1476,7 @@ $$
 totalInterestOutstanding = totalValueOutstanding - principalOutstanding
 $$
 
-##### 3.2.4.3 Transaction Pseudo-code
+##### 3.2.4.4 Transaction Pseudo-code
 
 The following is the pseudo-code for handling a Loan payment transaction.
 
@@ -1545,9 +1552,9 @@ function make_payment(amo unt, currentTime) -> (principalPaid, interestPaid, val
         loan.principalOutstanding = 0
         return (
           fullPayment.principal, // full payment repays the entire outstnading principal, i.e. equivalent to loan.principalOutstanding
-          fullPayment.interest,  // full payment repays any accrued interest since the last payment and additional full payment interest 
+          fullPayment.interest,  // full payment repays any accrued interest since the last payment and additional full payment interest
           loanValueChange,       // a full payment changes the total value of the loan
-          loan.closePaymentFee   // an early payment pays a specific closePaymentFee 
+          loan.closePaymentFee   // an early payment pays a specific closePaymentFee
         )
     }
 
@@ -1597,8 +1604,8 @@ function make_payment(amo unt, currentTime) -> (principalPaid, interestPaid, val
     let newTotalValue = periodicPayment x loan.paymentRemaining
     let overpayment = min(loan.principalOutstanding, amount - (totalPrincipalPaid + totalInterestPaid + totalFeePaid));
     let overpaymentInterestPortion = 0
-  
-    if overpayment > 0 && is_set(lsfOverpayment) {
+
+    if overpayment > 0 && is_set(lsfLoanOverpayment) && is_set(tfLoanOverpayment) {
         overpaymentInterestPortion = overpayment * loan.overpaymentInterestRate
         let feePortion = overpayment * loan.overpaymentFee
         let remainder = overpayment - overpaymentInterestPortion - feePortion
@@ -1632,7 +1639,7 @@ function make_payment(amo unt, currentTime) -> (principalPaid, interestPaid, val
     )
 ```
 
-##### 3.2.4.4 Failure Conditions
+##### 3.2.4.5 Failure Conditions
 
 Assume the payment is split into `principal`, `interest` and `fee`, and `totalDue = principal + interest + fee`. `totalDue` is the minimum payment due by the borrower.
 
@@ -1652,6 +1659,8 @@ Furthermore, assume `full_periodic_payments` variable represents the number of p
 
 - The submitter `AccountRoot.Account` is not equal to `Loan.Borrower`.
 
+- The `Loan.lsfLoanOverpayment` flag is not set, and the user set the `tfLoanOverpayment` flag.
+
 - `Loan.PaymentRemaining` or `Loan.TotalValueOutstanding` is `0`.
 
 - The Borrower paid insufficient amount: `full_periodic_payments < 0`.
@@ -1674,7 +1683,7 @@ Furthermore, assume `full_periodic_payments` variable represents the number of p
 
 - If `LastClosedLedger.CloseTime < Loan.NextPaymentDueDate` and `Amount` < `PeriodicPaymentAmount()`
 
-##### 3.2.4.5 State Changes
+##### 3.2.4.6 State Changes
 
 - `Loan` object state changes:
 

From 1607e70d6fefc44680c0a721b896e35b7263284d Mon Sep 17 00:00:00 2001
From: Vito <5780819+Tapanito@users.noreply.github.com>
Date: Wed, 22 Oct 2025 16:26:04 +0200
Subject: [PATCH 63/77] adds description of rounding error handling

---
 XLS-0066d-lending-protocol/README.md | 508 ++++++++++++++++++---------
 1 file changed, 333 insertions(+), 175 deletions(-)

diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md
index d621737f..39e612de 100644
--- a/XLS-0066d-lending-protocol/README.md
+++ b/XLS-0066d-lending-protocol/README.md
@@ -440,36 +440,38 @@ The `LoanID` is calculated as follows:
 
 #### 2.2.2 Fields
 
-| Field Name                | User Modifiable? | Constant? |     Required?      | JSON Type | Internal Type |                 Default Value                 | Description                                                                                                                                                    |
-| ------------------------- | :--------------: | :-------: | :----------------: | :-------: | :-----------: | :-------------------------------------------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `LedgerEntryType`         |       `No`       |   `Yes`   | :heavy_check_mark: | `string`  |   `UINT16`    |                   `0x0089`                    | Ledger object type.                                                                                                                                            |
-| `LedgerIndex`             |       `No`       |   `Yes`   | :heavy_check_mark: | `string`  |   `UINT16`    |                     `N/A`                     | Ledger object identifier.                                                                                                                                      |
-| `Flags`                   |      `Yes`       |   `No`    |                    | `string`  |   `UINT32`    |                       0                       | Ledger object flags.                                                                                                                                           |
-| `PreviousTxnID`           |       `No`       |   `No`    | :heavy_check_mark: | `string`  |   `HASH256`   |                     `N/A`                     | The ID of the transaction that last modified this object.                                                                                                      |
-| `PreviousTxnLgrSeq`       |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `UINT32`    |                     `N/A`                     | The ledger sequence containing the transaction that last modified this object.                                                                                 |
-| `LoanSequence`            |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                     `N/A`                     | The sequence number of the Loan.                                                                                                                               |
-| `OwnerNode`               |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT64`    |                     `N/A`                     | Identifies the page where this item is referenced in the `Borrower` owner's directory.                                                                         |
-| `LoanBrokerNode`          |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT64`    |                     `N/A`                     | Identifies the page where this item is referenced in the `LoanBroker`s owner directory.                                                                        |
-| `LoanBrokerID`            |       `No`       |   `Yes`   | :heavy_check_mark: | `string`  |   `HASH256`   |                     `N/A`                     | The ID of the `LoanBroker` associated with this Loan Instance.                                                                                                 |
-| `Borrower`                |       `No`       |   `Yes`   | :heavy_check_mark: | `string`  |  `AccountID`  |                     `N/A`                     | The address of the account that is the borrower.                                                                                                               |
-| `LoanOriginationFee`      |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `NUMBER`    |                     `N/A`                     | A nominal nds amount paid to the `LoanBroker.Owner` when the Loan is created.                                                                                  |
-| `LoanServiceFee`          |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `NUMBER`    |                     `N/A`                     | A nominal funds amount paid to the `LoanBroker.Owner` with every Loan payment.                                                                                 |
-| `LatePaymentFee`          |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `NUMBER`    |                     `N/A`                     | A nominal funds amount paid to the `LoanBroker.Owner` when a payment is late.                                                                                  |
-| `ClosePaymentFee`         |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `NUMBER`    |                     `N/A`                     | A nominal funds amount paid to the `LoanBroker.Owner` when a payment full payment is made.                                                                     |
-| `OverpaymentFee`          |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                     `N/A`                     | A fee charged on overpayments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%)                                              |
-| `InterestRate`            |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                     `N/A`                     | Annualized interest rate of the Loan in 1/10th basis points.                                                                                                   |
-| `LateInterestRate`        |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                     `N/A`                     | A premium is added to the interest rate for late payments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%)                  |
-| `CloseInterestRate`       |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                     `N/A`                     | An interest rate charged for repaying the Loan early in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%)                       |
-| `OverpaymentInterestRate` |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                     `N/A`                     | An interest rate charged on overpayments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%)                                   |
-| `StartDate`               |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |           `CurrentLedgerTimestamp`            | The timestamp of when the Loan started [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time).                 |
-| `PaymentInterval`         |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                     `N/A`                     | Number of seconds between Loan payments.                                                                                                                       |
-| `GracePeriod`             |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                     `N/A`                     | The number of seconds after the Loan's Payment Due Date that the Loan can be Defaulted.                                                                        |
-| `PreviousPaymentDate`     |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `UINT32`    |                      `0`                      | The timestamp of when the previous payment was made in [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time). |
-| `NextPaymentDueDate`      |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `UINT32`    | `LoanSet.StartDate + LoanSet.PaymentInterval` | The timestamp of when the next payment is due in [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time).       |
-| `PaymentRemaining`        |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `UINT32`    |            `LoanSet.PaymentTotal`             | The number of payments remaining on the Loan.                                                                                                                  |
-| `PrincipalOutstanding`    |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `NUMBER`    |         `LoanSet.PrincipalRequested`          | The principal amount due to be paid by the Borrower.                                                                                                           |
-| `TotalValueOutstanding`   |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `NUMBER`    |           `TotalValueOutstanding()`           | The total outstanding value of the Loan.                                                                                                                       |
-| `PrincipalRequested`      |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `NUMBER`    |         `LoanSet.PrincipalRequested`          | The amount of principal requested for the Loan.                                                                                                                |
+| Field Name                 | User Modifiable? | Constant? |     Required?      | JSON Type | Internal Type |                                   Default Value                                   | Description                                                                                                                                                    |
+| -------------------------- | :--------------: | :-------: | :----------------: | :-------: | :-----------: | :-------------------------------------------------------------------------------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `LedgerEntryType`          |       `No`       |   `Yes`   | :heavy_check_mark: | `string`  |   `UINT16`    |                                     `0x0089`                                      | Ledger object type.                                                                                                                                            |
+| `LedgerIndex`              |       `No`       |   `Yes`   | :heavy_check_mark: | `string`  |   `UINT16`    |                                       `N/A`                                       | Ledger object identifier.                                                                                                                                      |
+| `Flags`                    |      `Yes`       |   `No`    |                    | `string`  |   `UINT32`    |                                         0                                         | Ledger object flags.                                                                                                                                           |
+| `PreviousTxnID`            |       `No`       |   `No`    | :heavy_check_mark: | `string`  |   `HASH256`   |                                       `N/A`                                       | The ID of the transaction that last modified this object.                                                                                                      |
+| `PreviousTxnLgrSeq`        |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `UINT32`    |                                       `N/A`                                       | The ledger sequence containing the transaction that last modified this object.                                                                                 |
+| `LoanSequence`             |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                                       `N/A`                                       | The sequence number of the Loan.                                                                                                                               |
+| `OwnerNode`                |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT64`    |                                       `N/A`                                       | Identifies the page where this item is referenced in the `Borrower` owner's directory.                                                                         |
+| `LoanBrokerNode`           |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT64`    |                                       `N/A`                                       | Identifies the page where this item is referenced in the `LoanBroker`s owner directory.                                                                        |
+| `LoanBrokerID`             |       `No`       |   `Yes`   | :heavy_check_mark: | `string`  |   `HASH256`   |                                       `N/A`                                       | The ID of the `LoanBroker` associated with this Loan Instance.                                                                                                 |
+| `Borrower`                 |       `No`       |   `Yes`   | :heavy_check_mark: | `string`  |  `AccountID`  |                                       `N/A`                                       | The address of the account that is the borrower.                                                                                                               |
+| `LoanOriginationFee`       |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `NUMBER`    |                                       `N/A`                                       | A nominal nds amount paid to the `LoanBroker.Owner` when the Loan is created.                                                                                  |
+| `LoanServiceFee`           |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `NUMBER`    |                                       `N/A`                                       | A nominal funds amount paid to the `LoanBroker.Owner` with every Loan payment.                                                                                 |
+| `LatePaymentFee`           |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `NUMBER`    |                                       `N/A`                                       | A nominal funds amount paid to the `LoanBroker.Owner` when a payment is late.                                                                                  |
+| `ClosePaymentFee`          |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `NUMBER`    |                                       `N/A`                                       | A nominal funds amount paid to the `LoanBroker.Owner` when a payment full payment is made.                                                                     |
+| `OverpaymentFee`           |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                                       `N/A`                                       | A fee charged on overpayments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%)                                              |
+| `InterestRate`             |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                                       `N/A`                                       | Annualized interest rate of the Loan in 1/10th basis points.                                                                                                   |
+| `LateInterestRate`         |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                                       `N/A`                                       | A premium is added to the interest rate for late payments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%)                  |
+| `CloseInterestRate`        |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                                       `N/A`                                       | An interest rate charged for repaying the Loan early in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%)                       |
+| `OverpaymentInterestRate`  |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                                       `N/A`                                       | An interest rate charged on overpayments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%)                                   |
+| `StartDate`                |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                             `CurrentLedgerTimestamp`                              | The timestamp of when the Loan started [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time).                 |
+| `PaymentInterval`          |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                                       `N/A`                                       | Number of seconds between Loan payments.                                                                                                                       |
+| `GracePeriod`              |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                                       `N/A`                                       | The number of seconds after the Loan's Payment Due Date that the Loan can be Defaulted.                                                                        |
+| `PreviousPaymentDate`      |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `UINT32`    |                                        `0`                                        | The timestamp of when the previous payment was made in [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time). |
+| `NextPaymentDueDate`       |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `UINT32`    |                   `LoanSet.StartDate + LoanSet.PaymentInterval`                   | The timestamp of when the next payment is due in [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time).       |
+| `PaymentRemaining`         |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `UINT32`    |                              `LoanSet.PaymentTotal`                               | The number of payments remaining on the Loan.                                                                                                                  |
+| `TotalValueOutstanding`    |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `NUMBER`    |                             `TotalValueOutstanding()`                             | The total outstanding value of the Loan, including all fees and interest.                                                                                      |
+| `PrincipalOutstanding`     |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `NUMBER`    |                           `LoanSet.PrincipalRequested`                            | The principal amount that the Borrower still owes.                                                                                                             |
+| `ManagementFeeOutstanding` |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `NUMBER`    | `(TotalValueOutstanding() - PrincipalOutstanding) x LoanBroker.ManagementFeeRate` | The remaining Management Fee owed to the LoanBroker.                                                                                                           |
+| `PeriodicPayment`          |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `NUMBER`    |                              `LoanPeriodicPayment()`                              | The calculated periodic payment amount for each payment interval.                                                                                              |
+| `PrincipalRequested`       |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `NUMBER`    |                           `LoanSet.PrincipalRequested`                            | The amount of principal requested for the Loan.                                                                                                                |
 
 ##### 2.2.2.1 Flags
 
@@ -481,6 +483,22 @@ The `Loan` object supports the following flags:
 | `lsfLoanImpaired`    | `0x00020000` |    `Yes`    |      If set, indicates that the Loan is impaired.      |
 | `lsfLoanOverpayment` | `0x00040000` |    `No`     | If set, indicates that the Loan supports overpayments. |
 
+##### 2.2.2.2 TotalValueOutstanding
+
+The total outstanding value of the Loan, including all fees. To calculate the outstanding interest portion, use this formula: `TotalInterestOutstanding = TotalValueOutstanding - PrincipalOutstanding - ManagementFeeOutstanding`.
+
+##### 2.2.2.3 PrincipalOutstanding
+
+The principal amount that the Borrower still owes. This amount decreases each time the borrower makes a successful loan payment. This field ensures that the loan is fully settled on the final payment.
+
+##### 2.2.2.4 ManagementFeeOutstanding
+
+The remaining Management Fee owed to the LoanBroker. This amount decreases each time the borrower makes a successful loan payment. This field ensures that the loan is fully settled on the final payment.
+
+##### 2.2.2.5 PeriodicPayment
+
+The periodic payment amount represents the precise sum the Borrower must pay during each payment cycle. For practical implementation, this value should be rounded UP when processing payments. The system automatically recalculates the PeriodicPayment following any overpayment by the borrower. For instance, when dealing with MPT loans, the calculated `PeriodicPayment` may be `10.251`. However, since MPTs only support whole number representations, the borrower would need to pay `12` units. The system maintains the precise periodic payment value at maximum accuracy since it is frequently referenced throughout loan payment computations.
+
 #### 2.2.3 Ownership
 
 The `Loan` objects are stored in the ledger and tracked in two [Owner Directories](https://xrpl.org/docs/references/protocol/ledger-data/ledger-entry-types/directorynode).
@@ -1481,161 +1499,301 @@ $$
 The following is the pseudo-code for handling a Loan payment transaction.
 
 ```
-function compute_periodic_payment() -> (principal, interest):
-  let periodicRate = (loan.interestRate x loan.paymentInterval) / (365 * 24 * 60 * 60)
-
-  let raisedRate = (1 + periodicRate)^loan.paymentsRemaining
-  let periodicPayment = loan.principalOutstanding x (periodicRate x raisedRate) / (raisedRate - 1)
-
-  let interest = loan.principalOutstanding x periodicRate
-  let principal = periodicPayment - interest
-
-  return (principal, interest)
-
-function compute_late_payment_interest(currentTime) -> (lateInterest):
-
-    let secondsOverdue = lastLedgerClostTime() - loan.nextPaymentDueDate
-    let latePeriodicRate = (Loan.LateInterestRate x secondsOverdue) / (365 * 24 * 60 * 60)
-    let latePaymentInterest = Loan.PrincipalOutstanding x latePeriodicRate
-
-    return latePaymentInterest
-
-function compute_full_payment(currentTime) -> (principal, interest):
-  let periodicRate = (loan.interestRate x loan.paymentInterval) / (365 * 24 * 60 * 60)
-
-  let secondsSinceLastPayment = lastLedgerCloseTime() - max(loan.previousPaymentDate, loan.startDate)
-
-  let accruedInterest = loan.principalOutstanding x periodicRate x (secondsSinceLastPayment / loan.paymentInterval)
-  let prepaymentPenalty = loan.principalOutstanding x loan.closeInterestRate
-
-  return loan.principalOutstanding, accruedInterest + prepaymentPenalty
-
-function make_payment(amo unt, currentTime) -> (principalPaid, interestPaid, valueChange, feePaid):
-    if loan.paymentRemaining is 0 || loan.principalOutstanding is 0 {
+function compute_periodic_payment(principalOutstanding) -> (periodicPayment):
+    let periodicRate = (loan.interestRate * loan.paymentInterval) / (365 * 24 * 60 * 60)
+    let raisedRate = (1 + periodicRate)^loan.paymentsRemaining
+    
+    return principalOutstanding * (periodicRate * raisedRate) / (raisedRate - 1)
+
+function compute_late_payment_interest(currentTime) -> (lateInterest, managementFee):
+    let secondsOverdue = lastLedgerCloseTime() - loan.nextPaymentDueDate
+    let latePeriodicRate = (loan.lateInterestRate * secondsOverdue) / (365 * 24 * 60 * 60)
+    let latePaymentInterest = loan.principalOutstanding * latePeriodicRate
+    
+    let fee = (latePaymentInterest * loan.loanbroker.managementFeeRate).round(asset_scale, DOWN)
+    
+    return (latePaymentInterest - fee, fee)
+
+function compute_full_payment(currentTime) -> (principal, interest, fee):
+    let rawPrincipalOutstanding = principal_outstanding_from_periodic()
+    let periodicRate = (loan.interestRate * loan.paymentInterval) / (365 * 24 * 60 * 60)
+    let secondsSinceLastPayment = lastLedgerCloseTime() - max(loan.previousPaymentDate, loan.startDate)
+    
+    let accruedInterest = rawPrincipalOutstanding * periodicRate * (secondsSinceLastPayment / loan.paymentInterval)
+    let prepaymentPenalty = rawPrincipalOutstanding * loan.closeInterestRate
+    let interest = (accruedInterest + prepaymentPenalty).round(asset_scale, DOWN)
+    
+    let managementFee = (interest * loan.loanbroker.managementFeeRate).round(asset_scale, DOWN)
+    interest = interest - managementFee
+    
+    if managementFee <= loan.managementFeeOutstanding:
+        return (loan.principalOutstanding, interest, managementFee)
+    
+    return (loan.principalOutstanding, accruedInterest + prepaymentPenalty, managementFee)
+
+function principal_outstanding_from_periodic() -> (principalOutstanding):
+    # Given the outstanding principal we can calculate the periodic payment
+    # Equally, given the periodic payment we can calculate the principal outstanding at the current time
+    let periodicRate = (loan.interestRate * loan.paymentInterval) / (365 * 24 * 60 * 60)
+    
+    # If the loan is zero-interest, the outstanding principal is simply periodicPayment * paymentsRemaining
+    if periodicRate == 0:
+        return loan.periodicPayment * loan.paymentsRemaining
+    
+    let raisedRate = (1 + periodicRate)^loan.paymentsRemaining
+    let factor = (periodicRate * raisedRate) / (raisedRate - 1)
+    
+    return loan.periodicPayment / factor
+
+# This function calculates what the loan state should be given the periodic payment and remaining payments
+function calculate_true_loan_state() -> (principalOutstanding, interestOutstanding, managementFeeOutstanding):
+    let rawPrincipalOutstanding = principal_outstanding_from_periodic()
+    let rawInterestOutstanding = (loan.periodicPayment * loan.paymentsRemaining) - rawPrincipalOutstanding
+    let rawManagementFeeOutstanding = (rawInterestOutstanding * loan.loanbroker.managementFeeRate)
+    
+    # Exclude the management fee from the interest rate
+    rawInterestOutstanding = rawInterestOutstanding - rawManagementFeeOutstanding
+    
+    return (rawPrincipalOutstanding, rawInterestOutstanding, rawManagementFeeOutstanding)
+
+function calculate_payment_breakdown(principalOutstanding) -> (principal, interest):
+    let periodicRate = (loan.interestRate * loan.paymentInterval) / (365 * 24 * 60 * 60)
+    
+    if periodicRate == 0:
+        return (principalOutstanding / loan.paymentsRemaining, 0)
+    
+    let interest = principalOutstanding * periodicRate
+    let principal = loan.periodicPayment - interest
+    
+    return (principal, interest)
+
+function calculate_rounded_principal_payment(unroundedPrincipalOutstanding, unroundedPrincipalPayment) -> (principal):
+    # The diff captures by how much we deviated from the true principal value
+    # If the diff is negative, we need to slow down repayment as we overpaid principalOutstanding
+    # If the diff is positive, we need to speed up the repayment as we underpaid the principalOutstanding
+    let diff = (loan.principalOutstanding - unroundedPrincipalOutstanding).round(asset_scale, DOWN)
+    let roundedPrincipalPayment = (unroundedPrincipalPayment + diff).round(asset_scale, DOWN)
+    
+    # Ensure we do not have a negative principal payment
+    roundedPrincipalPayment = max(0, roundedPrincipalPayment)
+    
+    return min(roundedPrincipalPayment, loan.principalOutstanding)
+
+function calculate_rounded_interest_breakdown(roundedPrincipalPayment, unroundedInterestOutstanding, unroundedManagementFeeOutstanding, amount) -> (interest, fee):
+    if loan.interestRate == 0:
+        return (0, 0)
+    
+    let loanInterestOutstanding = loan.totalValueOutstanding - loan.principalOutstanding - loan.managementFeeOutstanding
+    
+    # If diffInterest is negative, we are overpaying the interest portion of the loan, we need to slow down
+    # If the diffInterest is positive, we are underpaying the interest portion, we need to speed up
+    let diffInterest = (loanInterestOutstanding - unroundedInterestOutstanding).round(asset_scale, DOWN)
+    let roundedInterestPayment = amount - roundedPrincipalPayment
+    
+    roundedInterestPayment = roundedInterestPayment + diffInterest
+    
+    # Ensure that we do not overpay the periodic payment amount
+    roundedInterestPayment = min(amount - roundedPrincipalPayment, roundedInterestPayment)
+    
+    # Since in the previous step we perform a subtraction, we need to ensure that we don't end up with a negative interest payment
+    roundedInterestPayment = max(0, roundedInterestPayment)
+    
+    # We have calculated the interest payment, we can now calculate the management fee portion
+    
+    # If diffManagementFee is negative, we are overpaying the fee portion, slow down
+    # If the diffManagementFee is positive, we are underpaying the fee portion, speed up
+    let diffManagementFee = (loan.managementFeeOutstanding - unroundedManagementFeeOutstanding).round(asset_scale, DOWN)
+    let roundedManagementFee = (roundedInterestPayment * loan.loanbroker.managementFeeRate).round(asset_scale, DOWN)
+    
+    roundedManagementFee = roundedManagementFee + diffManagementFee
+    
+    # Since the diffManagementFee can be negative, managementFee may end up negative, ensure that does not happen
+    roundedManagementFee = max(0, roundedManagementFee)
+    
+    # Finally, ensure that the management fee does not exceed the outstanding management fee
+    roundedManagementFee = min(roundedManagementFee, loan.managementFeeOutstanding)
+    
+    # Subtract the fee from interest, ensuring the interest portion is not negative
+    roundedInterestPayment = max(0, roundedInterestPayment - roundedManagementFee)
+    
+    # Finally, ensure the interest payment does not exceed the outstanding interest
+    roundedInterestPayment = min(loanInterestOutstanding, roundedInterestPayment)
+    
+    let excess = (amount - roundedPrincipalPayment - roundedInterestPayment - roundedManagementFee)
+    
+    # If we exceed the payment amount, take as much excess as possible from the interest
+    if excess < 0:
+        let part = min(roundedInterestPayment, abs(excess))
+        roundedInterestPayment = roundedInterestPayment - part
+        excess = excess + part
+    
+    # If there is any left, take as much as possible from the fee
+    if excess < 0:
+        let part = min(roundedManagementFee, abs(excess))
+        roundedManagementFee = roundedManagementFee - part
+        excess = excess + part
+    
+    return (roundedInterestPayment, roundedManagementFee)
+
+function compute_payment_due(amount) -> (principal, interest, managementFee):
+    # If this is the final payment, simply settle any outstanding amounts
+    if loan.paymentsRemaining == 1:
+        let outstandingInterest = loan.totalValueOutstanding - loan.principalOutstanding - loan.managementFeeOutstanding
+        return (loan.principalOutstanding, outstandingInterest, loan.managementFeeOutstanding)
+    
+    # Determine the true, unrounded state of the loan
+    let (unroundedPrincipalOutstanding, unroundedInterestOutstanding, unroundedManagementFeeOutstanding) = calculate_true_loan_state()
+    
+    # We do not need to know the interest portion
+    let (unroundedPrincipalPayment, _) = calculate_payment_breakdown(unroundedPrincipalOutstanding)
+    
+    # Given the true state we can calculate the rounded principal that accounts for deviation from the true state
+    let roundedPrincipalPayment = calculate_rounded_principal_payment(unroundedPrincipalOutstanding, unroundedPrincipalPayment)
+    
+    let (roundedInterestPayment, roundedManagementFee) = calculate_rounded_interest_breakdown(
+        roundedPrincipalPayment,
+        unroundedInterestOutstanding,
+        unroundedManagementFeeOutstanding,
+        amount
+    )
+    
+    return (roundedPrincipalPayment, roundedInterestPayment, roundedManagementFee)
+
+function do_overpayment(amount) -> (valueChange):
+    # Calculate true principal and interest outstanding
+    let (truePrincipalOutstanding, trueInterestOutstanding, trueManagementFeeOutstanding) = calculate_true_loan_state()
+    
+    # For an accurate overpayment we need to preserve rounding errors
+    # diffTotal incorporates rounding errors from principal and interest fee, note there is no interest rounding error as this value is derived
+    let diffTotal = loan.totalValueOutstanding - (truePrincipalOutstanding + trueInterestOutstanding + trueManagementFeeOutstanding)
+    let diffPrincipal = loan.principalOutstanding - truePrincipalOutstanding
+    let diffManagementFee = loan.managementFeeOutstanding - trueManagementFeeOutstanding
+    
+    let newPrincipalOutstanding = truePrincipalOutstanding - amount
+    let newPeriodicPayment = compute_periodic_payment(newPrincipalOutstanding)
+    
+    # From the given periodic payment, calculate the new total value outstanding
+    let newTotalValueOutstanding = (newPeriodicPayment * loan.paymentsRemaining).round(asset_scale, HALF_EVEN)
+    
+    # From the new total value, calculate the new interest outstanding and management fee outstanding
+    let newInterestOutstanding = newTotalValueOutstanding - newPrincipalOutstanding
+    let newManagementFeeOutstanding = (newInterestOutstanding * loan.loanbroker.managementFeeRate).round(asset_scale, HALF_EVEN)
+    
+    newInterestOutstanding = newInterestOutstanding - newManagementFeeOutstanding
+    
+    let roundedValueChange = newTotalValueOutstanding + diffTotal - (loan.totalValueOutstanding - amount)
+    
+    # Update loan state
+    loan.totalValueOutstanding = newTotalValueOutstanding + diffTotal
+    loan.principalOutstanding = (newPrincipalOutstanding + diffPrincipal).round(asset_scale, DOWN)
+    loan.managementFeeOutstanding = (newManagementFeeOutstanding + diffManagementFee).round(asset_scale, DOWN)
+    
+    return roundedValueChange
+
+function make_payment(amount, currentTime) -> (principalPaid, interestPaid, valueChange, feePaid):
+    if loan.paymentsRemaining == 0 || loan.principalOutstanding == 0:
         return "loan complete" error
-    }
-
-    // the payment is late
-    if loan.nextPaymentDueDate < currentTime {
-
-        let periodicPayment = compute_periodic_payment()
-        let lateInterest = compute_late_payment_interest(currentTime)
-
-        // insufficient funds
-        if amount < (periodicPayment + loan.serviceFee + lateInterest + loan.latePaymentFee) {
+    
+    # The payment is late
+    if loan.nextPaymentDueDate < currentTime:
+        let (principal, interest, managementFee) = compute_payment_due(amount)
+        let (lateInterest, lateManagementFee) = compute_late_payment_interest(currentTime)
+        
+        let totalManagementFee = managementFee + lateManagementFee
+        let totalDue = principal + interest + lateInterest + totalManagementFee + loan.serviceFee + loan.latePaymentFee
+        
+        # Insufficient funds
+        if amount < totalDue:
             return "insufficient amount paid" error
-        }
-
-        loan.paymentRemaining -= 1
+        
+        loan.paymentsRemaining = loan.paymentsRemaining - 1
         loan.previousPaymentDate = loan.nextPaymentDueDate
         loan.nextPaymentDueDate = loan.nextPaymentDueDate + loan.paymentInterval
-
-        loan.principalOutstanding -= periodicPayment.principal
-
+        loan.principalOutstanding = loan.principalOutstanding - principal
+        loan.managementFeeOutstanding = loan.managementFeeOutstanding - managementFee
+        
         return (
-          periodicPayment.principal,                // a late payment does not affect the principal portion due
-          periodicPayment.interest + lateInterest,  // a late payment incorporates both periodic interest and the late interest
-          lateInterest,                             // the value of the loan increases by the lateInterest amount
-          loan.serviceFee + loan.latePaymentFee     // the fee paid for a loan payment includes both: the regular service fee and the late payment fee
-          )
-    }
-
-    let fullPayment = compute_full_payment(currentTime)
-
-    // if the payment is equal or higher than full payment amount
-    // and there is more than one payment remaining, make a full payment
-    if amount >= fullPayment && loan.paymentRemaining > 1 {
-        let totalInterestOutstanding = loan.totalValueOutstanding - loan.principalOutstanding
-        let loanValueChange = fullPayment.interest - totalInterestOutstanding
-
-        loan.paymentRemaining = 0
+            principal,                                                    # A late payment does not affect the principal portion due
+            interest + lateInterest,                                      # A late payment incorporates both periodic interest and the late interest
+            lateInterest,                                                 # The value of the loan increases by the lateInterest amount
+            totalManagementFee + loan.serviceFee + loan.latePaymentFee   # The total fee paid for a loan payment
+        )
+    
+    let (fullPrincipal, fullInterest, fullManagementFee) = compute_full_payment(currentTime)
+    let fullPaymentAmount = fullPrincipal + fullInterest + fullManagementFee + loan.closePaymentFee
+    
+    # If the payment is equal or higher than full payment amount and there is more than one payment remaining, make a full payment
+    if amount >= fullPaymentAmount && loan.paymentsRemaining > 1:
+        let totalInterestOutstanding = loan.totalValueOutstanding - loan.principalOutstanding - loan.managementFeeOutstanding
+        let loanValueChange = fullInterest - totalInterestOutstanding
+        
+        loan.paymentsRemaining = 0
         loan.principalOutstanding = 0
+        loan.managementFeeOutstanding = 0
+        loan.totalValueOutstanding = 0
+        
         return (
-          fullPayment.principal, // full payment repays the entire outstnading principal, i.e. equivalent to loan.principalOutstanding
-          fullPayment.interest,  // full payment repays any accrued interest since the last payment and additional full payment interest
-          loanValueChange,       // a full payment changes the total value of the loan
-          loan.closePaymentFee   // an early payment pays a specific closePaymentFee
+            fullPrincipal,                      # Full payment repays the entire outstanding principal
+            fullInterest,                       # Full payment repays any accrued interest since the last payment and additional full payment interest
+            loanValueChange,                    # A full payment changes the total value of the loan
+            fullManagementFee + loan.closePaymentFee   # An early payment pays a specific closePaymentFee
         )
-    }
-
-    // Compute scheduled periodic payment. Override if this is the final installment to eliminate rounding dust.
-    let periodicPayment = compute_periodic_payment()
-    if loan.paymentRemaining == 1 {
-        // Final scheduled payment: pay exactly all remaining value (principal + interest) before rounding.
-        periodicPayment.principal = loan.principalOutstanding
-        periodicPayment.interest  = loan.totalValueOutstanding - loan.principalOutstanding
-    }
-
-    let periodicPaymentTotal = periodicPayment.principal + periodicPayment.interest
-
-    // Determine how many full periodic installments this single Amount can cover (cannot exceed remaining)
-    let fullPeriodicPayments = floor(amount / periodicPaymentTotal)
-    if fullPeriodicPayments < 1 {
-        return "insufficient amount paid" error
-    }
-
-    if fullPeriodicPayments > loan.paymentRemaining {
-        fullPeriodicPayments = loan.paymentRemaining
-    }
-
-    let totalPrincipalPaid = 0
-    let totalInterestPaid = 0
+    
+    # Handle regular payments and overpayments
+    let totalPaid = 0
+    let (totalPrincipalPaid, totalInterestPaid, totalFeePaid) = (0, 0, 0)
+    
+    # Process regular periodic payments
+    while totalPaid < amount && loan.paymentsRemaining > 0:
+        let (principal, interest, managementFee) = compute_payment_due(loan.periodicPayment.round(asset_scale, UP))
+        let paymentAmount = principal + interest + managementFee + loan.serviceFee
+        
+        # Check if we have enough funds for this payment
+        if totalPaid + paymentAmount > amount:
+            break
+        
+        # Apply the payment
+        loan.totalValueOutstanding = loan.totalValueOutstanding - (principal + interest + managementFee)
+        loan.principalOutstanding = loan.principalOutstanding - principal
+        loan.managementFeeOutstanding = loan.managementFeeOutstanding - managementFee
+        loan.paymentsRemaining = loan.paymentsRemaining - 1
+        
+        loan.nextPaymentDueDate = loan.nextPaymentDueDate + loan.paymentInterval
+        loan.previousPaymentDate = loan.nextPaymentDueDate - loan.paymentInterval
+        
+        totalPaid = totalPaid + paymentAmount
+        totalPrincipalPaid = totalPrincipalPaid + principal
+        totalInterestPaid = totalInterestPaid + interest
+        totalFeePaid = totalFeePaid + managementFee + loan.serviceFee
+    
     let loanValueChange = 0
-
-    while fullPeriodicPayments > 0 {
-        let periodicPayment = compute_periodic_payment()
-
-        // If after recomputation only one payment remains, force final payoff to avoid residual dust.
-        if loan.paymentRemaining == 1 {
-          periodicPayment.principal = loan.principalOutstanding
-          periodicPayment.interest  = loan.totalValueOutstanding - loan.principalOutstanding
-        }
-
-        totalPrincipalPaid += periodicPayment.principal
-        totalInterestPaid  += periodicPayment.interest
-        loan.paymentRemaining -= 1
-        loan.principalOutstanding -= periodicPayment.principal
-    }
-
-    let totalFeePaid = loan.serviceFee * fullPeriodicPayments
-    loan.nextPaymentDueDate = loan.nextPaymentDueDate + loan.paymentInterval * fullPeriodicPayments
-    loan.previousPaymentDate = loan.nextPaymentDueDate - loan.paymentInterval
-
-    let newTotalValue = periodicPayment x loan.paymentRemaining
-    let overpayment = min(loan.principalOutstanding, amount - (totalPrincipalPaid + totalInterestPaid + totalFeePaid));
-    let overpaymentInterestPortion = 0
-
-    if overpayment > 0 && is_set(lsfLoanOverpayment) && is_set(tfLoanOverpayment) {
-        overpaymentInterestPortion = overpayment * loan.overpaymentInterestRate
-        let feePortion = overpayment * loan.overpaymentFee
-        let remainder = overpayment - overpaymentInterestPortion - feePortion
-
-        if remainder > 0 {
-          totalPrincipalPaid += remainder
-          totalInterestPaid  += overpaymentInterestPortion
-          totalFeePaid       += feePortion
-          loan.principalOutstanding -= remainder
-        }
-    }
-
-    // we are paying off principal early, we have to recompute the periodic payment
-    let totalValueAfterOverpayment = compute_periodic_payment() x loan.paymentRemaining
-
-    // loan overpayment decreases future interest, and thus decreases the overall value of the loan
-    // no assumptions should be made about the sign of loanValueChange
-    loanValueChange = (totalVaultAfterOverpayment - newTotalValue) + overpaymentInterestPortion
-
-    // If final installment just executed, ensure outstanding principal hits zero (guard against residual 1-unit dust)
-    if loan.paymentRemaining == 0 {
-        loan.principalOutstanding = 0
-        loan.totalValueOutstanding = 0
-    }
-
+    # Handle overpayment if there are remaining payments, the loan supports overpayments, and there are funds remaining
+    if loan.paymentsRemaining > 0 && is_set(loan.lsfLoanOverpayment) && is_set(tfLoanOverpayment) && totalPaid < amount:
+        let overpaymentAmount = min(loan.principalOutstanding, amount - totalPaid)
+        
+        let overpaymentInterest = overpaymentAmount * loan.overpaymentInterestRate
+        let overpaymentManagementFee = overpaymentInterest * loan.loanbroker.managementFeeRate
+        let overpaymentFee = overpaymentAmount * loan.overpaymentFee
+        
+        overpaymentInterest = overpaymentInterest - overpaymentManagementFee
+        loanValueChange = loanValueChange + overpaymentInterest
+        
+        let overpaymentPrincipal = overpaymentAmount - overpaymentInterest - overpaymentManagementFee - overpaymentFee
+        
+        if overpaymentPrincipal > 0:
+            let valueChange = do_overpayment(overpaymentPrincipal)
+            loanValueChange = loanValueChange + valueChange
+            
+            totalPaid = totalPaid + overpaymentAmount
+            totalPrincipalPaid = totalPrincipalPaid + overpaymentPrincipal
+            totalInterestPaid = totalInterestPaid + overpaymentInterest
+            totalFeePaid = totalFeePaid + overpaymentManagementFee + overpaymentFee
+    
     return (
-      totalPrincipalPaid, // this will include the periodicPayment principal and any overpayment
-      totalInterestPaid,  // this will include the periodicPayment interest and any overpayment
-      loanValueChange,    // valueChange in loal total value by overpayment
-      totalFeePaid        // the total fee
+        totalPrincipalPaid,     # This will include the periodicPayment principal and any overpayment
+        totalInterestPaid,      # This will include the periodicPayment interest and any overpayment
+        loanValueChange,        # Value change in loan total value by overpayment
+        totalFeePaid            # The total fee
     )
 ```
 

From 5dda351c7c803ab8c729c6811a35076fe41c6001 Mon Sep 17 00:00:00 2001
From: Vito <5780819+Tapanito@users.noreply.github.com>
Date: Wed, 22 Oct 2025 16:29:55 +0200
Subject: [PATCH 64/77] adds balance checks to loan pay

---
 XLS-0066d-lending-protocol/README.md | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md
index 39e612de..79bfae80 100644
--- a/XLS-0066d-lending-protocol/README.md
+++ b/XLS-0066d-lending-protocol/README.md
@@ -1823,16 +1823,23 @@ Furthermore, assume `full_periodic_payments` variable represents the number of p
 
 - The Borrower paid insufficient amount: `full_periodic_payments < 0`.
 
+- If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is `XRP`:
+
+  - The `Balance` of the `AccountRoot` object of the Borrower is less than `totalDue`.
+
 - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `IOU`:
 
   - The `RippleState` object between the submitter account and the `Issuer` of the asset has the `lsfLowFreeze` or `lsfHighFreeze` flag set.
   - The `RippleState` between the `LoanBroker.Account` and the `Issuer` has the `lsfLowDeepFreeze` or `lsfHighDeepFreeze` flag set. (The Loan Broker _pseudo-account_ is frozen).
   - The `RippleState` between the `Vault(LoanBroker(Loan.LoanBrokerID).VaultID).Account` and the `Issuer` has the `lsfLowFreeze` or `lsfHighFreeze` flag set. (The Vault _pseudo-account_ is frozen).
   - The `AccountRoot` object of the `Issuer` has the `lsfGlobalFreeze` flag set.
+    - The `RippleState` object `Balance` < `totalDue` (Borrower has insufficient funds).
 
 - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `MPT`:
 
-  - The `MPToken` object for the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` of the submitter `AccountRoot` has `lsfMPTLocked` flag set.
+  - The `MPToken` object for the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` of the submitter `AccountRoot` has
+    - `lsfMPTLocked` flag set.
+    - `MPTAmount` < `totalDue` (inssuficient funds).
   - The `MPToken` object for the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` of the `LoanBroker.Account` `AccountRoot` has `lsfMPTLocked` flag set. (The Loan Broker _pseudo-account_ is locked).
   - The `MPToken` object for the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` of the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Account` `AccountRoot` has `lsfMPTLocked` flag set. (The Vault _pseudo-account_ is locked).
   - The `MPTokenIssuance` object of the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` has the `lsfMPTLocked` flag set.

From 1933a4a04f8f08f56c152553ad337f71d5475f6f Mon Sep 17 00:00:00 2001
From: Vito <5780819+Tapanito@users.noreply.github.com>
Date: Thu, 23 Oct 2025 10:43:55 +0200
Subject: [PATCH 65/77] adds loan scale field

---
 XLS-0066d-lending-protocol/README.md | 64 ++++++++++++++--------------
 1 file changed, 32 insertions(+), 32 deletions(-)

diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md
index 79bfae80..775ef1dd 100644
--- a/XLS-0066d-lending-protocol/README.md
+++ b/XLS-0066d-lending-protocol/README.md
@@ -440,38 +440,38 @@ The `LoanID` is calculated as follows:
 
 #### 2.2.2 Fields
 
-| Field Name                 | User Modifiable? | Constant? |     Required?      | JSON Type | Internal Type |                                   Default Value                                   | Description                                                                                                                                                    |
-| -------------------------- | :--------------: | :-------: | :----------------: | :-------: | :-----------: | :-------------------------------------------------------------------------------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `LedgerEntryType`          |       `No`       |   `Yes`   | :heavy_check_mark: | `string`  |   `UINT16`    |                                     `0x0089`                                      | Ledger object type.                                                                                                                                            |
-| `LedgerIndex`              |       `No`       |   `Yes`   | :heavy_check_mark: | `string`  |   `UINT16`    |                                       `N/A`                                       | Ledger object identifier.                                                                                                                                      |
-| `Flags`                    |      `Yes`       |   `No`    |                    | `string`  |   `UINT32`    |                                         0                                         | Ledger object flags.                                                                                                                                           |
-| `PreviousTxnID`            |       `No`       |   `No`    | :heavy_check_mark: | `string`  |   `HASH256`   |                                       `N/A`                                       | The ID of the transaction that last modified this object.                                                                                                      |
-| `PreviousTxnLgrSeq`        |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `UINT32`    |                                       `N/A`                                       | The ledger sequence containing the transaction that last modified this object.                                                                                 |
-| `LoanSequence`             |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                                       `N/A`                                       | The sequence number of the Loan.                                                                                                                               |
-| `OwnerNode`                |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT64`    |                                       `N/A`                                       | Identifies the page where this item is referenced in the `Borrower` owner's directory.                                                                         |
-| `LoanBrokerNode`           |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT64`    |                                       `N/A`                                       | Identifies the page where this item is referenced in the `LoanBroker`s owner directory.                                                                        |
-| `LoanBrokerID`             |       `No`       |   `Yes`   | :heavy_check_mark: | `string`  |   `HASH256`   |                                       `N/A`                                       | The ID of the `LoanBroker` associated with this Loan Instance.                                                                                                 |
-| `Borrower`                 |       `No`       |   `Yes`   | :heavy_check_mark: | `string`  |  `AccountID`  |                                       `N/A`                                       | The address of the account that is the borrower.                                                                                                               |
-| `LoanOriginationFee`       |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `NUMBER`    |                                       `N/A`                                       | A nominal nds amount paid to the `LoanBroker.Owner` when the Loan is created.                                                                                  |
-| `LoanServiceFee`           |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `NUMBER`    |                                       `N/A`                                       | A nominal funds amount paid to the `LoanBroker.Owner` with every Loan payment.                                                                                 |
-| `LatePaymentFee`           |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `NUMBER`    |                                       `N/A`                                       | A nominal funds amount paid to the `LoanBroker.Owner` when a payment is late.                                                                                  |
-| `ClosePaymentFee`          |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `NUMBER`    |                                       `N/A`                                       | A nominal funds amount paid to the `LoanBroker.Owner` when a payment full payment is made.                                                                     |
-| `OverpaymentFee`           |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                                       `N/A`                                       | A fee charged on overpayments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%)                                              |
-| `InterestRate`             |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                                       `N/A`                                       | Annualized interest rate of the Loan in 1/10th basis points.                                                                                                   |
-| `LateInterestRate`         |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                                       `N/A`                                       | A premium is added to the interest rate for late payments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%)                  |
-| `CloseInterestRate`        |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                                       `N/A`                                       | An interest rate charged for repaying the Loan early in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%)                       |
-| `OverpaymentInterestRate`  |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                                       `N/A`                                       | An interest rate charged on overpayments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%)                                   |
-| `StartDate`                |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                             `CurrentLedgerTimestamp`                              | The timestamp of when the Loan started [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time).                 |
-| `PaymentInterval`          |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                                       `N/A`                                       | Number of seconds between Loan payments.                                                                                                                       |
-| `GracePeriod`              |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                                       `N/A`                                       | The number of seconds after the Loan's Payment Due Date that the Loan can be Defaulted.                                                                        |
-| `PreviousPaymentDate`      |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `UINT32`    |                                        `0`                                        | The timestamp of when the previous payment was made in [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time). |
-| `NextPaymentDueDate`       |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `UINT32`    |                   `LoanSet.StartDate + LoanSet.PaymentInterval`                   | The timestamp of when the next payment is due in [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time).       |
-| `PaymentRemaining`         |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `UINT32`    |                              `LoanSet.PaymentTotal`                               | The number of payments remaining on the Loan.                                                                                                                  |
-| `TotalValueOutstanding`    |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `NUMBER`    |                             `TotalValueOutstanding()`                             | The total outstanding value of the Loan, including all fees and interest.                                                                                      |
-| `PrincipalOutstanding`     |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `NUMBER`    |                           `LoanSet.PrincipalRequested`                            | The principal amount that the Borrower still owes.                                                                                                             |
-| `ManagementFeeOutstanding` |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `NUMBER`    | `(TotalValueOutstanding() - PrincipalOutstanding) x LoanBroker.ManagementFeeRate` | The remaining Management Fee owed to the LoanBroker.                                                                                                           |
-| `PeriodicPayment`          |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `NUMBER`    |                              `LoanPeriodicPayment()`                              | The calculated periodic payment amount for each payment interval.                                                                                              |
-| `PrincipalRequested`       |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `NUMBER`    |                           `LoanSet.PrincipalRequested`                            | The amount of principal requested for the Loan.                                                                                                                |
+| Field Name                 | User Modifiable? | Constant? |     Required?      | JSON Type | Internal Type |                                   Default Value                                   | Description                                                                                                                                                           |
+| -------------------------- | :--------------: | :-------: | :----------------: | :-------: | :-----------: | :-------------------------------------------------------------------------------: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `LedgerEntryType`          |       `No`       |   `Yes`   | :heavy_check_mark: | `string`  |   `UINT16`    |                                     `0x0089`                                      | Ledger object type.                                                                                                                                                   |
+| `LedgerIndex`              |       `No`       |   `Yes`   | :heavy_check_mark: | `string`  |   `UINT16`    |                                       `N/A`                                       | Ledger object identifier.                                                                                                                                             |
+| `Flags`                    |      `Yes`       |   `No`    |                    | `string`  |   `UINT32`    |                                         0                                         | Ledger object flags.                                                                                                                                                  |
+| `PreviousTxnID`            |       `No`       |   `No`    | :heavy_check_mark: | `string`  |   `HASH256`   |                                       `N/A`                                       | The ID of the transaction that last modified this object.                                                                                                             |
+| `PreviousTxnLgrSeq`        |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `UINT32`    |                                       `N/A`                                       | The ledger sequence containing the transaction that last modified this object.                                                                                        |
+| `LoanSequence`             |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                                       `N/A`                                       | The sequence number of the Loan.                                                                                                                                      |
+| `OwnerNode`                |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT64`    |                                       `N/A`                                       | Identifies the page where this item is referenced in the `Borrower` owner's directory.                                                                                |
+| `LoanBrokerNode`           |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT64`    |                                       `N/A`                                       | Identifies the page where this item is referenced in the `LoanBroker`s owner directory.                                                                               |
+| `LoanBrokerID`             |       `No`       |   `Yes`   | :heavy_check_mark: | `string`  |   `HASH256`   |                                       `N/A`                                       | The ID of the `LoanBroker` associated with this Loan Instance.                                                                                                        |
+| `Borrower`                 |       `No`       |   `Yes`   | :heavy_check_mark: | `string`  |  `AccountID`  |                                       `N/A`                                       | The address of the account that is the borrower.                                                                                                                      |
+| `LoanOriginationFee`       |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `NUMBER`    |                                       `N/A`                                       | A nominal nds amount paid to the `LoanBroker.Owner` when the Loan is created.                                                                                         |
+| `LoanServiceFee`           |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `NUMBER`    |                                       `N/A`                                       | A nominal funds amount paid to the `LoanBroker.Owner` with every Loan payment.                                                                                        |
+| `LatePaymentFee`           |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `NUMBER`    |                                       `N/A`                                       | A nominal funds amount paid to the `LoanBroker.Owner` when a payment is late.                                                                                         |
+| `ClosePaymentFee`          |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `NUMBER`    |                                       `N/A`                                       | A nominal funds amount paid to the `LoanBroker.Owner` when a payment full payment is made.                                                                            |
+| `OverpaymentFee`           |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                                       `N/A`                                       | A fee charged on overpayments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%)                                                     |
+| `InterestRate`             |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                                       `N/A`                                       | Annualized interest rate of the Loan in 1/10th basis points.                                                                                                          |
+| `LateInterestRate`         |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                                       `N/A`                                       | A premium is added to the interest rate for late payments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%)                         |
+| `CloseInterestRate`        |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                                       `N/A`                                       | An interest rate charged for repaying the Loan early in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%)                              |
+| `OverpaymentInterestRate`  |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                                       `N/A`                                       | An interest rate charged on overpayments in 1/10th basis points. Valid values are between 0 and 100000 inclusive. (0 - 100%)                                          |
+| `StartDate`                |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                             `CurrentLedgerTimestamp`                              | The timestamp of when the Loan started [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time).                        |
+| `PaymentInterval`          |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                                       `N/A`                                       | Number of seconds between Loan payments.                                                                                                                              |
+| `GracePeriod`              |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                                       `N/A`                                       | The number of seconds after the Loan's Payment Due Date that the Loan can be Defaulted.                                                                               |
+| `PreviousPaymentDate`      |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `UINT32`    |                                        `0`                                        | The timestamp of when the previous payment was made in [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time).        |
+| `NextPaymentDueDate`       |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `UINT32`    |                   `LoanSet.StartDate + LoanSet.PaymentInterval`                   | The timestamp of when the next payment is due in [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time).              |
+| `PaymentRemaining`         |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `UINT32`    |                              `LoanSet.PaymentTotal`                               | The number of payments remaining on the Loan.                                                                                                                         |
+| `TotalValueOutstanding`    |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `NUMBER`    |                             `TotalValueOutstanding()`                             | The total outstanding value of the Loan, including all fees and interest.                                                                                             |
+| `PrincipalOutstanding`     |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `NUMBER`    |                           `LoanSet.PrincipalRequested`                            | The principal amount that the Borrower still owes.                                                                                                                    |
+| `ManagementFeeOutstanding` |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `NUMBER`    | `(TotalValueOutstanding() - PrincipalOutstanding) x LoanBroker.ManagementFeeRate` | The remaining Management Fee owed to the LoanBroker.                                                                                                                  |
+| `PeriodicPayment`          |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `NUMBER`    |                              `LoanPeriodicPayment()`                              | The calculated periodic payment amount for each payment interval.                                                                                                     |
+| `LoanScale`                |       `No`       |   `Yes`   |                    | `number`  |    `INT32`    |                                `LoanTotalValue()`                                 | The scale factor that ensures all computed amounts are rounded to the same number of decimal places. It is determined based on the total loan value at creation time. |
 
 ##### 2.2.2.1 Flags
 

From b855be24fcf7cf6e9fce5c0ec2b179ad30f7203f Mon Sep 17 00:00:00 2001
From: Vito <5780819+Tapanito@users.noreply.github.com>
Date: Thu, 23 Oct 2025 15:23:56 +0200
Subject: [PATCH 66/77] completely rewrites they loanPay transaction

---
 XLS-0066d-lending-protocol/README.md | 707 +++++++++++++--------------
 1 file changed, 341 insertions(+), 366 deletions(-)

diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md
index 775ef1dd..e670bf17 100644
--- a/XLS-0066d-lending-protocol/README.md
+++ b/XLS-0066d-lending-protocol/README.md
@@ -1223,58 +1223,85 @@ The Borrower submits a `LoanPay` transaction to make a Payment on the Loan.
 | Flag Name           |  Flag Value  | Description                                                                  |
 | ------------------- | :----------: | :--------------------------------------------------------------------------- |
 | `tfLoanOverpayment` | `0x00010000` | Indicates that remaining payment amount should be treated as an overpayment. |
+| `tfLoanFullPayment` | `0x00020000` | Indicates that the borrower is making a full early repayment.                |
 
-##### 3.2.4.2 Payment Types
+##### 3.2.4.2 Payment Processing
 
-A Loan payment has four types:
+A `LoanPay` transaction is processed according to a defined workflow that evaluates the payment's timing, amount, and any specified flags. This determines how the funds are applied to the loan's principal, interest, and associated fees.
 
-- Regular payment is made on time, where the payment size and schedule are calculated with a standard amortization [formula](https://en.wikipedia.org/wiki/Amortization_calculator).
+**Source of Truth**: The formulas in this section describe the financial theory for a conceptual understanding. The [pseudo-code](#3244-transaction-pseudo-code) describes the required implementation logic, which includes critical adjustments for rounding. **Implementations must follow the pseudo-code.**
 
-- A _late_ payment, when a Borrower makes a payment after `netxPaymentDueDate`. Late payments include a `LatePaymentFee` and `LateInterestRate`.
 
-- An early _full_ payment is when a Borrower pays the outstanding principal. A `CloseInterestRate` is charged on the outstanding principal.
+**Payment Rounding**: The `Loan.PeriodicPayment` field stores a high-precision value. However, payments must be made in the discrete, indivisible units of the loan's asset (e.g., XRP drops, whole MPTs, or the smallest unit of an IOU). Therefore, the borrower is expected to make a periodic payment that is rounded **up** to the asset's scale.
 
-- An overpayment occurs when a borrower makes a payment that exceeds the required minimum payment amount.
+For example:
+- If a loan is denominated in an asset that only supports whole numbers (like an MPT) and the calculated `Loan.PeriodicPayment` is `10.12345`, the borrower is expected to pay `11`.
+- If a loan is denominated in a USD IOU with two decimal places of precision and the `Loan.PeriodicPayment` is `25.54321`, the borrower is expected to pay `25.55`.
 
-The payment amount and timing determine the type of payment. A payment made before the `Loan.NextPaymentDueDate` is a regular payment and follows the standard amortization calculation. Any payment made after this date is considered a late payment.
+This rounded-up value, plus any applicable service fees, constitutes the minimum payment for a single period.
 
-The following diagram depicts how a payment is handled based on the amount paid.
 
-```
-   Rejected     Overpayment     Overpayment     Overpayment    Not charged
-|------------|---------------|---------------|---------------|-------------|
-      Periodic/Late      Periodic        Periodic          Full
-     Payment Amount  Payment Amount  Payment Amount   Payment Amount
-            I              II             N - 1
+Each payment consists of three components:
 
-                          Payment Amount
-```
+- **Principal**: The portion that reduces the outstanding loan principle.
+- **Interest**: The portion that covers the cost of borrowing for the period.
+- **Fees**: The portion that covers any applicable `serviceFee`, `managementFee`, `latePaymentFee`, or other charges.
+
+The system follows these steps to process a payment:
+
+1.  **Timing Verification**: The transaction is first classified as either **On-time** or **Late** by comparing the ledger's close time to the `Loan.NextPaymentDueDate`.
+
+2.  **Minimum Amount Validation**: The payment is checked against the minimum amount required for its timing classification. If the amount is insufficient, the transaction is rejected.
+
+    - **Late Minimum**: `periodicPayment + serviceFee + latePaymentFee + lateInterest`
+    - **On-time Minimum**: `periodicPayment + serviceFee`
+
+3.  **Scenario Handling**: Based on the timing and transaction flags, the system proceeds with one of the following paths:
+
+    - **A) Late Payment Processing**: If the payment is late, it must be for the exact amount calculated by the [late payment formula](#32422-late-payment).
+
+      - **Constraint**: Overpayments are not permitted on late payments. Any amount paid beyond the exact total due will be ignored.
+
+    - **B) On-Time Payment Processing**: If the payment is on-time, the system checks for special repayment scenarios before handling standard periodic payments.
+      - **i. Full Early Repayment**: If the `tfLoanFullPayment` flag is set and the amount is sufficient to cover the [full payment formula](#32424-early-full-repayment), the loan is closed.
+        - **Constraint**: This option is not available if only one payment remains on the loan.
+      - **ii. Sequential Periodic Payments**: If it is not a full repayment, the system applies the funds to as many complete periodic payment cycles as possible. A single cycle consists of the `periodicPayment` plus the `serviceFee`.
+      - **iii. Overpayment Application**: After all possible full periodic cycles are paid, any remaining amount is treated as an overpayment and applied to the principal.
+        - **Constraint**: This step only occurs for on-time payments and requires two flags to be set: `lsfLoanOverpayment` on the `Loan` object and `tfLoanOverpayment` on the `LoanPay` transaction. If these conditions are not met, the excess amount is ignored.
+
+**Note on Excess Funds**: In scenarios where funds are "ignored" (e.g., an overpayment on a late payment, or on a loan that does not permit overpayments), the transaction succeeds, the borrower is only charged the expected due amount, but not the excess.
 
-The minimum payment required is determined by whether the borrower makes the payment before or on the `NextPaymentDueDate` or if it is late. Any payment below the minimum amount required is rejected. With a single `LoanPay` transaction, the Borrower can make multiple loan payments. For example, if the periodic payment amount is 400 Tokens and the Borrower makes a payment of 900 Tokens, the payment will be treated as two periodic payments, moving the NextPaymentDueDate forward to two payment intervals, and the remaining 100 Tokens will be an overpayment.
+**The `valueChange` Concept**
 
-If the Loan Broker and the borrower have agreed to allow overpayments, any amount above the periodic payment is treated as an overpayment. However, if overpayments are not supported, the excess amount will not be charged and will remain with the borrower.
+The `valueChange` is a critical accounting mechanism that represents the change in the _total future interest_ the vault expects to earn from the loan. It is triggered by events that alter the original amortization schedule.
 
-Each payment comprises three parts, `principal`, `interest` and `fee`. The `principal` is an amount paid against the principal of the Loan, `interest` is the interest portion of the Loan, and `fee` is the fee part paid by the Borrower on top of `principal` and `interest`.
+- **Late Payment**: Adds new penalty interest, so `valueChange` is positive.
+- **Overpayment**: Overpayment reduces principal, and thus the future interest, thus `valueChange` is negative.
+- **Early Full Payment**: The `valueChange` for an early repayment can be either positive or negative, depending on the size of the early full payment configuration.
+
+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`).
 
 ###### 3.2.4.2.1 Regular Payment
 
-A periodic payment amount is calculated using the amortization payment formula:
+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.
 
 $$
 totalDue = periodicPayment + loanServiceFee
 $$
 
+The `periodicPayment` is calculated using the standard amortization formula, which ensures a constant payment amount over the life of the loan. This payment covers both the interest accrued for the period and a portion of the principal.
+
 $$
 periodicPayment = principalOutstanding \times \frac{periodicRate \times (1 + periodicRate)^{PaymentRemaining}}{(1 + periodicRate)^{PaymentRemaining} - 1}
 $$
 
-where the periodic interest rate is the interest rate charged per payment period:
+The `periodicRate` is the interest rate applied for each payment interval, derived from the annual rate:
 
 $$
 periodicRate = \frac{interestRate \times paymentInterval}{365 \times 24 \times 60 \times 60}
 $$
 
-The `principal` and `interest` portions can be derived as follows:
+From the calculated `periodicPayment`, the specific `interest` and `principal` portions for that period can be derived as follows:
 
 $$
 interest = principalOutstanding \times periodicRate
@@ -1284,15 +1311,19 @@ $$
 principal = periodicPayment - interest
 $$
 
-When only a single payment remains (PaymentRemaining = 1) the periodic payment is set equal to the current TotalValueOutstanding (i.e. principalOutstanding + interestOutstanding before any asset-specific rounding/truncation). This overrides the standard amortization formula for the last installment. The purpose is to eliminate residual dust created by iterative rounding (e.g. integer truncation for XRP drops or whole‑unit MPTs) that could otherwise make the loan impossible to fully repay. Repeated rounding of each scheduled payment can accumulate relative to the unrounded amortization schedule. Without this adjustment, the final formula-derived payment might be smaller or greater than the remaining outstanding value, leaving a non-zero or negative remainder that can never be cleared by any subsequent scheduled payment.
+**Special Handling for the Final Payment**
+
+When only a single payment remains (`PaymentRemaining = 1`), the standard amortization formula is overridden. Instead, the final `periodicPayment` is set to the exact `TotalValueOutstanding` (which is the sum of the remaining `principalOutstanding`, `interestOutstanding`, and `managementFeeOutstanding`).
+
+This crucial adjustment prevents "residual dust", small leftover amounts caused by the cumulative effect of rounding individual payments over the loan's term (e.g., truncating fractions for XRP drops or MPTs). Without this override, the final formula-calculated payment might not perfectly match the remaining balance, making it impossible to fully clear the debt.
 
 Formally:
 
 ```
 If PaymentRemaining > 1:
-  periodicPayment = formula result (rounded per asset rules)
+  periodicPayment = result from amortization formula (rounded per asset rules)
 If PaymentRemaining = 1:
-  periodicPayment = TotalValueOutstanding (rounded per asset rules; this sets TotalValueOutstanding to 0 after payment)
+  periodicPayment = TotalValueOutstanding (the sum of all remaining balances)
 ```
 
 Example (integer-only MPT):
@@ -1314,186 +1345,160 @@ Therefore, implementations must detect the single remaining payment case and sub
 
 ###### 3.2.4.2.2 Late Payment
 
-When a Borrower makes a payment after `NextPaymentDueDate`, they must pay a nominal late payment fee and an additional interest rate charged on the overdue amount for the unpaid period. The formula is as follows:
+When a Borrower makes a payment after `NextPaymentDueDate`, they must pay the standard `periodicPayment` plus a nominal `latePaymentFee` and additional penalty interest (`latePaymentInterest`) for the overdue period.
+
+The total amount due is calculated as:
 
 $$
-totalDue = periodicPayment + latePaymentFee + latePaymentInterest
+totalDue = periodicPayment + loanServiceFee + latePaymentFee + latePaymentInterest
 $$
 
+The penalty interest is calculated based on the number of seconds the payment is overdue:
+
 $$
 secondsOverdue = lastLedgerCloseTime - Loan.NextPaymentDueDate
 $$
 
-A special, late payment interest rate is applied for the over-due period:
-
 $$
 latePeriodicRate = \frac{lateInterestRate \times secondsOverdue}{365 \times 24 \times 60 \times 60}
 $$
 
 $$
-latePaymentInterest = principalOutstanding \times latePeriodicRate
+latePaymentInterest_{gross} = principalOutstanding \times latePeriodicRate
 $$
 
-A late payment pays more interest than calculated when increasing the Vault value in the `LoanSet` transaction. Therefore, the total Vault value captured by `Vault.AssetsTotal` must be recalculated. The increase in the `Vault.AssetsTotal` value is simply the `latePaymentInterest`.
+A portion of this gross late interest is allocated to the Loan Broker as a management fee. The remaining net interest increases the total value of the loan.
 
 $$
-valueChange = latePaymentInterest
+managementFee_{late} = latePaymentInterest_{gross} \times managementFeeRate
 $$
 
-Note that `valueChange > 0` for late payments, i.e. a late payment increases the value of the loan.
-
-###### 3.2.4.2.3 Loan Overpayment
-
-- Let $\mathcal{P}$ and $\mathcal{p}$ represent the total and outstanding Loan principal.
-- Let $\mathcal{I}$ and $\mathcal{i}$ represent the total and outstanding Loan interest computed from $\mathcal{P}$ and $\mathcal{p}$ respectively.
+The change in the total loan value is equent to the late payment interest, excluding any fees.
 
 $$
-excess = min(\mathcal{p}, paymentAmountMade - minimumPaymentAmount)
+valueChange = latePaymentInterest_{gross} - managementFee_{late}
 $$
 
-$$
-interestPortion = excess \times overpaymentInterestRate
-$$
+This `valueChange` represents the net increase in the loan's value, which must be reflected in `Vault.AssetsTotal`. However, this value change is not reflected in `Loan.TotalValueOutstanding` and `LoanBroker.DebtTotal` fields. It is an unanticipated increase in value. Note that `valueChange > 0` for late payments.
 
-$$
-feePortion = excess \times overpaymentFee
-$$
+###### 3.2.4.2.3 Loan Overpayment
 
-$$
-principalPortion = excess - interestPortion - feePortion
-$$
+An overpayment occurs when an on-time payment exceeds the amount required for one or more periodic payments. The excess amount is used to pay down the principal early, which reduces the total future interest owed. This process involves two key calculations: charging fees on the overpayment itself and re-amortizing the loan.
 
-$$
-\mathcal{p'} = \mathcal{p} - principalPortion
-$$
+**1. Processing the Overpayment Amount**
 
-Let $\mathcal{i}$ denote the outstanding interest computed from $\mathcal{p}$. Simillarly, let $\mathcal{i'}$ denote the outstanding interest computed from $\mathcal{p'}$. We compute the loan interest change as follows:
+First, fees and interest are calculated on the `overpaymentAmount` (the funds remaining after all full periodic payments are settled).
 
 $$
-valueChange =  \mathcal{i} - \mathcal{i'}
+overpaymentInterest_{gross} = overpaymentAmount \times overpaymentInterestRate
 $$
 
-###### 3.2.4.2.4 Early Full Repayment
-
-A Borrower can close a Loan early by submitting the total amount needed to do so. This amount is the sum of the remaining balance, any accrued interest, a prepayment penalty, and a prepayment fee.
+A management fee is taken from this interest:
 
 $$
-totalDue = principalOutstanding + accruedInterest + prepaymentPenalty + ClosePaymentFee
+managementFee_{overpayment} = overpaymentInterest_{gross} \times managementFeeRate
 $$
 
 $$
-secondsSinceLastPayment = lastLedgerCloseTime - max(Loan.previousPaymentDate, Loan.startDate)
-$$
-
-Accrued interest up to the point of early closure is calculated as follows:
-
-$$
-accruedInterest = principalOutstanding  \times periodicRate \times \frac{secondsSinceLastPayment}{paymentInterval}
+overpaymentInterest_{net} = overpaymentInterest_{gross} - managementFee_{overpayment}
 $$
 
-Finally, the Lender may charge a prepayment penalty for paying a loan early, which is calculated as follows:
+A percentage fee may also be charged:
 
 $$
-prepaymentPenalty = principalOutstanding \times closeInterestRate
+overpaymentFee = overpaymentAmount \times overpaymentFee
 $$
 
-An early payment pays less interest than calculated when increasing the Vault value in the `LoanSet` transaction. Therefore, the Vault value (captured by `Vault.AssetsTotal`) must be recalculated after an early payment.
-
-Assume a function `CurrentValue()` that returns `principalOutstanding` and `interestOutstanding` of the Loan. Furthermore, assume a function `ClosePayment()` that implements the Full Payment calculation. The function returns the total full payment due split into `principal` and `interest`.
-
-The value change for an early full repayment is calculated as follows:
+The portion of the overpayment that will be applied directly to the principal is:
 
 $$
-valueChange = (prepaymentPenalty) - (interestOutstanding - accruedInterest)
+principalPortion = overpaymentAmount - overpaymentInterest_{net} - managementFee_{overpayment} - overpaymentFee
 $$
 
-###### 3.2.4.2.5 Management Fee Calculations
+**2. Re-Amortizing the Loan and Calculating `valueChange`**
 
-The `LoanBroker` Management fee is charged against the interest portion of the Loan and subtracted from the total Loan value at Loan creation. However, the fee is charged only during Loan payments. Early and Late payments change the total value of the Loan by decreasing or increasing the value of total interest. Therefore, when an early, late or an overpayment payment is made, the management fee must be updated.
+Applying the `principalPortion` to the loan reduces the `PrincipalOutstanding`, which in turn changes the amortization schedule and the total interest that will be paid over the remainder of the loan's term. This change in total future interest is the primary `valueChange`.
 
-To update the management fee, we need to compute the new total management fee based on the new total interest after executing the early or late payment. Therefore, we need to capture the Loan value before the payment is made and the new value after the payment is made.
+The system performs the following logical steps (as detailed in the `do_overpayment` pseudo-code):
 
-For the calculation, assume the following variables:
+1.  The `principalPortion` is subtracted from the current `PrincipalOutstanding` to get a `newPrincipalOutstanding`.
+2.  A `newPeriodicPayment` is calculated using the standard amortization formula based on the `newPrincipalOutstanding` and the `paymentsRemaining`.
+3.  A `newTotalValueOutstanding` is calculated based on the `newPeriodicPayment` and `paymentsRemaining`.
+4.  The `valueChange` is the difference between the old and new total future interest, adjusted for any existing rounding discrepancies to ensure numerical stability.
 
-- Let $\mathcal{P}$ and $\mathcal{p}$ represent the total and outstanding Loan principal.
-- Let $\mathcal{I}$ and $\mathcal{i}$ represent the total and outstanding Loan interest computed from $\mathcal{P}$ and $\mathcal{p}$ respectively.
-- Let $\mathcal{V}$ and $\mathcal{v}$ represent the total and outstanding value of the Loan. $\mathcal{V} = \mathcal{P} + \mathcal{I}$ and $\mathcal{v} = \mathcal{p} + \mathcal{i}$.
-- Finally, let $\mathcal{m}$ represent the management fee rate of the Loan Broker.
+The total change in the loan's value from an overpayment is the sum of the net interest charged on the overpayment and the value change from re-amortization.
 
-Assume $f(\mathcal{v})$ is a Loan payment, $f(\mathcal{v}) = \mathcal{v'}$, the new outstanding loan value is equal to the application of the payment transaction to the current outstanding value. Furthermore, assume $\mathcal{V} \xrightarrow{f(\mathcal{v})}$ $\mathcal{V'}$, is the change in the Loan total value as the result of applying $f(\mathcal{v})$.
+$$
+valueChange = overpaymentInterest_{net} + valueChange_{re-amortization}
+$$
 
-we say that $\mathcal{V'} = \mathcal{P'} + \mathcal{I'}$. It's important to note that a payment transaction must never change the total principal. I.e. $\mathcal{P} = \mathcal{P'}$, the change in total value is caused by the change in total interest only.
+Note, that an overpayment typically decreases the overall value of the loan, as the reduction in future interest from re-amortization outweighs interest charged on the overpayment amount itself. However, it is possible for an overpayment to increase the value, if the overpayment interest portion is greater than the value change caused by re-amortization.
 
-$\Delta_{\mathcal{V}} = \mathcal{I'} - \mathcal{I}$ is the total value change of the Loan. When $\Delta_{\mathcal{V}} > 0$ the total value of the Loan increased, when $\Delta_{\mathcal{V}} < 0$ the total value decreased, and if $\Delta_{\mathcal{V}} = 0$ the value remained the same.
+###### 3.2.4.2.4 Early Full Repayment
 
-The total management fee is calculated as follows:
+A Borrower can close a Loan early by submitting the total amount needed to do so. This amount is the sum of the outstanding principal, any interest accrued since the last payment, a prepayment penalty, and a fixed prepayment fee.
 
 $$
-managementFeeTotal = \mathcal{I} \times \mathcal{m}
+totalDue = principalOutstanding + accruedInterest + prepaymentPenalty + ClosePaymentFee
 $$
 
-We compute the management fee paid so far as follows:
+The interest accrued since the last payment is calculated pro-rata:
 
 $$
-managementFeePaid = (\mathcal{I} - \mathcal{i}) \times \mathcal{m}
+secondsSinceLastPayment = lastLedgerCloseTime - max(Loan.previousPaymentDate, Loan.startDate)
 $$
 
 $$
-managementFeeDue = managementFeeTotal - managementFeePaid
+accruedInterest = principalOutstanding \times periodicRate \times \frac{secondsSinceLastPayment}{paymentInterval}
 $$
 
-Finally, we compute the change in management fee as follows:
+The Lender may also charge a prepayment penalty, calculated as a percentage of the outstanding principal:
 
 $$
-managementFeeChange = \mathcal{i'} \times \mathcal{m} - managementFeeDue
-$$
-
-The above calculation can be simplified to:
-
-$$
-managementFeeChange = \Delta_{\mathcal{V}} \times \mathcal{m}
+prepaymentPenalty = principalOutstanding \times closeInterestRate
 $$
 
-When the management fee change is negative, the Loan's value decreases, and thus, the Loan Broker's debt decreases.
-Intuitively, a negative fee change suggests that the fee must be returned, increasing the loan broker's debt.
+Because the borrower is not paying all of the originally scheduled future interest, the total value of the loan asset changes. This `valueChange` is the difference between the interest and penalties the vault _will receive_ versus the interest it _expected_ to receive.
 
-In contrast, if the management fee change is positive, the Loan's value increases, and a further fee must be deducted from the debt.
-Intuitively, a positive fee change suggests that an additional fee must be paid due to the increase in the interest paid.
+The total interest and penalties collected are `accruedInterest + prepaymentPenalty`. The total interest that was expected is `interestOutstanding`.
 
-The LoanBroker debt is then updated as:
+Therefore, the change in the loan's value is calculated as:
 
 $$
-LoanBroker.DebtTotal = LoanBroker.DebtTotal - managementFeeChange
+valueChange = (accruedInterest + prepaymentPenalty) - interestOutstanding
 $$
 
-##### 3.2.4.3 Total Loan Value Calculation
+The `valueChange` for an early repayment can be either positive or negative, depending on the size of the `prepaymentPenalty` relative to the `interestOutstanding`.
 
-At any point in time the following formulae can be used to calculate the total remaining value of the loan.
+- If `(accruedInterest + prepaymentPenalty) < interestOutstanding`, the `valueChange` will be negative, reflecting a decrease in the total value of the loan asset because the vault receives less interest than originally scheduled.
+- If `(accruedInterest + prepaymentPenalty) > interestOutstanding`, the `valueChange` will be positive. This can occur if the lender imposes a significant prepayment penalty that exceeds the forgiven future interest.
 
-The periodic interest rate is the interest rate charged per payment period.
+This change in value must be reflected in `Vault.AssetsTotal` and `LoanBroker.DebtTotal`, accounting for the corresponding change in the `managementFee`.
 
-$$
-periodicRate = \frac{interestRate \times paymentInterval}{365 \times 24 \times 60 \times 60}
-$$
+##### 3.2.4.3 Conceptual Loan Value
 
-The payment is computed based on the periodic rate, principal outstanding, and number of payments remaining. (This means the payment amount can decrease if the borrow pays principal early.)
+The value of a loan is based on the present value of its future payments. Conceptually, this can be understood through the standard amortization formulas.
+
+The `periodicPayment` is the constant amount required to pay off the `principalOutstanding` over the `PaymentRemaining` intervals at the given `periodicRate`.
 
 $$
 periodicPayment = principalOutstanding \times \frac{periodicRate \times (1 + periodicRate)^{PaymentRemaining}}{(1 + periodicRate)^{PaymentRemaining} - 1}
 $$
 
-The total loan value is simply:
+From this, the theoretical `totalValueOutstanding` is the sum of all remaining payments.
 
 $$
-totalValueOutstanding = periodicPayment \times PaymentRemaining
+\text{Theoretical } totalValueOutstanding = periodicPayment \times PaymentRemaining
 $$
 
-We calculate the total interest outstanding as follows:
+And the theoretical `totalInterestOutstanding` is the portion of that total value that is not principal.
 
 $$
-totalInterestOutstanding = totalValueOutstanding - principalOutstanding
+\text{Theoretical } totalInterestOutstanding = \text{Theoretical } totalValueOutstanding - principalOutstanding
 $$
 
+**Important Note**: These formulas describe the theoretical financial model. The actual values stored on the `Loan` ledger object (`TotalValueOutstanding`, `PrincipalOutstanding`, `ManagementFeeOutstanding`) are continuously adjusted during payment processing to account for asset-specific rounding rules. Therefore, implementations **must not** rely on these formulas to derive the live state of a loan. The stored ledger fields are the single source of truth.
+
 ##### 3.2.4.4 Transaction Pseudo-code
 
 The following is the pseudo-code for handling a Loan payment transaction.
@@ -1502,293 +1507,316 @@ The following is the pseudo-code for handling a Loan payment transaction.
 function compute_periodic_payment(principalOutstanding) -> (periodicPayment):
     let periodicRate = (loan.interestRate * loan.paymentInterval) / (365 * 24 * 60 * 60)
     let raisedRate = (1 + periodicRate)^loan.paymentsRemaining
-    
+
     return principalOutstanding * (periodicRate * raisedRate) / (raisedRate - 1)
 
 function compute_late_payment_interest(currentTime) -> (lateInterest, managementFee):
     let secondsOverdue = lastLedgerCloseTime() - loan.nextPaymentDueDate
     let latePeriodicRate = (loan.lateInterestRate * secondsOverdue) / (365 * 24 * 60 * 60)
     let latePaymentInterest = loan.principalOutstanding * latePeriodicRate
-    
-    let fee = (latePaymentInterest * loan.loanbroker.managementFeeRate).round(asset_scale, DOWN)
-    
+
+    let fee = (latePaymentInterest * loan.loanbroker.managementFeeRate).round(loan.loanScale, DOWN)
+
     return (latePaymentInterest - fee, fee)
 
 function compute_full_payment(currentTime) -> (principal, interest, fee):
-    let rawPrincipalOutstanding = principal_outstanding_from_periodic()
+    let truePrincipalOutstanding = principal_outstanding_from_periodic()
     let periodicRate = (loan.interestRate * loan.paymentInterval) / (365 * 24 * 60 * 60)
     let secondsSinceLastPayment = lastLedgerCloseTime() - max(loan.previousPaymentDate, loan.startDate)
-    
-    let accruedInterest = rawPrincipalOutstanding * periodicRate * (secondsSinceLastPayment / loan.paymentInterval)
-    let prepaymentPenalty = rawPrincipalOutstanding * loan.closeInterestRate
-    let interest = (accruedInterest + prepaymentPenalty).round(asset_scale, DOWN)
-    
-    let managementFee = (interest * loan.loanbroker.managementFeeRate).round(asset_scale, DOWN)
+
+    let accruedInterest = truePrincipalOutstanding * periodicRate * (secondsSinceLastPayment / loan.paymentInterval)
+    let prepaymentPenalty = truePrincipalOutstanding * loan.closeInterestRate
+    let interest = (accruedInterest + prepaymentPenalty).round(loan.loanScale, DOWN)
+
+    let managementFee = (interest * loan.loanbroker.managementFeeRate).round(loan.loanScale, DOWN)
     interest = interest - managementFee
-    
-    if managementFee <= loan.managementFeeOutstanding:
-        return (loan.principalOutstanding, interest, managementFee)
-    
-    return (loan.principalOutstanding, accruedInterest + prepaymentPenalty, managementFee)
+
+    return (loan.principalOutstanding, interest, managementFee)
 
 function principal_outstanding_from_periodic() -> (principalOutstanding):
     # Given the outstanding principal we can calculate the periodic payment
     # Equally, given the periodic payment we can calculate the principal outstanding at the current time
     let periodicRate = (loan.interestRate * loan.paymentInterval) / (365 * 24 * 60 * 60)
-    
+
     # If the loan is zero-interest, the outstanding principal is simply periodicPayment * paymentsRemaining
     if periodicRate == 0:
         return loan.periodicPayment * loan.paymentsRemaining
-    
+
     let raisedRate = (1 + periodicRate)^loan.paymentsRemaining
     let factor = (periodicRate * raisedRate) / (raisedRate - 1)
-    
+
     return loan.periodicPayment / factor
 
 # This function calculates what the loan state should be given the periodic payment and remaining payments
 function calculate_true_loan_state() -> (principalOutstanding, interestOutstanding, managementFeeOutstanding):
-    let rawPrincipalOutstanding = principal_outstanding_from_periodic()
-    let rawInterestOutstanding = (loan.periodicPayment * loan.paymentsRemaining) - rawPrincipalOutstanding
-    let rawManagementFeeOutstanding = (rawInterestOutstanding * loan.loanbroker.managementFeeRate)
-    
+    let truePrincipalOutstanding = principal_outstanding_from_periodic()
+    let trueInterestOutstanding = (loan.periodicPayment * loan.paymentsRemaining) - truePrincipalOutstanding
+    let trueManagementFeeOutstanding = (trueInterestOutstanding * loan.loanbroker.managementFeeRate)
+
     # Exclude the management fee from the interest rate
-    rawInterestOutstanding = rawInterestOutstanding - rawManagementFeeOutstanding
-    
-    return (rawPrincipalOutstanding, rawInterestOutstanding, rawManagementFeeOutstanding)
+    trueInterestOutstanding = trueInterestOutstanding - trueManagementFeeOutstanding
+
+    return (truePrincipalOutstanding, trueInterestOutstanding, trueManagementFeeOutstanding)
 
 function calculate_payment_breakdown(principalOutstanding) -> (principal, interest):
     let periodicRate = (loan.interestRate * loan.paymentInterval) / (365 * 24 * 60 * 60)
-    
+
     if periodicRate == 0:
         return (principalOutstanding / loan.paymentsRemaining, 0)
-    
+
     let interest = principalOutstanding * periodicRate
     let principal = loan.periodicPayment - interest
-    
+
     return (principal, interest)
 
-function calculate_rounded_principal_payment(unroundedPrincipalOutstanding, unroundedPrincipalPayment) -> (principal):
+function calculate_rounded_principal_payment(truePrincipalOutstanding, truePrincipalPayment) -> (principal):
     # The diff captures by how much we deviated from the true principal value
     # If the diff is negative, we need to slow down repayment as we overpaid principalOutstanding
     # If the diff is positive, we need to speed up the repayment as we underpaid the principalOutstanding
-    let diff = (loan.principalOutstanding - unroundedPrincipalOutstanding).round(asset_scale, DOWN)
-    let roundedPrincipalPayment = (unroundedPrincipalPayment + diff).round(asset_scale, DOWN)
-    
+    let diff = (loan.principalOutstanding - truePrincipalOutstanding).round(loan.loanScale, DOWN)
+    let roundedPrincipalPayment = (truePrincipalPayment + diff).round(loan.loanScale, DOWN)
+
     # Ensure we do not have a negative principal payment
     roundedPrincipalPayment = max(0, roundedPrincipalPayment)
-    
+
     return min(roundedPrincipalPayment, loan.principalOutstanding)
 
-function calculate_rounded_interest_breakdown(roundedPrincipalPayment, unroundedInterestOutstanding, unroundedManagementFeeOutstanding, amount) -> (interest, fee):
+
+function calculate_rounded_interest_breakdown(roundedPrincipalPayment, trueInterestOutstanding, trueManagementFeeOutstanding, roundedPeriodicPayment) -> (interest, fee):
     if loan.interestRate == 0:
         return (0, 0)
-    
+
     let loanInterestOutstanding = loan.totalValueOutstanding - loan.principalOutstanding - loan.managementFeeOutstanding
-    
-    # If diffInterest is negative, we are overpaying the interest portion of the loan, we need to slow down
-    # If the diffInterest is positive, we are underpaying the interest portion, we need to speed up
-    let diffInterest = (loanInterestOutstanding - unroundedInterestOutstanding).round(asset_scale, DOWN)
-    let roundedInterestPayment = amount - roundedPrincipalPayment
-    
+
+    # Calculate the accumulated rounding error for the interest portion.
+    # If diffInterest is negative, we have historically overpaid interest; we need to pay less now.
+    # If diffInterest is positive, we have historically underpaid interest; we need to pay more now to catch up.
+    let diffInterest = (loanInterestOutstanding - trueInterestOutstanding).round(loan.loanScale, DOWN)
+    let roundedInterestPayment = roundedPeriodicPayment - roundedPrincipalPayment
+
+    # Apply historical rounding error: if we underpaid interest previously (positive diff), pay more now.
     roundedInterestPayment = roundedInterestPayment + diffInterest
-    
-    # Ensure that we do not overpay the periodic payment amount
-    roundedInterestPayment = min(amount - roundedPrincipalPayment, roundedInterestPayment)
-    
-    # Since in the previous step we perform a subtraction, we need to ensure that we don't end up with a negative interest payment
+
+    # Ensure the adjusted interest payment doesn't cause the total payment to exceed the periodic amount.
+    # This caps the interest at the remaining portion of the periodic payment.
+    roundedInterestPayment = min(roundedPeriodicPayment - roundedPrincipalPayment, roundedInterestPayment)
+
+    # Since diffInterest can be negative, ensure the interest payment itself is not negative.
     roundedInterestPayment = max(0, roundedInterestPayment)
-    
-    # We have calculated the interest payment, we can now calculate the management fee portion
-    
-    # If diffManagementFee is negative, we are overpaying the fee portion, slow down
-    # If the diffManagementFee is positive, we are underpaying the fee portion, speed up
-    let diffManagementFee = (loan.managementFeeOutstanding - unroundedManagementFeeOutstanding).round(asset_scale, DOWN)
-    let roundedManagementFee = (roundedInterestPayment * loan.loanbroker.managementFeeRate).round(asset_scale, DOWN)
-    
+
+    # We have calculated the gross interest payment, we can now calculate the management fee portion.
+
+    # Calculate the accumulated rounding error for the management fee portion.
+    # If diffManagementFee is negative, we have overpaid the fee; pay less now.
+    # If diffManagementFee is positive, we have underpaid the fee; pay more now.
+    let diffManagementFee = (loan.managementFeeOutstanding - trueManagementFeeOutstanding).round(loan.loanScale, DOWN)
+    let roundedManagementFee = (roundedInterestPayment * loan.loanbroker.managementFeeRate).round(loan.loanScale, DOWN)
+
+    # Apply historical rounding error to the current fee payment.
     roundedManagementFee = roundedManagementFee + diffManagementFee
-    
-    # Since the diffManagementFee can be negative, managementFee may end up negative, ensure that does not happen
+
+    # Since diffManagementFee can be negative, ensure the final fee payment is not negative.
     roundedManagementFee = max(0, roundedManagementFee)
-    
-    # Finally, ensure that the management fee does not exceed the outstanding management fee
+
+    # Ensure the fee payment does not exceed the total outstanding management fee.
     roundedManagementFee = min(roundedManagementFee, loan.managementFeeOutstanding)
-    
-    # Subtract the fee from interest, ensuring the interest portion is not negative
+
+    # The final net interest payment is the gross interest minus the management fee. Ensure it's not negative.
     roundedInterestPayment = max(0, roundedInterestPayment - roundedManagementFee)
-    
-    # Finally, ensure the interest payment does not exceed the outstanding interest
+
+    # Finally, ensure the net interest payment does not exceed the total outstanding interest.
     roundedInterestPayment = min(loanInterestOutstanding, roundedInterestPayment)
-    
-    let excess = (amount - roundedPrincipalPayment - roundedInterestPayment - roundedManagementFee)
-    
-    # If we exceed the payment amount, take as much excess as possible from the interest
+
+    # --- Final Safety Check ---
+    # Compute if the sum of components exceeds the periodic payment amount due to rounding adjustments.
+    let excess = (roundedPeriodicPayment - roundedPrincipalPayment - roundedInterestPayment - roundedManagementFee)
+
+    # If the sum is too large (excess is negative), reduce the components to match the total.
+    # First, take as much excess as possible from the interest portion.
     if excess < 0:
         let part = min(roundedInterestPayment, abs(excess))
         roundedInterestPayment = roundedInterestPayment - part
         excess = excess + part
-    
-    # If there is any left, take as much as possible from the fee
+
+    # If there is still an excess, take as much as possible from the fee portion.
     if excess < 0:
         let part = min(roundedManagementFee, abs(excess))
         roundedManagementFee = roundedManagementFee - part
         excess = excess + part
-    
+
+    # if excess is still negative, this implies that the principal was calculated wrong.
+    # it should not happen, but if it does this is a critical failure.
+
     return (roundedInterestPayment, roundedManagementFee)
 
-function compute_payment_due(amount) -> (principal, interest, managementFee):
+function compute_payment_due(roundedPeriodicPayment) -> (principal, interest, managementFee):
     # If this is the final payment, simply settle any outstanding amounts
     if loan.paymentsRemaining == 1:
         let outstandingInterest = loan.totalValueOutstanding - loan.principalOutstanding - loan.managementFeeOutstanding
         return (loan.principalOutstanding, outstandingInterest, loan.managementFeeOutstanding)
-    
-    # Determine the true, unrounded state of the loan
-    let (unroundedPrincipalOutstanding, unroundedInterestOutstanding, unroundedManagementFeeOutstanding) = calculate_true_loan_state()
-    
+
+    # Determine the true state of the loan, excluding rounding errors
+    let (truePrincipalOutstanding, trueInterestOutstanding, trueManagementFeeOutstanding) = calculate_true_loan_state()
+
     # We do not need to know the interest portion
-    let (unroundedPrincipalPayment, _) = calculate_payment_breakdown(unroundedPrincipalOutstanding)
-    
+    let (truePrincipalPayment, _) = calculate_payment_breakdown(truePrincipalOutstanding)
+
     # Given the true state we can calculate the rounded principal that accounts for deviation from the true state
-    let roundedPrincipalPayment = calculate_rounded_principal_payment(unroundedPrincipalOutstanding, unroundedPrincipalPayment)
-    
+    let roundedPrincipalPayment = calculate_rounded_principal_payment(truePrincipalOutstanding, truePrincipalPayment)
+
     let (roundedInterestPayment, roundedManagementFee) = calculate_rounded_interest_breakdown(
         roundedPrincipalPayment,
-        unroundedInterestOutstanding,
-        unroundedManagementFeeOutstanding,
-        amount
+        trueInterestOutstanding,
+        trueManagementFeeOutstanding,
+        roundedPeriodicPayment
     )
-    
+
     return (roundedPrincipalPayment, roundedInterestPayment, roundedManagementFee)
 
 function do_overpayment(amount) -> (valueChange):
-    # Calculate true principal and interest outstanding
+    # Calculate the ideal, unrounded ("true") state of the loan before the overpayment.
     let (truePrincipalOutstanding, trueInterestOutstanding, trueManagementFeeOutstanding) = calculate_true_loan_state()
-    
-    # For an accurate overpayment we need to preserve rounding errors
-    # diffTotal incorporates rounding errors from principal and interest fee, note there is no interest rounding error as this value is derived
+
+    # For an accurate overpayment, we must preserve historical rounding errors.
+    # These 'diff' variables capture the difference between the on-ledger rounded state and the ideal 'true' state.
     let diffTotal = loan.totalValueOutstanding - (truePrincipalOutstanding + trueInterestOutstanding + trueManagementFeeOutstanding)
     let diffPrincipal = loan.principalOutstanding - truePrincipalOutstanding
     let diffManagementFee = loan.managementFeeOutstanding - trueManagementFeeOutstanding
-    
-    let newPrincipalOutstanding = truePrincipalOutstanding - amount
-    let newPeriodicPayment = compute_periodic_payment(newPrincipalOutstanding)
-    
-    # From the given periodic payment, calculate the new total value outstanding
-    let newTotalValueOutstanding = (newPeriodicPayment * loan.paymentsRemaining).round(asset_scale, HALF_EVEN)
-    
-    # From the new total value, calculate the new interest outstanding and management fee outstanding
-    let newInterestOutstanding = newTotalValueOutstanding - newPrincipalOutstanding
-    let newManagementFeeOutstanding = (newInterestOutstanding * loan.loanbroker.managementFeeRate).round(asset_scale, HALF_EVEN)
-    
-    newInterestOutstanding = newInterestOutstanding - newManagementFeeOutstanding
-    
-    let roundedValueChange = newTotalValueOutstanding + diffTotal - (loan.totalValueOutstanding - amount)
-    
-    # Update loan state
-    loan.totalValueOutstanding = newTotalValueOutstanding + diffTotal
-    loan.principalOutstanding = (newPrincipalOutstanding + diffPrincipal).round(asset_scale, DOWN)
-    loan.managementFeeOutstanding = (newManagementFeeOutstanding + diffManagementFee).round(asset_scale, DOWN)
-    
+
+    # Re-amortize the loan based on the new, lower principal after the overpayment is applied.
+    let newTruePrincipalOutstanding = truePrincipalOutstanding - amount
+    let newTruePeriodicPayment = compute_periodic_payment(newTruePrincipalOutstanding)
+
+    # From the new periodic payment, calculate the new ideal total value, interest, and fee.
+    let newTrueTotalValueOutstanding = (newTruePeriodicPayment * loan.paymentsRemaining).round(loan.loanScale, HALF_EVEN)
+    let newTrueInterestOutstanding = newTrueTotalValueOutstanding - newTruePrincipalOutstanding
+    let newTrueManagementFeeOutstanding = (newTrueInterestOutstanding * loan.loanbroker.managementFeeRate).round(loan.loanScale, HALF_EVEN)
+    newTrueInterestOutstanding = newTrueInterestOutstanding - newTrueManagementFeeOutstanding
+
+    # Set the new on-ledger total value, adjusting for the historical rounding errors to maintain consistency.
+    let newRoundedTotalValueOutstanding = (newTrueTotalValueOutstanding + diffTotal).round(loan.loanScale, DOWN)
+
+    # The old total value, for comparison, is what the on-ledger value would have been if we just subtracted the overpayment amount.
+    let oldRoundedTotalValueOutstanding = loan.totalValueOutstanding - amount
+
+    # The change in the loan's value is the difference between the new re-amortized value and the old value.
+    # This formula correctly calculates the change in future interest while preserving the existing rounding difference (diffTotal),
+    # which is present in both newRoundedTotalValueOutstanding and implicitly in oldRoundedTotalValueOutstanding.
+    let roundedValueChange = newRoundedTotalValueOutstanding - oldRoundedTotalValueOutstanding
+
+    # Update loan state by applying the preserved rounding differences to the new 'true' state.
+    loan.totalValueOutstanding = newTrueTotalValueOutstanding + diffTotal
+    loan.principalOutstanding = (newTruePrincipalOutstanding + diffPrincipal).round(loan.loanScale, DOWN)
+    loan.managementFeeOutstanding = (newTrueManagementFeeOutstanding + diffManagementFee).round(loan.loanScale, DOWN)
+
     return roundedValueChange
 
 function make_payment(amount, currentTime) -> (principalPaid, interestPaid, valueChange, feePaid):
     if loan.paymentsRemaining == 0 || loan.principalOutstanding == 0:
         return "loan complete" error
-    
+
     # The payment is late
     if loan.nextPaymentDueDate < currentTime:
         let (principal, interest, managementFee) = compute_payment_due(amount)
         let (lateInterest, lateManagementFee) = compute_late_payment_interest(currentTime)
-        
-        let totalManagementFee = managementFee + lateManagementFee
-        let totalDue = principal + interest + lateInterest + totalManagementFee + loan.serviceFee + loan.latePaymentFee
-        
+
+        # totalDue for late payment is the sum of expected periodic payment, and the accrued late interest and charges
+        let totalDue = (principal + interest + managementFee + loan.serviceFee) + (lateInterest + lateManagementFee + loan.latePaymentFee)
+
         # Insufficient funds
         if amount < totalDue:
             return "insufficient amount paid" error
-        
+
         loan.paymentsRemaining = loan.paymentsRemaining - 1
         loan.previousPaymentDate = loan.nextPaymentDueDate
         loan.nextPaymentDueDate = loan.nextPaymentDueDate + loan.paymentInterval
         loan.principalOutstanding = loan.principalOutstanding - principal
         loan.managementFeeOutstanding = loan.managementFeeOutstanding - managementFee
-        
+        # we do not adjust the total value by late interst or late managementFee as these were not included in the initial total value
+        loan.totalValueOutstanding = loan.totalValueOutstanding - (principal + interest + managementFee)
+
         return (
             principal,                                                    # A late payment does not affect the principal portion due
             interest + lateInterest,                                      # A late payment incorporates both periodic interest and the late interest
             lateInterest,                                                 # The value of the loan increases by the lateInterest amount
-            totalManagementFee + loan.serviceFee + loan.latePaymentFee   # The total fee paid for a loan payment
+            totalManagementFee + loan.serviceFee + loan.latePaymentFee    # The total fee paid for a loan payment
         )
-    
+
     let (fullPrincipal, fullInterest, fullManagementFee) = compute_full_payment(currentTime)
     let fullPaymentAmount = fullPrincipal + fullInterest + fullManagementFee + loan.closePaymentFee
-    
+
     # If the payment is equal or higher than full payment amount and there is more than one payment remaining, make a full payment
     if amount >= fullPaymentAmount && loan.paymentsRemaining > 1:
         let totalInterestOutstanding = loan.totalValueOutstanding - loan.principalOutstanding - loan.managementFeeOutstanding
         let loanValueChange = fullInterest - totalInterestOutstanding
-        
+
         loan.paymentsRemaining = 0
         loan.principalOutstanding = 0
         loan.managementFeeOutstanding = 0
         loan.totalValueOutstanding = 0
-        
+
         return (
             fullPrincipal,                      # Full payment repays the entire outstanding principal
             fullInterest,                       # Full payment repays any accrued interest since the last payment and additional full payment interest
             loanValueChange,                    # A full payment changes the total value of the loan
             fullManagementFee + loan.closePaymentFee   # An early payment pays a specific closePaymentFee
         )
-    
+
     # Handle regular payments and overpayments
     let totalPaid = 0
     let (totalPrincipalPaid, totalInterestPaid, totalFeePaid) = (0, 0, 0)
-    
+
     # Process regular periodic payments
     while totalPaid < amount && loan.paymentsRemaining > 0:
-        let (principal, interest, managementFee) = compute_payment_due(loan.periodicPayment.round(asset_scale, UP))
+        let (principal, interest, managementFee) = compute_payment_due(loan.periodicPayment.round(loan.loanScale, UP))
         let paymentAmount = principal + interest + managementFee + loan.serviceFee
-        
+
         # Check if we have enough funds for this payment
         if totalPaid + paymentAmount > amount:
             break
-        
+
         # Apply the payment
         loan.totalValueOutstanding = loan.totalValueOutstanding - (principal + interest + managementFee)
         loan.principalOutstanding = loan.principalOutstanding - principal
         loan.managementFeeOutstanding = loan.managementFeeOutstanding - managementFee
         loan.paymentsRemaining = loan.paymentsRemaining - 1
-        
+
         loan.nextPaymentDueDate = loan.nextPaymentDueDate + loan.paymentInterval
         loan.previousPaymentDate = loan.nextPaymentDueDate - loan.paymentInterval
-        
+
         totalPaid = totalPaid + paymentAmount
         totalPrincipalPaid = totalPrincipalPaid + principal
         totalInterestPaid = totalInterestPaid + interest
         totalFeePaid = totalFeePaid + managementFee + loan.serviceFee
-    
+
     let loanValueChange = 0
     # Handle overpayment if there are remaining payments, the loan supports overpayments, and there are funds remaining
     if loan.paymentsRemaining > 0 && is_set(loan.lsfLoanOverpayment) && is_set(tfLoanOverpayment) && totalPaid < amount:
         let overpaymentAmount = min(loan.principalOutstanding, amount - totalPaid)
-        
+
         let overpaymentInterest = overpaymentAmount * loan.overpaymentInterestRate
         let overpaymentManagementFee = overpaymentInterest * loan.loanbroker.managementFeeRate
-        let overpaymentFee = overpaymentAmount * loan.overpaymentFee
-        
         overpaymentInterest = overpaymentInterest - overpaymentManagementFee
+
+        let overpaymentFee = overpaymentAmount * loan.overpaymentFee
+
+        # the value of the loan will increase by the overpayment interest portion
         loanValueChange = loanValueChange + overpaymentInterest
-        
         let overpaymentPrincipal = overpaymentAmount - overpaymentInterest - overpaymentManagementFee - overpaymentFee
-        
+
+        # if the overpayment was not eaten by fees and interest, then apply it
         if overpaymentPrincipal > 0:
+            # the valueChange is the decrease in the total interest caused by overpaying the principal
             let valueChange = do_overpayment(overpaymentPrincipal)
+
+            # ajust the total loanValueChange
             loanValueChange = loanValueChange + valueChange
-            
+
             totalPaid = totalPaid + overpaymentAmount
             totalPrincipalPaid = totalPrincipalPaid + overpaymentPrincipal
             totalInterestPaid = totalInterestPaid + overpaymentInterest
             totalFeePaid = totalFeePaid + overpaymentManagementFee + overpaymentFee
-    
+
     return (
         totalPrincipalPaid,     # This will include the periodicPayment principal and any overpayment
         totalInterestPaid,      # This will include the periodicPayment interest and any overpayment
@@ -1799,29 +1827,25 @@ function make_payment(amount, currentTime) -> (principalPaid, interestPaid, valu
 
 ##### 3.2.4.5 Failure Conditions
 
-Assume the payment is split into `principal`, `interest` and `fee`, and `totalDue = principal + interest + fee`. `totalDue` is the minimum payment due by the borrower.
-
-Assume the payment is handled by a function that implements the [Pseudo-Code](#3242-transaction-pseudo-code) that returns `principalPaid`, `interestPaid`, `valueChange` and `feePaid`, where:
+- A `Loan` object with the specified `LoanID` does not exist on the ledger.
+- The `Account` submitting the transaction is not the `Loan.Borrower`.
+- The `Amount` field is invalid or specifies a negative value.
 
-- `principalPaid` is the amount of principal that the payment covered.
-- `interestPaid` is the amount of interest that the payment covered.
-- `feePaid` is the amount of fee that the payment covered.
-- `totalPaid = principalPaid + interestPaid + feePaid` is the total amount the borrower paid.
-- `valueChange` is the amount by which the total value of the Loan changed.
-  - If `valueChange` < `0`, Loan value decreased.
-  - If `valueChange` > `0`, Loan value increased, and if `valueChange` = `0` the value remained the same.
+- The loan is already fully paid (`Loan.PaymentRemaining` is `0` or `Loan.TotalValueOutstanding` is `0`).
+- The `tfLoanOverpayment` flag is set on the transaction, but the `lsfLoanOverpayment` flag is not set on the `Loan` object.
+- The `tfLoanFullPayment` flag is set, but only one payment remains on the loan (`Loan.PaymentRemaining` is `1`).
 
-Furthermore, assume `full_periodic_payments` variable represents the number of payment intervals that the payment covered.
+- If the payment is late (`LastLedgerCloseTime >= Loan.NextPaymentDueDate`):
 
-- A `Loan` object with specified `LoanID` does not exist on the ledger.
+  - The `Amount` is less than the calculated `totalDue` for a late payment, which is `periodicPayment + loanServiceFee + latePaymentFee + latePaymentInterest`.
 
-- The submitter `AccountRoot.Account` is not equal to `Loan.Borrower`.
+- If the payment is on-time (`LastLedgerCloseTime < Loan.NextPaymentDueDate`):
 
-- The `Loan.lsfLoanOverpayment` flag is not set, and the user set the `tfLoanOverpayment` flag.
+  - The `Amount` is less than the calculated `totalDue` for a periodic payment, which is `periodicPayment + loanServiceFee`.
 
-- `Loan.PaymentRemaining` or `Loan.TotalValueOutstanding` is `0`.
+- If the `tfLoanFullPayment` flag is specified:
 
-- The Borrower paid insufficient amount: `full_periodic_payments < 0`.
+  - The `Amount` is less than the calculated `totalDue` for a full early payment, which is `principalOutstanding + accruedInterest + prepaymentPenalty + ClosePaymentFee`.
 
 - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is `XRP`:
 
@@ -1833,7 +1857,7 @@ Furthermore, assume `full_periodic_payments` variable represents the number of p
   - The `RippleState` between the `LoanBroker.Account` and the `Issuer` has the `lsfLowDeepFreeze` or `lsfHighDeepFreeze` flag set. (The Loan Broker _pseudo-account_ is frozen).
   - The `RippleState` between the `Vault(LoanBroker(Loan.LoanBrokerID).VaultID).Account` and the `Issuer` has the `lsfLowFreeze` or `lsfHighFreeze` flag set. (The Vault _pseudo-account_ is frozen).
   - The `AccountRoot` object of the `Issuer` has the `lsfGlobalFreeze` flag set.
-    - The `RippleState` object `Balance` < `totalDue` (Borrower has insufficient funds).
+  - The `RippleState` object `Balance` < `totalDue` (Borrower has insufficient funds).
 
 - If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `MPT`:
 
@@ -1844,132 +1868,83 @@ Furthermore, assume `full_periodic_payments` variable represents the number of p
   - The `MPToken` object for the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` of the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Account` `AccountRoot` has `lsfMPTLocked` flag set. (The Vault _pseudo-account_ is locked).
   - The `MPTokenIssuance` object of the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` has the `lsfMPTLocked` flag set.
 
-- If `LastClosedLedger.CloseTime >= Loan.NextPaymentDueDate` and `Amount` < `LatePaymentAmount()`
-
-- If `LastClosedLedger.CloseTime < Loan.NextPaymentDueDate` and `Amount` < `PeriodicPaymentAmount()`
-
 ##### 3.2.4.6 State Changes
 
-- `Loan` object state changes:
-
-  - If `Loan(LoanID).Flags == lsfLoanImpaired`:
-
-    - `Loan(LoanID).Flags = 0`
-
-  - Decrease `Loan.PaymentRemaining` by `full_periodic_payments`.
-  - Decrease `Loan.PrincipalOutstanding` by `principalPaid`.
-  - Update `Loan.TotalValueOutstanding` = `(Loan.TotalValueOutstanding + valueChange) - (principalPaid + interestPaid)`.
-
-  - If `Loan.PaymentRemaining > 0` and `Loan.PrincipalOutstanding > 0`:
-
-    - Set the next payment date: `Loan.NextPaymentDueDate += Loan.PaymentInterval * full_periodic_payments`.
-    - Set the previous payment date: `Loan.PreviousPaymentDate = Loan.NextPaymentDueDate - Loan.PaymentInterval`.
-
-- `LoanBroker(Loan.LoanBrokerID)` object state changes:
+Upon successful validation, the `LoanPay` transaction is processed according to the logic defined in the [Transaction Pseudo-code](#3244-transaction-pseudo-code). This process yields four key results: `principalPaid`, `interestPaid`, `feePaid`, and `valueChange`. These values are then used to apply the following state changes.
 
-  - Compute the management fee:
+**1. High-Level Accounting**
 
-    - `feeManagement = interestPaid x LoanBroker.ManagementFeeRate`
+First, the system determines the final destination of all funds.
 
-  - Total paid, and what portion goes to the vault:
+1.  **Determine Fee Destination**: All collected fees are directed to one of two places:
 
-    - `totalPaid = principalPaid + interestPaid + feePaid`
-    - `totalPaidToVault = principalPaid + interestPaid`
-    - `totalPaidToBroker = feePaid`
+    - **If First-Loss Capital is sufficient** (`LoanBroker.CoverAvailable >= LoanBroker.DebtTotal * LoanBroker.CoverRateMinimum`): The fees are paid to the `LoanBroker.Owner`.
+    - **If First-Loss Capital is insufficient**: The fees are added to the first-loss pool to cover the deficit.
 
-  - Adjust the totals for the management fee:
+2.  **Define Final Fund Flows**:
+    - `totalPaidByBorrower = principalPaid + interestPaid + feePaid`
+    - `totalToVault = principalPaid + interestPaid`
+    - `totalToBroker` = The total fee amount, directed to either the `LoanBroker.Owner` or the `LoanBroker` pseudo-account's cover pool.
 
-    - `totalPaidToVault = totalPaidToVault - feeManagement`
-    - `totalPaidToBroker = totalPaidToBroker + feeManagement`
+**2. `Loan` Object State Changes**
 
-  - If there is **not enough** first-loss capital: `LoanBroker.CoverAvailable < LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`:
+The `Loan` object is updated to reflect the payment.
 
-    - Add the fee to to First Loss Cover Pool:
+- If the loan was impaired (`lsfLoanImpaired` flag was set), the flag is cleared.
 
-      - `LoanBroker.CoverAvailable = LoanBroker.CoverAvailable + (totalPaidToBroker)`
+- **For a Full Repayment**:
 
-  - Decrease LoanBroker Debt by the amount paid:
+  - All outstanding balance fields (`PrincipalOutstanding`, `TotalValueOutstanding`, `ManagementFeeOutstanding`) are set to `0`.
+  - `PaymentRemaining` is set to `0`.
 
-    - `LoanBroker.DebtTotal = LoanBroker.DebtTotal - (totalPaid - feePaid)`
+- **For Other Payments**:
 
-  - Update the LoanBroker Debt by the Loan value change:
+  - `PrincipalOutstanding` is decreased by the `principal` portion of each periodic payment settled.
+  - `ManagementFeeOutstanding` is decreased by the `managementFee` portion of each periodic payment settled.
+  - `TotalValueOutstanding` is decreased by the sum of the `principal`, `interest`, and `managementFee` portions of each periodic payment settled.
+  - If an overpayment occurred, `TotalValueOutstanding` is further adjusted by the `valueChange` resulting from re-amortization. It is **not** adjusted for `valueChange` from late payment interest, as that interest was not part of the original loan value.
+  - `PaymentRemaining` is decreased by `1` for each full periodic payment cycle covered.
+  - `NextPaymentDueDate` is advanced by `Loan.PaymentInterval` for each periodic payment cycle covered.
+  - `PreviousPaymentDate` is updated.
+  - If an overpayment was made:
+    - `PeriodicPayment` is recalculated based on the new outstanding principal and remaining term.
 
-    - `LoanBroker.DebtTotal = LoanBroker.DebtTotal + valueChange`
+**3. `LoanBroker` and `Vault` Object State Changes**
 
-  - Update the LoanBroker Debt by the change in the management fee:
+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.
 
-    - `LoanBroker.DebtTotal = LoanBroker.DebtTotal - (valueChange x LoanBroker.ManagementFeeRate)`
+- **`LoanBroker` Updates**:
 
-- `Vault(LoanBroker(Loan.LoanBrokerID).VaultID)` state changes:
+  - `LoanBroker.DebtTotal` is decreased by `totalToVault` (the principal and interest paid back).
+  - If the payment resulted in a `valueChange` from an overpayment or early full repayment, `LoanBroker.DebtTotal` is adjusted by that `valueChange`. It is **not** adjusted for `valueChange` from late payment interest, as this represents a penalty paid directly to the vault, not an alteration of the original debt schedule.
+  - If fees were directed to the cover pool, `LoanBroker.CoverAvailable` increases by `totalToBroker`.
 
-  - Increase available assets in the Vault by the amount paid:
+- **`Vault` Updates**:
+  - `Vault.AssetsAvailable` increases by `totalToVault`.
+  - `Vault.AssetsTotal` is always adjusted by the total `valueChange`, reflecting the net change in the vault's expected future earnings from the loan.
 
-    - `Vault.AssetsAvailable = Vault.AssetsAvailable + totalPaidToVault`
+**4. Low-Level Asset Transfers**
 
-  - Update the Vault total value by the change in the Loan total value:
+Finally, the actual asset transfers are executed on the ledger.
 
-    - `Vault.AssetsTotal = Vault.AssetsTotal + valueChange`
+- The borrower's balance is **decreased** by `totalPaidByBorrower`.
+- The `Vault` pseudo-account's balance is **increased** by `totalToVault`.
+- The `LoanBroker.Owner`'s balance OR the `LoanBroker` pseudo-account's balance is **increased** by `totalToBroker`, depending on the fee destination.
 
-  - Update the Vault total value by the change in the management fee:
-
-    - `Vault.AssetsTotal = Vault.AssetsTotal - (vaultChange x LoanBroker.managementFeeRate)`
-
-- If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is `XRP`:
-
-  - Increase the `Balance` field of `Vault` _pseudo-account_ `AccountRoot` by `principalPaid + (interestPaid - management_fee)`.
-
-  - If `LoanBroker.CoverAvailable >= LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`:
-
-    - Increase the `Balance` field of the `LoanBroker.Owner` `AccountRoot` by `feePaid + management_fee`.
-
-  - If `LoanBroker.CoverAvailable < LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`:
-
-    - Increase the `Balance` field of the `LoanBroker` _pseudo-account_ `AccountRoot` by `feePaid + management_fee`. (the payment and management fee was added to First Loss Capital, and thus transfered to the `LoanBroker` _pseudo-account_).
-    - Increase `LoanBroker.CoverAvailable` by `feePaid + management_fee`.
-
-  - Decrease the `Balance` field of the submitter `AccountRoot` by `principalPaid + interestPaid + feePaid`.
-
-- If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `IOU`:
+These transfers are performed according to the asset type:
 
-  - Increase the `RippleState` balance between the `Vault` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `principalPaid + (interestPaid - management_fee)`.
-
-  - If `LoanBroker.CoverAvailable < LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`:
-
-    - Increase the `RippleState` balance between the `LoanBroker` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `feePaid + management_fee` (the payment and management fee was added to First Loss Capital, and thus transfered to the `LoanBroker` _pseudo-account_).
-    - Increase `LoanBroker.CoverAvailable` by `feePaid + management_fee`.
-
-  - If the `RippleState` object between the `LoanBroker.Owner` `AccountRoot` and the `Issuer` of the asset has the `lsfLowDeepFreeze` or `lsfHighDeepFreeze` flag set (The LoanBroker cannot receive funds):
-
-    - Increase the `RippleState` balance between the `LoanBroker` _pseudo-account_ `AccountRoot` and the `Issuer` `AccountRoot` by `feePaid + management_fee` (the payment and management fee was added to First Loss Capital, and thus transfered to the `LoanBroker` _pseudo-account_).
-    - Increase `LoanBroker.CoverAvailable` by `feePaid + management_fee`.
-
-  - If `LoanBroker.CoverAvailable >= LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`:
-
-    - Increase the `RippleState` balance between the `LoanBroker.Owner` `AccountRoot` and the `Issuer` `AccountRoot` by `feePaid + management_fee`.
-
-  - Decrease the `RippleState` balance between the submitter `AccountRoot` and the `Issuer` `AccountRoot` by `principalPaid + interestPaid + feePaid`.
-
-- If the `Vault(LoanBroker(Loan(LoanID).LoanBrokerID).VaultID).Asset` is an `MPT`:
-
-  - Increase the `MPToken.MPTAmount` by `principalPaid + (interestPaid - management_fee)` of the `Vault` _pseudo-account_ `MPToken` object for the `Vault.Asset`.
-
-  - If `LoanBroker.CoverAvailable < LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`:
-
-    - Increase the `MPToken.MPTAmount` by `feePaid + management_fee` of the `LoanBroker` _pseudo-account_ `MPToken` object for the `Vault.Asset` (the payment and management fee was added to First Loss Capital, and thus transfered to the `LoanBroker` _pseudo-account_).
-    - Increase `LoanBroker.CoverAvailable` by `feePaid + management_fee`.
-
-  - The `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset` of the `LoanBroker.Owner` `AccountRoot` has `lsfMPTLocked` flag set (The LoanBroker cannot receive funds):
-
-    - Increase the `MPToken.MPTAmount` by `feePaid + management_fee` of the `LoanBroker` _pseudo-account_ `MPToken` object for the `Vault.Asset` (the payment and management fee was added to First Loss Capital, and thus transfered to the `LoanBroker` _pseudo-account_).
-    - Increase `LoanBroker.CoverAvailable` by `feePaid + management_fee`.
-
-  - If `LoanBroker.CoverAvailable >= LoanBroker.DebtTotal x LoanBroker.CoverRateMinimum`:
-
-    - Increase the `MPToken.MPTAmount` by `feePaid + management_fee` of the `LoanBroker.Owner` `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset`.
-
-  - Decrease the `MPToken.MPTAmount` by `principalPaid + interestPaid + feePaid` of the submitter `MPToken` object for the `Vault(LoanBroker(LoanBrokerID).VaultID).Asset`.
-
-[**Return to Index**](#index)
+- **If the asset is XRP**:
+  - The `Balance` of the borrower's `AccountRoot` is decreased.
+  - The `Balance` of the `Vault` pseudo-account's `AccountRoot` is increased.
+  - The `Balance` of the destination account for fees (`LoanBroker.Owner` or `LoanBroker` pseudo-account) is increased.
+- **If the asset is an IOU**:
+  - The `RippleState` balance between the borrower and the `Issuer` is decreased.
+  - The `RippleState` balance between the `Vault` pseudo-account and the `Issuer` is increased.
+  - The `RippleState` balance between the destination account for fees (`LoanBroker.Owner` or `LoanBroker` pseudo-account) and the `Issuer` is increased.
+- **If the asset is an MPT**:
+  - The `MPTAmount` in the borrower's `MPToken` object is decreased.
+  - The `MPTAmount` in the `Vault` pseudo-account's `MPToken` object is increased.
+  - The `MPTAmount` in the destination account for fees (`LoanBroker.Owner` or `LoanBroker` pseudo-account) `MPToken` object is increased.
 
 ##### 3.2.4.6 Invariants
 

From 789ee888095c8c8bdb1e8517fda812710c91c621 Mon Sep 17 00:00:00 2001
From: Vito <5780819+Tapanito@users.noreply.github.com>
Date: Thu, 23 Oct 2025 15:33:15 +0200
Subject: [PATCH 67/77] adds extra guards to loanset transaction

---
 XLS-0066d-lending-protocol/README.md | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md
index e670bf17..963a4802 100644
--- a/XLS-0066d-lending-protocol/README.md
+++ b/XLS-0066d-lending-protocol/README.md
@@ -975,6 +975,11 @@ The account specified in the `Account` field pays the transaction fee.
 - `PaymentInterval` is less than `60` seconds.
 - `GracePeriod` is greater than the `PaymentInterval`.
 
+- The combination of `PrincipalRequested`, `InterestRate`, `PaymentTotal`, and `PaymentInterval` results in a total interest amount that is zero or negative due to precision limitations. This can happen if the loan term is too short or the principal is too small for any interest to accrue to a representable value.
+- 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. This prevents the loan from being amortized correctly.
+- The calculated periodic payment is so small that it rounds down to zero when adjusted for the asset's precision (e.g., drops for XRP, or the smallest unit of an IOU/MPT).
+- The rounding of the periodic payment (due to asset precision) is significant enough that the total number of payments required to settle the loan is less than the specified `PaymentTotal`.
+
 - Insufficient assets in the Vault:
 
   - `Vault(LoanBroker(LoanBrokerID).VaultID).AssetsAvailable` < `Loan.PrincipalRequested`.
@@ -1231,16 +1236,15 @@ A `LoanPay` transaction is processed according to a defined workflow that evalua
 
 **Source of Truth**: The formulas in this section describe the financial theory for a conceptual understanding. The [pseudo-code](#3244-transaction-pseudo-code) describes the required implementation logic, which includes critical adjustments for rounding. **Implementations must follow the pseudo-code.**
 
-
 **Payment Rounding**: The `Loan.PeriodicPayment` field stores a high-precision value. However, payments must be made in the discrete, indivisible units of the loan's asset (e.g., XRP drops, whole MPTs, or the smallest unit of an IOU). Therefore, the borrower is expected to make a periodic payment that is rounded **up** to the asset's scale.
 
 For example:
+
 - If a loan is denominated in an asset that only supports whole numbers (like an MPT) and the calculated `Loan.PeriodicPayment` is `10.12345`, the borrower is expected to pay `11`.
 - If a loan is denominated in a USD IOU with two decimal places of precision and the `Loan.PeriodicPayment` is `25.54321`, the borrower is expected to pay `25.55`.
 
 This rounded-up value, plus any applicable service fees, constitutes the minimum payment for a single period.
 
-
 Each payment consists of three components:
 
 - **Principal**: The portion that reduces the outstanding loan principle.

From b9768478dc034da1d673533621fc70fae5ffed23 Mon Sep 17 00:00:00 2001
From: Vito <5780819+Tapanito@users.noreply.github.com>
Date: Thu, 23 Oct 2025 15:37:22 +0200
Subject: [PATCH 68/77] adds missing fullpayment flag in pseudo-code

---
 XLS-0066d-lending-protocol/README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md
index 963a4802..b7842298 100644
--- a/XLS-0066d-lending-protocol/README.md
+++ b/XLS-0066d-lending-protocol/README.md
@@ -1750,7 +1750,7 @@ function make_payment(amount, currentTime) -> (principalPaid, interestPaid, valu
     let fullPaymentAmount = fullPrincipal + fullInterest + fullManagementFee + loan.closePaymentFee
 
     # If the payment is equal or higher than full payment amount and there is more than one payment remaining, make a full payment
-    if amount >= fullPaymentAmount && loan.paymentsRemaining > 1:
+    if is_set(tfLoanFullPayment) && amount >= fullPaymentAmount && loan.paymentsRemaining > 1:
         let totalInterestOutstanding = loan.totalValueOutstanding - loan.principalOutstanding - loan.managementFeeOutstanding
         let loanValueChange = fullInterest - totalInterestOutstanding
 

From d644b04e859709603e4f6a4f6ac401684dbe7cea Mon Sep 17 00:00:00 2001
From: Vito <5780819+Tapanito@users.noreply.github.com>
Date: Fri, 24 Oct 2025 16:28:21 +0200
Subject: [PATCH 69/77] adds missing fee

---
 XLS-0066d-lending-protocol/README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md
index b7842298..f2ff494c 100644
--- a/XLS-0066d-lending-protocol/README.md
+++ b/XLS-0066d-lending-protocol/README.md
@@ -1498,7 +1498,7 @@ $$
 And the theoretical `totalInterestOutstanding` is the portion of that total value that is not principal.
 
 $$
-\text{Theoretical } totalInterestOutstanding = \text{Theoretical } totalValueOutstanding - principalOutstanding
+\text{Theoretical } totalInterestOutstanding = \text{Theoretical } totalValueOutstanding - principalOutstanding - managementFeeOutstanding
 $$
 
 **Important Note**: These formulas describe the theoretical financial model. The actual values stored on the `Loan` ledger object (`TotalValueOutstanding`, `PrincipalOutstanding`, `ManagementFeeOutstanding`) are continuously adjusted during payment processing to account for asset-specific rounding rules. Therefore, implementations **must not** rely on these formulas to derive the live state of a loan. The stored ledger fields are the single source of truth.

From 77d789c53d7cc19b16c97891ffe793e1d51019d6 Mon Sep 17 00:00:00 2001
From: Vito <5780819+Tapanito@users.noreply.github.com>
Date: Fri, 24 Oct 2025 16:33:00 +0200
Subject: [PATCH 70/77] adds missing fee

---
 XLS-0066d-lending-protocol/README.md | 15 ++++++++++++++-
 1 file changed, 14 insertions(+), 1 deletion(-)

diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md
index f2ff494c..ff32e2ee 100644
--- a/XLS-0066d-lending-protocol/README.md
+++ b/XLS-0066d-lending-protocol/README.md
@@ -1498,7 +1498,20 @@ $$
 And the theoretical `totalInterestOutstanding` is the portion of that total value that is not principal.
 
 $$
-\text{Theoretical } totalInterestOutstanding = \text{Theoretical } totalValueOutstanding - principalOutstanding - managementFeeOutstanding
+\text{Theoretical } totalInterestOutstanding = \text{Theoretical } totalValueOutstanding - principalOutstanding
+$$
+
+And the theoretical `managementFeeOutstanding` is the portion of the interest that is due to the loan broker.
+
+$$
+\text{Theoretical } managementFeeOutstanding = \text{Theoretical } totalInterestOutstanding \times managementFeeRate
+$$
+
+The true `totalInterestOutstanding` is then updated to reflect this.
+
+
+$$
+\text{Theoretical } totalInterestOutstanding = \text{Theoretical } totalInterestOutstanding - managementFeeOutstanding
 $$
 
 **Important Note**: These formulas describe the theoretical financial model. The actual values stored on the `Loan` ledger object (`TotalValueOutstanding`, `PrincipalOutstanding`, `ManagementFeeOutstanding`) are continuously adjusted during payment processing to account for asset-specific rounding rules. Therefore, implementations **must not** rely on these formulas to derive the live state of a loan. The stored ledger fields are the single source of truth.

From 853563d995533232b11ed4ceeb5c67da0e6c976e Mon Sep 17 00:00:00 2001
From: Vito <5780819+Tapanito@users.noreply.github.com>
Date: Mon, 27 Oct 2025 13:20:32 +0100
Subject: [PATCH 71/77] adds exclusivit check for overpayment and earrly
 payment flags

---
 XLS-0066d-lending-protocol/README.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md
index ff32e2ee..7ebca68e 100644
--- a/XLS-0066d-lending-protocol/README.md
+++ b/XLS-0066d-lending-protocol/README.md
@@ -1851,6 +1851,7 @@ function make_payment(amount, currentTime) -> (principalPaid, interestPaid, valu
 - The loan is already fully paid (`Loan.PaymentRemaining` is `0` or `Loan.TotalValueOutstanding` is `0`).
 - The `tfLoanOverpayment` flag is set on the transaction, but the `lsfLoanOverpayment` flag is not set on the `Loan` object.
 - The `tfLoanFullPayment` flag is set, but only one payment remains on the loan (`Loan.PaymentRemaining` is `1`).
+- Both `tfLoanOverpayment` and `tfLoanFullPayment` transaction flags are specified.
 
 - If the payment is late (`LastLedgerCloseTime >= Loan.NextPaymentDueDate`):
 

From 732b33e3eb1e05e77500367efb97eebbdcff3a93 Mon Sep 17 00:00:00 2001
From: Vito <5780819+Tapanito@users.noreply.github.com>
Date: Tue, 28 Oct 2025 19:31:54 +0100
Subject: [PATCH 72/77] renames PreviousPaymentDate to PreviousPaymentDueDate

---
 XLS-0066d-lending-protocol/README.md | 32 +++++++++++++++-------------
 1 file changed, 17 insertions(+), 15 deletions(-)

diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md
index 7ebca68e..f4f089eb 100644
--- a/XLS-0066d-lending-protocol/README.md
+++ b/XLS-0066d-lending-protocol/README.md
@@ -464,7 +464,7 @@ The `LoanID` is calculated as follows:
 | `StartDate`                |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                             `CurrentLedgerTimestamp`                              | The timestamp of when the Loan started [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time).                        |
 | `PaymentInterval`          |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                                       `N/A`                                       | Number of seconds between Loan payments.                                                                                                                              |
 | `GracePeriod`              |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                                       `N/A`                                       | The number of seconds after the Loan's Payment Due Date that the Loan can be Defaulted.                                                                               |
-| `PreviousPaymentDate`      |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `UINT32`    |                                        `0`                                        | The timestamp of when the previous payment was made in [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time).        |
+| `PreviousPaymentDueDate`      |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `UINT32`    |                                        `0`                                        | The timestamp of when the previous payment was made in [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time).        |
 | `NextPaymentDueDate`       |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `UINT32`    |                   `LoanSet.StartDate + LoanSet.PaymentInterval`                   | The timestamp of when the next payment is due in [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time).              |
 | `PaymentRemaining`         |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `UINT32`    |                              `LoanSet.PaymentTotal`                               | The number of payments remaining on the Loan.                                                                                                                         |
 | `TotalValueOutstanding`    |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `NUMBER`    |                             `TotalValueOutstanding()`                             | The total outstanding value of the Loan, including all fees and interest.                                                                                             |
@@ -1196,15 +1196,15 @@ The transaction deletes an existing `Loan` object.
 
     - Update the `Loan` object:
 
-    - `Loan(LoanID).Flags &= ~lsfLoanImpaired`
-    - `CandidateDueDate = max(Loan.PreviousPaymentDate, Loan.StartDate) + Loan.PaymentInterval`
+      - Unset `lsfLoanImpaired` flag
+      - `CandidateDueDate = max(Loan.PreviousPaymentDueDate, Loan.StartDate) + Loan.PaymentInterval`
 
-    - If `CandidateDueDate > currentTime` (the loan was unimpaired within the payment interval):
+      - If `CandidateDueDate > currentTime` (the loan was unimpaired within the payment interval):
 
-      - `Loan(LoanID).NextPaymentDueDate = CandidateDueDate`
+        - `Loan(LoanID).NextPaymentDueDate = CandidateDueDate`
 
-    - If `CandidateDueDate <= currentTime` (the loan was unimpaired after the original payment due date):
-      - `Loan(LoanID).NextPaymentDueDate = currentTime + Loan(LoanID).PaymentInterval`
+      - If `CandidateDueDate <= currentTime` (the loan was unimpaired after the original payment due date):
+        - `Loan(LoanID).NextPaymentDueDate = currentTime + Loan(LoanID).PaymentInterval`
 
 ##### 3.2.3.3 Invariants
 
@@ -1449,7 +1449,7 @@ $$
 The interest accrued since the last payment is calculated pro-rata:
 
 $$
-secondsSinceLastPayment = lastLedgerCloseTime - max(Loan.previousPaymentDate, Loan.startDate)
+secondsSinceLastPayment = lastLedgerCloseTime - max(Loan.PreviousPaymentDueDate, Loan.startDate)
 $$
 
 $$
@@ -1539,7 +1539,7 @@ function compute_late_payment_interest(currentTime) -> (lateInterest, management
 function compute_full_payment(currentTime) -> (principal, interest, fee):
     let truePrincipalOutstanding = principal_outstanding_from_periodic()
     let periodicRate = (loan.interestRate * loan.paymentInterval) / (365 * 24 * 60 * 60)
-    let secondsSinceLastPayment = lastLedgerCloseTime() - max(loan.previousPaymentDate, loan.startDate)
+    let secondsSinceLastPayment = lastLedgerCloseTime() - max(loan.PreviousPaymentDueDate, loan.startDate)
 
     let accruedInterest = truePrincipalOutstanding * periodicRate * (secondsSinceLastPayment / loan.paymentInterval)
     let prepaymentPenalty = truePrincipalOutstanding * loan.closeInterestRate
@@ -1745,7 +1745,7 @@ function make_payment(amount, currentTime) -> (principalPaid, interestPaid, valu
             return "insufficient amount paid" error
 
         loan.paymentsRemaining = loan.paymentsRemaining - 1
-        loan.previousPaymentDate = loan.nextPaymentDueDate
+        loan.PreviousPaymentDueDate = loan.nextPaymentDueDate
         loan.nextPaymentDueDate = loan.nextPaymentDueDate + loan.paymentInterval
         loan.principalOutstanding = loan.principalOutstanding - principal
         loan.managementFeeOutstanding = loan.managementFeeOutstanding - managementFee
@@ -1799,7 +1799,7 @@ function make_payment(amount, currentTime) -> (principalPaid, interestPaid, valu
         loan.paymentsRemaining = loan.paymentsRemaining - 1
 
         loan.nextPaymentDueDate = loan.nextPaymentDueDate + loan.paymentInterval
-        loan.previousPaymentDate = loan.nextPaymentDueDate - loan.paymentInterval
+        loan.PreviousPaymentDueDate = loan.nextPaymentDueDate - loan.paymentInterval
 
         totalPaid = totalPaid + paymentAmount
         totalPrincipalPaid = totalPrincipalPaid + principal
@@ -1853,11 +1853,11 @@ function make_payment(amount, currentTime) -> (principalPaid, interestPaid, valu
 - The `tfLoanFullPayment` flag is set, but only one payment remains on the loan (`Loan.PaymentRemaining` is `1`).
 - Both `tfLoanOverpayment` and `tfLoanFullPayment` transaction flags are specified.
 
-- If the payment is late (`LastLedgerCloseTime >= Loan.NextPaymentDueDate`):
+- If the payment is late (`LastLedgerCloseTime > Loan.NextPaymentDueDate`):
 
   - The `Amount` is less than the calculated `totalDue` for a late payment, which is `periodicPayment + loanServiceFee + latePaymentFee + latePaymentInterest`.
 
-- If the payment is on-time (`LastLedgerCloseTime < Loan.NextPaymentDueDate`):
+- If the payment is on-time (`LastLedgerCloseTime <= Loan.NextPaymentDueDate`):
 
   - The `Amount` is less than the calculated `totalDue` for a periodic payment, which is `periodicPayment + loanServiceFee`.
 
@@ -1906,7 +1906,8 @@ First, the system determines the final destination of all funds.
 
 **2. `Loan` Object State Changes**
 
-The `Loan` object is updated to reflect the payment.
+The `Loan` object is updated to reflect the payment
+.
 
 - If the loan was impaired (`lsfLoanImpaired` flag was set), the flag is cleared.
 
@@ -1923,7 +1924,7 @@ The `Loan` object is updated to reflect the payment.
   - If an overpayment occurred, `TotalValueOutstanding` is further adjusted by the `valueChange` resulting from re-amortization. It is **not** adjusted for `valueChange` from late payment interest, as that interest was not part of the original loan value.
   - `PaymentRemaining` is decreased by `1` for each full periodic payment cycle covered.
   - `NextPaymentDueDate` is advanced by `Loan.PaymentInterval` for each periodic payment cycle covered.
-  - `PreviousPaymentDate` is updated.
+  - `PreviousPaymentDueDate` is updated.
   - If an overpayment was made:
     - `PeriodicPayment` is recalculated based on the new outstanding principal and remaining term.
 
@@ -1935,6 +1936,7 @@ The `LoanBroker` and `Vault` objects are updated to reflect the new accounting s
 
   - `LoanBroker.DebtTotal` is decreased by `totalToVault` (the principal and interest paid back).
   - If the payment resulted in a `valueChange` from an overpayment or early full repayment, `LoanBroker.DebtTotal` is adjusted by that `valueChange`. It is **not** adjusted for `valueChange` from late payment interest, as this represents a penalty paid directly to the vault, not an alteration of the original debt schedule.
+    - `LoanBroker.DebtToal + valueChange`
   - If fees were directed to the cover pool, `LoanBroker.CoverAvailable` increases by `totalToBroker`.
 
 - **`Vault` Updates**:

From a383bf36ce6dfcd1f15e27054e82adf0afce2e86 Mon Sep 17 00:00:00 2001
From: Vito <5780819+Tapanito@users.noreply.github.com>
Date: Thu, 6 Nov 2025 17:54:38 +0100
Subject: [PATCH 73/77] adds updated LoanPay logic

---
 XLS-0066d-lending-protocol/README.md | 150 ++++++++++++---------------
 1 file changed, 68 insertions(+), 82 deletions(-)

diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md
index f4f089eb..b1d83f58 100644
--- a/XLS-0066d-lending-protocol/README.md
+++ b/XLS-0066d-lending-protocol/README.md
@@ -464,7 +464,7 @@ The `LoanID` is calculated as follows:
 | `StartDate`                |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                             `CurrentLedgerTimestamp`                              | The timestamp of when the Loan started [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time).                        |
 | `PaymentInterval`          |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                                       `N/A`                                       | Number of seconds between Loan payments.                                                                                                                              |
 | `GracePeriod`              |       `No`       |   `Yes`   | :heavy_check_mark: | `number`  |   `UINT32`    |                                       `N/A`                                       | The number of seconds after the Loan's Payment Due Date that the Loan can be Defaulted.                                                                               |
-| `PreviousPaymentDueDate`      |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `UINT32`    |                                        `0`                                        | The timestamp of when the previous payment was made in [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time).        |
+| `PreviousPaymentDueDate`   |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `UINT32`    |                                        `0`                                        | The timestamp of when the previous payment was made in [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time).        |
 | `NextPaymentDueDate`       |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `UINT32`    |                   `LoanSet.StartDate + LoanSet.PaymentInterval`                   | The timestamp of when the next payment is due in [Ripple Epoch](https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-time).              |
 | `PaymentRemaining`         |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `UINT32`    |                              `LoanSet.PaymentTotal`                               | The number of payments remaining on the Loan.                                                                                                                         |
 | `TotalValueOutstanding`    |       `No`       |   `No`    | :heavy_check_mark: | `number`  |   `NUMBER`    |                             `TotalValueOutstanding()`                             | The total outstanding value of the Loan, including all fees and interest.                                                                                             |
@@ -485,7 +485,7 @@ The `Loan` object supports the following flags:
 
 ##### 2.2.2.2 TotalValueOutstanding
 
-The total outstanding value of the Loan, including all fees. To calculate the outstanding interest portion, use this formula: `TotalInterestOutstanding = TotalValueOutstanding - PrincipalOutstanding - ManagementFeeOutstanding`.
+The total outstanding value of the Loan, including management fee charges against the interest. To calculate the outstanding interest portion, use this formula: `TotalInterestOutstanding = TotalValueOutstanding - PrincipalOutstanding - ManagementFeeOutstanding`.
 
 ##### 2.2.2.3 PrincipalOutstanding
 
@@ -497,7 +497,7 @@ The remaining Management Fee owed to the LoanBroker. This amount decreases each
 
 ##### 2.2.2.5 PeriodicPayment
 
-The periodic payment amount represents the precise sum the Borrower must pay during each payment cycle. For practical implementation, this value should be rounded UP when processing payments. The system automatically recalculates the PeriodicPayment following any overpayment by the borrower. For instance, when dealing with MPT loans, the calculated `PeriodicPayment` may be `10.251`. However, since MPTs only support whole number representations, the borrower would need to pay `12` units. The system maintains the precise periodic payment value at maximum accuracy since it is frequently referenced throughout loan payment computations.
+The periodic payment amount represents the precise sum the Borrower must pay during each payment cycle. For practical implementation, this value should be rounded UP when processing payments. The system automatically recalculates the PeriodicPayment following any overpayment by the borrower. For instance, when dealing with MPT loans, the calculated `PeriodicPayment` may be `10.251`. However, since MPTs only support whole number representations, the borrower would need to pay `11` units. The system maintains the precise periodic payment value at maximum accuracy since it is frequently referenced throughout loan payment computations.
 
 #### 2.2.3 Ownership
 
@@ -515,10 +515,24 @@ The `Loan` object costs one owner reserve for the `Borrower`.
 The loan's financial state is tracked through three key components:
 
 - **PrincipalOutstanding**: Represents the remaining principal balance that the borrower must repay to satisfy the original loan amount.
-- **TotalValueOutstanding**: Encompasses the complete remaining loan obligation, comprising both the outstanding principal and all scheduled interest payments based on the original amortization schedule. This value excludes any additional interest charges resulting from late payments.
-- **InterestOutstanding**: The total scheduled interest remaining on the loan, derived as `TotalValueOutstanding - PrincipalOutstanding`.
+- **TotalValueOutstanding**: Encompasses the complete remaining loan obligation, comprising the outstanding principal, all scheduled interest payments based on the original amortization schedule and the management fee paid on the interest. This value excludes any additional interest charges resulting from late payments, overpayments of full payments.
+- **InterestOutstanding**: The total scheduled interest (including fee) remaining on the loan, derived as `TotalValueOutstanding - PrincipalOutstanding`.
 
-**Asset-Specific Precision Handling**: For discrete asset types (MPTs and XRP denominated in drops), both `TotalValueOutstanding` and `PrincipalOutstanding` values are truncated to whole numbers to ensure compatibility with the underlying asset precision requirements.
+**Asset-Specific Precision Handling**: Different asset types on the XRP Ledger have varying levels of precision that directly impact loan value calculations:
+
+- **XRP (Drops)**: Only supports whole number values (1 drop = 0.000001 XRP)
+- **MPTs (Multi-Purpose Tokens)**: Only support whole number values
+- **IOUs**: Support up to 16 significant decimal digits
+
+For loans denominated in discrete asset types (XRP drops and MPTs), all monetary values must be rounded to whole numbers. This rounding requirement means that:
+
+1. **`TotalValueOutstanding`** is always rounded **up** to the nearest precision unit of an asset. This ensures the borrower pays at least the full theoretical value, preventing the loan from becoming underfunded due to rounding losses.
+
+2. **`PrincipalOutstanding`** and **`ManagementFeeOutstanding`** are rounded to the nearest even number after each payment to avoid over-deducting from the borrower.
+
+3. Due to the cumulative effect of rounding across multiple payment cycles, these on-ledger values may deviate by up to one asset unit from their theoretical mathematical values at any given time.
+
+**Important**: Implementations must **not** recalculate these values from the theoretical formulas during payment processing. The stored ledger values are the authoritative source of truth. The pseudo-code in [Section 3.2.4.4](#3244-transaction-pseudo-code) demonstrates how to properly handle these rounding discrepancies while maintaining loan integrity.
 
 **Late Payment Interest Treatment**: Late payment penalties and additional interest charges are calculated and collected separately from the core loan value. These charges do not modify the `TotalValueOutstanding` calculation, which remains anchored to the original scheduled payment terms.
 
@@ -545,7 +559,7 @@ The transaction creates a new `LoanBroker` object or updates an existing one.
 | `LoanBrokerID`         |                    |    `No`     | `string`  |   `HASH256`   |     `N/A`     | The Loan Broker ID that the transaction is modifying.                                                                                              |
 | `Flags`                |                    |    `Yes`    | `string`  |   `UINT32`    |       0       | Specifies the flags for the LoanBroker.                                                                                                            |
 | `Data`                 |                    |    `Yes`    | `string`  |    `BLOB`     |     None      | Arbitrary metadata in hex format. The field is limited to 256 bytes.                                                                               |
-| `ManagementFeeRate`    |                    |    `No`     | `number`  |   `UINT16`    |       0       | The 1/10th basis point fee charged by the Lending Protocol Owner. Valid values are between 0 and 10000 inclusive.                                  |
+| `ManagementFeeRate`    |                    |    `No`     | `number`  |   `UINT16`    |       0       | The 1/10th basis point fee charged by the Lending Protocol Owner. Valid values are between 0 and 10000 inclusive (1% - 10%).                       |
 | `DebtMaximum`          |                    |    `Yes`    | `number`  |   `NUMBER`    |       0       | The maximum amount the protocol can owe the Vault. The default value of 0 means there is no limit to the debt. Must not be negative.               |
 | `CoverRateMinimum`     |                    |    `No`     | `number`  |   `UINT32`    |       0       | The 1/10th basis point `DebtTotal` that the first loss capital must cover. Valid values are between 0 and 100000 inclusive.                        |
 | `CoverRateLiquidation` |                    |    `No`     | `number`  |   `UINT32`    |       0       | The 1/10th basis point of minimum required first loss capital liquidated to cover a Loan default. Valid values are between 0 and 100000 inclusive. |
@@ -1509,7 +1523,6 @@ $$
 
 The true `totalInterestOutstanding` is then updated to reflect this.
 
-
 $$
 \text{Theoretical } totalInterestOutstanding = \text{Theoretical } totalInterestOutstanding - managementFeeOutstanding
 $$
@@ -1521,8 +1534,15 @@ $$
 The following is the pseudo-code for handling a Loan payment transaction.
 
 ```
+function get_periodic_rate() -> (periodicRate):
+  # Convert annual rate to rate per payment interval
+  let SECONDS_PER_YEAR = 365 * 24 * 60 * 60
+  let periodicRate = (loan.interestRate * loan.paymentInterval) / SECONDS_PER_YEAR
+
+  return periodicRate
+
 function compute_periodic_payment(principalOutstanding) -> (periodicPayment):
-    let periodicRate = (loan.interestRate * loan.paymentInterval) / (365 * 24 * 60 * 60)
+    let periodicRate = get_periodic_rate()
     let raisedRate = (1 + periodicRate)^loan.paymentsRemaining
 
     return principalOutstanding * (periodicRate * raisedRate) / (raisedRate - 1)
@@ -1538,7 +1558,7 @@ function compute_late_payment_interest(currentTime) -> (lateInterest, management
 
 function compute_full_payment(currentTime) -> (principal, interest, fee):
     let truePrincipalOutstanding = principal_outstanding_from_periodic()
-    let periodicRate = (loan.interestRate * loan.paymentInterval) / (365 * 24 * 60 * 60)
+    let periodicRate = get_periodic_rate()
     let secondsSinceLastPayment = lastLedgerCloseTime() - max(loan.PreviousPaymentDueDate, loan.startDate)
 
     let accruedInterest = truePrincipalOutstanding * periodicRate * (secondsSinceLastPayment / loan.paymentInterval)
@@ -1553,7 +1573,7 @@ function compute_full_payment(currentTime) -> (principal, interest, fee):
 function principal_outstanding_from_periodic() -> (principalOutstanding):
     # Given the outstanding principal we can calculate the periodic payment
     # Equally, given the periodic payment we can calculate the principal outstanding at the current time
-    let periodicRate = (loan.interestRate * loan.paymentInterval) / (365 * 24 * 60 * 60)
+    let periodicRate = get_periodic_rate()
 
     # If the loan is zero-interest, the outstanding principal is simply periodicPayment * paymentsRemaining
     if periodicRate == 0:
@@ -1565,9 +1585,9 @@ function principal_outstanding_from_periodic() -> (principalOutstanding):
     return loan.periodicPayment / factor
 
 # This function calculates what the loan state should be given the periodic payment and remaining payments
-function calculate_true_loan_state() -> (principalOutstanding, interestOutstanding, managementFeeOutstanding):
+function calculate_true_loan_state(paymentsRemaining) -> (principalOutstanding, interestOutstanding, managementFeeOutstanding):
     let truePrincipalOutstanding = principal_outstanding_from_periodic()
-    let trueInterestOutstanding = (loan.periodicPayment * loan.paymentsRemaining) - truePrincipalOutstanding
+    let trueInterestOutstanding = (loan.periodicPayment * paymentsRemaining) - truePrincipalOutstanding
     let trueManagementFeeOutstanding = (trueInterestOutstanding * loan.loanbroker.managementFeeRate)
 
     # Exclude the management fee from the interest rate
@@ -1576,26 +1596,23 @@ function calculate_true_loan_state() -> (principalOutstanding, interestOutstandi
     return (truePrincipalOutstanding, trueInterestOutstanding, trueManagementFeeOutstanding)
 
 function calculate_payment_breakdown(principalOutstanding) -> (principal, interest):
-    let periodicRate = (loan.interestRate * loan.paymentInterval) / (365 * 24 * 60 * 60)
+    let periodicRate = get_periodic_rate()
 
     if periodicRate == 0:
-        return (principalOutstanding / loan.paymentsRemaining, 0)
+        return (principalOutstanding / paymentsRemaining, 0)
 
     let interest = principalOutstanding * periodicRate
     let principal = loan.periodicPayment - interest
 
     return (principal, interest)
 
-function calculate_rounded_principal_payment(truePrincipalOutstanding, truePrincipalPayment) -> (principal):
-    # The diff captures by how much we deviated from the true principal value
-    # If the diff is negative, we need to slow down repayment as we overpaid principalOutstanding
-    # If the diff is positive, we need to speed up the repayment as we underpaid the principalOutstanding
-    let diff = (loan.principalOutstanding - truePrincipalOutstanding).round(loan.loanScale, DOWN)
-    let roundedPrincipalPayment = (truePrincipalPayment + diff).round(loan.loanScale, DOWN)
+function calculate_rounded_principal_payment(truePrincipalOutstanding) -> (principal):
+    let roundedPrincipalPayment = (loan.principalOutstanding - truePrincipalOutstanding).round(loan.loanScale, DOWN)
 
     # Ensure we do not have a negative principal payment
     roundedPrincipalPayment = max(0, roundedPrincipalPayment)
 
+    # Ensure we do not exceed the outstanding principal amount
     return min(roundedPrincipalPayment, loan.principalOutstanding)
 
 
@@ -1605,32 +1622,17 @@ function calculate_rounded_interest_breakdown(roundedPrincipalPayment, trueInter
 
     let loanInterestOutstanding = loan.totalValueOutstanding - loan.principalOutstanding - loan.managementFeeOutstanding
 
-    # Calculate the accumulated rounding error for the interest portion.
-    # If diffInterest is negative, we have historically overpaid interest; we need to pay less now.
-    # If diffInterest is positive, we have historically underpaid interest; we need to pay more now to catch up.
-    let diffInterest = (loanInterestOutstanding - trueInterestOutstanding).round(loan.loanScale, DOWN)
-    let roundedInterestPayment = roundedPeriodicPayment - roundedPrincipalPayment
-
-    # Apply historical rounding error: if we underpaid interest previously (positive diff), pay more now.
-    roundedInterestPayment = roundedInterestPayment + diffInterest
-
-    # Ensure the adjusted interest payment doesn't cause the total payment to exceed the periodic amount.
-    # This caps the interest at the remaining portion of the periodic payment.
-    roundedInterestPayment = min(roundedPeriodicPayment - roundedPrincipalPayment, roundedInterestPayment)
+    # The interest due is the difference between our current value, and the future value after the next payment
+    let roundedInterestPayment = (loanInterestOutstanding - trueInterestOutstanding).round(loan.loanScale, HALF_EVEN)
 
     # Since diffInterest can be negative, ensure the interest payment itself is not negative.
     roundedInterestPayment = max(0, roundedInterestPayment)
 
-    # We have calculated the gross interest payment, we can now calculate the management fee portion.
-
-    # Calculate the accumulated rounding error for the management fee portion.
-    # If diffManagementFee is negative, we have overpaid the fee; pay less now.
-    # If diffManagementFee is positive, we have underpaid the fee; pay more now.
-    let diffManagementFee = (loan.managementFeeOutstanding - trueManagementFeeOutstanding).round(loan.loanScale, DOWN)
-    let roundedManagementFee = (roundedInterestPayment * loan.loanbroker.managementFeeRate).round(loan.loanScale, DOWN)
+    # This caps the interest at the remaining portion of the periodic payment.
+    roundedInterestPayment = min(roundedPeriodicPayment - roundedPrincipalPayment, roundedInterestPayment)
 
-    # Apply historical rounding error to the current fee payment.
-    roundedManagementFee = roundedManagementFee + diffManagementFee
+    # The Management Fee is the difference between our current value, and the future value after the next payment
+    let roundedManagementFee = (loan.managementFeeOutstanding - trueManagementFeeOutstanding).round(loan.loanScale, HALF_EVEN)
 
     # Since diffManagementFee can be negative, ensure the final fee payment is not negative.
     roundedManagementFee = max(0, roundedManagementFee)
@@ -1638,32 +1640,6 @@ function calculate_rounded_interest_breakdown(roundedPrincipalPayment, trueInter
     # Ensure the fee payment does not exceed the total outstanding management fee.
     roundedManagementFee = min(roundedManagementFee, loan.managementFeeOutstanding)
 
-    # The final net interest payment is the gross interest minus the management fee. Ensure it's not negative.
-    roundedInterestPayment = max(0, roundedInterestPayment - roundedManagementFee)
-
-    # Finally, ensure the net interest payment does not exceed the total outstanding interest.
-    roundedInterestPayment = min(loanInterestOutstanding, roundedInterestPayment)
-
-    # --- Final Safety Check ---
-    # Compute if the sum of components exceeds the periodic payment amount due to rounding adjustments.
-    let excess = (roundedPeriodicPayment - roundedPrincipalPayment - roundedInterestPayment - roundedManagementFee)
-
-    # If the sum is too large (excess is negative), reduce the components to match the total.
-    # First, take as much excess as possible from the interest portion.
-    if excess < 0:
-        let part = min(roundedInterestPayment, abs(excess))
-        roundedInterestPayment = roundedInterestPayment - part
-        excess = excess + part
-
-    # If there is still an excess, take as much as possible from the fee portion.
-    if excess < 0:
-        let part = min(roundedManagementFee, abs(excess))
-        roundedManagementFee = roundedManagementFee - part
-        excess = excess + part
-
-    # if excess is still negative, this implies that the principal was calculated wrong.
-    # it should not happen, but if it does this is a critical failure.
-
     return (roundedInterestPayment, roundedManagementFee)
 
 function compute_payment_due(roundedPeriodicPayment) -> (principal, interest, managementFee):
@@ -1672,14 +1648,15 @@ function compute_payment_due(roundedPeriodicPayment) -> (principal, interest, ma
         let outstandingInterest = loan.totalValueOutstanding - loan.principalOutstanding - loan.managementFeeOutstanding
         return (loan.principalOutstanding, outstandingInterest, loan.managementFeeOutstanding)
 
-    # Determine the true state of the loan, excluding rounding errors
-    let (truePrincipalOutstanding, trueInterestOutstanding, trueManagementFeeOutstanding) = calculate_true_loan_state()
-
-    # We do not need to know the interest portion
-    let (truePrincipalPayment, _) = calculate_payment_breakdown(truePrincipalOutstanding)
+    # Determine the true state of the loan, excluding rounding errors. This calculation gives where the loan state is meant to be
+    let (
+      truePrincipalOutstanding,
+      trueInterestOutstanding,
+      trueManagementFeeOutstanding
+    ) = calculate_true_loan_state(loan.paymentsRemaining - 1)
 
     # Given the true state we can calculate the rounded principal that accounts for deviation from the true state
-    let roundedPrincipalPayment = calculate_rounded_principal_payment(truePrincipalOutstanding, truePrincipalPayment)
+    let roundedPrincipalPayment = calculate_rounded_principal_payment(truePrincipalOutstanding)
 
     let (roundedInterestPayment, roundedManagementFee) = calculate_rounded_interest_breakdown(
         roundedPrincipalPayment,
@@ -1692,7 +1669,7 @@ function compute_payment_due(roundedPeriodicPayment) -> (principal, interest, ma
 
 function do_overpayment(amount) -> (valueChange):
     # Calculate the ideal, unrounded ("true") state of the loan before the overpayment.
-    let (truePrincipalOutstanding, trueInterestOutstanding, trueManagementFeeOutstanding) = calculate_true_loan_state()
+    let (truePrincipalOutstanding, trueInterestOutstanding, trueManagementFeeOutstanding) = calculate_true_loan_state(loan.paymetsRemaining)
 
     # For an accurate overpayment, we must preserve historical rounding errors.
     # These 'diff' variables capture the difference between the on-ledger rounded state and the ideal 'true' state.
@@ -1732,7 +1709,7 @@ function make_payment(amount, currentTime) -> (principalPaid, interestPaid, valu
     if loan.paymentsRemaining == 0 || loan.principalOutstanding == 0:
         return "loan complete" error
 
-    # The payment is late
+    # ======== STEP 1: Process Late Payment ======== #
     if loan.nextPaymentDueDate < currentTime:
         let (principal, interest, managementFee) = compute_payment_due(amount)
         let (lateInterest, lateManagementFee) = compute_late_payment_interest(currentTime)
@@ -1749,6 +1726,7 @@ function make_payment(amount, currentTime) -> (principalPaid, interestPaid, valu
         loan.nextPaymentDueDate = loan.nextPaymentDueDate + loan.paymentInterval
         loan.principalOutstanding = loan.principalOutstanding - principal
         loan.managementFeeOutstanding = loan.managementFeeOutstanding - managementFee
+        
         # we do not adjust the total value by late interst or late managementFee as these were not included in the initial total value
         loan.totalValueOutstanding = loan.totalValueOutstanding - (principal + interest + managementFee)
 
@@ -1758,7 +1736,7 @@ function make_payment(amount, currentTime) -> (principalPaid, interestPaid, valu
             lateInterest,                                                 # The value of the loan increases by the lateInterest amount
             totalManagementFee + loan.serviceFee + loan.latePaymentFee    # The total fee paid for a loan payment
         )
-
+    # ======== STEP 2: Process Full Payment ======== #
     let (fullPrincipal, fullInterest, fullManagementFee) = compute_full_payment(currentTime)
     let fullPaymentAmount = fullPrincipal + fullInterest + fullManagementFee + loan.closePaymentFee
 
@@ -1778,13 +1756,13 @@ function make_payment(amount, currentTime) -> (principalPaid, interestPaid, valu
             loanValueChange,                    # A full payment changes the total value of the loan
             fullManagementFee + loan.closePaymentFee   # An early payment pays a specific closePaymentFee
         )
-
+    # ======== STEP 3: Process Regular Payment(s) ======== #
     # Handle regular payments and overpayments
     let totalPaid = 0
     let (totalPrincipalPaid, totalInterestPaid, totalFeePaid) = (0, 0, 0)
 
-    # Process regular periodic payments
     while totalPaid < amount && loan.paymentsRemaining > 0:
+        # calculate the payment principal, interest and fee
         let (principal, interest, managementFee) = compute_payment_due(loan.periodicPayment.round(loan.loanScale, UP))
         let paymentAmount = principal + interest + managementFee + loan.serviceFee
 
@@ -1806,21 +1784,29 @@ function make_payment(amount, currentTime) -> (principalPaid, interestPaid, valu
         totalInterestPaid = totalInterestPaid + interest
         totalFeePaid = totalFeePaid + managementFee + loan.serviceFee
 
+    # ======== STEP 4: Process Overpayment ======== #
     let loanValueChange = 0
     # Handle overpayment if there are remaining payments, the loan supports overpayments, and there are funds remaining
     if loan.paymentsRemaining > 0 && is_set(loan.lsfLoanOverpayment) && is_set(tfLoanOverpayment) && totalPaid < amount:
         let overpaymentAmount = min(loan.principalOutstanding, amount - totalPaid)
-
+    
+        # ======== STEP 4.1: Determine Interest and Fee on Overpayment ======== #
+  
+        # overpayment amount is charged an interest that goes to the vault
         let overpaymentInterest = overpaymentAmount * loan.overpaymentInterestRate
-        let overpaymentManagementFee = overpaymentInterest * loan.loanbroker.managementFeeRate
+        # and a management fee that goes to the broker charged on the interest
+        
+        let overpaymentManagementFee = overpaymentInterest * loan.loanbroker.managementFeeRate        
         overpaymentInterest = overpaymentInterest - overpaymentManagementFee
 
+        # there is a second overpayment fee that goes to the broker
         let overpaymentFee = overpaymentAmount * loan.overpaymentFee
 
         # the value of the loan will increase by the overpayment interest portion
         loanValueChange = loanValueChange + overpaymentInterest
         let overpaymentPrincipal = overpaymentAmount - overpaymentInterest - overpaymentManagementFee - overpaymentFee
 
+        # ======== STEP 4.2: Determine how the overpayment changes the value of the Loan ======== #
         # if the overpayment was not eaten by fees and interest, then apply it
         if overpaymentPrincipal > 0:
             # the valueChange is the decrease in the total interest caused by overpaying the principal
@@ -1949,7 +1935,7 @@ Finally, the actual asset transfers are executed on the ledger.
 
 - The borrower's balance is **decreased** by `totalPaidByBorrower`.
 - The `Vault` pseudo-account's balance is **increased** by `totalToVault`.
-- The `LoanBroker.Owner`'s balance OR the `LoanBroker` pseudo-account's balance is **increased** by `totalToBroker`, depending on the fee destination.
+- The `LoanBroker.Owner`'s balance OR the `LoanBroker` pseudo-account's balance is **increawsed** by `totalToBroker`, depending on the fee destination.
 
 These transfers are performed according to the asset type:
 

From 805e1f42c7e3eacd6c2a6872d9d4097d94ccf1d2 Mon Sep 17 00:00:00 2001
From: Vito <5780819+Tapanito@users.noreply.github.com>
Date: Thu, 6 Nov 2025 17:58:30 +0100
Subject: [PATCH 74/77] improves CoverRate docs

---
 XLS-0066d-lending-protocol/README.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md
index b1d83f58..daaa1c71 100644
--- a/XLS-0066d-lending-protocol/README.md
+++ b/XLS-0066d-lending-protocol/README.md
@@ -570,6 +570,7 @@ The transaction creates a new `LoanBroker` object or updates an existing one.
 
   - `Vault` object with the specified `VaultID` does not exist on the ledger.
   - The submitter `AccountRoot.Account != Vault(VaultID).Owner`.
+  - One of `CoverRateMinimum` and `CoverRateLiquidation` is zero, and the other one is not. (Either both are zero, or both are non-zero)
 
 - If `LoanBrokerID` is specified:
 

From 3c2cdc59347523fa2f80570cf838d99e4b266931 Mon Sep 17 00:00:00 2001
From: Vito <5780819+Tapanito@users.noreply.github.com>
Date: Thu, 13 Nov 2025 18:09:20 +0100
Subject: [PATCH 75/77] restores excess handling

---
 XLS-0066d-lending-protocol/README.md | 27 +++++++++++++++++++++++++++
 1 file changed, 27 insertions(+)

diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md
index daaa1c71..75576486 100644
--- a/XLS-0066d-lending-protocol/README.md
+++ b/XLS-0066d-lending-protocol/README.md
@@ -1666,6 +1666,33 @@ function compute_payment_due(roundedPeriodicPayment) -> (principal, interest, ma
         roundedPeriodicPayment
     )
 
+    # --- Final Safety Check ---
+    # Compute if the sum of components exceeds the periodic payment amount due to rounding adjustments.
+
+    let excess = (roundedPeriodicPayment - roundedPrincipalPayment - roundedInterestPayment - roundedManagementFee)
+
+    # If the sum is too large (excess is negative), reduce the components to match the total.
+    # First, take as much excess as possible from the interest portion.
+    if excess < 0:
+        let part = min(roundedInterestPayment, abs(excess))
+        roundedInterestPayment = roundedInterestPayment - part
+        excess = excess + part
+
+    # If there is still an excess, take as much as possible from the fee portion.
+    if excess < 0:
+        let part = min(roundedManagementFee, abs(excess))
+        roundedManagementFee = roundedManagementFee - part
+        excess = excess + part
+    
+    # Finally, take as much as possible from the principal portion.
+    if excess < 0:
+        let part = min(roundedPrincipalPayment, abs(excess))
+        roundedPrincipalPayment = roundedPrincipalPayment - part
+        excess = excess + part
+
+    # if excess is still negative, this implies that the principal was calculated wrong.
+    # it should not happen, but if it does this is a critical failure.
+
     return (roundedPrincipalPayment, roundedInterestPayment, roundedManagementFee)
 
 function do_overpayment(amount) -> (valueChange):

From 2f0db965d07a2e636fae41677583e4b363663c76 Mon Sep 17 00:00:00 2001
From: Vito <5780819+Tapanito@users.noreply.github.com>
Date: Fri, 14 Nov 2025 21:01:54 +0100
Subject: [PATCH 76/77] adds equation glossary

---
 XLS-0066d-lending-protocol/README.md | 415 +++++++++++++++++++++++++++
 1 file changed, 415 insertions(+)

diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md
index 75576486..2222470f 100644
--- a/XLS-0066d-lending-protocol/README.md
+++ b/XLS-0066d-lending-protocol/README.md
@@ -1995,3 +1995,418 @@ A sequential identifier for Loans associated with a LoanBroker object. This valu
 ### A-1-2. Why the `LoanBrokerCoverClawback` cannot clawback the full LoanBroker.CoverAvailable amount?
 
 The `LoanBrokerCoverClawback` transaction allows the Issuer to clawback the `LoanBroker` First-Loss Capital, specifically the `LoanBroker.CoverAvailable` amount. The transaction cannot claw back the full CoverAvailable amount because the LoanBroker must maintain a minimum level of first-loss capital to protect depositors. This minimum is calculated as `LoanBroker.DebtTotal * LoanBroker.CoverRateMinimum`. When a `LoanBroker` has active loans, a complete clawback would leave depositors vulnerable to unexpected losses. Therefore, the system ensures that a minimum amount of first-loss capital is always maintained.
+
+## A-2 Equation Glossary
+
+Throughout the specification we use numerous equations to calculate various Loan properties. This consolidated reference groups related formulas by financial concept.
+
+**All time values are expressed in seconds unless otherwise noted.**
+
+### 1. Interest Rate Conversions
+
+All interest rate calculations convert annual rates to period-specific rates.
+
+#### 1.1 Periodic Rate (Regular Payments)
+
+$$
+periodicRate = \frac{interestRate \times paymentInterval}{secondsPerYear} \quad \text{(1)}
+$$
+
+**Where:**
+
+- `periodicRate` = Interest rate for one payment period
+- `interestRate` = Annual interest rate (from `Loan.InterestRate`)
+- `paymentInterval` = `Loan.PaymentInterval` (seconds between payments)
+- `secondsPerYear` = 31,536,000 (365 × 24 × 60 × 60)
+
+#### 1.2 Late Periodic Rate (Penalty Interest)
+
+$$
+latePeriodicRate = \frac{lateInterestRate \times secondsOverdue}{secondsPerYear} \quad \text{(2)}
+$$
+
+$$
+secondsOverdue = lastLedgerCloseTime - Loan.NextPaymentDueDate \quad \text{(3)}
+$$
+
+**Where:**
+
+- `latePeriodicRate` = Penalty interest rate for overdue period
+- `lateInterestRate` = `Loan.LateInterestRate` (annual penalty rate)
+- `secondsOverdue` = Duration payment is late (in seconds)
+
+#### 1.3 Time Since Last Payment
+
+$$
+secondsSinceLastPayment = lastLedgerCloseTime - \max(Loan.PreviousPaymentDueDate, Loan.StartDate) \quad \text{(4)}
+$$
+
+**Usage:** Calculates the time elapsed since the last payment was due (or since loan origination, whichever is later). Used in early full payment calculations ([Section 5.1](#51-total-due-components)).
+
+**Why `max()`?** 
+- For the first payment period, `PreviousPaymentDueDate = 0` (undefined)
+- Taking `max(0, StartDate)` ensures we use `StartDate` for the first calculation
+- For subsequent payments, `PreviousPaymentDueDate > StartDate`, so it's used instead
+
+### 2. Standard Amortization
+
+These formulas calculate the regular payment schedule and breakdown.
+
+#### 2.1 Periodic Payment Amount
+
+$$
+raisedRate = (1 + periodicRate)^{paymentsRemaining} \quad \text{(5)}
+$$
+
+$$
+factor = \frac{periodicRate \times raisedRate}{raisedRate - 1} \quad \text{(6)}
+$$
+
+$$
+periodicPayment = PrincipalOutstanding \times factor \quad \text{(7)}
+$$
+
+**Note**: The calculation is split into `raisedRate` and `factor` for numerical stability and to enable the reverse calculation in [Section 2.3](#23-reverse-calculation-principal-from-payment).
+
+**Special Cases:**
+
+- **Final Payment:** When `paymentsRemaining = 1`, override formula with `periodicPayment = TotalValueOutstanding`
+- **Zero Interest:** When `periodicRate = 0`, simplify to `periodicPayment = PrincipalOutstanding / paymentsRemaining`
+
+#### 2.2 Payment Breakdown
+
+From the periodic payment, derive the principal and interest portions:
+
+$$
+interest = PrincipalOutstanding \times periodicRate \quad \text{(8)}
+$$
+
+$$
+principal = periodicPayment - interest \quad \text{(9)}
+$$
+
+To 
+**Note:** For zero-interest loans, `principal = PrincipalOutstanding / paymentsRemaining` and `interest = 0`.
+
+#### 2.3 Reverse Calculation: Principal from Payment
+
+Used to detect rounding errors during overpayment processing:
+
+$$
+PrincipalOutstanding = \frac{periodicPayment}{factor} \quad \text{(10)}
+$$
+
+**Where:**
+
+- `factor` is defined by formula (6)
+
+**Usage:** This inverse formula is used in the pseudo-code to calculate the "true" principal outstanding before applying rounding adjustments. See [Section 3.2.4.4](#3244-transaction-pseudo-code) `principal_outstanding_from_periodic()` function.
+
+**Special Cases:**
+
+- **Zero Interest:** When `periodicRate = 0`, simplify to:
+  $$PrincipalOutstanding = periodicPayment \times paymentsRemaining \quad \text{(11)}$$
+
+### 3. Management Fee Calculations
+
+The Loan Broker charges a management fee as a percentage of the interest earned. This fee is calculated differently depending on the payment scenario.
+
+#### 3.1 Regular Payment Management Fee
+
+For standard periodic payments:
+
+$$
+managementFee = interest \times managementFeeRate \quad \text{(12)}
+$$
+
+**Where:**
+
+- `interest` = Interest portion of a payment (from formula 8)
+- `managementFeeRate` = `LoanBroker.ManagementFeeRate`
+
+#### 3.2 Late Payment Management Fee
+
+For late payments, the management fee applies to the penalty interest:
+
+$$
+managementFee_{late} = latePaymentInterest_{gross} \times managementFeeRate \quad \text{(13)}
+$$
+
+**Where:**
+
+- `latePaymentInterest_{gross}` = Gross late payment interest (from formula 15)
+- `managementFeeRate` = `LoanBroker.ManagementFeeRate`
+
+#### 3.3 Overpayment Management Fee
+
+For overpayments, the management fee applies to the interest charged on the overpayment amount:
+
+$$
+managementFee_{overpayment} = overpaymentInterest_{gross} \times managementFeeRate \quad \text{(14)}
+$$
+
+**Where:**
+
+- `overpaymentInterest_{gross}` = Gross overpayment interest (from formula 20)
+- `managementFeeRate` = `LoanBroker.ManagementFeeRate`
+
+**Note:** In all cases, the management fee is deducted from the gross interest before calculating the net interest that accrues to the vault.
+
+### 4. Late Payment
+
+A late payment adds penalty charges to the standard periodic payment.
+
+#### 4.1 Late Payment Components
+
+$$
+totalDue = periodicPayment + loanServiceFee + latePaymentFee + latePaymentInterest_{net} \quad \text{(15)}
+$$
+
+**Where:**
+
+$$
+latePaymentInterest_{gross} = PrincipalOutstanding \times latePeriodicRate \quad \text{(16)}
+$$
+
+$$
+latePaymentInterest_{net} = latePaymentInterest_{gross} - managementFee_{late} \quad \text{(17)}
+$$
+
+- `latePeriodicRate` is defined by formula (2)
+- `managementFee_{late}` is defined by formula (13)
+
+#### 4.2 Value Impact
+
+$$
+valueChange = latePaymentInterest_{net} \quad \text{(18)}
+$$
+
+**Note:**
+
+- `valueChange > 0` (always positive for late payments)
+- Not reflected in `Loan.TotalValueOutstanding` (unanticipated value increase)
+- Applied directly to `Vault.AssetsTotal` and `LoanBroker.DebtTotal`
+
+### 5. Overpayment Processing
+
+Overpayments reduce principal early and trigger loan re-amortization.
+
+#### 5.1 Overpayment Fees and Interest
+
+$$
+overpaymentAmount = amount - (periodicPaymentsCovered \times (periodicPayment + serviceFee)) \quad \text{(19)}
+$$
+
+**Where:**
+
+- `amount` = Total amount paid by the borrower
+- `periodicPaymentsCovered` = Number of complete periodic payment cycles covered by `amount`
+- Formula applies only after all full periodic cycles are paid
+- If `periodicPaymentsCovered = 0`, the payment is rejected as insufficient
+
+$$
+overpaymentInterest_{gross} = overpaymentAmount \times overpaymentInterestRate \quad \text{(20)}
+$$
+
+$$
+overpaymentInterest_{net} = overpaymentInterest_{gross} - managementFee_{overpayment} \quad \text{(21)}
+$$
+
+$$
+overpaymentFee = overpaymentAmount \times overpaymentFeeRate \quad \text{(22)}
+$$
+
+**Where:**
+
+- `managementFee_{overpayment}` is defined by formula (14)
+
+#### 5.2 Principal Reduction
+
+$$
+principalPortion = overpaymentAmount - overpaymentInterest_{net} - overpaymentFee \quad \text{(23)}
+$$
+
+**Where:**
+
+- `overpaymentAmount` = The excess funds after all periodic payments are settled (formula 19)
+- `overpaymentInterest_{net}` = Gross overpayment interest minus management fee (formula 21)
+- `overpaymentFee` = Percentage-based fee on the overpayment amount (formula 22)
+
+**Note:** The management fee has already been deducted from `overpaymentInterest_{net}`, so it does not appear as a separate term in this formula.
+
+#### 5.3 Re-amortization Value Change
+
+The pseudo-code in [Section 3.2.4.4](#3244-transaction-pseudo-code) handles re-amortization by preserving historical rounding errors while applying the overpayment to reduce the principal. The process follows these logical steps:
+
+**1. Calculate the "true" pre-overpayment state** (using formulas 10, 31-34):
+
+The system first determines what the loan state should be based on pure mathematical formulas, without rounding:
+
+```
+truePrincipalOutstanding = periodicPayment / factor
+trueTotalValue = periodicPayment × paymentsRemaining
+trueInterest = trueTotalValue - truePrincipalOutstanding
+trueManagementFee = trueInterest × managementFeeRate
+trueInterest = trueInterest - trueManagementFee
+```
+
+**2. Capture the rounding discrepancy:**
+
+$$
+diffTotal = TotalValueOutstanding_{ledger} - (truePrincipalOutstanding + trueInterest + trueManagementFee) \quad \text{(24a)}
+$$
+
+This `diffTotal` represents the cumulative effect of rounding across all previous payments. It must be preserved to maintain numerical stability.
+
+**3. Apply the overpayment:**
+
+$$
+newTruePrincipalOutstanding = truePrincipalOutstanding - principalPortion \quad \text{(24b)}
+$$
+
+**4. Re-calculate with reduced principal** (using formulas 5-7, 31):
+
+The system computes a new amortization schedule based on the reduced principal:
+
+```
+newPeriodicPayment = compute_periodic_payment(newTruePrincipalOutstanding)
+newTrueTotalValue = newPeriodicPayment × paymentsRemaining
+```
+
+**5. Preserve the rounding discrepancy:**
+
+$$
+newTotalValueOutstanding_{ledger} = newTrueTotalValue + diffTotal \quad \text{(24c)}
+$$
+
+By adding back the `diffTotal`, the system ensures that historical rounding errors don't compound or disappear.
+
+**6. Calculate the value change:**
+
+$$
+valueChange_{re-amortization} = newTotalValueOutstanding_{ledger} - (oldTotalValue - overpaymentAmount) \quad \text{(24)}
+$$
+
+**Where:**
+
+- `oldTotalValue` = `Loan.TotalValueOutstanding` before the overpayment
+- `newTotalValueOutstanding_{ledger}` = The new total value after re-amortization, adjusted for rounding
+- `overpaymentAmount` = The excess funds applied to principal
+
+**Note on Formula Numbering**: Formulas 24a-24c represent intermediate calculation steps in the re-amortization process. The final result is formula (24). This sub-numbering preserves the sequential formula count while showing the logical flow.
+
+**Why This Matters**: The `diffTotal` variable ensures that historical rounding errors are preserved across re-amortization, maintaining numerical stability. Without this adjustment, repeated overpayments could cause the loan's on-ledger values to drift from their theoretical equivalents, potentially leaving small "dust" amounts at the end of the loan term.
+
+**Note:** The `valueChange` from re-amortization is typically negative, as reducing principal early decreases future interest.
+
+#### 5.4 Total Value Change from Overpayment
+
+$$
+valueChange_{total} = overpaymentInterest_{net} + valueChange_{re-amortization} \quad \text{(25)}
+$$
+
+**Where:**
+
+- `overpaymentInterest_{net}` from formula (21)
+- `valueChange_{re-amortization}` from formula (24)
+
+**Note:** Typically `valueChange_{total} < 0` because the reduction in future interest from re-amortization outweighs the interest charged on the overpayment itself.
+
+### 6. Early Full Repayment
+
+Closes the loan before maturity with potential prepayment penalty.
+
+#### 6.1 Total Due Components
+
+$$
+totalDue = PrincipalOutstanding + accruedInterest + prepaymentPenalty + ClosePaymentFee \quad \text{(26)}
+$$
+
+**Where:**
+
+$$
+accruedInterest = PrincipalOutstanding \times periodicRate \times \frac{secondsSinceLastPayment}{paymentInterval} \quad \text{(27)}
+$$
+
+$$
+prepaymentPenalty = PrincipalOutstanding \times closeInterestRate \quad \text{(28)}
+$$
+
+- `periodicRate` is defined by formula (1)
+- `secondsSinceLastPayment` is defined by formula (4)
+
+#### 6.2 Value Impact
+
+$$
+valueChange = (accruedInterest + prepaymentPenalty) - totalInterestOutstanding_{net} \quad \text{(29)}
+$$
+
+**Where:**
+- `InterestOutstanding_{net}` = Remaining net interest from ledger values:
+  - $$totalInterestOutstanding_{net} = The total interest outstanding for the Loan
+- 
+**Note:**
+
+- Can be positive or negative depending on penalty size
+- Negative if `(accruedInterest + prepaymentPenalty) < InterestOutstanding_{net}` (vault loses future interest)
+- Positive if `(accruedInterest + prepaymentPenalty) > InterestOutstanding_{net}` (penalty exceeds forgiven interest)
+
+### 7. Theoretical Loan Value
+
+#### 7.1 Total Value Outstanding
+
+$$
+totalValueOutstanding = periodicPayment \times paymentsRemaining \quad \text{(30)}
+$$
+
+#### 7.2 Interest and Fee Breakdown
+
+$$
+totalInterestOutstanding_{gross} = totalValueOutstanding - PrincipalOutstanding \quad \text{(31)}
+$$
+
+$$
+managementFeeOutstanding = totalInterestOutstanding_{gross} \times managementFeeRate \quad \text{(32)}
+$$
+
+$$
+totalInterestOutstanding_{net} = totalInterestOutstanding_{gross} - managementFeeOutstanding \quad \text{(33)}
+$$
+
+### 8. First-Loss Capital (Default Coverage)
+
+Applied when a loan defaults.
+
+#### 8.1 Default Amounts
+
+$$
+DefaultAmount = PrincipalOutstanding + InterestOutstanding_{net} \quad \text{(34)}
+$$
+
+$$
+DefaultCovered = \min((DebtTotal \times CoverRateMinimum) \times CoverRateLiquidation, DefaultAmount) \quad \text{(35)}
+$$
+
+$$
+Loss = DefaultAmount - DefaultCovered \quad \text{(36)}
+$$
+
+$$
+FundsReturned = DefaultCovered \quad \text{(37)}
+$$
+
+**Where:**
+
+- `PrincipalOutstanding` = Outstanding principal balance (`Loan.PrincipalOutstanding`)
+- `InterestOutstanding_{net}` = Remaining net interest excluding management fee (formula 33 applied to ledger values)
+- `DebtTotal` = Total debt owed to vault (`LoanBroker.DebtTotal`)
+- `CoverRateMinimum` = Required coverage percentage (`LoanBroker.CoverRateMinimum`)
+- `CoverRateLiquidation` = Portion of minimum cover to liquidate (`LoanBroker.CoverRateLiquidation`)
+
+**Process:**
+
+1. Calculate coverage: `DefaultCovered = min((DebtTotal × CoverRateMinimum) × CoverRateLiquidation, DefaultAmount)` 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 cc0559a4bcb451c39a917bda1ea84622dab65abd Mon Sep 17 00:00:00 2001
From: Vito <5780819+Tapanito@users.noreply.github.com>
Date: Wed, 19 Nov 2025 10:29:30 +0100
Subject: [PATCH 77/77] adds valueChange to 3.2.4.2

---
 XLS-0066d-lending-protocol/README.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/XLS-0066d-lending-protocol/README.md b/XLS-0066d-lending-protocol/README.md
index 2222470f..6b278e39 100644
--- a/XLS-0066d-lending-protocol/README.md
+++ b/XLS-0066d-lending-protocol/README.md
@@ -1265,6 +1265,7 @@ Each payment consists of three components:
 - **Principal**: The portion that reduces the outstanding loan principle.
 - **Interest**: The portion that covers the cost of borrowing for the period.
 - **Fees**: The portion that covers any applicable `serviceFee`, `managementFee`, `latePaymentFee`, or other charges.
+- **ValueChange**: The amount by which the payment changed the value of the Loan.
 
 The system follows these steps to process a payment: