Ensure interest portion doesn't go negative

- Ensure principal part is not bigger than whole payment.
- Add some documentation.
This commit is contained in:
Ed Hennis
2025-10-06 16:48:43 -04:00
parent 1b31dbc4c9
commit 8d982758cb
2 changed files with 21 additions and 8 deletions

View File

@@ -564,6 +564,20 @@ LEDGER_ENTRY(ltLOAN, 0x0089, Loan, loan, ({
{sfPreviousPaymentDate, soeDEFAULT}, {sfPreviousPaymentDate, soeDEFAULT},
{sfNextPaymentDueDate, soeOPTIONAL}, {sfNextPaymentDueDate, soeOPTIONAL},
{sfPaymentRemaining, soeDEFAULT}, {sfPaymentRemaining, soeDEFAULT},
// The loan object tracks three values:
// - TotalValueOutstanding: The total amount owed by the borrower to
// the lender / vault.
// - PrincipalOutstanding: The portion of the TotalValueOutstanding
// that is from the prinicpal borrowed.
// - InterestOwed: The portion of the TotalValueOutstanding that
// represents interest owed to the vault.
// There are two additional values that can be computed from these:
// - InterestOutstanding: TotalValueOutstanding - PrincipalOutstanding
// This is the total amount of interest still pending on the loan,
// independent of management fees.
// - ManagementFeeOwed: InterestOutstanding - InterestOwed
// This is the amount of the total interest that will be sent to the
// broker as management fees.
{sfPrincipalOutstanding, soeDEFAULT}, {sfPrincipalOutstanding, soeDEFAULT},
{sfTotalValueOutstanding, soeDEFAULT}, {sfTotalValueOutstanding, soeDEFAULT},
{sfInterestOwed, soeDEFAULT}, {sfInterestOwed, soeDEFAULT},

View File

@@ -151,7 +151,8 @@ computePaymentComponents(
roundPeriodicPayment(asset, periodicPayment, scale); roundPeriodicPayment(asset, periodicPayment, scale);
if (paymentRemaining == 1 || totalValueOutstanding <= periodicPayment) if (paymentRemaining == 1 || totalValueOutstanding <= periodicPayment)
{ {
// If there's only one payment left, we need to pay off the principal. // If there's only one payment left, we need to pay off each of the loan
// parts.
// //
// The totalValueOutstanding should never be less than the // The totalValueOutstanding should never be less than the
// periodicPayment until the last scheduled payment, but if it ever is, // periodicPayment until the last scheduled payment, but if it ever is,
@@ -217,8 +218,9 @@ computePaymentComponents(
totalValueOutstanding - roundedPeriodicPayment) totalValueOutstanding - roundedPeriodicPayment)
return roundedPeriodicPayment; return roundedPeriodicPayment;
// Use the amount that will get principal outstanding as close to // Use the amount that will get principal outstanding as close to
// reference principal as possible. // reference principal as possible, but don't pay more than the rounded
return p; // periodic payment, or we'll end up with negative interest.
return std::min(p, roundedPeriodicPayment);
}(); }();
// if(count($A20), if(AB19 < $B$5, AB19 - Z19, CEILING($B$10-W20, 1)), "") // if(count($A20), if(AB19 < $B$5, AB19 - Z19, CEILING($B$10-W20, 1)), "")
@@ -234,13 +236,10 @@ computePaymentComponents(
"ripple::detail::computePaymentComponents", "ripple::detail::computePaymentComponents",
"valid rounded interest"); "valid rounded interest");
XRPL_ASSERT_PARTS( XRPL_ASSERT_PARTS(
roundedPrincipal >= 0 && roundedPrincipal <= principalOutstanding, roundedPrincipal >= 0 && roundedPrincipal <= principalOutstanding &&
isRounded(asset, roundedPrincipal, scale),
"ripple::detail::computePaymentComponents", "ripple::detail::computePaymentComponents",
"valid rounded principal"); "valid rounded principal");
XRPL_ASSERT_PARTS(
isRounded(asset, roundedPrincipal, scale),
"ripple::detail::computePaymentComponents",
"principal is rounded");
return { return {
.rawInterest = rawInterest, .rawInterest = rawInterest,