From b5b31efe0ba37f82d4fb9e7c7f1ce51df8ce9c68 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sun, 9 Nov 2025 17:37:46 -0500 Subject: [PATCH] Fix RIPD-3901 - faulty assert - Assert requires that an overpayment reduces the value of a loan. If the overall loan interest is low enough, it could leave it unchanged. Update the assert to require that the overpayment does not increase the value of the loan. - Adds a unit test provided by @gregtatcam to demonstrate this issue. --- src/test/app/Loan_test.cpp | 62 ++++++++++++++++++++ src/xrpld/app/misc/detail/LendingHelpers.cpp | 4 +- src/xrpld/app/tx/detail/LoanPay.cpp | 2 +- 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 357e233cea..ea05c6486b 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -6437,6 +6437,67 @@ protected: } } + void + testRIPD3901() + { + testcase("Crash with tfLoanOverpayment"); + using namespace jtx; + using namespace loan; + Account const lender{"lender"}; + Account const issuer{"issuer"}; + Account const borrower{"borrower"}; + Account const depositor{"depositor"}; + auto const txfee = fee(XRP(100)); + + Env env(*this); + Vault vault(env); + + env.fund(XRP(10'000), lender, issuer, borrower, depositor); + env.close(); + + auto [tx, vaultKeyLet] = + vault.create({.owner = lender, .asset = xrpIssue()}); + env(tx, txfee); + env.close(); + + env(vault.deposit( + {.depositor = depositor, + .id = vaultKeyLet.key, + .amount = XRP(1'000)}), + txfee); + env.close(); + + auto const brokerKeyLet = + keylet::loanbroker(lender.id(), env.seq(lender)); + + env(loanBroker::set(lender, vaultKeyLet.key), txfee); + env.close(); + + // BrokerInfo brokerInfo{xrpIssue(), keylet, vaultKeyLet, {}}; + + STAmount const debtMaximumRequest = XRPAmount(200'000); + + env(set(borrower, brokerKeyLet.key, debtMaximumRequest), + sig(sfCounterpartySignature, lender), + interestRate(TenthBips32(50'000)), + paymentTotal(2), + paymentInterval(150), + txflags(tfLoanOverpayment), + txfee); + env.close(); + + std::uint32_t const loanSequence = 1; + auto const loanKeylet = keylet::loan(brokerKeyLet.key, loanSequence); + + if (auto loan = env.le(loanKeylet); env.test.BEAST_EXPECT(loan)) + { + env(loan::pay(borrower, loanKeylet.key, XRPAmount(150'001)), + txflags(tfLoanOverpayment), + txfee); + env.close(); + } + } + public: void run() override @@ -6479,6 +6540,7 @@ public: testRIPD3831(); testRIPD3459(); + testRIPD3901(); } }; diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index ad0e98b07e..8c349aafa2 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -663,9 +663,9 @@ tryOverpayment( auto const valueChange = newRounded.interestOutstanding() - rounded.interestOutstanding(); XRPL_ASSERT_PARTS( - valueChange < beast::zero, + valueChange <= beast::zero, "ripple::detail::tryOverpayment", - "principal overpayment reduced value of loan"); + "principal overpayment did not increase value of loan"); return LoanPaymentParts{ .principalPaid = diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index 3edc109c80..4cad02d762 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -332,7 +332,7 @@ LoanPay::doApply() // It should not be possible to pay 0 total paymentParts->principalPaid + paymentParts->interestPaid > 0, "ripple::LoanPay::doApply", - "valid principal paid"); + "valid total paid"); XRPL_ASSERT_PARTS( paymentParts->feePaid >= 0, "ripple::LoanPay::doApply",