mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-21 03:26:01 +00:00
Update LoanPay
- Enable the rest of LoanPay. - Start updating the helper functions. - Tests are not expected to pass.
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user