Update LoanPay

- Enable the rest of LoanPay.
- Start updating the helper functions.
- Tests are not expected to pass.
This commit is contained in:
Ed Hennis
2025-10-03 22:38:17 -04:00
parent fb8dafa6a8
commit 96d0258f51
3 changed files with 244 additions and 162 deletions

View File

@@ -37,6 +37,7 @@ struct PaymentParts
Number rawPrincipal; Number rawPrincipal;
Number roundedInterest; Number roundedInterest;
Number roundedPrincipal; Number roundedPrincipal;
// We may not need roundedPayment
Number roundedPayment; Number roundedPayment;
bool final = false; bool final = false;
}; };
@@ -66,8 +67,7 @@ loanLatePaymentInterest(
Number const& principalOutstanding, Number const& principalOutstanding,
TenthBips32 lateInterestRate, TenthBips32 lateInterestRate,
NetClock::time_point parentCloseTime, NetClock::time_point parentCloseTime,
std::uint32_t startDate, std::uint32_t nextPaymentDueDate);
std::uint32_t prevPaymentDate);
Number Number
loanAccruedInterest( loanAccruedInterest(
@@ -139,8 +139,6 @@ computePaymentParts(
* The principal and interest portions can be derived as follows: * The principal and interest portions can be derived as follows:
* interest = principalOutstanding * periodicRate * interest = principalOutstanding * periodicRate
* principal = periodicPayment - interest * principal = periodicPayment - interest
*
* Because those values deal with funds, they need to be rounded.
*/ */
Number const rawInterest = referencePrincipal * periodicRate; Number const rawInterest = referencePrincipal * periodicRate;
Number const rawPrincipal = periodicPayment - rawInterest; Number const rawPrincipal = periodicPayment - rawInterest;
@@ -153,11 +151,45 @@ computePaymentParts(
"ripple::detail::computePaymentParts", "ripple::detail::computePaymentParts",
"valid raw principal"); "valid raw principal");
Number const roundedInterest = // if (count($A20), MIN(Z19, Z19 - FLOOR(AA19 - Y20, 1)), "")
roundToAsset(asset, rawInterest, scale, Number::upward); // Z19 = outstanding principal
Number const roundedPrincipal = roundedPeriodicPayment - roundedInterest; // AA19 = reference principal
// Y20 = raw principal
Number const roundedPrincipal = [&]() {
Number const p = std::max(
Number{},
std::min(
principalOutstanding,
principalOutstanding -
roundToAsset(
asset,
referencePrincipal - rawPrincipal,
scale,
Number::downward)));
// if the estimated principal payment would leave the principal higher
// than the "total "after payment" value of the loan, make the principal
// payment also take the principal down to that same "after" value.
// This should mean that all interest is paid, or that the loan has some
// tricky parameters.
if (principalOutstanding - p >
totalValueOutstanding - roundedPeriodicPayment)
return roundedPeriodicPayment;
// Use the amount that will get principal outstanding as close to
// reference principal as possible.
return p;
}();
// if(count($A20), if(AB19 < $B$5, AB19 - Z19, CEILING($B$10-W20, 1)), "")
// AB19 = total loan value
// $B$5 = periodic payment (unrounded)
// Z19 = outstanding principal
// $B$10 = periodic payment (rounded up)
// W20 = rounded principal
Number const roundedInterest = roundedPeriodicPayment - roundedPrincipal;
XRPL_ASSERT_PARTS( XRPL_ASSERT_PARTS(
roundedInterest >= 0, roundedInterest >= 0 && isRounded(asset, roundedInterest, scale),
"ripple::detail::computePaymentParts", "ripple::detail::computePaymentParts",
"valid rounded interest"); "valid rounded interest");
XRPL_ASSERT_PARTS( XRPL_ASSERT_PARTS(
@@ -213,6 +245,10 @@ computeLoanProperties(
{ {
auto const periodicRate = auto const periodicRate =
detail::loanPeriodicRate(interestRate, paymentInterval); detail::loanPeriodicRate(interestRate, paymentInterval);
XRPL_ASSERT(
interestRate == 0 || periodicRate > 0,
"ripple::loanMakePayment : valid rate");
auto const periodicPayment = detail::loanPeriodicPayment( auto const periodicPayment = detail::loanPeriodicPayment(
principalOutstanding, periodicRate, paymentsRemaining); principalOutstanding, periodicRate, paymentsRemaining);
Number const totalValueOutstanding = [&]() { Number const totalValueOutstanding = [&]() {
@@ -445,9 +481,8 @@ loanLatePaymentInterest(
Number const& principalOutstanding, Number const& principalOutstanding,
TenthBips32 lateInterestRate, TenthBips32 lateInterestRate,
NetClock::time_point parentCloseTime, NetClock::time_point parentCloseTime,
std::uint32_t startDate, std::uint32_t nextPaymentDueDate,
std::uint32_t prevPaymentDate, std::int32_t const& scale)
Number const& scale)
{ {
return roundToAsset( return roundToAsset(
asset, asset,
@@ -455,8 +490,7 @@ loanLatePaymentInterest(
principalOutstanding, principalOutstanding,
lateInterestRate, lateInterestRate,
parentCloseTime, parentCloseTime,
startDate, nextPaymentDueDate),
prevPaymentDate),
scale); scale);
} }
@@ -484,9 +518,34 @@ struct LoanPaymentParts
*/ */
Number valueChange; Number valueChange;
/// fee_paid is the amount of fee that the payment covered. /// fee_paid is the amount of fee that the payment covered.
Number feePaid; Number feeToPay;
}; };
template <class NumberProxy, class Int32Proxy>
void
doPayment(
PaymentParts const& payment,
NumberProxy& totalValueOutstandingProxy,
NumberProxy& principalOutstandingProxy,
NumberProxy& referencePrincipalProxy,
Int32Proxy& paymentRemainingProxy,
Int32Proxy& prevPaymentDateProxy,
Int32Proxy& nextDueDateProxy,
std::uint32_t paymentInterval)
{
paymentRemainingProxy -= 1;
// A single payment always pays the same amount of principal. Only the
// interest and fees are extra for a late payment
referencePrincipalProxy -= payment.rawPrincipal;
principalOutstandingProxy -= payment.roundedPrincipal;
totalValueOutstandingProxy -=
payment.roundedPrincipal + payment.roundedInterest;
// Make sure this does an assignment
prevPaymentDateProxy = nextDueDateProxy;
nextDueDateProxy += paymentInterval;
}
/* Handle possible late payments. /* Handle possible late payments.
* *
* If this function processed a late payment, the return value will be * If this function processed a late payment, the return value will be
@@ -501,25 +560,20 @@ struct LoanPaymentParts
* * section 3.2.4.1.2 (Late Payment) * * section 3.2.4.1.2 (Late Payment)
*/ */
template <AssetType A, class NumberProxy, class Int32Proxy> template <AssetType A, class NumberProxy, class Int32Proxy>
Expected<LoanPaymentParts, TER> Expected<std::pair<PaymentParts, LoanPaymentParts>, TER>
handleLatePayment( handleLatePayment(
A const& asset, A const& asset,
ApplyView& view, ApplyView& view,
NumberProxy& principalOutstandingProxy, NumberProxy& principalOutstandingProxy,
Int32Proxy& paymentRemainingProxy,
Int32Proxy& prevPaymentDateProxy,
Int32Proxy& nextDueDateProxy, Int32Proxy& nextDueDateProxy,
PaymentParts const& periodic, PaymentParts const& periodic,
std::uint32_t const startDate,
std::uint32_t const paymentInterval,
TenthBips32 const lateInterestRate, TenthBips32 const lateInterestRate,
std::int32_t loanScale, std::int32_t loanScale,
Number const& paymentFee,
Number const& latePaymentFee, Number const& latePaymentFee,
STAmount const& amount, STAmount const& amount,
beast::Journal j) beast::Journal j)
{ {
return Unexpected(temDISABLED);
#if LOANCOMPLETE
if (!hasExpired(view, nextDueDateProxy)) if (!hasExpired(view, nextDueDateProxy))
return Unexpected(tesSUCCESS); return Unexpected(tesSUCCESS);
@@ -531,17 +585,23 @@ handleLatePayment(
principalOutstandingProxy, principalOutstandingProxy,
lateInterestRate, lateInterestRate,
view.parentCloseTime(), view.parentCloseTime(),
startDate, nextDueDateProxy,
prevPaymentDateProxy,
loanScale); loanScale);
XRPL_ASSERT( XRPL_ASSERT(
latePaymentInterest >= 0, latePaymentInterest >= 0,
"ripple::handleLatePayment : valid late interest"); "ripple::handleLatePayment : valid late interest");
PaymentParts const late{ PaymentParts const late{
.interest = latePaymentInterest + periodic.interest, .rawInterest = periodic.rawInterest + latePaymentInterest,
.principal = periodic.principal, .rawPrincipal = periodic.rawPrincipal,
.fee = latePaymentFee + periodic.fee}; .roundedInterest = periodic.roundedInterest + latePaymentInterest,
auto const totalDue = late.principal + late.interest + late.fee; .roundedPrincipal = periodic.roundedPrincipal,
.roundedPayment = periodic.roundedPayment};
auto const fee = paymentFee + latePaymentFee;
auto const totalDue = late.roundedPrincipal + late.roundedInterest + fee;
XRPL_ASSERT_PARTS(
isRounded(asset, totalDue, loanScale),
"ripple::handleLatePayment",
"total due is rounded");
if (amount < totalDue) if (amount < totalDue)
{ {
@@ -550,23 +610,15 @@ handleLatePayment(
return Unexpected(tecINSUFFICIENT_PAYMENT); return Unexpected(tecINSUFFICIENT_PAYMENT);
} }
paymentRemainingProxy -= 1;
// A single payment always pays the same amount of principal. Only the
// interest and fees are extra for a late payment
principalOutstandingProxy -= late.principal;
// Make sure this does an assignment
prevPaymentDateProxy = nextDueDateProxy;
nextDueDateProxy += paymentInterval;
// A late payment increases the value of the loan by the difference // A late payment increases the value of the loan by the difference
// between periodic and late payment interest // between periodic and late payment interest
return LoanPaymentParts{ return std::make_pair(
.principalPaid = late.principal, late,
.interestPaid = late.interest, LoanPaymentParts{
.principalPaid = late.roundedPrincipal,
.interestPaid = late.roundedInterest,
.valueChange = latePaymentInterest, .valueChange = latePaymentInterest,
.feePaid = late.fee}; .feeToPay = fee});
#endif
} }
/* Handle possible full payments. /* Handle possible full payments.
@@ -641,7 +693,7 @@ handleFullPayment(
.principalPaid = principalOutstandingProxy, .principalPaid = principalOutstandingProxy,
.interestPaid = totalInterest, .interestPaid = totalInterest,
.valueChange = valueChange, .valueChange = valueChange,
.feePaid = closePaymentFee}; .feeToPay = closePaymentFee};
paymentRemainingProxy = 0; paymentRemainingProxy = 0;
principalOutstandingProxy = 0; principalOutstandingProxy = 0;
@@ -664,7 +716,9 @@ 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 principalOutstandingProxy = loan->at(sfPrincipalOutstanding); auto principalOutstandingProxy = loan->at(sfPrincipalOutstanding);
auto referencePrincipalProxy = loan->at(sfReferencePrincipal);
bool const allowOverpayment = loan->isFlag(lsfLoanOverpayment); bool const allowOverpayment = loan->isFlag(lsfLoanOverpayment);
TenthBips32 const interestRate{loan->at(sfInterestRate)}; TenthBips32 const interestRate{loan->at(sfInterestRate)};
@@ -679,7 +733,7 @@ loanMakePayment(
roundToAsset(asset, loan->at(sfClosePaymentFee), loanScale); roundToAsset(asset, loan->at(sfClosePaymentFee), loanScale);
TenthBips32 const overpaymentFee{loan->at(sfOverpaymentFee)}; TenthBips32 const overpaymentFee{loan->at(sfOverpaymentFee)};
std::uint32_t const paymentInterval = loan->at(sfPaymentInterval); auto const periodicPayment = loan->at(sfPeriodicPayment);
auto paymentRemainingProxy = loan->at(sfPaymentRemaining); auto paymentRemainingProxy = loan->at(sfPaymentRemaining);
auto prevPaymentDateProxy = loan->at(sfPreviousPaymentDate); auto prevPaymentDateProxy = loan->at(sfPreviousPaymentDate);
@@ -693,6 +747,7 @@ loanMakePayment(
return Unexpected(tecKILLED); return Unexpected(tecKILLED);
} }
std::uint32_t const paymentInterval = loan->at(sfPaymentInterval);
// Compute the normal periodic rate, payment, etc. // Compute the normal periodic rate, payment, etc.
// We'll need it in the remaining calculations // We'll need it in the remaining calculations
Number const periodicRate = Number const periodicRate =
@@ -701,66 +756,61 @@ loanMakePayment(
interestRate == 0 || periodicRate > 0, interestRate == 0 || periodicRate > 0,
"ripple::loanMakePayment : valid rate"); "ripple::loanMakePayment : valid rate");
// Don't round the payment amount. Only round the final computations
// using it.
Number const periodicPaymentAmount = detail::loanPeriodicPayment(
principalOutstandingProxy, periodicRate, paymentRemainingProxy);
XRPL_ASSERT( XRPL_ASSERT(
periodicPaymentAmount > 0, *totalValueOutstandingProxy > 0,
"ripple::computePeriodicPayment : valid payment"); "ripple::loanMakePayment : valid total value");
XRPL_ASSERT_PARTS(
*interestOwedProxy >= 0,
"ripple::loanMakePayment",
"valid interest owed");
auto const periodic = computePaymentParts( view.update(loan);
auto const periodic = detail::computePaymentParts(
asset, asset,
loanScale, loanScale,
totalValueOutstandingProxy, totalValueOutstandingProxy,
principalOutstandingProxy, principalOutstandingProxy,
periodicPaymentAmount, referencePrincipalProxy,
serviceFee, periodicPayment,
periodicRate, periodicRate,
paymentRemainingProxy); paymentRemainingProxy);
Number const totalValueOutstanding = loanTotalValueOutstanding(
asset, loanScale, periodicPaymentAmount, paymentRemainingProxy);
XRPL_ASSERT(
totalValueOutstanding > 0,
"ripple::loanMakePayment : valid total value");
Number const totalInterestOutstanding = loanTotalInterestOutstanding(
principalOutstandingProxy, totalValueOutstanding);
XRPL_ASSERT_PARTS(
totalInterestOutstanding >= 0,
"ripple::loanMakePayment",
"valid total interest");
XRPL_ASSERT_PARTS(
totalValueOutstanding - totalInterestOutstanding ==
principalOutstandingProxy,
"ripple::loanMakePayment",
"valid principal computation");
view.update(loan);
// ------------------------------------------------------------- // -------------------------------------------------------------
// late payment handling // late payment handling
if (auto const latePaymentParts = handleLatePayment( if (auto const latePaymentParts = handleLatePayment(
asset, asset,
view, view,
principalOutstandingProxy, principalOutstandingProxy,
paymentRemainingProxy,
prevPaymentDateProxy,
nextDueDateProxy, nextDueDateProxy,
periodic, periodic,
startDate,
paymentInterval,
lateInterestRate, lateInterestRate,
loanScale, loanScale,
serviceFee,
latePaymentFee, latePaymentFee,
amount, amount,
j)) j))
return *latePaymentParts; {
doPayment(
latePaymentParts->first,
totalValueOutstandingProxy,
principalOutstandingProxy,
referencePrincipalProxy,
paymentRemainingProxy,
prevPaymentDateProxy,
nextDueDateProxy,
paymentInterval);
return latePaymentParts->second;
}
else if (latePaymentParts.error()) else if (latePaymentParts.error())
return latePaymentParts; return Unexpected(latePaymentParts.error());
// ------------------------------------------------------------- // -------------------------------------------------------------
// full payment handling // full payment handling
auto const totalInterestOutstanding =
totalValueOutstandingProxy - principalOutstandingProxy;
if (auto const fullPaymentParts = handleFullPayment( if (auto const fullPaymentParts = handleFullPayment(
asset, asset,
view, view,
@@ -783,11 +833,16 @@ loanMakePayment(
// ------------------------------------------------------------- // -------------------------------------------------------------
// regular periodic payment handling // regular periodic payment handling
return Unexpected(temDISABLED);
#if LOANCOMPLETE
// if the payment is not late nor if it's a full payment, then it must // if the payment is not late nor if it's a full payment, then it must
// be a periodic one, with possible overpayments // be a periodic one, with possible overpayments
auto const totalDue = periodic.interest + periodic.principal + periodic.fee; auto const totalDue = periodic.interest + periodic.principal + periodic.fee;
// TODO: Don't attempt to figure out the number of payments beforehand. Just
// loop over making payments until the `amount` is used up or the loan is
// paid off.
std::optional<NumberRoundModeGuard> mg(Number::downward); std::optional<NumberRoundModeGuard> mg(Number::downward);
std::int64_t const fullPeriodicPayments = [&]() { std::int64_t const fullPeriodicPayments = [&]() {
std::int64_t const full{amount / totalDue}; std::int64_t const full{amount / totalDue};
@@ -826,7 +881,7 @@ loanMakePayment(
loanScale, loanScale,
totalValueOutstandingProxy, totalValueOutstandingProxy,
principalOutstandingProxy, principalOutstandingProxy,
periodicPaymentAmount, periodicPayment,
serviceFee, serviceFee,
periodicRate, periodicRate,
paymentRemainingProxy); paymentRemainingProxy);
@@ -845,7 +900,7 @@ loanMakePayment(
future.reset(); future.reset();
} }
Number totalFeePaid = serviceFee * fullPeriodicPayments; Number totalfeeToPay = serviceFee * fullPeriodicPayments;
Number const newInterest = loanTotalInterestOutstanding( Number const newInterest = loanTotalInterestOutstanding(
asset, asset,
@@ -863,7 +918,7 @@ loanMakePayment(
{ {
Number const overpayment = std::min( Number const overpayment = std::min(
principalOutstandingProxy.value(), principalOutstandingProxy.value(),
amount - (totalPrincipalPaid + totalInterestPaid + totalFeePaid)); amount - (totalPrincipalPaid + totalInterestPaid + totalfeeToPay));
if (roundToAsset(asset, overpayment, loanScale) > 0) if (roundToAsset(asset, overpayment, loanScale) > 0)
{ {
@@ -885,7 +940,7 @@ loanMakePayment(
overpaymentInterestPortion = interestPortion; overpaymentInterestPortion = interestPortion;
totalPrincipalPaid += remainder; totalPrincipalPaid += remainder;
totalInterestPaid += interestPortion; totalInterestPaid += interestPortion;
totalFeePaid += feePortion; totalfeeToPay += feePortion;
principalOutstandingProxy -= remainder; principalOutstandingProxy -= remainder;
} }
@@ -908,13 +963,14 @@ loanMakePayment(
roundToAsset(asset, loanValueChange, loanScale) == loanValueChange, roundToAsset(asset, loanValueChange, loanScale) == loanValueChange,
"ripple::loanMakePayment : loanValueChange rounded"); "ripple::loanMakePayment : loanValueChange rounded");
XRPL_ASSERT( XRPL_ASSERT(
roundToAsset(asset, totalFeePaid, loanScale) == totalFeePaid, roundToAsset(asset, totalfeeToPay, loanScale) == totalfeeToPay,
"ripple::loanMakePayment : totalFeePaid rounded"); "ripple::loanMakePayment : totalfeeToPay rounded");
return LoanPaymentParts{ return LoanPaymentParts{
.principalPaid = totalPrincipalPaid, .principalPaid = totalPrincipalPaid,
.interestPaid = totalInterestPaid, .interestPaid = totalInterestPaid,
.valueChange = loanValueChange, .valueChange = loanValueChange,
.feePaid = totalFeePaid}; .feeToPay = totalfeeToPay};
#endif
} }
} // namespace ripple } // namespace ripple

View File

@@ -94,20 +94,18 @@ loanLatePaymentInterest(
Number const& principalOutstanding, Number const& principalOutstanding,
TenthBips32 lateInterestRate, TenthBips32 lateInterestRate,
NetClock::time_point parentCloseTime, NetClock::time_point parentCloseTime,
std::uint32_t startDate, std::uint32_t nextPaymentDueDate)
std::uint32_t prevPaymentDate)
{ {
/* /*
* This formula is from the XLS-66 spec, section 3.2.4.1.2 (Late payment), * This formula is from the XLS-66 spec, section 3.2.4.1.2 (Late payment),
* specifically "latePaymentInterest = ..." * specifically "latePaymentInterest = ..."
*
* The spec is to be updated to base the duration on the next due date
*/ */
auto const lastPaymentDate = std::max(prevPaymentDate, startDate); auto const secondsOverdue =
parentCloseTime.time_since_epoch().count() - nextPaymentDueDate;
auto const secondsSinceLastPayment = auto const rate = loanPeriodicRate(lateInterestRate, secondsOverdue);
parentCloseTime.time_since_epoch().count() - lastPaymentDate;
auto const rate =
loanPeriodicRate(lateInterestRate, secondsSinceLastPayment);
return principalOutstanding * rate; return principalOutstanding * rate;
} }

View File

@@ -122,8 +122,6 @@ LoanPay::preclaim(PreclaimContext const& ctx)
TER TER
LoanPay::doApply() LoanPay::doApply()
{ {
return temDISABLED;
#if LOANCOMPLETE
auto const& tx = ctx_.tx; auto const& tx = ctx_.tx;
auto& view = ctx_.view(); auto& view = ctx_.view();
@@ -149,37 +147,13 @@ LoanPay::doApply()
//------------------------------------------------------ //------------------------------------------------------
// Loan object state changes // Loan object state changes
std::int32_t const loanScale = loanSle->at(sfLoanScale);
// Unimpair the loan if it was impaired. Do this before the payment is // Unimpair the loan if it was impaired. Do this before the payment is
// attempted, so the original values can be used. If the payment fails, this // attempted, so the original values can be used. If the payment fails, this
// change will be discarded. // change will be discarded.
if (loanSle->isFlag(lsfLoanImpaired)) if (loanSle->isFlag(lsfLoanImpaired))
{ {
TenthBips32 const interestRate{loanSle->at(sfInterestRate)}; LoanManage::unimpairLoan(view, loanSle, vaultSle, j_);
auto const principalOutstanding = loanSle->at(sfPrincipalOutstanding);
TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)};
auto const paymentInterval = loanSle->at(sfPaymentInterval);
auto const paymentsRemaining = loanSle->at(sfPaymentRemaining);
auto const interestOutstanding = loanInterestOutstandingMinusFee(
asset,
loanScale,
principalOutstanding.value(),
interestRate,
paymentInterval,
paymentsRemaining,
managementFeeRate);
LoanManage::unimpairLoan(
view,
loanSle,
vaultSle,
principalOutstanding,
interestOutstanding,
paymentInterval,
j_);
} }
Expected<LoanPaymentParts, TER> paymentParts = Expected<LoanPaymentParts, TER> paymentParts =
@@ -193,7 +167,8 @@ LoanPay::doApply()
view.update(loanSle); view.update(loanSle);
XRPL_ASSERT_PARTS( XRPL_ASSERT_PARTS(
paymentParts->principalPaid > 0, // It is possible to pay 0 interest
paymentParts->principalPaid >= 0,
"ripple::LoanPay::doApply", "ripple::LoanPay::doApply",
"valid principal paid"); "valid principal paid");
XRPL_ASSERT_PARTS( XRPL_ASSERT_PARTS(
@@ -201,78 +176,83 @@ LoanPay::doApply()
"ripple::LoanPay::doApply", "ripple::LoanPay::doApply",
"valid interest paid"); "valid interest paid");
XRPL_ASSERT_PARTS( XRPL_ASSERT_PARTS(
paymentParts->feePaid >= 0, paymentParts->feeToPay >= 0,
"ripple::LoanPay::doApply", "ripple::LoanPay::doApply",
"valid fee paid"); "valid fee paid");
if (paymentParts->principalPaid <= 0 || paymentParts->interestPaid < 0 || if (paymentParts->principalPaid < 0 || paymentParts->interestPaid < 0 ||
paymentParts->feePaid < 0) paymentParts->feeToPay < 0)
{ {
// LCOV_EXCL_START // LCOV_EXCL_START
JLOG(j_.fatal()) << "Loan payment computation returned invalid values."; JLOG(j_.fatal()) << "Loan payment computation returned invalid values.";
return tecINTERNAL; return tecLIMIT_EXCEEDED;
// LCOV_EXCL_STOP // LCOV_EXCL_STOP
} }
std::int32_t const loanScale = loanSle->at(sfLoanScale);
//------------------------------------------------------ //------------------------------------------------------
// LoanBroker object state changes // LoanBroker object state changes
view.update(brokerSle); view.update(brokerSle);
TenthBips32 managementFeeRate{brokerSle->at(sfManagementFeeRate)}; TenthBips32 managementFeeRate{brokerSle->at(sfManagementFeeRate)};
auto interestOwedProxy = loanSle->at(sfInterestOwed);
auto const [managementFee, interestPaidToVault] = [&]() {
auto const managementFee = roundToAsset( auto const managementFee = roundToAsset(
asset, asset,
tenthBipsOfValue(paymentParts->interestPaid, managementFeeRate), tenthBipsOfValue(paymentParts->interestPaid, managementFeeRate),
loanScale); loanScale);
auto const interest = paymentParts->interestPaid - managementFee;
auto const owed = *interestOwedProxy;
if (interest > owed)
return std::make_pair(interest - owed, owed);
return std::make_pair(managementFee, interest);
}();
XRPL_ASSERT_PARTS(
managementFee >= 0 && interestPaidToVault >= 0 &&
(managementFee + interestPaidToVault ==
paymentParts->interestPaid) &&
isRounded(asset, managementFee, loanScale) &&
isRounded(asset, interestPaidToVault, loanScale),
"ripple::LoanPay::doApply",
"management fee computation is valid");
auto const totalPaidToVault =
paymentParts->principalPaid + interestPaidToVault;
auto const totalPaidToVault = paymentParts->principalPaid + auto const totalPaidToBroker = paymentParts->feeToPay + managementFee;
paymentParts->interestPaid - managementFee;
auto const totalPaidToBroker = paymentParts->feePaid + managementFee;
XRPL_ASSERT_PARTS( XRPL_ASSERT_PARTS(
(totalPaidToVault + totalPaidToBroker) == (totalPaidToVault + totalPaidToBroker) ==
(paymentParts->principalPaid + paymentParts->interestPaid + (paymentParts->principalPaid + paymentParts->interestPaid +
paymentParts->feePaid), paymentParts->feeToPay),
"ripple::LoanPay::doApply", "ripple::LoanPay::doApply",
"payments add up"); "payments add up");
// If there is not enough first-loss capital auto debtTotalProxy = brokerSle->at(sfDebtTotal);
auto coverAvailableField = brokerSle->at(sfCoverAvailable);
auto debtTotalField = brokerSle->at(sfDebtTotal);
TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)};
bool const sufficientCover = coverAvailableField >= // Decrease LoanBroker Debt by the amount paid, add the Loan value change
roundToAsset(asset, // (which might be negative). debtDecrease may be negative, increasing the
tenthBipsOfValue(debtTotalField.value(), coverRateMinimum), // debt
loanScale); auto const debtDecrease = totalPaidToVault - paymentParts->valueChange;
if (!sufficientCover)
{
// Add the fee to First Loss Cover Pool
coverAvailableField += totalPaidToBroker;
}
auto const brokerPayee =
sufficientCover ? brokerOwner : brokerPseudoAccount;
// Decrease LoanBroker Debt by the amount paid, add the Loan value change,
// and subtract the change in the management fee
auto const vaultValueChange = valueMinusManagementFee(
asset, paymentParts->valueChange, managementFeeRate, loanScale);
// debtDecrease may be negative, increasing the debt
auto const debtDecrease = totalPaidToVault - vaultValueChange;
XRPL_ASSERT_PARTS( XRPL_ASSERT_PARTS(
roundToAsset(asset, debtDecrease, loanScale) == debtDecrease, isRounded(asset, debtDecrease, loanScale),
"ripple::LoanPay::doApply", "ripple::LoanPay::doApply",
"debtDecrease rounding good"); "debtDecrease rounding good");
if (debtDecrease >= debtTotalField) // Despite our best efforts, it's possible for rounding errors to accumulate
debtTotalField = 0; // in the loan broker's debt total. This is because the broker may have more
// that one loan with significantly different scales.
if (debtDecrease >= debtTotalProxy)
debtTotalProxy = 0;
else else
debtTotalField -= debtDecrease; debtTotalProxy -= debtDecrease;
//------------------------------------------------------ //------------------------------------------------------
// Vault object state changes // Vault object state changes
view.update(vaultSle); view.update(vaultSle);
vaultSle->at(sfAssetsAvailable) += totalPaidToVault; vaultSle->at(sfAssetsAvailable) += totalPaidToVault;
vaultSle->at(sfAssetsTotal) += vaultValueChange; vaultSle->at(sfAssetsTotal) += paymentParts->valueChange;
interestOwedProxy -= interestPaidToVault;
XRPL_ASSERT_PARTS( XRPL_ASSERT_PARTS(
*vaultSle->at(sfAssetsAvailable) <= *vaultSle->at(sfAssetsTotal), *vaultSle->at(sfAssetsAvailable) <= *vaultSle->at(sfAssetsTotal),
"ripple::LoanPay::doApply", "ripple::LoanPay::doApply",
@@ -287,16 +267,37 @@ LoanPay::doApply()
"amount is sufficient"); "amount is sufficient");
XRPL_ASSERT_PARTS( XRPL_ASSERT_PARTS(
paidToVault + paidToBroker <= paymentParts->principalPaid + paidToVault + paidToBroker <= paymentParts->principalPaid +
paymentParts->interestPaid + paymentParts->feePaid, paymentParts->interestPaid + paymentParts->feeToPay,
"ripple::LoanPay::doApply", "ripple::LoanPay::doApply",
"payment agreement"); "payment agreement");
// Determine where to send the broker's fee
auto coverAvailableProxy = brokerSle->at(sfCoverAvailable);
TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)};
bool const sufficientCover = coverAvailableProxy >=
roundToAsset(asset,
tenthBipsOfValue(debtTotalProxy.value(), coverRateMinimum),
loanScale);
if (!sufficientCover)
{
// If there is not enough first-loss capital, add the fee to First Loss
// Cover Pool. Note that this moves the entire fee - it does not attempt
// to split it. The broker can Withdraw it later if they want, or leave
// it for future needs.
coverAvailableProxy += totalPaidToBroker;
}
auto const brokerPayee =
sufficientCover ? brokerOwner : brokerPseudoAccount;
#if !NDEBUG
auto const accountBalanceBefore = auto const accountBalanceBefore =
accountHolds(view, account_, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); accountHolds(view, account_, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_);
auto const vaultBalanceBefore = accountHolds( auto const vaultBalanceBefore = accountHolds(
view, vaultPseudoAccount, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); view, vaultPseudoAccount, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_);
auto const brokerBalanceBefore = accountHolds( auto const brokerBalanceBefore = accountHolds(
view, brokerPayee, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); view, brokerPayee, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_);
#endif
if (auto const ter = accountSend( if (auto const ter = accountSend(
view, view,
@@ -315,8 +316,35 @@ LoanPay::doApply()
WaiveTransferFee::Yes)) WaiveTransferFee::Yes))
return ter; return ter;
return tesSUCCESS; #if !NDEBUG
auto const accountBalanceAfter =
accountHolds(view, account_, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_);
auto const vaultBalanceAfter = accountHolds(
view, vaultPseudoAccount, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_);
auto const brokerBalanceAfter = accountHolds(
view, brokerPayee, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_);
auto const balanceScale = std::max(
{accountBalanceBefore.exponent(),
vaultBalanceBefore.exponent(),
brokerBalanceBefore.exponent(),
accountBalanceAfter.exponent(),
vaultBalanceAfter.exponent(),
brokerBalanceAfter.exponent()});
XRPL_ASSERT_PARTS(
roundToAsset(
asset,
accountBalanceBefore + vaultBalanceBefore + brokerBalanceBefore,
balanceScale) ==
roundToAsset(
asset,
accountBalanceAfter + vaultBalanceAfter + brokerBalanceAfter,
balanceScale),
"ripple::LoanPay::doApply",
"funds are conserved (with rounding)");
#endif #endif
return tesSUCCESS;
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------