From 20358587ce1edcb4ed7502f5e1b83e9dc68098bc Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Fri, 24 Jul 2026 11:55:58 +0200 Subject: [PATCH] fix: Use cash-basis-aware debt delta for LoanSet DebtMaximum guard Remove unused vaultMaximum local left over from the AssetsMaximum guard refactor, and stop the DebtMaximum guard from unconditionally adding interestDue to the projected DebtTotal. Reuse the existing loanOriginationDeltas dispatcher, which already excludes interestDue under cash-basis accounting. --- src/libxrpl/tx/transactors/lending/LoanSet.cpp | 10 ++++------ src/test/app/Loan_test.cpp | 15 +++++++++------ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/libxrpl/tx/transactors/lending/LoanSet.cpp b/src/libxrpl/tx/transactors/lending/LoanSet.cpp index c5b9c420a9..a4575e40b0 100644 --- a/src/libxrpl/tx/transactors/lending/LoanSet.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanSet.cpp @@ -439,9 +439,8 @@ LoanSet::doApply() principalRequested, properties.loanState.managementFeeDue); - auto const vaultMaximum = *vaultSle->at(sfAssetsMaximum); XRPL_ASSERT_PARTS( - vaultMaximum == 0 || vaultMaximum > *vaultTotalProxy, + *vaultSle->at(sfAssetsMaximum) == 0 || *vaultSle->at(sfAssetsMaximum) > *vaultTotalProxy, "xrpl::LoanSet::doApply", "Vault is below maximum limit"); @@ -492,8 +491,9 @@ LoanSet::doApply() auto const loanAssetsToBorrower = principalRequested - originationFee; - auto const newDebtDelta = principalRequested + state.interestDue; - auto const newDebtTotal = brokerSle->at(sfDebtTotal) + newDebtDelta; + auto const [assetsTotalDelta, debtTotalDelta] = + loanOriginationDeltas(ctx_.view().rules(), vaultSle, principalRequested, state.interestDue); + auto const newDebtTotal = brokerSle->at(sfDebtTotal) + debtTotalDelta; if (auto const debtMaximum = brokerSle->at(sfDebtMaximum); debtMaximum != 0 && debtMaximum < newDebtTotal) { @@ -635,8 +635,6 @@ LoanSet::doApply() view.insert(loan); // Update the balances in the vault - auto const [assetsTotalDelta, debtTotalDelta] = - loanOriginationDeltas(ctx_.view().rules(), vaultSle, principalRequested, state.interestDue); vaultAvailableProxy -= principalRequested; vaultTotalProxy += assetsTotalDelta; XRPL_ASSERT_PARTS( diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index ad21a82462..8a6f1669df 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -8684,8 +8684,7 @@ protected: } // AssetsMaximum guard checks interestDue headroom only under - // whole-life accounting (cash-basis never adds interest to - // AssetsTotal). DebtMaximum guard is unconditional either way. + // whole-life accounting; DebtMaximum guard also varies by model. auto runVaultGuard = [&](FeatureBitset features, Number const& slack, TER expected) { Env env(*this, features); @@ -8761,12 +8760,16 @@ protected: runVaultGuard(all_ | featureLendingProtocolV1_1, oneDrop, tesSUCCESS); } - for (auto const features : {all_ | featureLendingProtocolV1_1, all_}) + // DebtMaximum guard: cash-basis projects principal-only DebtTotal; + // whole-life projects principal + interestDue. + for (auto const cashBasis : {true, false}) { testcase( - std::string("cash-basis: LoanSet DebtMaximum guard unaffected by amendment (") + - (features[featureLendingProtocolV1_1] ? "enabled)" : "disabled)")); - Number const newDebtTotal = principalOutstandingCash + interestDueCash; + std::string("LoanSet DebtMaximum guard (") + + (cashBasis ? "cash-basis)" : "whole-life)")); + auto const features = cashBasis ? all_ | featureLendingProtocolV1_1 : all_; + Number const newDebtTotal = + principalOutstandingCash + (cashBasis ? Number{} : interestDueCash); runBrokerGuard(features, newDebtTotal - oneDrop, tecLIMIT_EXCEEDED); runBrokerGuard(features, newDebtTotal, tesSUCCESS); }