Start implementing LoanManage transaction (untested)

- Also add a ValidLoan invariant
This commit is contained in:
Ed Hennis
2025-05-02 17:52:40 -04:00
parent 71de1a0d93
commit ef2a0edc67
5 changed files with 443 additions and 1 deletions

View File

@@ -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>

View File

@@ -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

View File

@@ -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

View 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

View 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