mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-20 11:05:54 +00:00
Start implementing LoanPay transaction
This commit is contained in:
@@ -776,7 +776,6 @@ TRANSACTION(ttLOAN_DRAW, 81, LoanDraw, noPriv, ({
|
|||||||
{sfAmount, soeREQUIRED, soeMPTSupported},
|
{sfAmount, soeREQUIRED, soeMPTSupported},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
#if 0
|
|
||||||
/** The Borrower uses this transaction to make a Payment on the Loan. */
|
/** The Borrower uses this transaction to make a Payment on the Loan. */
|
||||||
#if TRANSACTION_INCLUDE
|
#if TRANSACTION_INCLUDE
|
||||||
# include <xrpld/app/tx/detail/LoanPay.h>
|
# include <xrpld/app/tx/detail/LoanPay.h>
|
||||||
@@ -785,7 +784,6 @@ TRANSACTION(ttLOAN_PAY, 82, LoanPay, noPriv, ({
|
|||||||
{sfLoanID, soeREQUIRED},
|
{sfLoanID, soeREQUIRED},
|
||||||
{sfAmount, soeREQUIRED, soeMPTSupported},
|
{sfAmount, soeREQUIRED, soeMPTSupported},
|
||||||
}))
|
}))
|
||||||
#endif
|
|
||||||
|
|
||||||
/** This system-generated transaction type is used to update the status of the various amendments.
|
/** This system-generated transaction type is used to update the status of the various amendments.
|
||||||
|
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ class Loan_test : public beast::unit_test::suite
|
|||||||
{
|
{
|
||||||
TenthBips16 const managementFeeRate{
|
TenthBips16 const managementFeeRate{
|
||||||
brokerSle->at(sfManagementFeeRate)};
|
brokerSle->at(sfManagementFeeRate)};
|
||||||
auto const loanInterest = LoanInterestOutstandingToVault(
|
auto const loanInterest = LoanInterestOutstandingMinusFee(
|
||||||
broker.asset,
|
broker.asset,
|
||||||
principalOutstanding,
|
principalOutstanding,
|
||||||
interestRate,
|
interestRate,
|
||||||
@@ -261,7 +261,7 @@ class Loan_test : public beast::unit_test::suite
|
|||||||
env.test.BEAST_EXPECT(
|
env.test.BEAST_EXPECT(
|
||||||
vaultSle->at(sfLossUnrealized) ==
|
vaultSle->at(sfLossUnrealized) ==
|
||||||
principalOutstanding +
|
principalOutstanding +
|
||||||
LoanInterestOutstandingToVault(
|
LoanInterestOutstandingMinusFee(
|
||||||
broker.asset,
|
broker.asset,
|
||||||
principalOutstanding,
|
principalOutstanding,
|
||||||
interestRate,
|
interestRate,
|
||||||
|
|||||||
@@ -20,6 +20,8 @@
|
|||||||
#ifndef RIPPLE_APP_MISC_LENDINGHELPERS_H_INCLUDED
|
#ifndef RIPPLE_APP_MISC_LENDINGHELPERS_H_INCLUDED
|
||||||
#define RIPPLE_APP_MISC_LENDINGHELPERS_H_INCLUDED
|
#define RIPPLE_APP_MISC_LENDINGHELPERS_H_INCLUDED
|
||||||
|
|
||||||
|
#include <xrpld/ledger/ApplyView.h>
|
||||||
|
|
||||||
#include <xrpl/basics/Number.h>
|
#include <xrpl/basics/Number.h>
|
||||||
#include <xrpl/protocol/Asset.h>
|
#include <xrpl/protocol/Asset.h>
|
||||||
#include <xrpl/protocol/Feature.h>
|
#include <xrpl/protocol/Feature.h>
|
||||||
@@ -31,21 +33,22 @@ struct PreflightContext;
|
|||||||
|
|
||||||
// Lending protocol has dependencies, so capture them here.
|
// Lending protocol has dependencies, so capture them here.
|
||||||
bool
|
bool
|
||||||
LendingProtocolEnabled(PreflightContext const& ctx);
|
lendingProtocolEnabled(PreflightContext const& ctx);
|
||||||
|
|
||||||
namespace detail {
|
namespace detail {
|
||||||
// These functions should rarely be used directly. More often, the ultimate
|
// These functions should rarely be used directly. More often, the ultimate
|
||||||
// result needs to be roundToAsset'd.
|
// result needs to be roundToAsset'd.
|
||||||
|
|
||||||
Number
|
struct LoanPaymentParts
|
||||||
LoanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval);
|
{
|
||||||
|
Number principalPaid;
|
||||||
|
Number interestPaid;
|
||||||
|
Number valueChange;
|
||||||
|
Number feePaid;
|
||||||
|
};
|
||||||
|
|
||||||
Number
|
Number
|
||||||
LoanPeriodicPayment(
|
LoanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval);
|
||||||
Number principalOutstanding,
|
|
||||||
TenthBips32 interestRate,
|
|
||||||
std::uint32_t paymentInterval,
|
|
||||||
std::uint32_t paymentsRemaining);
|
|
||||||
|
|
||||||
Number
|
Number
|
||||||
LoanTotalValueOutstanding(
|
LoanTotalValueOutstanding(
|
||||||
@@ -61,27 +64,110 @@ LoanTotalInterestOutstanding(
|
|||||||
std::uint32_t paymentInterval,
|
std::uint32_t paymentInterval,
|
||||||
std::uint32_t paymentsRemaining);
|
std::uint32_t paymentsRemaining);
|
||||||
|
|
||||||
|
Number
|
||||||
|
LoanPeriodicPayment(
|
||||||
|
Number principalOutstanding,
|
||||||
|
TenthBips32 interestRate,
|
||||||
|
std::uint32_t paymentInterval,
|
||||||
|
std::uint32_t paymentsRemaining);
|
||||||
|
|
||||||
|
Number
|
||||||
|
LoanLatePaymentInterest(
|
||||||
|
Number principalOutstanding,
|
||||||
|
TenthBips32 lateInterestRate,
|
||||||
|
NetClock::time_point parentCloseTime,
|
||||||
|
std::uint32_t startDate,
|
||||||
|
std::uint32_t prevPaymentDate);
|
||||||
|
|
||||||
|
LoanPaymentParts
|
||||||
|
LoanComputePaymentParts(ApplyView& view, SLE::ref loan);
|
||||||
|
|
||||||
} // namespace detail
|
} // namespace detail
|
||||||
|
|
||||||
template <AssetType A>
|
template <AssetType A>
|
||||||
Number
|
Number
|
||||||
LoanInterestOutstandingToVault(
|
MinusFee(A const& asset, Number value, TenthBips32 managementFeeRate)
|
||||||
|
{
|
||||||
|
return roundToAsset(
|
||||||
|
asset, tenthBipsOfValue(value, tenthBipsPerUnity - managementFeeRate));
|
||||||
|
}
|
||||||
|
|
||||||
|
template <AssetType A>
|
||||||
|
Number
|
||||||
|
LoanInterestOutstandingMinusFee(
|
||||||
A const& asset,
|
A const& asset,
|
||||||
Number principalOutstanding,
|
Number principalOutstanding,
|
||||||
TenthBips32 interestRate,
|
TenthBips32 interestRate,
|
||||||
std::uint32_t paymentInterval,
|
std::uint32_t paymentInterval,
|
||||||
std::uint32_t paymentsRemaining,
|
std::uint32_t paymentsRemaining,
|
||||||
TenthBips32 managementFeeRate)
|
TenthBips32 managementFeeRate)
|
||||||
|
{
|
||||||
|
return MinusFee(
|
||||||
|
asset,
|
||||||
|
detail::LoanTotalInterestOutstanding(
|
||||||
|
principalOutstanding,
|
||||||
|
interestRate,
|
||||||
|
paymentInterval,
|
||||||
|
paymentsRemaining),
|
||||||
|
managementFeeRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <AssetType A>
|
||||||
|
Number
|
||||||
|
LoanPeriodicPayment(
|
||||||
|
A const& asset,
|
||||||
|
Number principalOutstanding,
|
||||||
|
TenthBips32 interestRate,
|
||||||
|
std::uint32_t paymentInterval,
|
||||||
|
std::uint32_t paymentsRemaining)
|
||||||
{
|
{
|
||||||
return roundToAsset(
|
return roundToAsset(
|
||||||
asset,
|
asset,
|
||||||
tenthBipsOfValue(
|
detail::LoanPeriodicPayment(
|
||||||
detail::LoanTotalInterestOutstanding(
|
principalOutstanding,
|
||||||
principalOutstanding,
|
interestRate,
|
||||||
interestRate,
|
paymentInterval,
|
||||||
paymentInterval,
|
paymentsRemaining));
|
||||||
paymentsRemaining),
|
}
|
||||||
tenthBipsPerUnity - managementFeeRate));
|
|
||||||
|
template <AssetType A>
|
||||||
|
Number
|
||||||
|
LoanLatePaymentInterest(
|
||||||
|
A const& asset,
|
||||||
|
Number principalOutstanding,
|
||||||
|
TenthBips32 lateInterestRate,
|
||||||
|
NetClock::time_point parentCloseTime,
|
||||||
|
std::uint32_t startDate,
|
||||||
|
std::uint32_t prevPaymentDate)
|
||||||
|
{
|
||||||
|
return roundToAsset(
|
||||||
|
asset,
|
||||||
|
detail::LoanLatePaymentInterest(
|
||||||
|
principalOutstanding,
|
||||||
|
lateInterestRate,
|
||||||
|
parentCloseTime,
|
||||||
|
startDate,
|
||||||
|
prevPaymentDate));
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LoanPaymentParts
|
||||||
|
{
|
||||||
|
STAmount principalPaid;
|
||||||
|
STAmount interestPaid;
|
||||||
|
STAmount valueChange;
|
||||||
|
STAmount feePaid;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <AssetType A>
|
||||||
|
LoanPaymentParts
|
||||||
|
LoanComputePaymentParts(A const& asset, ApplyView& view, SLE::ref loan)
|
||||||
|
{
|
||||||
|
auto const parts = detail::LoanComputePaymentParts(view, loan);
|
||||||
|
return LoanPaymentParts{
|
||||||
|
roundToAsset(asset, parts.principalPaid),
|
||||||
|
roundToAsset(asset, parts.interestPaid),
|
||||||
|
roundToAsset(asset, parts.valueChange),
|
||||||
|
roundToAsset(asset, parts.feePaid)};
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace ripple
|
} // namespace ripple
|
||||||
|
|||||||
@@ -21,13 +21,15 @@
|
|||||||
//
|
//
|
||||||
#include <xrpld/app/tx/detail/Transactor.h>
|
#include <xrpld/app/tx/detail/Transactor.h>
|
||||||
#include <xrpld/app/tx/detail/VaultCreate.h>
|
#include <xrpld/app/tx/detail/VaultCreate.h>
|
||||||
|
#include <xrpld/ledger/View.h>
|
||||||
|
|
||||||
#include <xrpl/protocol/Feature.h>
|
#include <xrpl/protocol/Feature.h>
|
||||||
|
#include <xrpl/protocol/st.h>
|
||||||
|
|
||||||
namespace ripple {
|
namespace ripple {
|
||||||
|
|
||||||
bool
|
bool
|
||||||
LendingProtocolEnabled(PreflightContext const& ctx)
|
lendingProtocolEnabled(PreflightContext const& ctx)
|
||||||
{
|
{
|
||||||
return ctx.rules.enabled(featureLendingProtocol) &&
|
return ctx.rules.enabled(featureLendingProtocol) &&
|
||||||
VaultCreate::isEnabled(ctx);
|
VaultCreate::isEnabled(ctx);
|
||||||
@@ -44,24 +46,6 @@ LoanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval)
|
|||||||
(365 * 24 * 60 * 60);
|
(365 * 24 * 60 * 60);
|
||||||
}
|
}
|
||||||
|
|
||||||
Number
|
|
||||||
LoanPeriodicPayment(
|
|
||||||
Number principalOutstanding,
|
|
||||||
TenthBips32 interestRate,
|
|
||||||
std::uint32_t paymentInterval,
|
|
||||||
std::uint32_t paymentsRemaining)
|
|
||||||
{
|
|
||||||
if (principalOutstanding == 0 || paymentsRemaining == 0)
|
|
||||||
return 0;
|
|
||||||
Number const periodicRate = LoanPeriodicRate(interestRate, paymentInterval);
|
|
||||||
|
|
||||||
// TODO: Need a better name
|
|
||||||
Number const timeFactor = power(1 + periodicRate, paymentsRemaining);
|
|
||||||
|
|
||||||
return principalOutstanding * (periodicRate * timeFactor) /
|
|
||||||
(timeFactor - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
Number
|
Number
|
||||||
LoanTotalValueOutstanding(
|
LoanTotalValueOutstanding(
|
||||||
Number principalOutstanding,
|
Number principalOutstanding,
|
||||||
@@ -92,6 +76,216 @@ LoanTotalInterestOutstanding(
|
|||||||
principalOutstanding;
|
principalOutstanding;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Number
|
||||||
|
LoanPeriodicPayment(
|
||||||
|
Number principalOutstanding,
|
||||||
|
Number periodicRate,
|
||||||
|
std::uint32_t paymentsRemaining)
|
||||||
|
{
|
||||||
|
// TODO: Need a better name
|
||||||
|
Number const timeFactor = power(1 + periodicRate, paymentsRemaining);
|
||||||
|
|
||||||
|
return principalOutstanding * (periodicRate * timeFactor) /
|
||||||
|
(timeFactor - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Number
|
||||||
|
LoanPeriodicPayment(
|
||||||
|
Number principalOutstanding,
|
||||||
|
TenthBips32 interestRate,
|
||||||
|
std::uint32_t paymentInterval,
|
||||||
|
std::uint32_t paymentsRemaining)
|
||||||
|
{
|
||||||
|
if (principalOutstanding == 0 || paymentsRemaining == 0)
|
||||||
|
return 0;
|
||||||
|
Number const periodicRate = LoanPeriodicRate(interestRate, paymentInterval);
|
||||||
|
|
||||||
|
return LoanPeriodicPayment(
|
||||||
|
principalOutstanding, periodicRate, paymentsRemaining);
|
||||||
|
}
|
||||||
|
|
||||||
|
Number
|
||||||
|
LoanLatePaymentInterest(
|
||||||
|
Number principalOutstanding,
|
||||||
|
TenthBips32 lateInterestRate,
|
||||||
|
NetClock::time_point parentCloseTime,
|
||||||
|
std::uint32_t startDate,
|
||||||
|
std::uint32_t prevPaymentDate)
|
||||||
|
{
|
||||||
|
auto const lastPaymentDate = std::max(prevPaymentDate, startDate);
|
||||||
|
|
||||||
|
auto const secondsSinceLastPayment =
|
||||||
|
parentCloseTime.time_since_epoch().count() - lastPaymentDate;
|
||||||
|
|
||||||
|
auto const rate =
|
||||||
|
LoanPeriodicRate(lateInterestRate, secondsSinceLastPayment);
|
||||||
|
|
||||||
|
return principalOutstanding * rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
Number
|
||||||
|
LoanAccruedInterest(
|
||||||
|
Number principalOutstanding,
|
||||||
|
TenthBips32 periodicRate,
|
||||||
|
NetClock::time_point parentCloseTime,
|
||||||
|
std::uint32_t startDate,
|
||||||
|
std::uint32_t prevPaymentDate,
|
||||||
|
std::uint32_t paymentInterval)
|
||||||
|
{
|
||||||
|
auto const lastPaymentDate = std::max(prevPaymentDate, startDate);
|
||||||
|
|
||||||
|
auto const secondsSinceLastPayment =
|
||||||
|
parentCloseTime.time_since_epoch().count() - lastPaymentDate;
|
||||||
|
|
||||||
|
return tenthBipsOfValue(
|
||||||
|
principalOutstanding * secondsSinceLastPayment, periodicRate) /
|
||||||
|
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 detail
|
||||||
|
|
||||||
} // namespace ripple
|
} // namespace ripple
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ namespace ripple {
|
|||||||
bool
|
bool
|
||||||
LoanBrokerCoverDeposit::isEnabled(PreflightContext const& ctx)
|
LoanBrokerCoverDeposit::isEnabled(PreflightContext const& ctx)
|
||||||
{
|
{
|
||||||
return LendingProtocolEnabled(ctx);
|
return lendingProtocolEnabled(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
NotTEC
|
NotTEC
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ namespace ripple {
|
|||||||
bool
|
bool
|
||||||
LoanBrokerCoverWithdraw::isEnabled(PreflightContext const& ctx)
|
LoanBrokerCoverWithdraw::isEnabled(PreflightContext const& ctx)
|
||||||
{
|
{
|
||||||
return LendingProtocolEnabled(ctx);
|
return lendingProtocolEnabled(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
NotTEC
|
NotTEC
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ namespace ripple {
|
|||||||
bool
|
bool
|
||||||
LoanBrokerDelete::isEnabled(PreflightContext const& ctx)
|
LoanBrokerDelete::isEnabled(PreflightContext const& ctx)
|
||||||
{
|
{
|
||||||
return LendingProtocolEnabled(ctx);
|
return lendingProtocolEnabled(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
NotTEC
|
NotTEC
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ namespace ripple {
|
|||||||
bool
|
bool
|
||||||
LoanBrokerSet::isEnabled(PreflightContext const& ctx)
|
LoanBrokerSet::isEnabled(PreflightContext const& ctx)
|
||||||
{
|
{
|
||||||
return LendingProtocolEnabled(ctx);
|
return lendingProtocolEnabled(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
NotTEC
|
NotTEC
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ namespace ripple {
|
|||||||
bool
|
bool
|
||||||
LoanDelete::isEnabled(PreflightContext const& ctx)
|
LoanDelete::isEnabled(PreflightContext const& ctx)
|
||||||
{
|
{
|
||||||
return LendingProtocolEnabled(ctx);
|
return lendingProtocolEnabled(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
NotTEC
|
NotTEC
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ namespace ripple {
|
|||||||
bool
|
bool
|
||||||
LoanDraw::isEnabled(PreflightContext const& ctx)
|
LoanDraw::isEnabled(PreflightContext const& ctx)
|
||||||
{
|
{
|
||||||
return LendingProtocolEnabled(ctx);
|
return lendingProtocolEnabled(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
NotTEC
|
NotTEC
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ namespace ripple {
|
|||||||
bool
|
bool
|
||||||
LoanManage::isEnabled(PreflightContext const& ctx)
|
LoanManage::isEnabled(PreflightContext const& ctx)
|
||||||
{
|
{
|
||||||
return LendingProtocolEnabled(ctx);
|
return lendingProtocolEnabled(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::uint32_t
|
std::uint32_t
|
||||||
@@ -377,7 +377,7 @@ LoanManage::doApply()
|
|||||||
TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)};
|
TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)};
|
||||||
auto const paymentInterval = loanSle->at(sfPaymentInterval);
|
auto const paymentInterval = loanSle->at(sfPaymentInterval);
|
||||||
auto const paymentsRemaining = loanSle->at(sfPaymentRemaining);
|
auto const paymentsRemaining = loanSle->at(sfPaymentRemaining);
|
||||||
auto const interestOutstanding = LoanInterestOutstandingToVault(
|
auto const interestOutstanding = LoanInterestOutstandingMinusFee(
|
||||||
vaultAsset,
|
vaultAsset,
|
||||||
principalOutstanding.value(),
|
principalOutstanding.value(),
|
||||||
interestRate,
|
interestRate,
|
||||||
|
|||||||
228
src/xrpld/app/tx/detail/LoanPay.cpp
Normal file
228
src/xrpld/app/tx/detail/LoanPay.cpp
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
//==============================================================================
|
||||||
|
|
||||||
|
#include <xrpld/app/tx/detail/LoanPay.h>
|
||||||
|
//
|
||||||
|
#include <xrpld/app/misc/LendingHelpers.h>
|
||||||
|
#include <xrpld/ledger/ApplyView.h>
|
||||||
|
#include <xrpld/ledger/View.h>
|
||||||
|
|
||||||
|
#include <xrpl/basics/Log.h>
|
||||||
|
#include <xrpl/basics/Number.h>
|
||||||
|
#include <xrpl/basics/chrono.h>
|
||||||
|
#include <xrpl/beast/utility/Journal.h>
|
||||||
|
#include <xrpl/beast/utility/instrumentation.h>
|
||||||
|
#include <xrpl/protocol/AccountID.h>
|
||||||
|
#include <xrpl/protocol/Feature.h>
|
||||||
|
#include <xrpl/protocol/Indexes.h>
|
||||||
|
#include <xrpl/protocol/Protocol.h>
|
||||||
|
#include <xrpl/protocol/PublicKey.h>
|
||||||
|
#include <xrpl/protocol/SField.h>
|
||||||
|
#include <xrpl/protocol/STAmount.h>
|
||||||
|
#include <xrpl/protocol/STNumber.h>
|
||||||
|
#include <xrpl/protocol/STObject.h>
|
||||||
|
#include <xrpl/protocol/STXChainBridge.h>
|
||||||
|
#include <xrpl/protocol/TER.h>
|
||||||
|
#include <xrpl/protocol/TxFlags.h>
|
||||||
|
#include <xrpl/protocol/XRPAmount.h>
|
||||||
|
|
||||||
|
namespace ripple {
|
||||||
|
|
||||||
|
bool
|
||||||
|
LoanPay::isEnabled(PreflightContext const& ctx)
|
||||||
|
{
|
||||||
|
return lendingProtocolEnabled(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
NotTEC
|
||||||
|
LoanPay::doPreflight(PreflightContext const& ctx)
|
||||||
|
{
|
||||||
|
if (ctx.tx[sfLoanID] == beast::zero)
|
||||||
|
return temINVALID;
|
||||||
|
|
||||||
|
if (ctx.tx[sfAmount] <= beast::zero)
|
||||||
|
return temBAD_AMOUNT;
|
||||||
|
|
||||||
|
return tesSUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
TER
|
||||||
|
LoanPay::preclaim(PreclaimContext const& ctx)
|
||||||
|
{
|
||||||
|
auto const& tx = ctx.tx;
|
||||||
|
|
||||||
|
auto const account = tx[sfAccount];
|
||||||
|
auto const loanID = tx[sfLoanID];
|
||||||
|
auto const amount = tx[sfAmount];
|
||||||
|
|
||||||
|
auto const loanSle = ctx.view.read(keylet::loan(loanID));
|
||||||
|
if (!loanSle)
|
||||||
|
{
|
||||||
|
JLOG(ctx.j.warn()) << "Loan does not exist.";
|
||||||
|
return tecNO_ENTRY;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto const principalOutstanding = loanSle->at(sfPrincipalOutstanding);
|
||||||
|
TenthBips32 const interestRate{loanSle->at(sfInterestRate)};
|
||||||
|
auto const paymentInterval = loanSle->at(sfPaymentInterval);
|
||||||
|
auto const paymentRemaining = loanSle->at(sfPaymentRemaining);
|
||||||
|
TenthBips32 const lateInterestRate{loanSle->at(sfLateInterestRate)};
|
||||||
|
auto const latePaymentFee = loanSle->at(sfLatePaymentFee);
|
||||||
|
auto const prevPaymentDate = loanSle->at(sfPreviousPaymentDate);
|
||||||
|
auto const startDate = loanSle->at(sfStartDate);
|
||||||
|
auto const nextDueDate = loanSle->at(sfNextPaymentDueDate);
|
||||||
|
|
||||||
|
if (loanSle->at(sfBorrower) != account)
|
||||||
|
{
|
||||||
|
JLOG(ctx.j.warn()) << "Loan does not belong to the account.";
|
||||||
|
return tecNO_PERMISSION;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasExpired(ctx.view, startDate))
|
||||||
|
{
|
||||||
|
JLOG(ctx.j.warn()) << "Loan has not started yet.";
|
||||||
|
return tecTOO_SOON;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (paymentRemaining == 0 || principalOutstanding == 0)
|
||||||
|
{
|
||||||
|
JLOG(ctx.j.warn()) << "Loan is already paid off.";
|
||||||
|
return tecKILLED;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto const loanBrokerID = loanSle->at(sfLoanBrokerID);
|
||||||
|
auto const loanBrokerSle = ctx.view.read(keylet::loanbroker(loanBrokerID));
|
||||||
|
if (!loanBrokerSle)
|
||||||
|
{
|
||||||
|
// This should be impossible
|
||||||
|
// LCOV_EXCL_START
|
||||||
|
JLOG(ctx.j.fatal()) << "LoanBroker does not exist.";
|
||||||
|
return tefBAD_LEDGER;
|
||||||
|
// LCOV_EXCL_STOP
|
||||||
|
}
|
||||||
|
auto const brokerPseudoAccount = loanBrokerSle->at(sfAccount);
|
||||||
|
auto const vaultID = loanBrokerSle->at(sfVaultID);
|
||||||
|
auto const vaultSle = ctx.view.read(keylet::vault(vaultID));
|
||||||
|
if (!vaultSle)
|
||||||
|
{
|
||||||
|
// This should be impossible
|
||||||
|
// LCOV_EXCL_START
|
||||||
|
JLOG(ctx.j.fatal()) << "Vault does not exist.";
|
||||||
|
return tefBAD_LEDGER;
|
||||||
|
// LCOV_EXCL_STOP
|
||||||
|
}
|
||||||
|
auto const asset = vaultSle->at(sfAsset);
|
||||||
|
|
||||||
|
if (amount.asset() != asset)
|
||||||
|
{
|
||||||
|
JLOG(ctx.j.warn()) << "Loan amount does not match the Vault asset.";
|
||||||
|
return tecWRONG_ASSET;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFrozen(ctx.view, brokerPseudoAccount, asset))
|
||||||
|
{
|
||||||
|
JLOG(ctx.j.warn()) << "Loan Broker pseudo-account is frozen.";
|
||||||
|
return asset.holds<Issue>() ? tecFROZEN : tecLOCKED;
|
||||||
|
}
|
||||||
|
if (asset.holds<Issue>())
|
||||||
|
{
|
||||||
|
auto const issue = asset.get<Issue>();
|
||||||
|
if (isDeepFrozen(ctx.view, account, issue.currency, issue.account))
|
||||||
|
{
|
||||||
|
JLOG(ctx.j.warn()) << "Borrower account is frozen.";
|
||||||
|
return tecFROZEN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
TER
|
||||||
|
LoanPay::doApply()
|
||||||
|
{
|
||||||
|
auto const& tx = ctx_.tx;
|
||||||
|
auto& view = ctx_.view();
|
||||||
|
|
||||||
|
auto const amount = tx[sfAmount];
|
||||||
|
|
||||||
|
auto const loanID = tx[sfLoanID];
|
||||||
|
auto const loanSle = view.peek(keylet::loan(loanID));
|
||||||
|
if (!loanSle)
|
||||||
|
return tefBAD_LEDGER; // LCOV_EXCL_LINE
|
||||||
|
|
||||||
|
auto const brokerID = loanSle->at(sfLoanBrokerID);
|
||||||
|
auto const brokerSle = view.peek(keylet::loanbroker(brokerID));
|
||||||
|
if (!brokerSle)
|
||||||
|
return tefBAD_LEDGER; // LCOV_EXCL_LINE
|
||||||
|
auto const brokerPseudoAccount = brokerSle->at(sfAccount);
|
||||||
|
|
||||||
|
if (auto const ter = accountSend(
|
||||||
|
view,
|
||||||
|
brokerPseudoAccount,
|
||||||
|
account_,
|
||||||
|
amount,
|
||||||
|
j_,
|
||||||
|
WaiveTransferFee::Yes))
|
||||||
|
return ter;
|
||||||
|
|
||||||
|
loanSle->at(sfAssetsAvailable) -= amount;
|
||||||
|
view.update(loanSle);
|
||||||
|
|
||||||
|
return tesSUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
} // namespace ripple
|
||||||
53
src/xrpld/app/tx/detail/LoanPay.h
Normal file
53
src/xrpld/app/tx/detail/LoanPay.h
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
/*
|
||||||
|
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_TX_LOANPAY_H_INCLUDED
|
||||||
|
#define RIPPLE_TX_LOANPAY_H_INCLUDED
|
||||||
|
|
||||||
|
#include <xrpld/app/tx/detail/Transactor.h>
|
||||||
|
|
||||||
|
namespace ripple {
|
||||||
|
|
||||||
|
class LoanPay : public Transactor
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
|
||||||
|
|
||||||
|
explicit LoanPay(ApplyContext& ctx) : Transactor(ctx)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
isEnabled(PreflightContext const& ctx);
|
||||||
|
|
||||||
|
static NotTEC
|
||||||
|
doPreflight(PreflightContext const& ctx);
|
||||||
|
|
||||||
|
static TER
|
||||||
|
preclaim(PreclaimContext const& ctx);
|
||||||
|
|
||||||
|
TER
|
||||||
|
doApply() override;
|
||||||
|
};
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
} // namespace ripple
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -49,7 +49,7 @@ namespace ripple {
|
|||||||
bool
|
bool
|
||||||
LoanSet::isEnabled(PreflightContext const& ctx)
|
LoanSet::isEnabled(PreflightContext const& ctx)
|
||||||
{
|
{
|
||||||
return LendingProtocolEnabled(ctx);
|
return lendingProtocolEnabled(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::uint32_t
|
std::uint32_t
|
||||||
@@ -359,7 +359,7 @@ LoanSet::doApply()
|
|||||||
TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)};
|
TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)};
|
||||||
// The portion of the loan interest that will go to the vault (total
|
// The portion of the loan interest that will go to the vault (total
|
||||||
// interest minus the management fee)
|
// interest minus the management fee)
|
||||||
auto const loanInterestToVault = LoanInterestOutstandingToVault(
|
auto const loanInterestToVault = LoanInterestOutstandingMinusFee(
|
||||||
vaultAsset,
|
vaultAsset,
|
||||||
principalRequested,
|
principalRequested,
|
||||||
interestRate,
|
interestRate,
|
||||||
|
|||||||
Reference in New Issue
Block a user