Implement LoanPay (untested)

- Also fix the case of the names of the new Lending helper functions.
- Also add an XRPL_ASSERT2, which splits the parts of the assert message
  so I don't have to remember the proper formatting.
This commit is contained in:
Ed Hennis
2025-05-07 23:44:13 -04:00
parent 37f365a053
commit cc8d36892a
7 changed files with 504 additions and 272 deletions

View File

@@ -39,6 +39,8 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#endif
#define XRPL_ASSERT ALWAYS_OR_UNREACHABLE
#define XRPL_ASSERT2(cond, location, message, ...) \
XRPL_ASSERT(cond, location##" : "##message)
// How to use the instrumentation macros:
//

View File

@@ -159,7 +159,7 @@ class Loan_test : public beast::unit_test::suite
{
TenthBips16 const managementFeeRate{
brokerSle->at(sfManagementFeeRate)};
auto const loanInterest = LoanInterestOutstandingMinusFee(
auto const loanInterest = loanInterestOutstandingMinusFee(
broker.asset,
principalOutstanding,
interestRate,
@@ -261,7 +261,7 @@ class Loan_test : public beast::unit_test::suite
env.test.BEAST_EXPECT(
vaultSle->at(sfLossUnrealized) ==
principalOutstanding +
LoanInterestOutstandingMinusFee(
loanInterestOutstandingMinusFee(
broker.asset,
principalOutstanding,
interestRate,

View File

@@ -21,11 +21,17 @@
#define RIPPLE_APP_MISC_LENDINGHELPERS_H_INCLUDED
#include <xrpld/ledger/ApplyView.h>
#include <xrpld/ledger/View.h>
#include <xrpl/basics/Expected.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Number.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/st.h>
namespace ripple {
@@ -48,53 +54,101 @@ struct LoanPaymentParts
};
Number
LoanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval);
loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval);
Number
LoanTotalValueOutstanding(
loanPeriodicPayment(
Number principalOutstanding,
Number periodicRate,
std::uint32_t paymentsRemaining);
Number
loanPeriodicPayment(
Number principalOutstanding,
TenthBips32 interestRate,
std::uint32_t paymentInterval,
std::uint32_t paymentsRemaining);
Number
LoanTotalInterestOutstanding(
loanTotalValueOutstanding(
Number periodicPayment,
std::uint32_t paymentsRemaining);
Number
loanTotalValueOutstanding(
Number principalOutstanding,
TenthBips32 interestRate,
std::uint32_t paymentInterval,
std::uint32_t paymentsRemaining);
Number
LoanPeriodicPayment(
loanTotalInterestOutstanding(
Number principalOutstanding,
Number totalValueOutstanding);
Number
loanTotalInterestOutstanding(
Number principalOutstanding,
TenthBips32 interestRate,
std::uint32_t paymentInterval,
std::uint32_t paymentsRemaining);
Number
LoanLatePaymentInterest(
loanLatePaymentInterest(
Number principalOutstanding,
TenthBips32 lateInterestRate,
NetClock::time_point parentCloseTime,
std::uint32_t startDate,
std::uint32_t prevPaymentDate);
LoanPaymentParts
LoanComputePaymentParts(ApplyView& view, SLE::ref loan);
Number
loanAccruedInterest(
Number principalOutstanding,
Number periodicRate,
NetClock::time_point parentCloseTime,
std::uint32_t startDate,
std::uint32_t prevPaymentDate,
std::uint32_t paymentInterval);
struct PeriodicPayment
{
Number interest;
Number principal;
};
template <AssetType A>
PeriodicPayment
computePeriodicPaymentParts(
A const& asset,
Number const& principalOutstanding,
Number const& periodicPaymentAmount,
Number const& periodicRate)
{
Number const interest =
roundToAsset(asset, principalOutstanding * periodicRate);
XRPL_ASSERT(
interest > 0,
"ripple::detail::computePeriodicPayment : valid interest");
Number const principal = periodicPaymentAmount - interest;
XRPL_ASSERT(
principal > 0 && principal <= principalOutstanding,
"ripple::detail::computePeriodicPayment : valid principal");
return {interest, principal};
}
inline Number
minusManagementFee(Number value, TenthBips32 managementFeeRate)
{
return tenthBipsOfValue(value, tenthBipsPerUnity - managementFeeRate);
}
} // namespace detail
template <AssetType A>
Number
MinusFee(A const& asset, Number value, TenthBips32 managementFeeRate)
{
return roundToAsset(
asset, tenthBipsOfValue(value, tenthBipsPerUnity - managementFeeRate));
}
template <AssetType A>
Number
LoanInterestOutstandingMinusFee(
loanInterestOutstandingMinusFee(
A const& asset,
Number principalOutstanding,
TenthBips32 interestRate,
@@ -102,19 +156,20 @@ LoanInterestOutstandingMinusFee(
std::uint32_t paymentsRemaining,
TenthBips32 managementFeeRate)
{
return MinusFee(
return roundToAsset(
asset,
detail::LoanTotalInterestOutstanding(
principalOutstanding,
interestRate,
paymentInterval,
paymentsRemaining),
managementFeeRate);
detail::minusManagementFee(
detail::loanTotalInterestOutstanding(
principalOutstanding,
interestRate,
paymentInterval,
paymentsRemaining),
managementFeeRate));
}
template <AssetType A>
Number
LoanPeriodicPayment(
loanPeriodicPayment(
A const& asset,
Number principalOutstanding,
TenthBips32 interestRate,
@@ -123,7 +178,7 @@ LoanPeriodicPayment(
{
return roundToAsset(
asset,
detail::LoanPeriodicPayment(
detail::loanPeriodicPayment(
principalOutstanding,
interestRate,
paymentInterval,
@@ -132,7 +187,7 @@ LoanPeriodicPayment(
template <AssetType A>
Number
LoanLatePaymentInterest(
loanLatePaymentInterest(
A const& asset,
Number principalOutstanding,
TenthBips32 lateInterestRate,
@@ -142,7 +197,7 @@ LoanLatePaymentInterest(
{
return roundToAsset(
asset,
detail::LoanLatePaymentInterest(
detail::loanLatePaymentInterest(
principalOutstanding,
lateInterestRate,
parentCloseTime,
@@ -152,22 +207,292 @@ LoanLatePaymentInterest(
struct LoanPaymentParts
{
STAmount principalPaid;
STAmount interestPaid;
STAmount valueChange;
STAmount feePaid;
Number principalPaid;
Number interestPaid;
Number valueChange;
Number feePaid;
};
template <AssetType A>
LoanPaymentParts
LoanComputePaymentParts(A const& asset, ApplyView& view, SLE::ref loan)
Expected<LoanPaymentParts, TER>
loanComputePaymentParts(
A const& asset,
ApplyView& view,
SLE::ref loan,
STAmount const& amount,
beast::Journal j)
{
auto const parts = detail::LoanComputePaymentParts(view, loan);
auto principalOutstandingField = loan->at(sfPrincipalOutstanding);
bool const allowOverpayment = loan->isFlag(lsfLoanOverpayment);
TenthBips32 const interestRate{loan->at(sfInterestRate)};
TenthBips32 const lateInterestRate{loan->at(sfLateInterestRate)};
TenthBips32 const closeInterestRate{loan->at(sfCloseInterestRate)};
TenthBips32 const overpaymentInterestRate{
loan->at(sfOverpaymentInterestRate)};
Number const serviceFee = loan->at(sfLoanServiceFee);
Number const latePaymentFee = loan->at(sfLatePaymentFee);
Number const closePaymentFee = loan->at(sfClosePaymentFee);
TenthBips32 const overpaymentFee{loan->at(sfOverpaymentFee)};
std::uint32_t const paymentInterval = loan->at(sfPaymentInterval);
auto paymentRemainingField = loan->at(sfPaymentRemaining);
auto prevPaymentDateField = loan->at(sfPreviousPaymentDate);
std::uint32_t const startDate = loan->at(sfStartDate);
auto nextDueDateField = loan->at(sfNextPaymentDueDate);
if (paymentRemainingField == 0 || principalOutstandingField == 0)
{
// Loan complete
JLOG(j.warn()) << "Loan is already paid off.";
return Unexpected(tecKILLED);
}
// Compute the normal periodic rate, payment, etc.
// We'll need it in the remaining calculations
Number const periodicRate =
detail::loanPeriodicRate(interestRate, paymentInterval);
XRPL_ASSERT(
periodicRate > 0, "ripple::loanComputePaymentParts : valid rate");
Number const periodicPaymentAmount = roundToAsset(
asset,
detail::loanPeriodicPayment(
principalOutstandingField, periodicRate, paymentRemainingField));
XRPL_ASSERT(
periodicPaymentAmount > 0,
"ripple::computePeriodicPayment : valid payment");
auto const periodic = detail::computePeriodicPaymentParts(
asset, principalOutstandingField, periodicPaymentAmount, periodicRate);
Number const totalValueOutstanding = detail::loanTotalValueOutstanding(
periodicPaymentAmount, paymentRemainingField);
XRPL_ASSERT(
totalValueOutstanding > 0,
"ripple::loanComputePaymentParts : valid total value");
Number const totalInterestOutstanding =
detail::loanTotalInterestOutstanding(
principalOutstandingField, totalValueOutstanding);
XRPL_ASSERT(
totalInterestOutstanding > 0,
"ripple::loanComputePaymentParts : valid total interest");
view.update(loan);
// -------------------------------------------------------------
// late payment handling
if (hasExpired(view, nextDueDateField))
{
// the payment is late
auto const latePaymentInterest = loanLatePaymentInterest(
asset,
principalOutstandingField,
lateInterestRate,
view.parentCloseTime(),
startDate,
prevPaymentDateField);
XRPL_ASSERT(
latePaymentInterest >= 0,
"ripple::loanComputePaymentParts : valid late interest");
auto const latePaymentAmount =
periodicPaymentAmount + latePaymentInterest + latePaymentFee;
if (amount < latePaymentAmount)
{
JLOG(j.warn()) << "Late loan payment amount is insufficient. Due: "
<< latePaymentAmount << ", paid: " << amount;
return Unexpected(tecINSUFFICIENT_PAYMENT);
}
paymentRemainingField -= 1;
// A single payment always pays the same amount of principal. Only the
// interest and fees are extra for a late payment
principalOutstandingField -= periodic.principal;
// Make sure this does an assignment
prevPaymentDateField = nextDueDateField;
nextDueDateField += paymentInterval;
// A late payment increases the value of the loan by the difference
// between periodic and late payment interest
return LoanPaymentParts{
periodic.principal,
latePaymentInterest + periodic.interest,
latePaymentInterest,
latePaymentFee};
}
// -------------------------------------------------------------
// full payment handling
if (paymentRemainingField > 1)
{
// If there is more than one payment remaining, see if enough was paid
// for a full payment
auto const accruedInterest = roundToAsset(
asset,
detail::loanAccruedInterest(
principalOutstandingField,
periodicRate,
view.parentCloseTime(),
startDate,
prevPaymentDateField,
paymentInterval));
XRPL_ASSERT(
accruedInterest >= 0,
"ripple::loanComputePaymentParts : valid accrued interest");
auto const closePrepaymentInterest = roundToAsset(
asset,
tenthBipsOfValue(
principalOutstandingField.value(), closeInterestRate));
XRPL_ASSERT(
closePrepaymentInterest >= 0,
"ripple::loanComputePaymentParts : valid prepayment "
"interest");
auto const closeFullPayment = principalOutstandingField +
accruedInterest + closePrepaymentInterest + closePaymentFee;
// if the payment is equal or higher than full payment amount, make a
// full payment
if (amount >= closeFullPayment)
{
paymentRemainingField = 0;
principalOutstandingField = 0;
// A full payment decreases the value of the loan by the
// difference between the interest paid and the expected
// outstanding interest return
auto const valueChange = accruedInterest - totalInterestOutstanding;
XRPL_ASSERT(
valueChange <= 0,
"ripple::loanComputePaymentParts : valid full payment "
"value change");
return LoanPaymentParts{
principalOutstandingField,
accruedInterest,
valueChange,
closePaymentFee};
}
}
// -------------------------------------------------------------
// normal payment handling
// if the payment is not late nor if it's a full payment, then it must be a
// periodic one, with possible overpayments
std::optional<NumberRoundModeGuard> mg(Number::downward);
std::int64_t const fullPeriodicPayments{amount / periodicPaymentAmount};
mg.reset();
// Temporary asserts
XRPL_ASSERT(
amount >= periodicPaymentAmount || fullPeriodicPayments == 0,
"temp full periodic rounding");
XRPL_ASSERT(
amount < periodicPaymentAmount || fullPeriodicPayments >= 1,
"temp full periodic rounding");
if (fullPeriodicPayments < 1)
{
JLOG(j.warn()) << "Periodic loan payment amount is insufficient. Due: "
<< periodicPaymentAmount << ", paid: " << amount;
return Unexpected(tecINSUFFICIENT_PAYMENT);
}
nextDueDateField += paymentInterval * fullPeriodicPayments;
prevPaymentDateField = nextDueDateField - paymentInterval;
Number totalPrincipalPaid = 0;
Number totalInterestPaid = 0;
Number loanValueChange = 0;
std::optional<detail::PeriodicPayment> future = periodic;
for (int i = 0; i < fullPeriodicPayments; ++i)
{
// Only do the work if we need to
if (!future)
future = detail::computePeriodicPaymentParts(
asset,
principalOutstandingField,
periodicPaymentAmount,
periodicRate);
XRPL_ASSERT(
future->interest < periodic.interest,
"ripple::loanComputePaymentParts : decreasing interest");
XRPL_ASSERT(
future->principal > periodic.principal,
"ripple::loanComputePaymentParts : increasing principal");
totalPrincipalPaid += future->principal;
totalInterestPaid += future->interest;
paymentRemainingField -= 1;
principalOutstandingField -= future->principal;
future.reset();
}
Number totalFeePaid = serviceFee * fullPeriodicPayments;
if (allowOverpayment)
{
Number const overpayment = std::min(
principalOutstandingField.value(),
amount - periodicPaymentAmount * fullPeriodicPayments);
if (overpayment > 0)
{
Number const interestPortion = roundToAsset(
asset, tenthBipsOfValue(overpayment, overpaymentInterestRate));
Number const feePortion = roundToAsset(
asset, tenthBipsOfValue(overpayment, overpaymentFee));
Number const remainder = overpayment - interestPortion - feePortion;
// Don't process an overpayment if the whole amount (or more!) gets
// eaten by fees
if (remainder > 0)
{
totalPrincipalPaid += remainder;
totalInterestPaid += interestPortion;
totalFeePaid += feePortion;
principalOutstandingField -= remainder;
Number const newInterest = roundToAsset(
asset,
detail::loanTotalInterestOutstanding(
principalOutstandingField,
interestRate,
paymentInterval,
paymentRemainingField));
loanValueChange =
(newInterest - totalInterestOutstanding) + interestPortion;
}
}
}
XRPL_ASSERT(
loanValueChange <= 0,
"ripple::loanComputePaymentParts : valid normal / overpayment "
"value change");
// Check the final results are rounded, to double-check that the
// intermediate steps were rounded.
XRPL_ASSERT(
roundToAsset(asset, totalPrincipalPaid) == totalPrincipalPaid,
"ripple::loanComputePaymentParts : totalPrincipalPaid rounded");
XRPL_ASSERT(
roundToAsset(asset, totalInterestPaid) == totalInterestPaid,
"ripple::loanComputePaymentParts : totalInterestPaid rounded");
XRPL_ASSERT(
roundToAsset(asset, loanValueChange) == loanValueChange,
"ripple::loanComputePaymentParts : loanValueChange rounded");
XRPL_ASSERT(
roundToAsset(asset, totalFeePaid) == totalFeePaid,
"ripple::loanComputePaymentParts : totalFeePaid rounded");
return LoanPaymentParts{
roundToAsset(asset, parts.principalPaid),
roundToAsset(asset, parts.interestPaid),
roundToAsset(asset, parts.valueChange),
roundToAsset(asset, parts.feePaid)};
totalPrincipalPaid, totalInterestPaid, loanValueChange, totalFeePaid};
}
} // namespace ripple

View File

@@ -21,10 +21,6 @@
//
#include <xrpld/app/tx/detail/Transactor.h>
#include <xrpld/app/tx/detail/VaultCreate.h>
#include <xrpld/ledger/View.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/st.h>
namespace ripple {
@@ -38,7 +34,7 @@ lendingProtocolEnabled(PreflightContext const& ctx)
namespace detail {
Number
LoanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval)
loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval)
{
// Need floating point math for this one, since we're dividing by some
// large numbers
@@ -47,37 +43,7 @@ LoanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval)
}
Number
LoanTotalValueOutstanding(
Number principalOutstanding,
TenthBips32 interestRate,
std::uint32_t paymentInterval,
std::uint32_t paymentsRemaining)
{
return LoanPeriodicPayment(
principalOutstanding,
interestRate,
paymentInterval,
paymentsRemaining) *
paymentsRemaining;
}
Number
LoanTotalInterestOutstanding(
Number principalOutstanding,
TenthBips32 interestRate,
std::uint32_t paymentInterval,
std::uint32_t paymentsRemaining)
{
return LoanTotalValueOutstanding(
principalOutstanding,
interestRate,
paymentInterval,
paymentsRemaining) -
principalOutstanding;
}
Number
LoanPeriodicPayment(
loanPeriodicPayment(
Number principalOutstanding,
Number periodicRate,
std::uint32_t paymentsRemaining)
@@ -90,7 +56,7 @@ LoanPeriodicPayment(
}
Number
LoanPeriodicPayment(
loanPeriodicPayment(
Number principalOutstanding,
TenthBips32 interestRate,
std::uint32_t paymentInterval,
@@ -98,14 +64,62 @@ LoanPeriodicPayment(
{
if (principalOutstanding == 0 || paymentsRemaining == 0)
return 0;
Number const periodicRate = LoanPeriodicRate(interestRate, paymentInterval);
Number const periodicRate = loanPeriodicRate(interestRate, paymentInterval);
return LoanPeriodicPayment(
return loanPeriodicPayment(
principalOutstanding, periodicRate, paymentsRemaining);
}
Number
LoanLatePaymentInterest(
loanTotalValueOutstanding(
Number periodicPayment,
std::uint32_t paymentsRemaining)
{
return periodicPayment * paymentsRemaining;
}
Number
loanTotalValueOutstanding(
Number principalOutstanding,
TenthBips32 interestRate,
std::uint32_t paymentInterval,
std::uint32_t paymentsRemaining)
{
return loanTotalValueOutstanding(
loanPeriodicPayment(
principalOutstanding,
interestRate,
paymentInterval,
paymentsRemaining),
paymentsRemaining);
}
Number
loanTotalInterestOutstanding(
Number principalOutstanding,
Number totalValueOutstanding)
{
return totalValueOutstanding - principalOutstanding;
}
Number
loanTotalInterestOutstanding(
Number principalOutstanding,
TenthBips32 interestRate,
std::uint32_t paymentInterval,
std::uint32_t paymentsRemaining)
{
return loanTotalInterestOutstanding(
principalOutstanding,
loanTotalValueOutstanding(
principalOutstanding,
interestRate,
paymentInterval,
paymentsRemaining));
}
Number
loanLatePaymentInterest(
Number principalOutstanding,
TenthBips32 lateInterestRate,
NetClock::time_point parentCloseTime,
@@ -118,15 +132,15 @@ LoanLatePaymentInterest(
parentCloseTime.time_since_epoch().count() - lastPaymentDate;
auto const rate =
LoanPeriodicRate(lateInterestRate, secondsSinceLastPayment);
loanPeriodicRate(lateInterestRate, secondsSinceLastPayment);
return principalOutstanding * rate;
}
Number
LoanAccruedInterest(
loanAccruedInterest(
Number principalOutstanding,
TenthBips32 periodicRate,
Number periodicRate,
NetClock::time_point parentCloseTime,
std::uint32_t startDate,
std::uint32_t prevPaymentDate,
@@ -137,155 +151,10 @@ LoanAccruedInterest(
auto const secondsSinceLastPayment =
parentCloseTime.time_since_epoch().count() - lastPaymentDate;
return tenthBipsOfValue(
principalOutstanding * secondsSinceLastPayment, periodicRate) /
return principalOutstanding * periodicRate * secondsSinceLastPayment /
paymentInterval;
}
LoanPaymentParts
LoanComputePaymentParts(ApplyView& view, SLE::ref loan)
{
Number const principalOutstanding = loan->at(sfPrincipalOutstanding);
TenthBips32 const interestRate{loan->at(sfInterestRate)};
TenthBips32 const lateInterestRate{loan->at(sfLateInterestRate)};
TenthBips32 const closeInterestRate{loan->at(sfCloseInterestRate)};
Number const latePaymentFee = loan->at(sfLatePaymentFee);
Number const closePaymentFee = loan->at(sfClosePaymentFee);
std::uint32_t const paymentInterval = loan->at(sfPaymentInterval);
std::uint32_t const paymentRemaining = loan->at(sfPaymentRemaining);
std::uint32_t const prevPaymentDate = loan->at(sfPreviousPaymentDate);
std::uint32_t const startDate = loan->at(sfStartDate);
std::uint32_t const nextDueDate = loan->at(sfNextPaymentDueDate);
// Compute the normal periodic rate, payment, etc.
// We'll need it in the remaining calculations
Number const periodicRate = LoanPeriodicRate(interestRate, paymentInterval);
Number const periodicPaymentAmount = LoanPeriodicPayment(
principalOutstanding, periodicRate, paymentRemaining);
Number const periodicInterest = principalOutstanding * periodicRate;
Number const periodicPrincipal = periodicPaymentAmount - periodicInterest;
// the payment is late
if (hasExpired(view, nextDueDate))
{
auto const latePaymentInterest = LoanLatePaymentInterest(
principalOutstanding,
lateInterestRate,
view.parentCloseTime(),
startDate,
prevPaymentDate);
auto const latePaymentAmount =
periodicPaymentAmount + latePaymentInterest + latePaymentFee;
loan->at(sfPaymentRemaining) -= 1;
// A single payment always pays the same amount of principal. Only the
// interest and fees are extra
loan->at(sfPrincipalOutstanding) -= periodicPrincipal;
// Make sure this does an assignment
loan->at(sfPreviousPaymentDate) = loan->at(sfNextPaymentDueDate);
loan->at(sfNextPaymentDueDate) += paymentInterval;
// A late payment increases the value of the loan by the difference
// between periodic and late payment interest
return {
periodicPrincipal,
latePaymentInterest + periodicInterest,
latePaymentInterest,
latePaymentFee};
}
auto const accruedInterest = LoanAccruedInterest(
principalOutstanding,
interestRate,
view.parentCloseTime(),
startDate,
prevPaymentDate,
paymentInterval);
auto const prepaymentPenalty =
tenthBipsOfValue(principalOutstanding, closeInterestRate);
assert(0);
return {0, 0, 0, 0};
/*
function make_payment(amount, current_time) -> (principal_paid, interest_paid,
value_change, fee_paid): if loan.payments_remaining is 0 ||
loan.principal_outstanding is 0 { return "loan complete" error
}
.....
let full_payment = loan.compute_full_payment(current_time)
// if the payment is equal or higher than full payment amount
// and there is more than one payment remaining, make a full payment
if amount >= full_payment && loan.payments_remaining > 1 {
loan.payments_remaining = 0
loan.principal_outstanding = 0
// A full payment decreases the value of the loan by the difference
between the interest paid and the expected outstanding interest return
(full_payment.principal, full_payment.interest, full_payment.interest -
loan.compute_current_value().interest, full_payment.fee)
}
// if the payment is not late nor if it's a full payment, then it must be a
periodic once
let periodic_payment = loan.compute_periodic_payment()
let full_periodic_payments = floor(amount / periodic_payment)
if full_periodic_payments < 1 {
return "insufficient amount paid" error
}
loan.payments_remaining -= full_periodic_payments
loan.next_payment_due_date = loan.next_payment_due_date +
loan.payment_interval * full_periodic_payments loan.last_payment_date =
loan.next_payment_due_date - loan.payment_interval
let total_principal_paid = 0
let total_interest_paid = 0
let loan_value_change = 0
let total_fee_paid = loan.service_fee * full_periodic_payments
while full_periodic_payments > 0 {
total_principal_paid += periodic_payment.principal
total_interest_paid += periodic_payment.interest
periodic_payment = loan.compute_periodic_payment()
full_periodic_payments -= 1
}
loan.principal_outstanding -= total_principal_paid
let overpayment = min(loan.principal_outstanding, amount % periodic_payment)
if overpayment > 0 && is_set(lsfOverpayment) {
let interest_portion = overpayment * loan.overpayment_interest_rate
let fee_portion = overpayment * loan.overpayment_fee
let remainder = overpayment - interest_portion - fee_portion
total_principal_paid += remainder
total_interest_paid += interest_portion
total_fee_paid += fee_portion
let current_value = loan.compute_current_value()
loan.principal_outstanding -= remainder
let new_value = loan.compute_current_value()
loan_value_change = (new_value.interest - current_value.interest) +
interest_portion
}
return (total_principal_paid, total_interest_paid, loan_value_change,
total_fee_paid)
*/
}
} // namespace detail
} // namespace ripple

View File

@@ -377,7 +377,7 @@ LoanManage::doApply()
TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)};
auto const paymentInterval = loanSle->at(sfPaymentInterval);
auto const paymentsRemaining = loanSle->at(sfPaymentRemaining);
auto const interestOutstanding = LoanInterestOutstandingMinusFee(
auto const interestOutstanding = loanInterestOutstandingMinusFee(
vaultAsset,
principalOutstanding.value(),
interestRate,

View File

@@ -150,42 +150,6 @@ LoanPay::preclaim(PreclaimContext const& ctx)
}
}
auto const periodicPaymentAmount = LoanPeriodicPayment(
asset,
principalOutstanding,
interestRate,
paymentInterval,
paymentRemaining);
if (hasExpired(ctx.view, nextDueDate))
{
// Need to pay the late payment amount
auto const latePaymentInterest = LoanLatePaymentInterest(
asset,
principalOutstanding,
lateInterestRate,
ctx.view.parentCloseTime(),
startDate,
prevPaymentDate);
auto const latePaymentAmount =
periodicPaymentAmount + latePaymentInterest + latePaymentFee;
if (amount < latePaymentAmount)
{
JLOG(ctx.j.warn())
<< "Late loan payment amount is insufficient. Due: "
<< latePaymentAmount << ", paid: " << amount;
return tecINSUFFICIENT_PAYMENT;
}
}
else if (amount < periodicPaymentAmount)
{
// Need to pay the regular payment amount
JLOG(ctx.j.warn())
<< "Periodic loan payment amount is insufficient. Due: "
<< periodicPaymentAmount << ", paid: " << amount;
return tecINSUFFICIENT_PAYMENT;
}
return tesSUCCESS;
}
@@ -206,19 +170,91 @@ LoanPay::doApply()
auto const brokerSle = view.peek(keylet::loanbroker(brokerID));
if (!brokerSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const brokerOwner = brokerSle->at(sfOwner);
auto const brokerPseudoAccount = brokerSle->at(sfAccount);
auto const vaultID = brokerSle->at(sfVaultID);
auto const vaultSle = view.peek(keylet::vault(vaultID));
if (!vaultSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const vaultPseudoAccount = vaultSle->at(sfAccount);
auto const asset = vaultSle->at(sfAsset);
//------------------------------------------------------
// Loan object state changes
view.update(loanSle);
Expected<LoanPaymentParts, TER> paymentParts =
loanComputePaymentParts(asset, view, loanSle, amount, j_);
if (!paymentParts)
return paymentParts.error();
// If the loan was impaired, it isn't anymore.
loanSle->clearFlag(lsfLoanImpaired);
//------------------------------------------------------
// LoanBroker object state changes
view.update(brokerSle);
TenthBips32 managementFeeRate{brokerSle->at(sfManagementFeeRate)};
auto const managementFee = roundToAsset(
asset, tenthBipsOfValue(paymentParts->interestPaid, managementFeeRate));
auto const totalPaidToVault = paymentParts->principalPaid +
paymentParts->interestPaid - managementFee;
auto const totalFee = paymentParts->feePaid + managementFee;
// If there is not enough first-loss capital
auto coverAvailableField = brokerSle->at(sfCoverAvailable);
auto debtTotalField = brokerSle->at(sfDebtTotal);
TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)};
bool const sufficientCover = coverAvailableField >=
tenthBipsOfValue(debtTotalField.value(), coverRateMinimum);
if (!sufficientCover)
{
// Add the fee to to First Loss Cover Pool
coverAvailableField += totalFee;
}
// Decrease LoanBroker Debt by the amount paid, add the Loan value change,
// and subtract the change in the management fee
auto const vaultValueChange = paymentParts->valueChange -
tenthBipsOfValue(paymentParts->valueChange, managementFeeRate);
debtTotalField += vaultValueChange - totalPaidToVault;
//------------------------------------------------------
// Vault object state changes
view.update(vaultSle);
vaultSle->at(sfAssetsAvailable) += totalPaidToVault;
vaultSle->at(sfAssetsTotal) += vaultValueChange;
// Move funds
STAmount const paidToVault(asset, totalPaidToVault);
STAmount const paidToBroker(asset, totalFee);
XRPL_ASSERT2(
paidToVault + paidToBroker == amount,
"ripple::LoanPay::doApply",
"correct payment totals");
if (auto const ter = accountSend(
view,
brokerPseudoAccount,
account_,
amount,
vaultPseudoAccount,
paidToVault,
j_,
WaiveTransferFee::Yes))
return ter;
if (auto const ter = accountSend(
view,
account_,
sufficientCover ? brokerOwner : brokerPseudoAccount,
paidToBroker,
j_,
WaiveTransferFee::Yes))
return ter;
loanSle->at(sfAssetsAvailable) -= amount;
view.update(loanSle);
return tesSUCCESS;
}

View File

@@ -359,7 +359,7 @@ LoanSet::doApply()
TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)};
// The portion of the loan interest that will go to the vault (total
// interest minus the management fee)
auto const loanInterestToVault = LoanInterestOutstandingMinusFee(
auto const loanInterestToVault = loanInterestOutstandingMinusFee(
vaultAsset,
principalRequested,
interestRate,