mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-28 23:15:52 +00:00
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:
@@ -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:
|
||||
//
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user