diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index e9780211de..29eefe9f76 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -2758,6 +2758,15 @@ protected: state.paymentRemaining, broker.params.managementFeeRate); +#if LOANFILLSHORTAGE + BEAST_EXPECT( + paymentComponents.trackedValueDelta == + roundedPeriodicPayment || + (paymentComponents.specialCase == + detail::PaymentSpecialCase::final && + paymentComponents.trackedValueDelta < + roundedPeriodicPayment)); +#else BEAST_EXPECTS( paymentComponents.specialCase == detail::PaymentSpecialCase::final || @@ -2767,6 +2776,7 @@ protected: to_string(paymentComponents.trackedValueDelta) + ", periodic payment: " + to_string(roundedPeriodicPayment)); +#endif xrpl::LoanState const nextTrueState = computeTheoreticalLoanState( diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index a8354ff049..3552b8cedf 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -1057,18 +1057,42 @@ computePaymentComponents( "xrpl::detail::computePaymentComponents", "excess non-negative"); }; - // Helper to reduce deltas when they collectively exceed a limit. - // Order matters: we prefer to reduce interest first (most flexible), - // then management fee, then principal (least flexible). +#if LOANFILLSHORTAGE + auto giveTo = + [](Number& component, Number& shortage, Number const& maximum) { + if (shortage > beast::zero) + { + // Put as much of the shortage as we can into the provided part + // and the total + auto part = std::min(maximum - component, shortage); + component += part; + shortage -= part; + } + // If the shortage goes negative, we put too much, which should be + // impossible + XRPL_ASSERT_PARTS( + shortage >= beast::zero, + "ripple::detail::computePaymentComponents", + "excess non-negative"); + }; +#endif auto addressExcess = [&takeFrom](LoanStateDeltas& deltas, Number& excess) { // This order is based on where errors are the least problematic takeFrom(deltas.interest, excess); takeFrom(deltas.managementFee, excess); takeFrom(deltas.principal, excess); }; - - // Check if deltas exceed the total outstanding value. This should never - // happen due to earlier caps, but handle it defensively. +#if LOANFILLSHORTAGE + auto addressShortage = [&giveTo]( + LoanStateDeltas& deltas, + Number& shortage, + LoanState const& current) { + giveTo(deltas.interestDueDelta, shortage, current.interestDue); + giveTo( + deltas.managementFeeDueDelta, shortage, current.managementFeeDue); + giveTo(deltas.principalDelta, shortage, current.principalOutstanding); + }; +#endif Number totalOverpayment = deltas.total() - currentLedgerState.valueOutstanding; @@ -1097,14 +1121,44 @@ computePaymentComponents( addressExcess(deltas, excess); shortage = -excess; } +#if LOANFILLSHORTAGE + else if (shortage > beast::zero && totalOverpayment < beast::zero) + { + // If there's a shortage, and there's room in the loan itself, we can + // top up the parts to make the payment correct. + shortage = std::min(-totalOverpayment, shortage); + addressShortage(deltas, shortage, currentLedgerState); + } - // At this point, shortage >= 0 means we're paying less than the full - // periodic payment (due to rounding or component caps). - // shortage < 0 would mean we're trying to pay more than allowed (bug). + // The shortage should never be negative, which indicates that the parts are + // trying to take more than the whole payment. The shortage should not be + // positive, either, which indicates that we're not going to take the whole + // payment amount. Only the last payment should be allowed to have a + // shortage, and that's handled in a special case above. + XRPL_ASSERT_PARTS( + shortage == beast::zero, + "ripple::detail::computePaymentComponents", + "no shortage or excess"); +#else + // The shortage should never be negative, which indicates that the + // parts are trying to take more than the whole payment. The + // shortage may be positive, which indicates that we're not going to + // take the whole payment amount. XRPL_ASSERT_PARTS( shortage >= beast::zero, "xrpl::detail::computePaymentComponents", "no shortage or excess"); +#endif +#if LOANCOMPLETE + /* + // This used to be part of the above assert. It will eventually be removed + // if proved accurate + || + (shortage > beast::zero && + ((asset.integral() && shortage < 3) || + (scale - shortage.exponent() > 14))) + */ +#endif // Final validation that all components are valid XRPL_ASSERT_PARTS(