fixes overpayment interest calculation

This commit is contained in:
Vito
2025-11-27 15:33:34 +01:00
parent 0650e6e89d
commit 55c508da1b
3 changed files with 93 additions and 10 deletions

View File

@@ -0,0 +1,74 @@
#include <xrpl/beast/unit_test/suite.h>
//
#include <test/jtx.h>
#include <test/jtx/mpt.h>
#include <xrpld/app/misc/LendingHelpers.h>
#include <xrpld/app/misc/LoadFeeTrack.h>
#include <xrpld/app/tx/detail/Batch.h>
#include <xrpld/app/tx/detail/LoanSet.h>
#include <xrpl/beast/xor_shift_engine.h>
#include <xrpl/protocol/SField.h>
#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

View File

@@ -387,6 +387,20 @@ struct LoanStateDeltas
nonNegative();
};
std::pair<Number, Number>
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,

View File

@@ -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{