mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-18 18:15:50 +00:00
Start implementing LoanPay transaction
This commit is contained in:
@@ -776,7 +776,6 @@ TRANSACTION(ttLOAN_DRAW, 81, LoanDraw, noPriv, ({
|
||||
{sfAmount, soeREQUIRED, soeMPTSupported},
|
||||
}))
|
||||
|
||||
#if 0
|
||||
/** The Borrower uses this transaction to make a Payment on the Loan. */
|
||||
#if TRANSACTION_INCLUDE
|
||||
# include <xrpld/app/tx/detail/LoanPay.h>
|
||||
@@ -785,7 +784,6 @@ TRANSACTION(ttLOAN_PAY, 82, LoanPay, noPriv, ({
|
||||
{sfLoanID, soeREQUIRED},
|
||||
{sfAmount, soeREQUIRED, soeMPTSupported},
|
||||
}))
|
||||
#endif
|
||||
|
||||
/** 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{
|
||||
brokerSle->at(sfManagementFeeRate)};
|
||||
auto const loanInterest = LoanInterestOutstandingToVault(
|
||||
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 +
|
||||
LoanInterestOutstandingToVault(
|
||||
LoanInterestOutstandingMinusFee(
|
||||
broker.asset,
|
||||
principalOutstanding,
|
||||
interestRate,
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
#ifndef 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/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
@@ -31,21 +33,22 @@ struct PreflightContext;
|
||||
|
||||
// Lending protocol has dependencies, so capture them here.
|
||||
bool
|
||||
LendingProtocolEnabled(PreflightContext const& ctx);
|
||||
lendingProtocolEnabled(PreflightContext const& ctx);
|
||||
|
||||
namespace detail {
|
||||
// These functions should rarely be used directly. More often, the ultimate
|
||||
// result needs to be roundToAsset'd.
|
||||
|
||||
Number
|
||||
LoanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval);
|
||||
struct LoanPaymentParts
|
||||
{
|
||||
Number principalPaid;
|
||||
Number interestPaid;
|
||||
Number valueChange;
|
||||
Number feePaid;
|
||||
};
|
||||
|
||||
Number
|
||||
LoanPeriodicPayment(
|
||||
Number principalOutstanding,
|
||||
TenthBips32 interestRate,
|
||||
std::uint32_t paymentInterval,
|
||||
std::uint32_t paymentsRemaining);
|
||||
LoanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval);
|
||||
|
||||
Number
|
||||
LoanTotalValueOutstanding(
|
||||
@@ -61,27 +64,110 @@ LoanTotalInterestOutstanding(
|
||||
std::uint32_t paymentInterval,
|
||||
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
|
||||
|
||||
template <AssetType A>
|
||||
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,
|
||||
Number principalOutstanding,
|
||||
TenthBips32 interestRate,
|
||||
std::uint32_t paymentInterval,
|
||||
std::uint32_t paymentsRemaining,
|
||||
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(
|
||||
asset,
|
||||
tenthBipsOfValue(
|
||||
detail::LoanTotalInterestOutstanding(
|
||||
principalOutstanding,
|
||||
interestRate,
|
||||
paymentInterval,
|
||||
paymentsRemaining),
|
||||
tenthBipsPerUnity - managementFeeRate));
|
||||
detail::LoanPeriodicPayment(
|
||||
principalOutstanding,
|
||||
interestRate,
|
||||
paymentInterval,
|
||||
paymentsRemaining));
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -21,13 +21,15 @@
|
||||
//
|
||||
#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 {
|
||||
|
||||
bool
|
||||
LendingProtocolEnabled(PreflightContext const& ctx)
|
||||
lendingProtocolEnabled(PreflightContext const& ctx)
|
||||
{
|
||||
return ctx.rules.enabled(featureLendingProtocol) &&
|
||||
VaultCreate::isEnabled(ctx);
|
||||
@@ -44,24 +46,6 @@ LoanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval)
|
||||
(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
|
||||
LoanTotalValueOutstanding(
|
||||
Number principalOutstanding,
|
||||
@@ -92,6 +76,216 @@ LoanTotalInterestOutstanding(
|
||||
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 ripple
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace ripple {
|
||||
bool
|
||||
LoanBrokerCoverDeposit::isEnabled(PreflightContext const& ctx)
|
||||
{
|
||||
return LendingProtocolEnabled(ctx);
|
||||
return lendingProtocolEnabled(ctx);
|
||||
}
|
||||
|
||||
NotTEC
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace ripple {
|
||||
bool
|
||||
LoanBrokerCoverWithdraw::isEnabled(PreflightContext const& ctx)
|
||||
{
|
||||
return LendingProtocolEnabled(ctx);
|
||||
return lendingProtocolEnabled(ctx);
|
||||
}
|
||||
|
||||
NotTEC
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace ripple {
|
||||
bool
|
||||
LoanBrokerDelete::isEnabled(PreflightContext const& ctx)
|
||||
{
|
||||
return LendingProtocolEnabled(ctx);
|
||||
return lendingProtocolEnabled(ctx);
|
||||
}
|
||||
|
||||
NotTEC
|
||||
|
||||
@@ -48,7 +48,7 @@ namespace ripple {
|
||||
bool
|
||||
LoanBrokerSet::isEnabled(PreflightContext const& ctx)
|
||||
{
|
||||
return LendingProtocolEnabled(ctx);
|
||||
return lendingProtocolEnabled(ctx);
|
||||
}
|
||||
|
||||
NotTEC
|
||||
|
||||
@@ -47,7 +47,7 @@ namespace ripple {
|
||||
bool
|
||||
LoanDelete::isEnabled(PreflightContext const& ctx)
|
||||
{
|
||||
return LendingProtocolEnabled(ctx);
|
||||
return lendingProtocolEnabled(ctx);
|
||||
}
|
||||
|
||||
NotTEC
|
||||
|
||||
@@ -47,7 +47,7 @@ namespace ripple {
|
||||
bool
|
||||
LoanDraw::isEnabled(PreflightContext const& ctx)
|
||||
{
|
||||
return LendingProtocolEnabled(ctx);
|
||||
return lendingProtocolEnabled(ctx);
|
||||
}
|
||||
|
||||
NotTEC
|
||||
|
||||
@@ -48,7 +48,7 @@ namespace ripple {
|
||||
bool
|
||||
LoanManage::isEnabled(PreflightContext const& ctx)
|
||||
{
|
||||
return LendingProtocolEnabled(ctx);
|
||||
return lendingProtocolEnabled(ctx);
|
||||
}
|
||||
|
||||
std::uint32_t
|
||||
@@ -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 = LoanInterestOutstandingToVault(
|
||||
auto const interestOutstanding = LoanInterestOutstandingMinusFee(
|
||||
vaultAsset,
|
||||
principalOutstanding.value(),
|
||||
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
|
||||
LoanSet::isEnabled(PreflightContext const& ctx)
|
||||
{
|
||||
return LendingProtocolEnabled(ctx);
|
||||
return lendingProtocolEnabled(ctx);
|
||||
}
|
||||
|
||||
std::uint32_t
|
||||
@@ -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 = LoanInterestOutstandingToVault(
|
||||
auto const loanInterestToVault = LoanInterestOutstandingMinusFee(
|
||||
vaultAsset,
|
||||
principalRequested,
|
||||
interestRate,
|
||||
|
||||
Reference in New Issue
Block a user