From 8d982758cbe65a912d9e3cad2fcb11fcbacec0e8 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 6 Oct 2025 16:48:43 -0400 Subject: [PATCH] Ensure interest portion doesn't go negative - Ensure principal part is not bigger than whole payment. - Add some documentation. --- include/xrpl/protocol/detail/ledger_entries.macro | 14 ++++++++++++++ src/xrpld/app/misc/LendingHelpers.h | 15 +++++++-------- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 3c5fd13b69..b48151f5cc 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -564,6 +564,20 @@ LEDGER_ENTRY(ltLOAN, 0x0089, Loan, loan, ({ {sfPreviousPaymentDate, soeDEFAULT}, {sfNextPaymentDueDate, soeOPTIONAL}, {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}, {sfTotalValueOutstanding, soeDEFAULT}, {sfInterestOwed, soeDEFAULT}, diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index 24d87e9b17..d13bb78a19 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -151,7 +151,8 @@ computePaymentComponents( roundPeriodicPayment(asset, periodicPayment, scale); 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 // periodicPayment until the last scheduled payment, but if it ever is, @@ -217,8 +218,9 @@ computePaymentComponents( totalValueOutstanding - roundedPeriodicPayment) return roundedPeriodicPayment; // Use the amount that will get principal outstanding as close to - // reference principal as possible. - return p; + // reference principal as possible, but don't pay more than the rounded + // 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)), "") @@ -234,13 +236,10 @@ computePaymentComponents( "ripple::detail::computePaymentComponents", "valid rounded interest"); XRPL_ASSERT_PARTS( - roundedPrincipal >= 0 && roundedPrincipal <= principalOutstanding, + roundedPrincipal >= 0 && roundedPrincipal <= principalOutstanding && + isRounded(asset, roundedPrincipal, scale), "ripple::detail::computePaymentComponents", "valid rounded principal"); - XRPL_ASSERT_PARTS( - isRounded(asset, roundedPrincipal, scale), - "ripple::detail::computePaymentComponents", - "principal is rounded"); return { .rawInterest = rawInterest,