mirror of
https://github.com/XRPLF/rippled.git
synced 2025-12-06 17:27:55 +00:00
Start implementing LoanManage transaction (untested)
- Also add a ValidLoan invariant
This commit is contained in:
@@ -759,6 +759,7 @@ TRANSACTION(ttLOAN_SET, 78, LoanSet, noPriv, ({
|
||||
TRANSACTION(ttLOAN_DELETE, 79, LoanDelete, noPriv, ({
|
||||
{sfLoanID, soeREQUIRED},
|
||||
}))
|
||||
#endif
|
||||
|
||||
/** This transaction is used to change the delinquency status of an existing Loan */
|
||||
#if TRANSACTION_INCLUDE
|
||||
@@ -768,6 +769,7 @@ TRANSACTION(ttLOAN_MANAGE, 80, LoanManage, noPriv, ({
|
||||
{sfLoanID, soeREQUIRED},
|
||||
}))
|
||||
|
||||
#if 0
|
||||
/** The Borrower uses this transaction to draws funds from the Loan. */
|
||||
#if TRANSACTION_INCLUDE
|
||||
# include <xrpld/app/tx/detail/LoanDraw.h>
|
||||
|
||||
@@ -1953,4 +1953,46 @@ ValidLoanBroker::finalize(
|
||||
return true;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
void
|
||||
ValidLoan::visitEntry(
|
||||
bool isDelete,
|
||||
std::shared_ptr<SLE const> const& before,
|
||||
std::shared_ptr<SLE const> const& after)
|
||||
{
|
||||
if (after && after->getType() == ltLOAN)
|
||||
{
|
||||
loans_.emplace_back(before, after);
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
ValidLoan::finalize(
|
||||
STTx const& tx,
|
||||
TER const,
|
||||
XRPAmount const,
|
||||
ReadView const& view,
|
||||
beast::Journal const& j)
|
||||
{
|
||||
bool const enforce = view.rules().enabled(featureLendingProtocol);
|
||||
|
||||
for (auto const& [before, after] : loans_)
|
||||
{
|
||||
// https://github.com/Tapanito/XRPL-Standards/blob/xls-66-lending-protocol/XLS-0066d-lending-protocol/README.md#3223-invariants
|
||||
// If `Loan.PaymentRemaining = 0` then `Loan.PrincipalOutstanding = 0`
|
||||
if (after->at(sfPaymentRemaining) == 0 &&
|
||||
after->at(sfPrincipalOutstanding) != 0)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
enforce,
|
||||
"ripple::ValidLoan::finalize : Enforcing "
|
||||
"invariant: zero payments remaining");
|
||||
if (enforce)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
@@ -717,6 +717,34 @@ public:
|
||||
beast::Journal const&);
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Invariants: Loans are internally consistent
|
||||
*
|
||||
* 1. If `Loan.PaymentRemaining = 0` then `Loan.PrincipalOutstanding = 0`
|
||||
*
|
||||
*/
|
||||
class ValidLoan
|
||||
{
|
||||
// Pair is <before, after>. After is used for most of the checks, except
|
||||
// those that check changed values.
|
||||
std::vector<std::pair<SLE::const_pointer, SLE::const_pointer>> loans_;
|
||||
|
||||
public:
|
||||
void
|
||||
visitEntry(
|
||||
bool,
|
||||
std::shared_ptr<SLE const> const&,
|
||||
std::shared_ptr<SLE const> const&);
|
||||
|
||||
bool
|
||||
finalize(
|
||||
STTx const&,
|
||||
TER const,
|
||||
XRPAmount const,
|
||||
ReadView const&,
|
||||
beast::Journal const&);
|
||||
};
|
||||
|
||||
// additional invariant checks can be declared above and then added to this
|
||||
// tuple
|
||||
using InvariantChecks = std::tuple<
|
||||
@@ -739,7 +767,8 @@ using InvariantChecks = std::tuple<
|
||||
ValidPermissionedDomain,
|
||||
NoModifiedUnmodifiableFields,
|
||||
ValidPseudoAccounts,
|
||||
ValidLoanBroker>;
|
||||
ValidLoanBroker,
|
||||
ValidLoan>;
|
||||
|
||||
/**
|
||||
* @brief get a tuple of all invariant checks
|
||||
|
||||
313
src/xrpld/app/tx/detail/LoanManage.cpp
Normal file
313
src/xrpld/app/tx/detail/LoanManage.cpp
Normal file
@@ -0,0 +1,313 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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/LoanManage.h>
|
||||
//
|
||||
#include <xrpld/app/tx/detail/LoanBrokerSet.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
|
||||
LoanManage::isEnabled(PreflightContext const& ctx)
|
||||
{
|
||||
return lendingProtocolEnabled(ctx);
|
||||
}
|
||||
|
||||
std::uint32_t
|
||||
LoanManage::getFlagsMask(PreflightContext const& ctx)
|
||||
{
|
||||
return tfLoanManageMask;
|
||||
}
|
||||
|
||||
NotTEC
|
||||
LoanManage::doPreflight(PreflightContext const& ctx)
|
||||
{
|
||||
// Is combining flags legal?
|
||||
int numFlags = 0;
|
||||
for (auto const flag : {
|
||||
tfLoanDefault,
|
||||
tfLoanImpair,
|
||||
tfLoanUnimpair,
|
||||
})
|
||||
{
|
||||
if (ctx.tx.isFlag(flag))
|
||||
++numFlags;
|
||||
}
|
||||
if (numFlags > 1)
|
||||
{
|
||||
JLOG(ctx.j.warn())
|
||||
<< "LoanManage: Only one of tfLoanDefault, tfLoanImpair, or "
|
||||
"tfLoanUnimpair can be set.";
|
||||
return temINVALID_FLAG;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
LoanManage::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
auto const& tx = ctx.tx;
|
||||
|
||||
auto const account = tx[sfAccount];
|
||||
auto const loanID = tx[sfLoanID];
|
||||
|
||||
auto const loanSle = ctx.view.read(keylet::loan(loanID));
|
||||
if (!loanSle)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Loan does not exist.";
|
||||
return tecNO_ENTRY;
|
||||
}
|
||||
// Impairment only allows certain transitions.
|
||||
// 1. Once it's in default, it can't be changed.
|
||||
// 2. It can get worse: unimpaired -> impaired -> default
|
||||
// 3. It can get better: impaired -> unimpaired
|
||||
// 4. If it's in a state, it can't be put in that state again.
|
||||
// TODO: implement this.
|
||||
if (loanSle->isFlag(lsfLoanDefault))
|
||||
{
|
||||
JLOG(ctx.j.warn())
|
||||
<< "Loan is in default. A defaulted loan can not be modified.";
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
if (loanSle->isFlag(lsfLoanImpaired) && tx.isFlag(tfLoanImpair))
|
||||
{
|
||||
JLOG(ctx.j.warn())
|
||||
<< "Loan is impaired. A loan can not be impaired twice.";
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
if (loanSle->at(sfPaymentRemaining) == 0)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Loan is fully paid. A loan can not be modified "
|
||||
"after it is fully paid.";
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
if (tx.isFlag(tfLoanDefault) &&
|
||||
!hasExpired(
|
||||
ctx.view,
|
||||
loanSle->at(sfNextPaymentDueDate) + loanSle->at(sfGracePeriod)))
|
||||
{
|
||||
JLOG(ctx.j.warn())
|
||||
<< "Loan is not in default. A loan can not be defaulted before the "
|
||||
"next payment due date.";
|
||||
return tecTOO_SOON;
|
||||
}
|
||||
|
||||
auto const loanBrokerID = loanSle->at(sfLoanBrokerID);
|
||||
auto const loanBrokerSle = ctx.view.read(keylet::loanbroker(loanBrokerID));
|
||||
if (!loanBrokerSle)
|
||||
{
|
||||
// should be impossible
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
}
|
||||
if (loanBrokerSle->at(sfOwner) != account)
|
||||
{
|
||||
JLOG(ctx.j.warn())
|
||||
<< "LoanBroker for Loan does not belong to the account.";
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
defaultLoan(
|
||||
ApplyView& view,
|
||||
SLE::ref loanSle,
|
||||
SLE::ref brokerSle,
|
||||
SLE::ref vaultSle,
|
||||
Number const& principalOutstanding,
|
||||
Number const& interestOutstanding,
|
||||
beast::Journal j)
|
||||
{
|
||||
return temDISABLED;
|
||||
}
|
||||
|
||||
TER
|
||||
impairLoan(
|
||||
ApplyView& view,
|
||||
SLE::ref loanSle,
|
||||
SLE::ref brokerSle,
|
||||
SLE::ref vaultSle,
|
||||
Number const& principalOutstanding,
|
||||
Number const& interestOutstanding,
|
||||
beast::Journal j)
|
||||
{
|
||||
// Update the Vault object(set "paper loss")
|
||||
vaultSle->at(sfLossUnrealized) +=
|
||||
principalOutstanding + interestOutstanding;
|
||||
view.update(vaultSle);
|
||||
|
||||
// Update the Loan object
|
||||
loanSle->at(sfFlags) = lsfLoanImpaired;
|
||||
auto nextDueProxy = loanSle->at(sfNextPaymentDueDate);
|
||||
if (!hasExpired(view, nextDueProxy))
|
||||
{
|
||||
// loan payment is not yet late -
|
||||
// move the next payment due date to now
|
||||
nextDueProxy = view.parentCloseTime().time_since_epoch().count();
|
||||
}
|
||||
view.update(loanSle);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
unimpairLoan(
|
||||
ApplyView& view,
|
||||
SLE::ref loanSle,
|
||||
SLE::ref brokerSle,
|
||||
SLE::ref vaultSle,
|
||||
Number const& principalOutstanding,
|
||||
Number const& interestOutstanding,
|
||||
beast::Journal j)
|
||||
{
|
||||
// Update the Vault object(clear "paper loss")
|
||||
vaultSle->at(sfLossUnrealized) -=
|
||||
principalOutstanding + interestOutstanding;
|
||||
view.update(vaultSle);
|
||||
|
||||
// Update the Loan object
|
||||
loanSle->at(sfFlags) = 0;
|
||||
auto const paymentInterval = loanSle->at(sfPaymentInterval);
|
||||
auto const normalPaymentDueDate =
|
||||
loanSle->at(sfPreviousPaymentDate) + paymentInterval;
|
||||
if (!hasExpired(view, normalPaymentDueDate))
|
||||
{
|
||||
// loan was unimpaired within the payment interval
|
||||
loanSle->at(sfNextPaymentDueDate) = normalPaymentDueDate;
|
||||
}
|
||||
else
|
||||
{
|
||||
// loan was unimpaired after the original payment due date
|
||||
loanSle->at(sfNextPaymentDueDate) =
|
||||
view.parentCloseTime().time_since_epoch().count() +
|
||||
loanSle->at(sfPaymentInterval);
|
||||
}
|
||||
view.update(loanSle);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
LoanManage::doApply()
|
||||
{
|
||||
auto const& tx = ctx_.tx;
|
||||
auto& view = ctx_.view();
|
||||
|
||||
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 vaultSle = view.peek(keylet ::vault(brokerSle->at(sfVaultID)));
|
||||
if (!vaultSle)
|
||||
return tefBAD_LEDGER; // LCOV_EXCL_LINE
|
||||
auto const vaultAsset = vaultSle->at(sfAsset);
|
||||
|
||||
TenthBips32 const interestRate{loanSle->at(sfInterestRate)};
|
||||
auto const principalOutstanding = loanSle->at(sfPrincipalOutstanding);
|
||||
auto const interestOutstanding =
|
||||
tenthBipsOfValue(principalOutstanding, interestRate);
|
||||
|
||||
// Valid flag combinations are checked in preflight. Not flags is valid -
|
||||
// just a noop.
|
||||
if (tx.isFlag(tfLoanDefault))
|
||||
{
|
||||
if (auto const ter = defaultLoan(
|
||||
view,
|
||||
loanSle,
|
||||
brokerSle,
|
||||
vaultSle,
|
||||
principalOutstanding,
|
||||
interestOutstanding,
|
||||
j_))
|
||||
return ter;
|
||||
}
|
||||
if (tx.isFlag(tfLoanImpair))
|
||||
{
|
||||
if (auto const ter = impairLoan(
|
||||
view,
|
||||
loanSle,
|
||||
brokerSle,
|
||||
vaultSle,
|
||||
principalOutstanding,
|
||||
interestOutstanding,
|
||||
j_))
|
||||
return ter;
|
||||
}
|
||||
if (tx.isFlag(tfLoanUnimpair))
|
||||
{
|
||||
if (auto const ter = unimpairLoan(
|
||||
view,
|
||||
loanSle,
|
||||
brokerSle,
|
||||
vaultSle,
|
||||
principalOutstanding,
|
||||
interestOutstanding,
|
||||
j_))
|
||||
return ter;
|
||||
}
|
||||
/*
|
||||
auto const brokerOwner = brokerSle->at(sfOwner);
|
||||
auto const brokerOwnerSle = view.peek(keylet::account(brokerOwner));
|
||||
|
||||
auto const vaultPseudo = vaultSle->at(sfAccount);
|
||||
|
||||
auto const brokerPseudo = brokerSle->at(sfAccount);
|
||||
auto const brokerPseudoSle = view.peek(keylet::account(brokerPseudo));
|
||||
auto const principalRequested = tx[sfPrincipalRequested];
|
||||
TenthBips32 const interestRate{tx[~sfInterestRate].value_or(0)};
|
||||
auto const originationFee = tx[~sfLoanOriginationFee];
|
||||
auto const loanAssetsAvailable =
|
||||
principalRequested - originationFee.value_or(Number{});
|
||||
*/
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
} // namespace ripple
|
||||
56
src/xrpld/app/tx/detail/LoanManage.h
Normal file
56
src/xrpld/app/tx/detail/LoanManage.h
Normal file
@@ -0,0 +1,56 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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_LOANMANAGE_H_INCLUDED
|
||||
#define RIPPLE_TX_LOANMANAGE_H_INCLUDED
|
||||
|
||||
#include <xrpld/app/tx/detail/Transactor.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
class LoanManage : public Transactor
|
||||
{
|
||||
public:
|
||||
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
|
||||
|
||||
explicit LoanManage(ApplyContext& ctx) : Transactor(ctx)
|
||||
{
|
||||
}
|
||||
|
||||
static bool
|
||||
isEnabled(PreflightContext const& ctx);
|
||||
|
||||
static std::uint32_t
|
||||
getFlagsMask(PreflightContext const& ctx);
|
||||
|
||||
static NotTEC
|
||||
doPreflight(PreflightContext const& ctx);
|
||||
|
||||
static TER
|
||||
preclaim(PreclaimContext const& ctx);
|
||||
|
||||
TER
|
||||
doApply() override;
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user