From 37f365a053c735d373ca775781773962e5fb9aa9 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 7 May 2025 18:01:59 -0400 Subject: [PATCH] Start implementing LoanPay transaction --- .../xrpl/protocol/detail/transactions.macro | 2 - src/test/app/Loan_test.cpp | 4 +- src/xrpld/app/misc/LendingHelpers.h | 118 +++++++-- src/xrpld/app/misc/detail/LendingHelpers.cpp | 232 ++++++++++++++++-- .../app/tx/detail/LoanBrokerCoverDeposit.cpp | 2 +- .../app/tx/detail/LoanBrokerCoverWithdraw.cpp | 2 +- src/xrpld/app/tx/detail/LoanBrokerDelete.cpp | 2 +- src/xrpld/app/tx/detail/LoanBrokerSet.cpp | 2 +- src/xrpld/app/tx/detail/LoanDelete.cpp | 2 +- src/xrpld/app/tx/detail/LoanDraw.cpp | 2 +- src/xrpld/app/tx/detail/LoanManage.cpp | 4 +- src/xrpld/app/tx/detail/LoanPay.cpp | 228 +++++++++++++++++ src/xrpld/app/tx/detail/LoanPay.h | 53 ++++ src/xrpld/app/tx/detail/LoanSet.cpp | 4 +- 14 files changed, 608 insertions(+), 49 deletions(-) create mode 100644 src/xrpld/app/tx/detail/LoanPay.cpp create mode 100644 src/xrpld/app/tx/detail/LoanPay.h diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 6de416ea21..037779b294 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -776,7 +776,6 @@ TRANSACTION(ttLOAN_DRAW, 81, LoanDraw, noPriv, ({ {sfAmount, soeREQUIRED, soeMPTSupported}, })) -#if 0 /** The Borrower uses this transaction to make a Payment on the Loan. */ #if TRANSACTION_INCLUDE # include @@ -785,7 +784,6 @@ TRANSACTION(ttLOAN_PAY, 82, LoanPay, noPriv, ({ {sfLoanID, soeREQUIRED}, {sfAmount, soeREQUIRED, soeMPTSupported}, })) -#endif /** This system-generated transaction type is used to update the status of the various amendments. diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 2f99039ed8..84207fd016 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -159,7 +159,7 @@ class Loan_test : public beast::unit_test::suite { TenthBips16 const managementFeeRate{ brokerSle->at(sfManagementFeeRate)}; - auto const loanInterest = LoanInterestOutstandingToVault( + auto const loanInterest = LoanInterestOutstandingMinusFee( broker.asset, principalOutstanding, interestRate, @@ -261,7 +261,7 @@ class Loan_test : public beast::unit_test::suite env.test.BEAST_EXPECT( vaultSle->at(sfLossUnrealized) == principalOutstanding + - LoanInterestOutstandingToVault( + LoanInterestOutstandingMinusFee( broker.asset, principalOutstanding, interestRate, diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index 0b300aa753..9f9d19a90d 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -20,6 +20,8 @@ #ifndef RIPPLE_APP_MISC_LENDINGHELPERS_H_INCLUDED #define RIPPLE_APP_MISC_LENDINGHELPERS_H_INCLUDED +#include + #include #include #include @@ -31,21 +33,22 @@ struct PreflightContext; // Lending protocol has dependencies, so capture them here. bool -LendingProtocolEnabled(PreflightContext const& ctx); +lendingProtocolEnabled(PreflightContext const& ctx); namespace detail { // These functions should rarely be used directly. More often, the ultimate // result needs to be roundToAsset'd. -Number -LoanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval); +struct LoanPaymentParts +{ + Number principalPaid; + Number interestPaid; + Number valueChange; + Number feePaid; +}; Number -LoanPeriodicPayment( - Number principalOutstanding, - TenthBips32 interestRate, - std::uint32_t paymentInterval, - std::uint32_t paymentsRemaining); +LoanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval); Number LoanTotalValueOutstanding( @@ -61,27 +64,110 @@ LoanTotalInterestOutstanding( std::uint32_t paymentInterval, std::uint32_t paymentsRemaining); +Number +LoanPeriodicPayment( + Number principalOutstanding, + TenthBips32 interestRate, + std::uint32_t paymentInterval, + std::uint32_t paymentsRemaining); + +Number +LoanLatePaymentInterest( + Number principalOutstanding, + TenthBips32 lateInterestRate, + NetClock::time_point parentCloseTime, + std::uint32_t startDate, + std::uint32_t prevPaymentDate); + +LoanPaymentParts +LoanComputePaymentParts(ApplyView& view, SLE::ref loan); + } // namespace detail template Number -LoanInterestOutstandingToVault( +MinusFee(A const& asset, Number value, TenthBips32 managementFeeRate) +{ + return roundToAsset( + asset, tenthBipsOfValue(value, tenthBipsPerUnity - managementFeeRate)); +} + +template +Number +LoanInterestOutstandingMinusFee( A const& asset, Number principalOutstanding, TenthBips32 interestRate, std::uint32_t paymentInterval, std::uint32_t paymentsRemaining, TenthBips32 managementFeeRate) +{ + return MinusFee( + asset, + detail::LoanTotalInterestOutstanding( + principalOutstanding, + interestRate, + paymentInterval, + paymentsRemaining), + managementFeeRate); +} + +template +Number +LoanPeriodicPayment( + A const& asset, + Number principalOutstanding, + TenthBips32 interestRate, + std::uint32_t paymentInterval, + std::uint32_t paymentsRemaining) { return roundToAsset( asset, - tenthBipsOfValue( - detail::LoanTotalInterestOutstanding( - principalOutstanding, - interestRate, - paymentInterval, - paymentsRemaining), - tenthBipsPerUnity - managementFeeRate)); + detail::LoanPeriodicPayment( + principalOutstanding, + interestRate, + paymentInterval, + paymentsRemaining)); +} + +template +Number +LoanLatePaymentInterest( + A const& asset, + Number principalOutstanding, + TenthBips32 lateInterestRate, + NetClock::time_point parentCloseTime, + std::uint32_t startDate, + std::uint32_t prevPaymentDate) +{ + return roundToAsset( + asset, + detail::LoanLatePaymentInterest( + principalOutstanding, + lateInterestRate, + parentCloseTime, + startDate, + prevPaymentDate)); +} + +struct LoanPaymentParts +{ + STAmount principalPaid; + STAmount interestPaid; + STAmount valueChange; + STAmount feePaid; +}; + +template +LoanPaymentParts +LoanComputePaymentParts(A const& asset, ApplyView& view, SLE::ref loan) +{ + auto const parts = detail::LoanComputePaymentParts(view, loan); + return LoanPaymentParts{ + roundToAsset(asset, parts.principalPaid), + roundToAsset(asset, parts.interestPaid), + roundToAsset(asset, parts.valueChange), + roundToAsset(asset, parts.feePaid)}; } } // namespace ripple diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index 7b6e186922..1044727260 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -21,13 +21,15 @@ // #include #include +#include #include +#include namespace ripple { bool -LendingProtocolEnabled(PreflightContext const& ctx) +lendingProtocolEnabled(PreflightContext const& ctx) { return ctx.rules.enabled(featureLendingProtocol) && VaultCreate::isEnabled(ctx); @@ -44,24 +46,6 @@ LoanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval) (365 * 24 * 60 * 60); } -Number -LoanPeriodicPayment( - Number principalOutstanding, - TenthBips32 interestRate, - std::uint32_t paymentInterval, - std::uint32_t paymentsRemaining) -{ - if (principalOutstanding == 0 || paymentsRemaining == 0) - return 0; - Number const periodicRate = LoanPeriodicRate(interestRate, paymentInterval); - - // TODO: Need a better name - Number const timeFactor = power(1 + periodicRate, paymentsRemaining); - - return principalOutstanding * (periodicRate * timeFactor) / - (timeFactor - 1); -} - Number LoanTotalValueOutstanding( Number principalOutstanding, @@ -92,6 +76,216 @@ LoanTotalInterestOutstanding( principalOutstanding; } +Number +LoanPeriodicPayment( + Number principalOutstanding, + Number periodicRate, + std::uint32_t paymentsRemaining) +{ + // TODO: Need a better name + Number const timeFactor = power(1 + periodicRate, paymentsRemaining); + + return principalOutstanding * (periodicRate * timeFactor) / + (timeFactor - 1); +} + +Number +LoanPeriodicPayment( + Number principalOutstanding, + TenthBips32 interestRate, + std::uint32_t paymentInterval, + std::uint32_t paymentsRemaining) +{ + if (principalOutstanding == 0 || paymentsRemaining == 0) + return 0; + Number const periodicRate = LoanPeriodicRate(interestRate, paymentInterval); + + return LoanPeriodicPayment( + principalOutstanding, periodicRate, paymentsRemaining); +} + +Number +LoanLatePaymentInterest( + Number principalOutstanding, + TenthBips32 lateInterestRate, + NetClock::time_point parentCloseTime, + std::uint32_t startDate, + std::uint32_t prevPaymentDate) +{ + auto const lastPaymentDate = std::max(prevPaymentDate, startDate); + + auto const secondsSinceLastPayment = + parentCloseTime.time_since_epoch().count() - lastPaymentDate; + + auto const rate = + LoanPeriodicRate(lateInterestRate, secondsSinceLastPayment); + + return principalOutstanding * rate; +} + +Number +LoanAccruedInterest( + Number principalOutstanding, + TenthBips32 periodicRate, + NetClock::time_point parentCloseTime, + std::uint32_t startDate, + std::uint32_t prevPaymentDate, + std::uint32_t paymentInterval) +{ + auto const lastPaymentDate = std::max(prevPaymentDate, startDate); + + auto const secondsSinceLastPayment = + parentCloseTime.time_since_epoch().count() - lastPaymentDate; + + return tenthBipsOfValue( + principalOutstanding * secondsSinceLastPayment, periodicRate) / + paymentInterval; +} + +LoanPaymentParts +LoanComputePaymentParts(ApplyView& view, SLE::ref loan) +{ + Number const principalOutstanding = loan->at(sfPrincipalOutstanding); + + TenthBips32 const interestRate{loan->at(sfInterestRate)}; + TenthBips32 const lateInterestRate{loan->at(sfLateInterestRate)}; + TenthBips32 const closeInterestRate{loan->at(sfCloseInterestRate)}; + + Number const latePaymentFee = loan->at(sfLatePaymentFee); + Number const closePaymentFee = loan->at(sfClosePaymentFee); + + std::uint32_t const paymentInterval = loan->at(sfPaymentInterval); + std::uint32_t const paymentRemaining = loan->at(sfPaymentRemaining); + + std::uint32_t const prevPaymentDate = loan->at(sfPreviousPaymentDate); + std::uint32_t const startDate = loan->at(sfStartDate); + std::uint32_t const nextDueDate = loan->at(sfNextPaymentDueDate); + + // Compute the normal periodic rate, payment, etc. + // We'll need it in the remaining calculations + Number const periodicRate = LoanPeriodicRate(interestRate, paymentInterval); + Number const periodicPaymentAmount = LoanPeriodicPayment( + principalOutstanding, periodicRate, paymentRemaining); + Number const periodicInterest = principalOutstanding * periodicRate; + Number const periodicPrincipal = periodicPaymentAmount - periodicInterest; + + // the payment is late + if (hasExpired(view, nextDueDate)) + { + auto const latePaymentInterest = LoanLatePaymentInterest( + principalOutstanding, + lateInterestRate, + view.parentCloseTime(), + startDate, + prevPaymentDate); + auto const latePaymentAmount = + periodicPaymentAmount + latePaymentInterest + latePaymentFee; + + loan->at(sfPaymentRemaining) -= 1; + // A single payment always pays the same amount of principal. Only the + // interest and fees are extra + loan->at(sfPrincipalOutstanding) -= periodicPrincipal; + + // Make sure this does an assignment + loan->at(sfPreviousPaymentDate) = loan->at(sfNextPaymentDueDate); + loan->at(sfNextPaymentDueDate) += paymentInterval; + + // A late payment increases the value of the loan by the difference + // between periodic and late payment interest + return { + periodicPrincipal, + latePaymentInterest + periodicInterest, + latePaymentInterest, + latePaymentFee}; + } + + auto const accruedInterest = LoanAccruedInterest( + principalOutstanding, + interestRate, + view.parentCloseTime(), + startDate, + prevPaymentDate, + paymentInterval); + auto const prepaymentPenalty = + tenthBipsOfValue(principalOutstanding, closeInterestRate); + + assert(0); + return {0, 0, 0, 0}; + /* +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 + } + + ..... + + 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(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 + + 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) + */ +} + } // namespace detail } // namespace ripple diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp index 56100f6e49..d82a59ed0d 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp @@ -45,7 +45,7 @@ namespace ripple { bool LoanBrokerCoverDeposit::isEnabled(PreflightContext const& ctx) { - return LendingProtocolEnabled(ctx); + return lendingProtocolEnabled(ctx); } NotTEC diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp index 065b2e73b0..c9cd668e15 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp @@ -45,7 +45,7 @@ namespace ripple { bool LoanBrokerCoverWithdraw::isEnabled(PreflightContext const& ctx) { - return LendingProtocolEnabled(ctx); + return lendingProtocolEnabled(ctx); } NotTEC diff --git a/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp b/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp index 436b50045c..f76b1e8a0f 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp @@ -45,7 +45,7 @@ namespace ripple { bool LoanBrokerDelete::isEnabled(PreflightContext const& ctx) { - return LendingProtocolEnabled(ctx); + return lendingProtocolEnabled(ctx); } NotTEC diff --git a/src/xrpld/app/tx/detail/LoanBrokerSet.cpp b/src/xrpld/app/tx/detail/LoanBrokerSet.cpp index 348e1e502d..7589a5817a 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerSet.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerSet.cpp @@ -48,7 +48,7 @@ namespace ripple { bool LoanBrokerSet::isEnabled(PreflightContext const& ctx) { - return LendingProtocolEnabled(ctx); + return lendingProtocolEnabled(ctx); } NotTEC diff --git a/src/xrpld/app/tx/detail/LoanDelete.cpp b/src/xrpld/app/tx/detail/LoanDelete.cpp index c2367392c8..d29c03e969 100644 --- a/src/xrpld/app/tx/detail/LoanDelete.cpp +++ b/src/xrpld/app/tx/detail/LoanDelete.cpp @@ -47,7 +47,7 @@ namespace ripple { bool LoanDelete::isEnabled(PreflightContext const& ctx) { - return LendingProtocolEnabled(ctx); + return lendingProtocolEnabled(ctx); } NotTEC diff --git a/src/xrpld/app/tx/detail/LoanDraw.cpp b/src/xrpld/app/tx/detail/LoanDraw.cpp index 596137bcb4..1f01639fe0 100644 --- a/src/xrpld/app/tx/detail/LoanDraw.cpp +++ b/src/xrpld/app/tx/detail/LoanDraw.cpp @@ -47,7 +47,7 @@ namespace ripple { bool LoanDraw::isEnabled(PreflightContext const& ctx) { - return LendingProtocolEnabled(ctx); + return lendingProtocolEnabled(ctx); } NotTEC diff --git a/src/xrpld/app/tx/detail/LoanManage.cpp b/src/xrpld/app/tx/detail/LoanManage.cpp index f310aa6a19..02f8c31ebb 100644 --- a/src/xrpld/app/tx/detail/LoanManage.cpp +++ b/src/xrpld/app/tx/detail/LoanManage.cpp @@ -48,7 +48,7 @@ namespace ripple { bool LoanManage::isEnabled(PreflightContext const& ctx) { - return LendingProtocolEnabled(ctx); + return lendingProtocolEnabled(ctx); } std::uint32_t @@ -377,7 +377,7 @@ LoanManage::doApply() TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; auto const paymentInterval = loanSle->at(sfPaymentInterval); auto const paymentsRemaining = loanSle->at(sfPaymentRemaining); - auto const interestOutstanding = LoanInterestOutstandingToVault( + auto const interestOutstanding = LoanInterestOutstandingMinusFee( vaultAsset, principalOutstanding.value(), interestRate, diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp new file mode 100644 index 0000000000..77955ca54f --- /dev/null +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -0,0 +1,228 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +// +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { + +bool +LoanPay::isEnabled(PreflightContext const& ctx) +{ + return lendingProtocolEnabled(ctx); +} + +NotTEC +LoanPay::doPreflight(PreflightContext const& ctx) +{ + if (ctx.tx[sfLoanID] == beast::zero) + return temINVALID; + + if (ctx.tx[sfAmount] <= beast::zero) + return temBAD_AMOUNT; + + return tesSUCCESS; +} + +TER +LoanPay::preclaim(PreclaimContext const& ctx) +{ + auto const& tx = ctx.tx; + + auto const account = tx[sfAccount]; + auto const loanID = tx[sfLoanID]; + auto const amount = tx[sfAmount]; + + auto const loanSle = ctx.view.read(keylet::loan(loanID)); + if (!loanSle) + { + JLOG(ctx.j.warn()) << "Loan does not exist."; + return tecNO_ENTRY; + } + + auto const principalOutstanding = loanSle->at(sfPrincipalOutstanding); + TenthBips32 const interestRate{loanSle->at(sfInterestRate)}; + auto const paymentInterval = loanSle->at(sfPaymentInterval); + auto const paymentRemaining = loanSle->at(sfPaymentRemaining); + TenthBips32 const lateInterestRate{loanSle->at(sfLateInterestRate)}; + auto const latePaymentFee = loanSle->at(sfLatePaymentFee); + auto const prevPaymentDate = loanSle->at(sfPreviousPaymentDate); + auto const startDate = loanSle->at(sfStartDate); + auto const nextDueDate = loanSle->at(sfNextPaymentDueDate); + + if (loanSle->at(sfBorrower) != account) + { + JLOG(ctx.j.warn()) << "Loan does not belong to the account."; + return tecNO_PERMISSION; + } + + if (!hasExpired(ctx.view, startDate)) + { + JLOG(ctx.j.warn()) << "Loan has not started yet."; + return tecTOO_SOON; + } + + if (paymentRemaining == 0 || principalOutstanding == 0) + { + JLOG(ctx.j.warn()) << "Loan is already paid off."; + return tecKILLED; + } + + auto const loanBrokerID = loanSle->at(sfLoanBrokerID); + auto const loanBrokerSle = ctx.view.read(keylet::loanbroker(loanBrokerID)); + if (!loanBrokerSle) + { + // This should be impossible + // LCOV_EXCL_START + JLOG(ctx.j.fatal()) << "LoanBroker does not exist."; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } + auto const brokerPseudoAccount = loanBrokerSle->at(sfAccount); + auto const vaultID = loanBrokerSle->at(sfVaultID); + auto const vaultSle = ctx.view.read(keylet::vault(vaultID)); + if (!vaultSle) + { + // This should be impossible + // LCOV_EXCL_START + JLOG(ctx.j.fatal()) << "Vault does not exist."; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } + auto const asset = vaultSle->at(sfAsset); + + if (amount.asset() != asset) + { + JLOG(ctx.j.warn()) << "Loan amount does not match the Vault asset."; + return tecWRONG_ASSET; + } + + if (isFrozen(ctx.view, brokerPseudoAccount, asset)) + { + JLOG(ctx.j.warn()) << "Loan Broker pseudo-account is frozen."; + return asset.holds() ? tecFROZEN : tecLOCKED; + } + if (asset.holds()) + { + auto const issue = asset.get(); + if (isDeepFrozen(ctx.view, account, issue.currency, issue.account)) + { + JLOG(ctx.j.warn()) << "Borrower account is frozen."; + return tecFROZEN; + } + } + + auto const periodicPaymentAmount = LoanPeriodicPayment( + asset, + principalOutstanding, + interestRate, + paymentInterval, + paymentRemaining); + + if (hasExpired(ctx.view, nextDueDate)) + { + // Need to pay the late payment amount + auto const latePaymentInterest = LoanLatePaymentInterest( + asset, + principalOutstanding, + lateInterestRate, + ctx.view.parentCloseTime(), + startDate, + prevPaymentDate); + auto const latePaymentAmount = + periodicPaymentAmount + latePaymentInterest + latePaymentFee; + if (amount < latePaymentAmount) + { + JLOG(ctx.j.warn()) + << "Late loan payment amount is insufficient. Due: " + << latePaymentAmount << ", paid: " << amount; + return tecINSUFFICIENT_PAYMENT; + } + } + else if (amount < periodicPaymentAmount) + { + // Need to pay the regular payment amount + JLOG(ctx.j.warn()) + << "Periodic loan payment amount is insufficient. Due: " + << periodicPaymentAmount << ", paid: " << amount; + return tecINSUFFICIENT_PAYMENT; + } + + return tesSUCCESS; +} + +TER +LoanPay::doApply() +{ + auto const& tx = ctx_.tx; + auto& view = ctx_.view(); + + auto const amount = tx[sfAmount]; + + auto const loanID = tx[sfLoanID]; + auto const loanSle = view.peek(keylet::loan(loanID)); + if (!loanSle) + return tefBAD_LEDGER; // LCOV_EXCL_LINE + + auto const brokerID = loanSle->at(sfLoanBrokerID); + auto const brokerSle = view.peek(keylet::loanbroker(brokerID)); + if (!brokerSle) + return tefBAD_LEDGER; // LCOV_EXCL_LINE + auto const brokerPseudoAccount = brokerSle->at(sfAccount); + + if (auto const ter = accountSend( + view, + brokerPseudoAccount, + account_, + amount, + j_, + WaiveTransferFee::Yes)) + return ter; + + loanSle->at(sfAssetsAvailable) -= amount; + view.update(loanSle); + + return tesSUCCESS; +} + +//------------------------------------------------------------------------------ + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/LoanPay.h b/src/xrpld/app/tx/detail/LoanPay.h new file mode 100644 index 0000000000..f608a7fb38 --- /dev/null +++ b/src/xrpld/app/tx/detail/LoanPay.h @@ -0,0 +1,53 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_LOANPAY_H_INCLUDED +#define RIPPLE_TX_LOANPAY_H_INCLUDED + +#include + +namespace ripple { + +class LoanPay : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit LoanPay(ApplyContext& ctx) : Transactor(ctx) + { + } + + static bool + isEnabled(PreflightContext const& ctx); + + static NotTEC + doPreflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +//------------------------------------------------------------------------------ + +} // namespace ripple + +#endif diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index fd6f9c294b..2e3521468c 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -49,7 +49,7 @@ namespace ripple { bool LoanSet::isEnabled(PreflightContext const& ctx) { - return LendingProtocolEnabled(ctx); + return lendingProtocolEnabled(ctx); } std::uint32_t @@ -359,7 +359,7 @@ LoanSet::doApply() TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; // The portion of the loan interest that will go to the vault (total // interest minus the management fee) - auto const loanInterestToVault = LoanInterestOutstandingToVault( + auto const loanInterestToVault = LoanInterestOutstandingMinusFee( vaultAsset, principalRequested, interestRate,