mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-19 02:25:52 +00:00
- Implement AccountSendMulti - Document the derivations of loan components. - Add "loanPrincipalFromPeriodicPayment" helper. - Removed sfReferencePrincipal - LoanSet and LoanPay can create MPTokens as a side effect - LoanPay will send the fee to cover if the broker owner is deep frozen, and fail if both of them are deep frozen. - LoanPay will check auth for the receivers, or create holdings for the submitting account if needed. - LoanSet will fail if principal requested is not positive - Handle overpayment in a separate function - Add a test helper to check that balance changes went as expected - Fix more tests
1328 lines
44 KiB
C++
1328 lines
44 KiB
C++
//------------------------------------------------------------------------------
|
|
/*
|
|
This file is part of rippled: https://github.com/ripple/rippled
|
|
Copyright (c) 2025 Ripple Labs Inc.
|
|
|
|
Permission to use, copy, modify, and/or distribute this software for any
|
|
purpose with or without fee is hereby granted, provided that the above
|
|
copyright notice and this permission notice appear in all copies.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
*/
|
|
//==============================================================================
|
|
|
|
#ifndef RIPPLE_APP_MISC_LENDINGHELPERS_H_INCLUDED
|
|
#define RIPPLE_APP_MISC_LENDINGHELPERS_H_INCLUDED
|
|
|
|
#include <xrpl/ledger/View.h>
|
|
#include <xrpl/protocol/st.h>
|
|
|
|
namespace ripple {
|
|
|
|
struct PreflightContext;
|
|
|
|
// Lending protocol has dependencies, so capture them here.
|
|
bool
|
|
checkLendingProtocolDependencies(PreflightContext const& ctx);
|
|
|
|
Number
|
|
loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval);
|
|
|
|
// This structure is used internally to compute the breakdown of a
|
|
// single loan payment
|
|
struct PaymentComponents
|
|
{
|
|
Number rawInterest;
|
|
Number rawPrincipal;
|
|
Number roundedInterest;
|
|
Number roundedPrincipal;
|
|
// We may not need roundedPayment
|
|
Number roundedPayment;
|
|
bool final = false;
|
|
bool extra = false;
|
|
};
|
|
|
|
// This structure is explained in the XLS-66 spec, section 3.2.4.4 (Failure
|
|
// Conditions)
|
|
struct LoanPaymentParts
|
|
{
|
|
/// principal_paid is the amount of principal that the payment covered.
|
|
Number principalPaid;
|
|
/// interest_paid is the amount of interest that the payment covered.
|
|
Number interestPaid;
|
|
/**
|
|
* value_change is the amount by which the total value of the Loan changed.
|
|
* If value_change < 0, Loan value decreased.
|
|
* If value_change > 0, Loan value increased.
|
|
* This is 0 for regular payments.
|
|
*/
|
|
Number valueChange;
|
|
/// fee_paid is the amount of fee that the payment covered.
|
|
Number feeToPay;
|
|
|
|
LoanPaymentParts&
|
|
operator+=(LoanPaymentParts const& other)
|
|
{
|
|
principalPaid += other.principalPaid;
|
|
interestPaid += other.interestPaid;
|
|
valueChange += other.valueChange;
|
|
feeToPay += other.feeToPay;
|
|
return *this;
|
|
}
|
|
};
|
|
|
|
/// Ensure the periodic payment is always rounded consistently
|
|
template <AssetType A>
|
|
Number
|
|
roundPeriodicPayment(
|
|
A const& asset,
|
|
Number const& periodicPayment,
|
|
std::int32_t scale)
|
|
{
|
|
return roundToAsset(asset, periodicPayment, scale, Number::upward);
|
|
}
|
|
|
|
namespace detail {
|
|
// These functions should rarely be used directly. More often, the ultimate
|
|
// result needs to be roundToAsset'd.
|
|
|
|
Number
|
|
loanPeriodicPayment(
|
|
Number const& principalOutstanding,
|
|
Number const& periodicRate,
|
|
std::uint32_t paymentsRemaining);
|
|
|
|
Number
|
|
loanPeriodicPayment(
|
|
Number const& principalOutstanding,
|
|
TenthBips32 interestRate,
|
|
std::uint32_t paymentInterval,
|
|
std::uint32_t paymentsRemaining);
|
|
|
|
Number
|
|
loanPrincipalFromPeriodicPayment(
|
|
Number const& periodicPayment,
|
|
Number const& periodicRate,
|
|
std::uint32_t paymentsRemaining);
|
|
|
|
Number
|
|
loanLatePaymentInterest(
|
|
Number const& principalOutstanding,
|
|
TenthBips32 lateInterestRate,
|
|
NetClock::time_point parentCloseTime,
|
|
std::uint32_t nextPaymentDueDate);
|
|
|
|
Number
|
|
loanAccruedInterest(
|
|
Number const& principalOutstanding,
|
|
Number const& periodicRate,
|
|
NetClock::time_point parentCloseTime,
|
|
std::uint32_t startDate,
|
|
std::uint32_t prevPaymentDate,
|
|
std::uint32_t paymentInterval);
|
|
|
|
inline Number
|
|
minusFee(Number const& value, TenthBips32 managementFeeRate)
|
|
{
|
|
return tenthBipsOfValue(value, tenthBipsPerUnity - managementFeeRate);
|
|
}
|
|
|
|
template <AssetType A>
|
|
PaymentComponents
|
|
computePaymentComponents(
|
|
A const& asset,
|
|
std::int32_t scale,
|
|
Number const& totalValueOutstanding,
|
|
Number const& principalOutstanding,
|
|
Number const& periodicPayment,
|
|
Number const& periodicRate,
|
|
std::uint32_t paymentRemaining)
|
|
{
|
|
/*
|
|
* This function is derived from the XLS-66 spec, section 3.2.4.1.1 (Regular
|
|
* Payment)
|
|
*/
|
|
XRPL_ASSERT_PARTS(
|
|
isRounded(asset, totalValueOutstanding, scale) &&
|
|
isRounded(asset, principalOutstanding, scale),
|
|
"ripple::detail::computePaymentComponents",
|
|
"Outstanding values are rounded");
|
|
auto const roundedPeriodicPayment =
|
|
roundPeriodicPayment(asset, periodicPayment, scale);
|
|
if (paymentRemaining == 1 ||
|
|
totalValueOutstanding <= roundedPeriodicPayment)
|
|
{
|
|
// If there's only one payment left, we need to pay off each of the loan
|
|
// parts. It's probably impossible for the subtraction to result in a
|
|
// negative value, but don't leave anything to chance.
|
|
Number interest =
|
|
std::max(Number{}, totalValueOutstanding - principalOutstanding);
|
|
|
|
// Pay everything off
|
|
return {
|
|
.rawInterest = interest,
|
|
.rawPrincipal = principalOutstanding,
|
|
.roundedInterest = interest,
|
|
.roundedPrincipal = principalOutstanding,
|
|
.roundedPayment = interest + principalOutstanding,
|
|
.final = true};
|
|
}
|
|
|
|
Number const rawValueOutstanding = periodicPayment * paymentRemaining;
|
|
Number const rawPrincipalOutstanding = loanPrincipalFromPeriodicPayment(
|
|
periodicPayment, periodicRate, paymentRemaining);
|
|
Number const rawInterestOutstanding =
|
|
rawValueOutstanding - rawInterestOutstanding;
|
|
|
|
/*
|
|
* From the spec, once the periodicPayment is computed:
|
|
*
|
|
* The principal and interest portions can be derived as follows:
|
|
* interest = principalOutstanding * periodicRate
|
|
* principal = periodicPayment - interest
|
|
*/
|
|
Number const rawInterest = rawPrincipalOutstanding * periodicRate;
|
|
Number const rawPrincipal = periodicPayment - rawInterest;
|
|
XRPL_ASSERT_PARTS(
|
|
rawInterest >= 0,
|
|
"ripple::detail::computePaymentComponents",
|
|
"valid raw interest");
|
|
XRPL_ASSERT_PARTS(
|
|
rawPrincipal >= 0 && rawPrincipal <= rawPrincipalOutstanding,
|
|
"ripple::detail::computePaymentComponents",
|
|
"valid raw principal");
|
|
|
|
/*
|
|
Critical Calculation: Balancing Principal and Interest Outstanding
|
|
|
|
This calculation maintains a delicate balance between keeping
|
|
principal outstanding and interest outstanding as close as possible to
|
|
reference values. However, we cannot perfectly match the reference
|
|
values due to rounding issues.
|
|
|
|
Key considerations:
|
|
1. Since the periodic payment is rounded up, we have excess funds
|
|
that can be used to pay down the loan faster than the reference
|
|
calculation.
|
|
|
|
2. We must ensure that loan repayment is not too fast, otherwise we
|
|
will end up with negative principal outstanding or negative
|
|
interest outstanding.
|
|
|
|
3. We cannot allow the borrower to repay interest ahead of schedule.
|
|
If the borrower makes an overpayment, the interest portion could
|
|
go negative, requiring complex recalculation to refund the borrower by
|
|
reflecting the overpayment in the principal portion of the loan.
|
|
*/
|
|
|
|
Number const roundedPrincipal = [&]() {
|
|
auto const p = roundToAsset(
|
|
asset,
|
|
// Compute the delta that will get the tracked principalOutstanding
|
|
// amount as close to the raw principal amount after the payment as
|
|
// possible.
|
|
principalOutstanding - (rawPrincipalOutstanding - rawPrincipal),
|
|
scale,
|
|
Number::downward);
|
|
|
|
XRPL_ASSERT_PARTS(
|
|
p > 0,
|
|
"rippled::detail::computePaymentComponents",
|
|
"principal part positive");
|
|
XRPL_ASSERT_PARTS(
|
|
p <= principalOutstanding,
|
|
"rippled::detail::computePaymentComponents",
|
|
"principal part not larger than outstanding principal");
|
|
XRPL_ASSERT_PARTS(
|
|
p <= roundedPeriodicPayment,
|
|
"rippled::detail::computePaymentComponents",
|
|
"principal part not larger than total payment");
|
|
|
|
// Make sure nothing goes negative
|
|
if (p > roundedPeriodicPayment || p > principalOutstanding)
|
|
return std::min(roundedPeriodicPayment, principalOutstanding);
|
|
else if (p < 0)
|
|
return Number{};
|
|
|
|
return p;
|
|
}();
|
|
|
|
Number const roundedInterest = [&]() {
|
|
// Zero interest means ZERO interest
|
|
if (periodicRate == 0)
|
|
return Number{};
|
|
|
|
// Compute the rounded interest outstanding
|
|
auto const interestOutstanding =
|
|
totalValueOutstanding - principalOutstanding;
|
|
// Compute the delta that will simply treat the rest of the rounded
|
|
// fixed payment amount as interest.
|
|
auto const iDiff = roundedPeriodicPayment - roundedPrincipal;
|
|
|
|
// Compute the delta that will get the untracked interestOutstanding
|
|
// amount as close as possible to the raw interest amount after the
|
|
// payment as possible.
|
|
auto const iSync = interestOutstanding -
|
|
(roundToAsset(asset, rawInterestOutstanding, scale) -
|
|
roundToAsset(asset, rawInterest, scale));
|
|
XRPL_ASSERT_PARTS(
|
|
isRounded(asset, iSync, scale),
|
|
"ripple::detail::computePaymentComponents",
|
|
"iSync is rounded");
|
|
|
|
// Use the smaller of the two to ensure we don't overpay interest.
|
|
auto const i = std::min({iSync, iDiff, interestOutstanding});
|
|
|
|
// No negative interest!
|
|
if (i < 0)
|
|
return Number{};
|
|
return i;
|
|
}();
|
|
|
|
XRPL_ASSERT_PARTS(
|
|
roundedInterest >= 0 && isRounded(asset, roundedInterest, scale),
|
|
"ripple::detail::computePaymentComponents",
|
|
"valid rounded interest");
|
|
XRPL_ASSERT_PARTS(
|
|
roundedPrincipal >= 0 && roundedPrincipal <= principalOutstanding &&
|
|
roundedPrincipal <= roundedPeriodicPayment &&
|
|
isRounded(asset, roundedPrincipal, scale),
|
|
"ripple::detail::computePaymentComponents",
|
|
"valid rounded principal");
|
|
XRPL_ASSERT_PARTS(
|
|
roundedPrincipal + roundedInterest <= roundedPeriodicPayment,
|
|
"ripple::detail::computePaymentComponents",
|
|
"payment parts fit within payment limit");
|
|
|
|
return {
|
|
.rawInterest = rawInterest,
|
|
.rawPrincipal = rawPrincipal,
|
|
.roundedInterest = roundedInterest,
|
|
.roundedPrincipal = roundedPrincipal,
|
|
.roundedPayment = roundedPeriodicPayment};
|
|
}
|
|
|
|
struct PaymentComponentsPlus : public PaymentComponents
|
|
{
|
|
Number fee{0};
|
|
Number valueChange{0};
|
|
|
|
PaymentComponentsPlus(
|
|
PaymentComponents const& p,
|
|
Number f,
|
|
Number v = Number{})
|
|
: PaymentComponents(p), fee(f), valueChange(v)
|
|
{
|
|
}
|
|
};
|
|
|
|
template <class NumberProxy, class UInt32Proxy, class UInt32OptionalProxy>
|
|
LoanPaymentParts
|
|
doPayment(
|
|
PaymentComponentsPlus const& payment,
|
|
NumberProxy& totalValueOutstandingProxy,
|
|
NumberProxy& principalOutstandingProxy,
|
|
UInt32Proxy& paymentRemainingProxy,
|
|
UInt32Proxy& prevPaymentDateProxy,
|
|
UInt32OptionalProxy& nextDueDateProxy,
|
|
std::uint32_t paymentInterval)
|
|
{
|
|
XRPL_ASSERT_PARTS(
|
|
nextDueDateProxy,
|
|
"ripple::detail::doPayment",
|
|
"Next due date proxy set");
|
|
auto const totalValueDelta = payment.roundedPrincipal +
|
|
payment.roundedInterest - payment.valueChange;
|
|
if (!payment.extra)
|
|
{
|
|
if (payment.final)
|
|
{
|
|
XRPL_ASSERT_PARTS(
|
|
principalOutstandingProxy == payment.roundedPrincipal,
|
|
"ripple::detail::doPayment",
|
|
"Full principal payment");
|
|
XRPL_ASSERT_PARTS(
|
|
totalValueOutstandingProxy == totalValueDelta,
|
|
"ripple::detail::doPayment",
|
|
"Full value payment");
|
|
|
|
paymentRemainingProxy = 0;
|
|
|
|
prevPaymentDateProxy = *nextDueDateProxy;
|
|
// Remove the field. This is the only condition where nextDueDate is
|
|
// allowed to be removed.
|
|
nextDueDateProxy = std::nullopt;
|
|
}
|
|
else
|
|
{
|
|
XRPL_ASSERT_PARTS(
|
|
principalOutstandingProxy > payment.roundedPrincipal,
|
|
"ripple::detail::doPayment",
|
|
"Full principal payment");
|
|
XRPL_ASSERT_PARTS(
|
|
totalValueOutstandingProxy > totalValueDelta,
|
|
"ripple::detail::doPayment",
|
|
"Full value payment");
|
|
|
|
paymentRemainingProxy -= 1;
|
|
|
|
prevPaymentDateProxy = *nextDueDateProxy;
|
|
// STObject::OptionalField does not define operator+=, so do it the
|
|
// old-fashioned way.
|
|
nextDueDateProxy = *nextDueDateProxy + paymentInterval;
|
|
}
|
|
}
|
|
|
|
principalOutstandingProxy -= payment.roundedPrincipal;
|
|
totalValueOutstandingProxy -= totalValueDelta;
|
|
|
|
XRPL_ASSERT_PARTS(
|
|
// Use an explicit cast because the template parameter can be
|
|
// ValueProxy<Number> or Number
|
|
static_cast<Number>(principalOutstandingProxy) <=
|
|
static_cast<Number>(totalValueOutstandingProxy),
|
|
"ripple::detail::doPayment",
|
|
"principal does not exceed total");
|
|
|
|
return LoanPaymentParts{
|
|
.principalPaid = payment.roundedPrincipal,
|
|
.interestPaid = payment.roundedInterest,
|
|
.valueChange = payment.valueChange,
|
|
.feeToPay = payment.fee};
|
|
}
|
|
|
|
template <
|
|
AssetType A,
|
|
class NumberProxy,
|
|
class UInt32Proxy,
|
|
class UInt32OptionalProxy>
|
|
Expected<LoanPaymentParts, TER>
|
|
doOverpayment(
|
|
A const& asset,
|
|
ApplyView& view,
|
|
PaymentComponentsPlus const& overpaymentComponents,
|
|
NumberProxy& totalValueOutstandingProxy,
|
|
NumberProxy& principalOutstandingProxy,
|
|
NumberProxy& periodicPaymentProxy,
|
|
TenthBips32 const interestRate,
|
|
std::uint32_t const paymentInterval,
|
|
UInt32Proxy& paymentRemainingProxy,
|
|
UInt32Proxy& prevPaymentDateProxy,
|
|
UInt32OptionalProxy& nextDueDateProxy,
|
|
TenthBips16 managementFeeRate,
|
|
beast::Journal j)
|
|
{
|
|
Number const totalInterestOutstandingBefore =
|
|
totalValueOutstandingProxy - principalOutstandingProxy;
|
|
|
|
// Compute what the properties would be if the loan was new in its current
|
|
// state. They are not likely to match the original properties. We're
|
|
// interested in the error.
|
|
auto const oldLoanProperties = computeLoanProperties(
|
|
asset,
|
|
principalOutstandingProxy,
|
|
interestRate,
|
|
paymentInterval,
|
|
paymentRemainingProxy,
|
|
managementFeeRate);
|
|
|
|
auto const accumulatedError =
|
|
oldLoanProperties.totalValueOutstanding - totalValueOutstandingProxy;
|
|
|
|
{
|
|
// Use temp variables to do the payment, so they can be thrown away if
|
|
// they don't work
|
|
Number totalValueOutstanding = totalValueOutstandingProxy;
|
|
Number principalOutstanding = principalOutstandingProxy;
|
|
std::uint32_t paymentRemaining = paymentRemainingProxy;
|
|
std::uint32_t prevPaymentDate = prevPaymentDateProxy;
|
|
std::optional<std::uint32_t> nextDueDate = nextDueDateProxy;
|
|
|
|
auto const paymentParts = detail::doPayment(
|
|
overpaymentComponents,
|
|
totalValueOutstanding,
|
|
principalOutstanding,
|
|
paymentRemaining,
|
|
prevPaymentDate,
|
|
nextDueDate,
|
|
paymentInterval);
|
|
|
|
auto newLoanProperties = computeLoanProperties(
|
|
asset,
|
|
principalOutstanding,
|
|
interestRate,
|
|
paymentInterval,
|
|
paymentRemaining,
|
|
managementFeeRate);
|
|
|
|
newLoanProperties.totalValueOutstanding += accumulatedError;
|
|
|
|
if (newLoanProperties.firstPaymentPrincipal <= 0 &&
|
|
principalOutstanding > 0)
|
|
{
|
|
// The overpayment has caused the loan to be in a state
|
|
// where no further principal can be paid.
|
|
JLOG(j.warn())
|
|
<< "Loan overpayment would cause loan to be stuck. "
|
|
"Rejecting overpayment, but normal payments are unaffected.";
|
|
return Unexpected(tesSUCCESS);
|
|
}
|
|
// Check that the other computed values are valid
|
|
if (newLoanProperties.interestOwedToVault < 0 ||
|
|
newLoanProperties.totalValueOutstanding <= 0 ||
|
|
newLoanProperties.periodicPayment <= 0)
|
|
{
|
|
// LCOV_EXCL_START
|
|
JLOG(j.warn()) << "Computed loan properties are invalid. Does "
|
|
"not compute. TotalValueOutstanding: "
|
|
<< newLoanProperties.totalValueOutstanding
|
|
<< ", PeriodicPayment: "
|
|
<< newLoanProperties.periodicPayment
|
|
<< ", InterestOwedToVault: "
|
|
<< newLoanProperties.interestOwedToVault;
|
|
return Unexpected(tesSUCCESS);
|
|
// LCOV_EXCL_STOP
|
|
}
|
|
|
|
totalValueOutstandingProxy =
|
|
newLoanProperties.totalValueOutstanding + accumulatedError;
|
|
principalOutstandingProxy = principalOutstanding;
|
|
periodicPaymentProxy = newLoanProperties.periodicPayment;
|
|
|
|
XRPL_ASSERT_PARTS(
|
|
paymentRemainingProxy == paymentRemaining,
|
|
"ripple::detail::doOverpayment",
|
|
"paymentRemaining is unchanged");
|
|
paymentRemainingProxy = paymentRemaining;
|
|
XRPL_ASSERT_PARTS(
|
|
prevPaymentDateProxy == prevPaymentDate,
|
|
"ripple::detail::doOverpayment",
|
|
"prevPaymentDate is unchanged");
|
|
prevPaymentDateProxy = prevPaymentDate;
|
|
XRPL_ASSERT_PARTS(
|
|
nextDueDateProxy == nextDueDate,
|
|
"ripple::detail::doOverpayment",
|
|
"nextDueDate is unchanged");
|
|
nextDueDateProxy = nextDueDate;
|
|
|
|
auto const totalInterestOutstandingAfter =
|
|
totalValueOutstanding - principalOutstanding;
|
|
|
|
return paymentParts;
|
|
}
|
|
}
|
|
|
|
/* Handle possible late payments.
|
|
*
|
|
* If this function processed a late payment, the return value will be
|
|
* a LoanPaymentParts object. If the loan is not late, the return will be an
|
|
* Unexpected(tesSUCCESS). Otherwise, it'll be an Unexpected with the error code
|
|
* the caller is expected to return.
|
|
*
|
|
*
|
|
* This function is an implementation of the XLS-66 spec, based on
|
|
* * section 3.2.4.3 (Transaction Pseudo-code), specifically the bit
|
|
* labeled "the payment is late"
|
|
* * section 3.2.4.1.2 (Late Payment)
|
|
*/
|
|
template <AssetType A>
|
|
Expected<PaymentComponentsPlus, TER>
|
|
handleLatePayment(
|
|
A const& asset,
|
|
ApplyView const& view,
|
|
Number const& principalOutstanding,
|
|
std::int32_t nextDueDate,
|
|
PaymentComponentsPlus const& periodic,
|
|
TenthBips32 lateInterestRate,
|
|
std::int32_t loanScale,
|
|
Number const& latePaymentFee,
|
|
STAmount const& amount,
|
|
beast::Journal j)
|
|
{
|
|
if (!hasExpired(view, nextDueDate))
|
|
return Unexpected(tesSUCCESS);
|
|
|
|
// the payment is late
|
|
// Late payment interest is only the part of the interest that comes from
|
|
// being late, as computed by 3.2.4.1.2.
|
|
auto const latePaymentInterest = loanLatePaymentInterest(
|
|
asset,
|
|
principalOutstanding,
|
|
lateInterestRate,
|
|
view.parentCloseTime(),
|
|
nextDueDate,
|
|
loanScale);
|
|
XRPL_ASSERT(
|
|
latePaymentInterest >= 0,
|
|
"ripple::detail::handleLatePayment : valid late interest");
|
|
PaymentComponentsPlus const late{
|
|
PaymentComponents{
|
|
.rawInterest = periodic.rawInterest + latePaymentInterest,
|
|
.rawPrincipal = periodic.rawPrincipal,
|
|
.roundedInterest = periodic.roundedInterest + latePaymentInterest,
|
|
.roundedPrincipal = periodic.roundedPrincipal,
|
|
.roundedPayment = periodic.roundedPayment},
|
|
// A late payment pays both the normal fee, and the extra fee
|
|
periodic.fee + latePaymentFee,
|
|
// A late payment increases the value of the loan by the difference
|
|
// between periodic and late payment interest
|
|
latePaymentInterest};
|
|
auto const totalDue =
|
|
late.roundedPrincipal + late.roundedInterest + late.fee;
|
|
XRPL_ASSERT_PARTS(
|
|
isRounded(asset, totalDue, loanScale),
|
|
"ripple::detail::handleLatePayment",
|
|
"total due is rounded");
|
|
|
|
if (amount < totalDue)
|
|
{
|
|
JLOG(j.warn()) << "Late loan payment amount is insufficient. Due: "
|
|
<< totalDue << ", paid: " << amount;
|
|
return Unexpected(tecINSUFFICIENT_PAYMENT);
|
|
}
|
|
|
|
return late;
|
|
}
|
|
|
|
/* Handle possible full payments.
|
|
*
|
|
* If this function processed a full payment, the return value will be
|
|
* a PaymentComponentsPlus object. If the payment should not be considered as a
|
|
* full payment, the return will be an Unexpected(tesSUCCESS). Otherwise, it'll
|
|
* be an Unexpected with the error code the caller is expected to return.
|
|
*/
|
|
template <AssetType A>
|
|
Expected<PaymentComponentsPlus, TER>
|
|
handleFullPayment(
|
|
A const& asset,
|
|
ApplyView& view,
|
|
Number const& principalOutstanding,
|
|
Number const& periodicPayment,
|
|
std::uint32_t paymentRemaining,
|
|
std::uint32_t prevPaymentDate,
|
|
std::uint32_t const startDate,
|
|
std::uint32_t const paymentInterval,
|
|
TenthBips32 const closeInterestRate,
|
|
std::int32_t loanScale,
|
|
Number const& totalInterestOutstanding,
|
|
Number const& periodicRate,
|
|
Number const& closePaymentFee,
|
|
STAmount const& amount,
|
|
beast::Journal j)
|
|
{
|
|
if (paymentRemaining <= 1)
|
|
// If this is the last payment, it has to be a regular payment
|
|
return Unexpected(tesSUCCESS);
|
|
|
|
Number const rawValueOutstanding = periodicPayment * paymentRemaining;
|
|
Number const rawPrincipalOutstanding = loanPrincipalFromPeriodicPayment(
|
|
periodicPayment, periodicRate, paymentRemaining);
|
|
|
|
auto const totalInterest = calculateFullPaymentInterest(
|
|
asset,
|
|
rawPrincipalOutstanding,
|
|
periodicRate,
|
|
view.parentCloseTime(),
|
|
paymentInterval,
|
|
prevPaymentDate,
|
|
startDate,
|
|
closeInterestRate,
|
|
loanScale);
|
|
|
|
auto const closeFullPayment =
|
|
principalOutstanding + totalInterest + closePaymentFee;
|
|
|
|
if (amount < closeFullPayment)
|
|
// If the payment is less than the full payment amount, it's not
|
|
// sufficient to be a full payment, but that's not an error.
|
|
return Unexpected(tesSUCCESS);
|
|
|
|
// Make a full payment
|
|
|
|
PaymentComponentsPlus const result{
|
|
PaymentComponents{
|
|
.rawInterest =
|
|
principalOutstanding + totalInterest - rawPrincipalOutstanding,
|
|
.rawPrincipal = rawPrincipalOutstanding,
|
|
.roundedInterest = totalInterest,
|
|
.roundedPrincipal = principalOutstanding,
|
|
.roundedPayment = principalOutstanding + totalInterest,
|
|
.final = true},
|
|
// A full payment only pays the single close payment fee
|
|
closePaymentFee,
|
|
// A full payment decreases the value of the loan by the
|
|
// difference between the interest paid and the expected
|
|
// outstanding interest return
|
|
totalInterest - totalInterestOutstanding};
|
|
|
|
return result;
|
|
}
|
|
|
|
} // namespace detail
|
|
|
|
template <AssetType A>
|
|
Number
|
|
valueMinusFee(
|
|
A const& asset,
|
|
Number const& value,
|
|
TenthBips32 managementFeeRate,
|
|
std::int32_t scale)
|
|
{
|
|
return roundToAsset(
|
|
asset, detail::minusFee(value, managementFeeRate), scale);
|
|
}
|
|
|
|
struct LoanProperties
|
|
{
|
|
Number periodicPayment;
|
|
Number totalValueOutstanding;
|
|
Number interestOwedToVault;
|
|
std::int32_t loanScale;
|
|
Number firstPaymentPrincipal;
|
|
};
|
|
|
|
template <AssetType A>
|
|
LoanProperties
|
|
computeLoanProperties(
|
|
A const& asset,
|
|
Number const& principalOutstanding,
|
|
TenthBips32 interestRate,
|
|
std::uint32_t paymentInterval,
|
|
std::uint32_t paymentsRemaining,
|
|
TenthBips32 managementFeeRate)
|
|
{
|
|
auto const periodicRate = loanPeriodicRate(interestRate, paymentInterval);
|
|
XRPL_ASSERT(
|
|
interestRate == 0 || periodicRate > 0,
|
|
"ripple::computeLoanProperties : valid rate");
|
|
|
|
auto const periodicPayment = detail::loanPeriodicPayment(
|
|
principalOutstanding, periodicRate, paymentsRemaining);
|
|
Number const totalValueOutstanding = [&]() {
|
|
NumberRoundModeGuard mg(Number::to_nearest);
|
|
// Use STAmount's internal rounding instead of roundToAsset, because
|
|
// we're going to use this result to determine the scale for all the
|
|
// other rounding.
|
|
return STAmount{
|
|
asset,
|
|
/*
|
|
* This formula is from the XLS-66 spec, section 3.2.4.2 (Total
|
|
* Loan Value Calculation), specifically "totalValueOutstanding
|
|
* = ..."
|
|
*/
|
|
periodicPayment * paymentsRemaining};
|
|
}();
|
|
// Base the loan scale on the total value, since that's going to be the
|
|
// biggest number involved (barring unusual parameters for late, full, or
|
|
// over payments)
|
|
auto const loanScale = totalValueOutstanding.exponent();
|
|
|
|
auto const firstPaymentPrincipal = [&]() {
|
|
// Compute the unrounded parts for the first payment. Ensure that the
|
|
// principal payment will actually change the principal.
|
|
auto const paymentComponents = detail::computePaymentComponents(
|
|
asset,
|
|
loanScale,
|
|
totalValueOutstanding,
|
|
principalOutstanding,
|
|
periodicPayment,
|
|
periodicRate,
|
|
paymentsRemaining);
|
|
|
|
// The rounded principal part needs to be large enough to affect the
|
|
// principal. What to do if not is left to the caller
|
|
return paymentComponents.roundedPrincipal;
|
|
}();
|
|
|
|
auto const interestOwedToVault = valueMinusFee(
|
|
asset,
|
|
/*
|
|
* This formula is from the XLS-66 spec, section 3.2.4.2 (Total Loan
|
|
* Value Calculation), specifically "totalInterestOutstanding = ..."
|
|
*/
|
|
totalValueOutstanding - principalOutstanding,
|
|
managementFeeRate,
|
|
loanScale);
|
|
|
|
return LoanProperties{
|
|
.periodicPayment = periodicPayment,
|
|
.totalValueOutstanding = totalValueOutstanding,
|
|
.interestOwedToVault = interestOwedToVault,
|
|
.loanScale = loanScale,
|
|
.firstPaymentPrincipal = firstPaymentPrincipal};
|
|
}
|
|
|
|
template <AssetType A>
|
|
Number
|
|
calculateFullPaymentInterest(
|
|
A const& asset,
|
|
Number const& rawPrincipalOutstanding,
|
|
Number const& periodicRate,
|
|
NetClock::time_point parentCloseTime,
|
|
std::uint32_t paymentInterval,
|
|
std::uint32_t prevPaymentDate,
|
|
std::uint32_t startDate,
|
|
TenthBips32 closeInterestRate,
|
|
std::int32_t loanScale)
|
|
{
|
|
// If there is more than one payment remaining, see if enough was
|
|
// paid for a full payment
|
|
auto const accruedInterest = roundToAsset(
|
|
asset,
|
|
detail::loanAccruedInterest(
|
|
rawPrincipalOutstanding,
|
|
periodicRate,
|
|
parentCloseTime,
|
|
startDate,
|
|
prevPaymentDate,
|
|
paymentInterval),
|
|
loanScale);
|
|
XRPL_ASSERT(
|
|
accruedInterest >= 0,
|
|
"ripple::detail::handleFullPayment : valid accrued interest");
|
|
|
|
auto const prepaymentPenalty = roundToAsset(
|
|
asset,
|
|
tenthBipsOfValue(rawPrincipalOutstanding, closeInterestRate),
|
|
loanScale);
|
|
XRPL_ASSERT(
|
|
prepaymentPenalty >= 0,
|
|
"ripple::detail::handleFullPayment : valid prepayment "
|
|
"interest");
|
|
return accruedInterest + prepaymentPenalty;
|
|
}
|
|
|
|
template <AssetType A>
|
|
Number
|
|
calculateFullPaymentInterest(
|
|
A const& asset,
|
|
Number const& periodicPayment,
|
|
Number const& periodicRate,
|
|
std::uint32_t paymentRemaining,
|
|
NetClock::time_point parentCloseTime,
|
|
std::uint32_t paymentInterval,
|
|
std::uint32_t prevPaymentDate,
|
|
std::uint32_t startDate,
|
|
TenthBips32 closeInterestRate,
|
|
std::int32_t loanScale)
|
|
{
|
|
Number const rawPrincipalOutstanding =
|
|
detail::loanPrincipalFromPeriodicPayment(
|
|
periodicPayment, periodicRate, paymentRemaining);
|
|
|
|
return calculateFullPaymentInterest(
|
|
asset,
|
|
rawPrincipalOutstanding,
|
|
periodicRate,
|
|
parentCloseTime,
|
|
paymentInterval,
|
|
prevPaymentDate,
|
|
startDate,
|
|
closeInterestRate,
|
|
loanScale);
|
|
}
|
|
|
|
#if LOANCOMPLETE
|
|
template <AssetType A>
|
|
Number
|
|
loanPeriodicPayment(
|
|
A const& asset,
|
|
Number const& principalOutstanding,
|
|
Number const& periodicRate,
|
|
std::uint32_t paymentsRemaining,
|
|
std::int32_t scale)
|
|
{
|
|
return roundPeriodicPayment(
|
|
asset,
|
|
detail::loanPeriodicPayment(
|
|
principalOutstanding, periodicRate, paymentsRemaining),
|
|
scale);
|
|
}
|
|
|
|
template <AssetType A>
|
|
Number
|
|
loanPeriodicPayment(
|
|
A const& asset,
|
|
Number const& principalOutstanding,
|
|
TenthBips32 interestRate,
|
|
std::uint32_t paymentInterval,
|
|
std::uint32_t paymentsRemaining,
|
|
std::int32_t scale)
|
|
{
|
|
return loanPeriodicPayment(
|
|
asset,
|
|
principalOutstanding,
|
|
loanPeriodicRate(interestRate, paymentInterval),
|
|
paymentsRemaining,
|
|
scale);
|
|
}
|
|
|
|
template <AssetType A>
|
|
Number
|
|
loanTotalValueOutstanding(
|
|
A asset,
|
|
std::int32_t scale,
|
|
Number const& periodicPayment,
|
|
std::uint32_t paymentsRemaining)
|
|
{
|
|
return roundToAsset(
|
|
asset,
|
|
/*
|
|
* This formula is from the XLS-66 spec, section 3.2.4.2 (Total Loan
|
|
* Value Calculation), specifically "totalValueOutstanding = ..."
|
|
*/
|
|
periodicPayment * paymentsRemaining,
|
|
scale,
|
|
Number::upward);
|
|
}
|
|
|
|
template <AssetType A>
|
|
Number
|
|
loanTotalValueOutstanding(
|
|
A asset,
|
|
std::int32_t scale,
|
|
Number const& principalOutstanding,
|
|
TenthBips32 interestRate,
|
|
std::uint32_t paymentInterval,
|
|
std::uint32_t paymentsRemaining)
|
|
{
|
|
/*
|
|
* This function is derived from the XLS-66 spec, section 3.2.4.2 (Total
|
|
* Loan Value Calculation)
|
|
*/
|
|
return loanTotalValueOutstanding(
|
|
asset,
|
|
scale,
|
|
loanPeriodicPayment(
|
|
asset,
|
|
principalOutstanding,
|
|
interestRate,
|
|
paymentInterval,
|
|
paymentsRemaining,
|
|
scale),
|
|
paymentsRemaining);
|
|
}
|
|
|
|
inline Number
|
|
loanTotalInterestOutstanding(
|
|
Number const& principalOutstanding,
|
|
Number const& totalValueOutstanding)
|
|
{
|
|
/*
|
|
* This formula is from the XLS-66 spec, section 3.2.4.2 (Total Loan
|
|
* Value Calculation), specifically "totalInterestOutstanding = ..."
|
|
*/
|
|
return totalValueOutstanding - principalOutstanding;
|
|
}
|
|
|
|
template <AssetType A>
|
|
Number
|
|
loanTotalInterestOutstanding(
|
|
A asset,
|
|
std::int32_t scale,
|
|
Number const& principalOutstanding,
|
|
TenthBips32 interestRate,
|
|
std::uint32_t paymentInterval,
|
|
std::uint32_t paymentsRemaining)
|
|
{
|
|
/*
|
|
* This formula is derived from the XLS-66 spec, section 3.2.4.2 (Total Loan
|
|
* Value Calculation)
|
|
*/
|
|
return loanTotalInterestOutstanding(
|
|
principalOutstanding,
|
|
loanTotalValueOutstanding(
|
|
asset,
|
|
scale,
|
|
principalOutstanding,
|
|
interestRate,
|
|
paymentInterval,
|
|
paymentsRemaining));
|
|
}
|
|
#endif
|
|
|
|
template <AssetType A>
|
|
Number
|
|
loanInterestOutstandingMinusFee(
|
|
A const& asset,
|
|
Number const& totalInterestOutstanding,
|
|
TenthBips32 managementFeeRate,
|
|
std::int32_t scale)
|
|
{
|
|
return valueMinusFee(
|
|
asset, totalInterestOutstanding, managementFeeRate, scale);
|
|
}
|
|
|
|
#if LOANCOMPLETE
|
|
template <AssetType A>
|
|
Number
|
|
loanInterestOutstandingMinusFee(
|
|
A const& asset,
|
|
std::int32_t scale,
|
|
Number const& principalOutstanding,
|
|
TenthBips32 interestRate,
|
|
std::uint32_t paymentInterval,
|
|
std::uint32_t paymentsRemaining,
|
|
TenthBips32 managementFeeRate)
|
|
{
|
|
return loanInterestOutstandingMinusFee(
|
|
asset,
|
|
loanTotalInterestOutstanding(
|
|
asset,
|
|
scale,
|
|
principalOutstanding,
|
|
interestRate,
|
|
paymentInterval,
|
|
paymentsRemaining),
|
|
managementFeeRate,
|
|
scale);
|
|
}
|
|
#endif
|
|
|
|
template <AssetType A>
|
|
Number
|
|
loanLatePaymentInterest(
|
|
A const& asset,
|
|
Number const& principalOutstanding,
|
|
TenthBips32 lateInterestRate,
|
|
NetClock::time_point parentCloseTime,
|
|
std::uint32_t nextPaymentDueDate,
|
|
std::int32_t const& scale)
|
|
{
|
|
return roundToAsset(
|
|
asset,
|
|
detail::loanLatePaymentInterest(
|
|
principalOutstanding,
|
|
lateInterestRate,
|
|
parentCloseTime,
|
|
nextPaymentDueDate),
|
|
scale);
|
|
}
|
|
|
|
template <AssetType A>
|
|
bool
|
|
isRounded(A const& asset, Number const& value, std::int32_t scale)
|
|
{
|
|
return roundToAsset(asset, value, scale, Number::downward) ==
|
|
roundToAsset(asset, value, scale, Number::upward);
|
|
}
|
|
|
|
template <AssetType A>
|
|
Expected<LoanPaymentParts, TER>
|
|
loanMakePayment(
|
|
A const& asset,
|
|
ApplyView& view,
|
|
SLE::ref loan,
|
|
STAmount const& amount,
|
|
TenthBips16 managementFeeRate,
|
|
beast::Journal j)
|
|
{
|
|
/*
|
|
* This function is an implementation of the XLS-66 spec,
|
|
* section 3.2.4.3 (Transaction Pseudo-code)
|
|
*/
|
|
auto principalOutstandingProxy = loan->at(sfPrincipalOutstanding);
|
|
auto paymentRemainingProxy = loan->at(sfPaymentRemaining);
|
|
|
|
if (paymentRemainingProxy == 0 || principalOutstandingProxy == 0)
|
|
{
|
|
// Loan complete
|
|
JLOG(j.warn()) << "Loan is already paid off.";
|
|
return Unexpected(tecKILLED);
|
|
}
|
|
|
|
// Next payment due date must be set unless the loan is complete
|
|
auto nextDueDateProxy = loan->at(~sfNextPaymentDueDate);
|
|
if (!nextDueDateProxy)
|
|
{
|
|
JLOG(j.warn()) << "Loan next payment due date is not set.";
|
|
return Unexpected(tecINTERNAL);
|
|
}
|
|
|
|
std::int32_t const loanScale = loan->at(sfLoanScale);
|
|
auto totalValueOutstandingProxy = loan->at(sfTotalValueOutstanding);
|
|
|
|
TenthBips32 const interestRate{loan->at(sfInterestRate)};
|
|
TenthBips32 const lateInterestRate{loan->at(sfLateInterestRate)};
|
|
TenthBips32 const closeInterestRate{loan->at(sfCloseInterestRate)};
|
|
|
|
Number const serviceFee = loan->at(sfLoanServiceFee);
|
|
Number const latePaymentFee = loan->at(sfLatePaymentFee);
|
|
Number const closePaymentFee =
|
|
roundToAsset(asset, loan->at(sfClosePaymentFee), loanScale);
|
|
|
|
auto const periodicPayment = loan->at(sfPeriodicPayment);
|
|
|
|
auto prevPaymentDateProxy = loan->at(sfPreviousPaymentDate);
|
|
std::uint32_t const startDate = loan->at(sfStartDate);
|
|
|
|
std::uint32_t const paymentInterval = loan->at(sfPaymentInterval);
|
|
// Compute the normal periodic rate, payment, etc.
|
|
// We'll need it in the remaining calculations
|
|
Number const periodicRate = loanPeriodicRate(interestRate, paymentInterval);
|
|
XRPL_ASSERT(
|
|
interestRate == 0 || periodicRate > 0,
|
|
"ripple::loanMakePayment : valid rate");
|
|
|
|
XRPL_ASSERT(
|
|
*totalValueOutstandingProxy > 0,
|
|
"ripple::loanMakePayment : valid total value");
|
|
|
|
view.update(loan);
|
|
|
|
detail::PaymentComponentsPlus const periodic{
|
|
detail::computePaymentComponents(
|
|
asset,
|
|
loanScale,
|
|
totalValueOutstandingProxy,
|
|
principalOutstandingProxy,
|
|
periodicPayment,
|
|
periodicRate,
|
|
paymentRemainingProxy),
|
|
serviceFee};
|
|
|
|
// -------------------------------------------------------------
|
|
// late payment handling
|
|
if (auto const latePaymentComponents = detail::handleLatePayment(
|
|
asset,
|
|
view,
|
|
principalOutstandingProxy,
|
|
*nextDueDateProxy,
|
|
periodic,
|
|
lateInterestRate,
|
|
loanScale,
|
|
latePaymentFee,
|
|
amount,
|
|
j))
|
|
{
|
|
return doPayment(
|
|
*latePaymentComponents,
|
|
totalValueOutstandingProxy,
|
|
principalOutstandingProxy,
|
|
paymentRemainingProxy,
|
|
prevPaymentDateProxy,
|
|
nextDueDateProxy,
|
|
paymentInterval);
|
|
}
|
|
else if (latePaymentComponents.error())
|
|
// error() will be the TER returned if a payment is not made. It will
|
|
// only evaluate to true if it's unsuccessful. Otherwise, tesSUCCESS
|
|
// means nothing was done, so continue.
|
|
return Unexpected(latePaymentComponents.error());
|
|
|
|
// -------------------------------------------------------------
|
|
// full payment handling
|
|
auto const totalInterestOutstanding =
|
|
totalValueOutstandingProxy - principalOutstandingProxy;
|
|
|
|
if (auto const fullPaymentComponents = detail::handleFullPayment(
|
|
asset,
|
|
view,
|
|
principalOutstandingProxy,
|
|
periodicPayment,
|
|
paymentRemainingProxy,
|
|
prevPaymentDateProxy,
|
|
startDate,
|
|
paymentInterval,
|
|
closeInterestRate,
|
|
loanScale,
|
|
totalInterestOutstanding,
|
|
periodicRate,
|
|
closePaymentFee,
|
|
amount,
|
|
j))
|
|
return doPayment(
|
|
*fullPaymentComponents,
|
|
totalValueOutstandingProxy,
|
|
principalOutstandingProxy,
|
|
paymentRemainingProxy,
|
|
prevPaymentDateProxy,
|
|
nextDueDateProxy,
|
|
paymentInterval);
|
|
else if (fullPaymentComponents.error())
|
|
// error() will be the TER returned if a payment is not made. It will
|
|
// only evaluate to true if it's unsuccessful. Otherwise, tesSUCCESS
|
|
// means nothing was done, so continue.
|
|
return Unexpected(fullPaymentComponents.error());
|
|
|
|
// -------------------------------------------------------------
|
|
// regular periodic 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
|
|
|
|
// This will keep a running total of what is actually paid, if the payment
|
|
// is sufficient for a single payment
|
|
Number totalPaid =
|
|
periodic.roundedInterest + periodic.roundedPrincipal + periodic.fee;
|
|
|
|
if (amount < totalPaid)
|
|
{
|
|
JLOG(j.warn()) << "Periodic loan payment amount is insufficient. Due: "
|
|
<< totalPaid << ", paid: " << amount;
|
|
return Unexpected(tecINSUFFICIENT_PAYMENT);
|
|
}
|
|
|
|
LoanPaymentParts totalParts = detail::doPayment(
|
|
periodic,
|
|
totalValueOutstandingProxy,
|
|
principalOutstandingProxy,
|
|
paymentRemainingProxy,
|
|
prevPaymentDateProxy,
|
|
nextDueDateProxy,
|
|
paymentInterval);
|
|
|
|
std::size_t numPayments = 1;
|
|
|
|
while (totalPaid < amount && paymentRemainingProxy > 0)
|
|
{
|
|
// Try to make more payments
|
|
detail::PaymentComponentsPlus const nextPayment{
|
|
detail::computePaymentComponents(
|
|
asset,
|
|
loanScale,
|
|
totalValueOutstandingProxy,
|
|
principalOutstandingProxy,
|
|
periodicPayment,
|
|
periodicRate,
|
|
paymentRemainingProxy),
|
|
periodic.fee};
|
|
XRPL_ASSERT(
|
|
nextPayment.rawInterest <= periodic.rawInterest,
|
|
"ripple::loanMakePayment : decreasing interest");
|
|
XRPL_ASSERT(
|
|
nextPayment.rawPrincipal >= periodic.rawPrincipal,
|
|
"ripple::loanMakePayment : increasing principal");
|
|
|
|
// the fee part doesn't change
|
|
auto const due = nextPayment.roundedInterest +
|
|
nextPayment.roundedPrincipal + periodic.fee;
|
|
|
|
if (amount < totalPaid + due)
|
|
// We're done making payments.
|
|
break;
|
|
|
|
totalPaid += due;
|
|
totalParts += detail::doPayment(
|
|
nextPayment,
|
|
totalValueOutstandingProxy,
|
|
principalOutstandingProxy,
|
|
paymentRemainingProxy,
|
|
prevPaymentDateProxy,
|
|
nextDueDateProxy,
|
|
paymentInterval);
|
|
++numPayments;
|
|
|
|
if (nextPayment.final)
|
|
break;
|
|
}
|
|
|
|
XRPL_ASSERT_PARTS(
|
|
totalParts.principalPaid + totalParts.interestPaid +
|
|
totalParts.feeToPay ==
|
|
totalPaid,
|
|
"ripple::loanMakePayment",
|
|
"payment parts add up");
|
|
XRPL_ASSERT_PARTS(
|
|
totalParts.valueChange == 0,
|
|
"ripple::loanMakePayment",
|
|
"no value change");
|
|
XRPL_ASSERT_PARTS(
|
|
totalParts.feeToPay == periodic.fee * numPayments,
|
|
"ripple::loanMakePayment",
|
|
"fee parts add up");
|
|
|
|
// -------------------------------------------------------------
|
|
// overpayment handling
|
|
if (loan->isFlag(lsfLoanOverpayment) && paymentRemainingProxy > 0 &&
|
|
nextDueDateProxy && totalPaid < amount)
|
|
{
|
|
TenthBips32 const overpaymentInterestRate{
|
|
loan->at(sfOverpaymentInterestRate)};
|
|
TenthBips32 const overpaymentFeeRate{loan->at(sfOverpaymentFee)};
|
|
|
|
Number const overpayment = amount - totalPaid;
|
|
XRPL_ASSERT(
|
|
overpayment > 0 && isRounded(asset, overpayment, loanScale),
|
|
"ripple::loanMakePayment : valid overpayment amount");
|
|
|
|
Number const fee = roundToAsset(
|
|
asset,
|
|
tenthBipsOfValue(overpayment, overpaymentFeeRate),
|
|
loanScale);
|
|
|
|
Number const payment = overpayment - fee;
|
|
|
|
// TODO: Is the overpaymentInterestRate an APR or flat?
|
|
|
|
Number const interest =
|
|
tenthBipsOfValue(payment, overpaymentInterestRate);
|
|
Number const roundedInterest = roundToAsset(asset, interest, loanScale);
|
|
|
|
detail::PaymentComponentsPlus overpaymentComponents{
|
|
PaymentComponents{
|
|
.rawInterest = interest,
|
|
.rawPrincipal = payment - interest,
|
|
.roundedInterest = roundedInterest,
|
|
.roundedPrincipal = payment - roundedInterest,
|
|
.roundedPayment = payment,
|
|
.extra = true},
|
|
fee,
|
|
roundedInterest};
|
|
|
|
// Don't process an overpayment if the whole amount (or more!)
|
|
// gets eaten by fees and interest.
|
|
if (overpaymentComponents.rawPrincipal > 0 &&
|
|
overpaymentComponents.roundedPrincipal > 0)
|
|
{
|
|
auto periodicPaymentProxy = loan->at(sfPeriodicPayment);
|
|
if (auto const overResult = detail::doOverpayment(
|
|
asset,
|
|
view,
|
|
overpaymentComponents,
|
|
totalValueOutstandingProxy,
|
|
principalOutstandingProxy,
|
|
periodicPaymentProxy,
|
|
interestRate,
|
|
paymentInterval,
|
|
paymentRemainingProxy,
|
|
prevPaymentDateProxy,
|
|
nextDueDateProxy,
|
|
managementFeeRate,
|
|
j))
|
|
totalParts += *overResult;
|
|
else if (overResult.error())
|
|
// error() will be the TER returned if a payment is not made. It
|
|
// will only evaluate to true if it's unsuccessful. Otherwise,
|
|
// tesSUCCESS means nothing was done, so continue.
|
|
return Unexpected(overResult.error());
|
|
}
|
|
}
|
|
|
|
// Check the final results are rounded, to double-check that the
|
|
// intermediate steps were rounded.
|
|
XRPL_ASSERT(
|
|
isRounded(asset, totalParts.principalPaid, loanScale),
|
|
"ripple::loanMakePayment : total principal paid rounded");
|
|
XRPL_ASSERT(
|
|
isRounded(asset, totalParts.interestPaid, loanScale),
|
|
"ripple::loanMakePayment : total interest paid rounded");
|
|
XRPL_ASSERT(
|
|
isRounded(asset, totalParts.valueChange, loanScale),
|
|
"ripple::loanMakePayment : loan value change rounded");
|
|
XRPL_ASSERT(
|
|
isRounded(asset, totalParts.feeToPay, loanScale),
|
|
"ripple::loanMakePayment : total fee to pay rounded");
|
|
return totalParts;
|
|
}
|
|
|
|
} // namespace ripple
|
|
|
|
#endif // RIPPLE_APP_MISC_LENDINGHELPERS_H_INCLUDED
|