Handle overpayment calculations

This commit is contained in:
Ed Hennis
2025-10-06 18:31:44 -04:00
parent 8d982758cb
commit eeec90ee74
2 changed files with 153 additions and 85 deletions

View File

@@ -45,6 +45,7 @@ struct PaymentComponents
// We may not need roundedPayment // We may not need roundedPayment
Number roundedPayment; Number roundedPayment;
bool final = false; bool final = false;
bool extra = false;
}; };
// This structure is explained in the XLS-66 spec, section 3.2.4.4 (Failure // This structure is explained in the XLS-66 spec, section 3.2.4.4 (Failure
@@ -279,6 +280,8 @@ doPayment(
nextDueDateProxy, nextDueDateProxy,
"ripple::detail::doPayment", "ripple::detail::doPayment",
"Next due date proxy set"); "Next due date proxy set");
if (!payment.extra)
{
if (payment.final) if (payment.final)
{ {
paymentRemainingProxy = 0; paymentRemainingProxy = 0;
@@ -306,12 +309,12 @@ doPayment(
paymentRemainingProxy -= 1; paymentRemainingProxy -= 1;
prevPaymentDateProxy = *nextDueDateProxy; prevPaymentDateProxy = *nextDueDateProxy;
// STObject::OptionalField does not define operator+=, and I don't want // STObject::OptionalField does not define operator+=, and I don't
// to add one right now. // want to add one right now.
nextDueDateProxy = *nextDueDateProxy + paymentInterval; nextDueDateProxy = *nextDueDateProxy + paymentInterval;
} }
// A single payment always pays the same amount of principal. Only the }
// interest and fees are extra for a late payment
referencePrincipalProxy -= payment.rawPrincipal; referencePrincipalProxy -= payment.rawPrincipal;
principalOutstandingProxy -= payment.roundedPrincipal; principalOutstandingProxy -= payment.roundedPrincipal;
totalValueOutstandingProxy -= totalValueOutstandingProxy -=
@@ -800,6 +803,7 @@ loanMakePayment(
ApplyView& view, ApplyView& view,
SLE::ref loan, SLE::ref loan,
STAmount const& amount, STAmount const& amount,
TenthBips16 managementFeeRate,
beast::Journal j) beast::Journal j)
{ {
/* /*
@@ -826,20 +830,16 @@ loanMakePayment(
std::int32_t const loanScale = loan->at(sfLoanScale); std::int32_t const loanScale = loan->at(sfLoanScale);
auto totalValueOutstandingProxy = loan->at(sfTotalValueOutstanding); auto totalValueOutstandingProxy = loan->at(sfTotalValueOutstanding);
auto interestOwedProxy = loan->at(sfInterestOwed);
auto referencePrincipalProxy = loan->at(sfReferencePrincipal); auto referencePrincipalProxy = loan->at(sfReferencePrincipal);
TenthBips32 const interestRate{loan->at(sfInterestRate)}; TenthBips32 const interestRate{loan->at(sfInterestRate)};
TenthBips32 const lateInterestRate{loan->at(sfLateInterestRate)}; TenthBips32 const lateInterestRate{loan->at(sfLateInterestRate)};
TenthBips32 const closeInterestRate{loan->at(sfCloseInterestRate)}; TenthBips32 const closeInterestRate{loan->at(sfCloseInterestRate)};
TenthBips32 const overpaymentInterestRate{
loan->at(sfOverpaymentInterestRate)};
Number const serviceFee = loan->at(sfLoanServiceFee); Number const serviceFee = loan->at(sfLoanServiceFee);
Number const latePaymentFee = loan->at(sfLatePaymentFee); Number const latePaymentFee = loan->at(sfLatePaymentFee);
Number const closePaymentFee = Number const closePaymentFee =
roundToAsset(asset, loan->at(sfClosePaymentFee), loanScale); roundToAsset(asset, loan->at(sfClosePaymentFee), loanScale);
TenthBips32 const overpaymentFee{loan->at(sfOverpaymentFee)};
auto const periodicPayment = loan->at(sfPeriodicPayment); auto const periodicPayment = loan->at(sfPeriodicPayment);
@@ -857,10 +857,6 @@ loanMakePayment(
XRPL_ASSERT( XRPL_ASSERT(
*totalValueOutstandingProxy > 0, *totalValueOutstandingProxy > 0,
"ripple::loanMakePayment : valid total value"); "ripple::loanMakePayment : valid total value");
XRPL_ASSERT_PARTS(
*interestOwedProxy >= 0,
"ripple::loanMakePayment",
"valid interest owed");
view.update(loan); view.update(loan);
@@ -1027,68 +1023,138 @@ loanMakePayment(
"ripple::loanMakePayment", "ripple::loanMakePayment",
"fee parts add up"); "fee parts add up");
return Unexpected(temDISABLED);
#if LOANCOMPLETE
// ------------------------------------------------------------- // -------------------------------------------------------------
// overpayment handling // overpayment handling
Number overpaymentInterestPortion = 0;
if (loan->isFlag(lsfLoanOverpayment)) if (loan->isFlag(lsfLoanOverpayment))
{ {
Number const overpayment = std::min( TenthBips32 const overpaymentInterestRate{
principalOutstandingProxy.value(), loan->at(sfOverpaymentInterestRate)};
amount - (totalPrincipalPaid + totalInterestPaid + totalfeeToPay)); TenthBips32 const overpaymentFeeRate{loan->at(sfOverpaymentFee)};
if (roundToAsset(asset, overpayment, loanScale) > 0) Number const overpayment = amount -
{ (totalParts.principalPaid + totalParts.interestPaid +
Number const interestPortion = roundToAsset( totalParts.feeToPay);
XRPL_ASSERT(
overpayment >= 0 && isRounded(asset, overpayment, loanScale),
"ripple::loanMakePayment : valid overpayment amount");
Number const fee = roundToAsset(
asset, asset,
tenthBipsOfValue(overpayment, overpaymentInterestRate), tenthBipsOfValue(overpayment, overpaymentFeeRate),
loanScale); loanScale);
Number const feePortion = roundToAsset(
asset, Number const payment = overpayment - fee;
tenthBipsOfValue(overpayment, overpaymentFee),
loanScale); Number const interest =
Number const remainder = roundToAsset( tenthBipsOfValue(payment, overpaymentInterestRate);
asset, overpayment - interestPortion - feePortion, loanScale); Number const roundedInterest = roundToAsset(asset, interest, loanScale);
detail::PaymentComponentsPlus overpaymentComponents{
PaymentComponents{
.rawInterest = interest,
.rawPrincipal = payment - interest,
.roundedInterest = roundedInterest,
.roundedPrincipal = payment - roundedInterest,
.roundedPayment = payment,
.extra = true},
fee};
// Don't process an overpayment if the whole amount (or more!) // Don't process an overpayment if the whole amount (or more!)
// gets eaten by fees // gets eaten by fees and interest.
if (remainder > 0) if (overpaymentComponents.rawPrincipal > 0 &&
overpaymentComponents.roundedPrincipal > 0)
{ {
overpaymentInterestPortion = interestPortion; Number const totalInterestOutstandingBefore =
totalPrincipalPaid += remainder; totalValueOutstandingProxy - principalOutstandingProxy;
totalInterestPaid += interestPortion;
totalfeeToPay += feePortion;
principalOutstandingProxy -= remainder; auto const oldLoanProperties = computeLoanProperties(
} asset,
principalOutstandingProxy,
referencePrincipalProxy,
interestRate,
paymentInterval,
paymentRemainingProxy,
managementFeeRate);
auto const accumulatedError =
oldLoanProperties.totalValueOutstanding -
totalValueOutstandingProxy;
totalParts += detail::doPayment(
overpaymentComponents,
totalValueOutstandingProxy,
principalOutstandingProxy,
referencePrincipalProxy,
paymentRemainingProxy,
prevPaymentDateProxy,
nextDueDateProxy,
paymentInterval);
auto const newLoanProperties = computeLoanProperties(
asset,
principalOutstandingProxy,
referencePrincipalProxy,
interestRate,
paymentInterval,
paymentRemainingProxy,
managementFeeRate);
if (newLoanProperties.firstPaymentPrincipal <= 0 &&
*principalOutstandingProxy > 0)
{
// The overpayment has caused the loan to be in a state where
// no further principal can be paid.
JLOG(j.warn())
<< "Loan overpayment would cause loan to be stuck. "
"Rejecting overpayment.";
return Unexpected(tecLIMIT_EXCEEDED);
} }
// Check that the other computed values are valid
if (newLoanProperties.interestOwedToVault < 0 ||
newLoanProperties.totalValueOutstanding <= 0 ||
newLoanProperties.periodicPayment <= 0)
{
// LCOV_EXCL_START
JLOG(j.warn()) << "Computed loan properties are invalid. Does "
"not compute. TotalValueOutstanding: "
<< newLoanProperties.totalValueOutstanding
<< ", PeriodicPayment: "
<< newLoanProperties.periodicPayment
<< ", InterestOwedToVault: "
<< newLoanProperties.interestOwedToVault;
return Unexpected(tecINTERNAL);
// LCOV_EXCL_STOP
} }
totalParts.valueChange += (newTotalInterest - totalInterestOutstanding) + totalValueOutstandingProxy =
overpaymentInterestPortion; newLoanProperties.totalValueOutstanding;
loan->at(sfPeriodicPayment) = newLoanProperties.periodicPayment;
loan->at(sfInterestOwed) = newLoanProperties.interestOwedToVault;
auto const totalInterestOutstandingAfter =
totalValueOutstandingProxy - principalOutstandingProxy;
totalParts.valueChange += totalInterestOutstandingBefore -
totalInterestOutstandingAfter +
overpaymentComponents.roundedInterest + accumulatedError;
}
}
// Check the final results are rounded, to double-check that the // Check the final results are rounded, to double-check that the
// intermediate steps were rounded. // intermediate steps were rounded.
XRPL_ASSERT( XRPL_ASSERT(
roundToAsset(asset, totalPrincipalPaid, loanScale) == isRounded(asset, totalParts.principalPaid, loanScale),
totalPrincipalPaid, "ripple::loanMakePayment : total principal paid rounded");
"ripple::loanMakePayment : totalPrincipalPaid rounded");
XRPL_ASSERT( XRPL_ASSERT(
roundToAsset(asset, totalInterestPaid, loanScale) == totalInterestPaid, isRounded(asset, totalParts.interestPaid, loanScale),
"ripple::loanMakePayment : totalInterestPaid rounded"); "ripple::loanMakePayment : total interest paid rounded");
XRPL_ASSERT( XRPL_ASSERT(
roundToAsset(asset, loanValueChange, loanScale) == loanValueChange, isRounded(asset, totalParts.valueChange, loanScale),
"ripple::loanMakePayment : loanValueChange rounded"); "ripple::loanMakePayment : loan value change rounded");
XRPL_ASSERT( XRPL_ASSERT(
roundToAsset(asset, totalfeeToPay, loanScale) == totalfeeToPay, isRounded(asset, totalParts.feeToPay, loanScale),
"ripple::loanMakePayment : totalfeeToPay rounded"); "ripple::loanMakePayment : total fee to pay rounded");
return LoanPaymentParts{ return totalParts;
.principalPaid = totalPrincipalPaid,
.interestPaid = totalInterestPaid,
.valueChange = loanValueChange,
.feeToPay = totalfeeToPay};
#endif
} }
} // namespace ripple } // namespace ripple

View File

@@ -234,8 +234,10 @@ LoanPay::doApply()
LoanManage::unimpairLoan(view, loanSle, vaultSle, j_); LoanManage::unimpairLoan(view, loanSle, vaultSle, j_);
} }
TenthBips16 managementFeeRate{brokerSle->at(sfManagementFeeRate)};
Expected<LoanPaymentParts, TER> paymentParts = Expected<LoanPaymentParts, TER> paymentParts =
loanMakePayment(asset, view, loanSle, amount, j_); loanMakePayment(asset, view, loanSle, amount, managementFeeRate, j_);
if (!paymentParts) if (!paymentParts)
return paymentParts.error(); return paymentParts.error();
@@ -245,11 +247,12 @@ LoanPay::doApply()
view.update(loanSle); view.update(loanSle);
XRPL_ASSERT_PARTS( XRPL_ASSERT_PARTS(
// It is possible to pay 0 interest // It is possible to pay 0 principal
paymentParts->principalPaid >= 0, paymentParts->principalPaid >= 0,
"ripple::LoanPay::doApply", "ripple::LoanPay::doApply",
"valid principal paid"); "valid principal paid");
XRPL_ASSERT_PARTS( XRPL_ASSERT_PARTS(
// It is possible to pay 0 interest
paymentParts->interestPaid >= 0, paymentParts->interestPaid >= 0,
"ripple::LoanPay::doApply", "ripple::LoanPay::doApply",
"valid interest paid"); "valid interest paid");
@@ -272,7 +275,6 @@ LoanPay::doApply()
// LoanBroker object state changes // LoanBroker object state changes
view.update(brokerSle); view.update(brokerSle);
TenthBips32 managementFeeRate{brokerSle->at(sfManagementFeeRate)};
auto interestOwedProxy = loanSle->at(sfInterestOwed); auto interestOwedProxy = loanSle->at(sfInterestOwed);
auto const [managementFee, interestPaidToVault] = [&]() { auto const [managementFee, interestPaidToVault] = [&]() {
@@ -283,7 +285,7 @@ LoanPay::doApply()
auto const interest = paymentParts->interestPaid - managementFee; auto const interest = paymentParts->interestPaid - managementFee;
auto const owed = *interestOwedProxy; auto const owed = *interestOwedProxy;
if (interest > owed) if (interest > owed)
return std::make_pair(interest - owed, owed); return std::make_pair(paymentParts->interestPaid - owed, owed);
return std::make_pair(managementFee, interest); return std::make_pair(managementFee, interest);
}(); }();
XRPL_ASSERT_PARTS( XRPL_ASSERT_PARTS(