From dd6c4e35c0082a2755214a4bf201d9f460bab2ee Mon Sep 17 00:00:00 2001 From: JCW Date: Tue, 7 Jul 2026 19:23:42 +0100 Subject: [PATCH] Address AI comments --- src/libxrpl/tx/invariants/LoanInvariant.cpp | 14 +++++++++++++- src/test/app/Invariants_test.cpp | 21 +++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/libxrpl/tx/invariants/LoanInvariant.cpp b/src/libxrpl/tx/invariants/LoanInvariant.cpp index 7ae5e4b407..9d25cebf2b 100644 --- a/src/libxrpl/tx/invariants/LoanInvariant.cpp +++ b/src/libxrpl/tx/invariants/LoanInvariant.cpp @@ -86,6 +86,15 @@ ValidLoan::finalize( return false; } } + // Interest due (the total value owed less principal and management fee) + // must never be negative. + if (after->at(sfTotalValueOutstanding) - after->at(sfPrincipalOutstanding) - + after->at(sfManagementFeeOutstanding) < + beast::kZero) + { + JLOG(j.fatal()) << "Invariant failed: Loan interest due is negative"; + return false; + } // Must be positive - STNumber for (auto const field : { &sfPeriodicPayment, @@ -112,7 +121,10 @@ ValidLoan::finalize( return false; } - if (loan->at(sfPaymentRemaining) != 0) + if (loan->at(sfPaymentRemaining) != 0 || + loan->at(sfTotalValueOutstanding) != beast::kZero || + loan->at(sfPrincipalOutstanding) != beast::kZero || + loan->at(sfManagementFeeOutstanding) != beast::kZero) { JLOG(j.fatal()) << "Invariant failed: Loan deleted while not fully " "paid off"; diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index 4d45c53867..9054ef4527 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -3814,6 +3814,27 @@ class Invariants_test : public beast::unit_test::Suite "the change in the loan claim")); } + // Loan interest due (total value less principal and management fee) + // must never be negative. A neutral transaction type is used so the + // vault invariants short-circuit and only the loan check fires. The + // loan object is created directly with principal 100, total value 90 + // and management fee 0, so interest due = 90 - 100 - 0 = -10 (< 0) + // while every individual field stays non-negative. + doInvariantCheck( + {"Loan interest due is negative"}, + [&](Account const& a1, Account const&, ApplyContext& ac) { + auto const vaultKeylet = keylet::vault(a1.id(), ac.view().seq()); + auto const loanKeylet = keylet::loan(vaultKeylet.key, 1); + auto sleLoan = std::make_shared(loanKeylet); + sleLoan->at(sfPrincipalOutstanding) = Number(100); + sleLoan->at(sfTotalValueOutstanding) = Number(90); + sleLoan->at(sfManagementFeeOutstanding) = Number(0); + sleLoan->at(sfPeriodicPayment) = Number(1); + sleLoan->setFieldU32(sfPaymentRemaining, 1); + ac.view().insert(sleLoan); + return true; + }); + // ttVAULT_SET: owner is immutable doInvariantCheck( {"violation of vault immutable data"},