From 55c508da1bd5b510c18ca2201f19a963d9183b45 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Thu, 27 Nov 2025 15:33:34 +0100 Subject: [PATCH] fixes overpayment interest calculation --- src/test/app/LendingHelpers_test.cpp | 74 ++++++++++++++++++++ src/xrpld/app/misc/LendingHelpers.h | 14 ++++ src/xrpld/app/misc/detail/LendingHelpers.cpp | 15 ++-- 3 files changed, 93 insertions(+), 10 deletions(-) create mode 100644 src/test/app/LendingHelpers_test.cpp diff --git a/src/test/app/LendingHelpers_test.cpp b/src/test/app/LendingHelpers_test.cpp new file mode 100644 index 0000000000..cf0d140ccc --- /dev/null +++ b/src/test/app/LendingHelpers_test.cpp @@ -0,0 +1,74 @@ +#include +// +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include "test/jtx/amount.h" + +namespace ripple { +namespace test { + +class LendingHelpers_test : public beast::unit_test::suite +{ + void + testComputeOverpaymentComponents() + { + testcase("computeOverpaymentComponents"); + using namespace jtx; + using namespace ripple::detail; + + Account const issuer{"issuer"}; + PrettyAsset const IOU = issuer["IOU"]; + int32_t const loanScale = 1; + auto const overpayment = Number{1'000}; + auto const overpaymentInterestRate = TenthBips32{10'000}; // 10% + auto const overpaymentFeeRate = TenthBips32{10'000}; // 10% + auto const managementFeeRate = TenthBips16{10'000}; // 10% + + auto const expectedOverpaymentFee = Number{100}; // 10% of 1,000 + auto const expectedOverpaymentInterestGross = + Number{100}; // 10% of 1,000 + auto const expectedOverpaymentInterestNet = + Number{90}; // 100 - 10% of 100 + auto const expectedOverpaymentManagementFee = Number{10}; // 10% of + auto const expectedPrincipalPortion = Number{800}; // 1,000 - 100 - 100 + + auto const components = detail::computeOverpaymentComponents( + IOU, + loanScale, + overpayment, + overpaymentInterestRate, + overpaymentFeeRate, + managementFeeRate); + + BEAST_EXPECT( + components.untrackedManagementFee == expectedOverpaymentFee); + BEAST_EXPECT( + components.untrackedInterest == expectedOverpaymentInterestNet); + BEAST_EXPECT( + components.trackedManagementFeeDelta == + expectedOverpaymentManagementFee); + BEAST_EXPECT( + components.trackedPrincipalDelta == expectedPrincipalPortion); + } + +public: + void + run() override + { + testComputeOverpaymentComponents(); + } +}; + +BEAST_DEFINE_TESTSUITE(LendingHelpers, app, ripple); + +} // namespace test +} // namespace ripple \ No newline at end of file diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index 559af28a47..29950da18a 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -387,6 +387,20 @@ struct LoanStateDeltas nonNegative(); }; +std::pair +computeInterestAndFeeParts( + Number const& interest, + TenthBips16 managementFeeRate); + +ExtendedPaymentComponents +computeOverpaymentComponents( + Asset const& asset, + int32_t const loanScale, + Number const& overpayment, + TenthBips32 const overpaymentInterestRate, + TenthBips32 const overpaymentFeeRate, + TenthBips16 const managementFeeRate); + PaymentComponents computePaymentComponents( Asset const& asset, diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index 1361b0679a..e1e46746ab 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -1225,19 +1225,14 @@ computeOverpaymentComponents( // This interest doesn't follow the normal amortization schedule - it's // a one-time charge for paying early. // Equation (20) and (21) from XLS-66 spec, Section A-2 Equation Glossary - auto const [rawOverpaymentInterest, _] = [&]() { - Number const interest = - tenthBipsOfValue(overpayment, overpaymentInterestRate); - return detail::computeInterestAndFeeParts(interest, managementFeeRate); - }(); - - // Round the penalty interest components to the loan scale auto const [roundedOverpaymentInterest, roundedOverpaymentManagementFee] = [&]() { - Number const interest = - roundToAsset(asset, rawOverpaymentInterest, loanScale); + auto const interest = roundToAsset( + asset, + tenthBipsOfValue(overpayment, overpaymentInterestRate), + loanScale); return detail::computeInterestAndFeeParts( - asset, interest, managementFeeRate, loanScale); + interest, managementFeeRate); }(); auto const result = detail::ExtendedPaymentComponents{