mirror of
https://github.com/XRPLF/rippled.git
synced 2026-07-23 23:20:33 +00:00
Implementation
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h> // IWYU pragma: keep
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
@@ -18,11 +19,13 @@
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/Units.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
@@ -551,4 +554,117 @@ loanMakePayment(
|
||||
LoanPaymentType const paymentType,
|
||||
beast::Journal j);
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
//
|
||||
// Loan application helpers (shared by LoanSet and LoanAccept)
|
||||
//
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** Verify the loan asset can be held and that none of the accounts involved in
|
||||
* disbursing the loan are frozen in a way that would block the fund flows.
|
||||
*
|
||||
* Checks, in order: that a holding for the asset can be created, that the vault
|
||||
* pseudo-account (the sender) is not frozen, that the broker pseudo-account (a
|
||||
* fallback fee recipient) is not deep frozen, that the borrower (a future payer
|
||||
* and fund recipient) is not frozen, and that the broker owner (a fee
|
||||
* recipient) is not deep frozen.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
checkLoanFreeze(
|
||||
ReadView const& view,
|
||||
Asset const& asset,
|
||||
AccountID const& vaultPseudo,
|
||||
AccountID const& brokerPseudo,
|
||||
AccountID const& borrower,
|
||||
AccountID const& brokerOwner,
|
||||
beast::Journal j);
|
||||
|
||||
/** Validate a loan against the vault and broker limits.
|
||||
*
|
||||
* Checks the vault maximum, precision loss, loan guards, the computed loan
|
||||
* properties, the broker debt maximum, and the broker's first-loss cover.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
checkLoanLimits(
|
||||
ApplyView& view,
|
||||
STTx const& tx,
|
||||
SLE::ref brokerSle,
|
||||
SLE::ref vaultSle,
|
||||
Asset const& vaultAsset,
|
||||
Number const& principalRequested,
|
||||
TenthBips32 interestRate,
|
||||
std::uint32_t paymentTotal,
|
||||
LoanProperties const& properties,
|
||||
LoanState const& state,
|
||||
std::vector<OptionaledField<STNumber>> const& valueFields,
|
||||
beast::Journal j);
|
||||
|
||||
/** Increment the borrower's owner count for the new loan object and verify the
|
||||
* borrower still meets its reserve requirement.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
reserveLoanOwner(
|
||||
ApplyView& view,
|
||||
AccountID const& borrower,
|
||||
SLE::ref borrowerSle,
|
||||
AccountID const& signingAccount,
|
||||
XRPAmount preFeeBalance,
|
||||
beast::Journal j);
|
||||
|
||||
/** Transfer the loan principal to the borrower and the origination fee, if any,
|
||||
* to the LoanBroker owner. Creates holdings as necessary.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
disburseLoan(
|
||||
ApplyView& view,
|
||||
AccountID const& borrower,
|
||||
SLE::ref borrowerSle,
|
||||
AccountID const& brokerOwner,
|
||||
SLE::ref brokerOwnerSle,
|
||||
AccountID const& vaultPseudo,
|
||||
Asset const& vaultAsset,
|
||||
Number const& loanAssetsToBorrower,
|
||||
Number const& originationFee,
|
||||
AccountID const& signingAccount,
|
||||
AccountID const& counterparty,
|
||||
beast::Journal j);
|
||||
|
||||
/** Adjust the broker's total outstanding debt. */
|
||||
void
|
||||
adjustBrokerDebtTotal(
|
||||
SLE::ref brokerSle,
|
||||
Number const& newDebtDelta,
|
||||
Asset const& vaultAsset,
|
||||
int vaultScale);
|
||||
|
||||
/** Update the LoanBroker ledger entry for a newly created loan.
|
||||
*
|
||||
* Adjusts the broker's outstanding debt total and owner count, advances the
|
||||
* broker's loan sequence, and persists the entry.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
updateLoanBroker(
|
||||
ApplyView& view,
|
||||
SLE::ref brokerSle,
|
||||
Number const& newDebtDelta,
|
||||
Asset const& vaultAsset,
|
||||
int vaultScale,
|
||||
beast::Journal j);
|
||||
|
||||
/** Link the loan into the broker pseudo-account's directory.
|
||||
*
|
||||
* Done for both flows when the loan is created by LoanSet.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
linkLoanBroker(ApplyView& view, AccountID const& brokerPseudo, SLE::pointer& loan);
|
||||
|
||||
/** Make the borrower the owner of the loan by linking it into the borrower's
|
||||
* directory.
|
||||
*
|
||||
* Done by LoanSet for the immediate flow, and deferred to LoanAccept for the
|
||||
* two-step (pending) flow.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
linkLoanBorrower(ApplyView& view, AccountID const& borrower, SLE::pointer& loan);
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -1070,14 +1070,14 @@ TRANSACTION(ttLOAN_MANAGE, 82, LoanManage,
|
||||
{sfLoanID, SoeRequired},
|
||||
}))
|
||||
|
||||
/** This transaction accepts a Loan. Currently a no-op. */
|
||||
/** The Borrower uses this transaction to accept a pending Loan. */
|
||||
#if TRANSACTION_INCLUDE
|
||||
# include <xrpl/tx/transactors/lending/LoanAccept.h>
|
||||
#endif
|
||||
TRANSACTION(ttLOAN_ACCEPT, 83, LoanAccept,
|
||||
Delegation::NotDelegable,
|
||||
featureLendingProtocolV1_1,
|
||||
NoPriv, ({
|
||||
MayAuthorizeMpt | MustModifyVault, ({
|
||||
{sfLoanID, SoeRequired},
|
||||
}))
|
||||
|
||||
|
||||
@@ -19,9 +19,15 @@ public:
|
||||
{
|
||||
}
|
||||
|
||||
static bool
|
||||
checkExtraFeatures(PreflightContext const& ctx);
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
static TER
|
||||
preclaim(PreclaimContext const& ctx);
|
||||
|
||||
TER
|
||||
doApply() override;
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/helpers/LendingHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STNumber.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
@@ -19,9 +22,14 @@ namespace xrpl {
|
||||
class LoanSet : public Transactor
|
||||
{
|
||||
private:
|
||||
/* Returns true if the transaction is using the two-step (Borrower) flow. */
|
||||
static std::uint32_t
|
||||
getStartDate(ReadView const& view, STTx const& tx);
|
||||
/* Returns true if the transaction is using the two-step flow. */
|
||||
static bool
|
||||
isTwoStepFlow(STTx const& tx, Rules const& rules);
|
||||
/* Returns true if the transaction is using the one-step flow. */
|
||||
static bool
|
||||
isOneStepFlow(STTx const& tx);
|
||||
|
||||
public:
|
||||
static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal;
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
@@ -20,13 +23,16 @@
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/Units.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
@@ -2151,4 +2157,288 @@ loanMakePayment(
|
||||
"xrpl::loanMakePayment : fee paid is valid");
|
||||
return totalParts;
|
||||
}
|
||||
|
||||
TER
|
||||
checkLoanFreeze(
|
||||
ReadView const& view,
|
||||
Asset const& asset,
|
||||
AccountID const& vaultPseudo,
|
||||
AccountID const& brokerPseudo,
|
||||
AccountID const& borrower,
|
||||
AccountID const& brokerOwner,
|
||||
beast::Journal j)
|
||||
{
|
||||
if (auto const ter = canAddHolding(view, asset))
|
||||
return ter;
|
||||
|
||||
// vaultPseudo is going to send funds, so it can't be frozen.
|
||||
if (auto const ret = checkFrozen(view, vaultPseudo, asset))
|
||||
{
|
||||
JLOG(j.warn()) << "Vault pseudo-account is frozen.";
|
||||
return ret;
|
||||
}
|
||||
|
||||
// brokerPseudo is the fallback account to receive LoanPay fees, even if the
|
||||
// broker owner is unable to accept them. Don't create the loan if it is
|
||||
// deep frozen.
|
||||
if (auto const ret = checkDeepFrozen(view, brokerPseudo, asset))
|
||||
{
|
||||
JLOG(j.warn()) << "Broker pseudo-account is frozen.";
|
||||
return ret;
|
||||
}
|
||||
|
||||
// borrower is eventually going to have to pay back the loan, so it can't be
|
||||
// frozen now. It is also going to receive funds, so it can't be deep
|
||||
// frozen, but being frozen is a prerequisite for being deep frozen, so
|
||||
// checking the one is sufficient.
|
||||
if (auto const ret = checkFrozen(view, borrower, asset))
|
||||
{
|
||||
JLOG(j.warn()) << "Borrower account is frozen.";
|
||||
return ret;
|
||||
}
|
||||
// brokerOwner is going to receive funds if there's an origination fee, so
|
||||
// it can't be deep frozen
|
||||
if (auto const ret = checkDeepFrozen(view, brokerOwner, asset))
|
||||
{
|
||||
JLOG(j.warn()) << "Broker owner account is frozen.";
|
||||
return ret;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
checkLoanLimits(
|
||||
ApplyView& view,
|
||||
STTx const& tx,
|
||||
SLE::ref brokerSle,
|
||||
SLE::ref vaultSle,
|
||||
Asset const& vaultAsset,
|
||||
Number const& principalRequested,
|
||||
TenthBips32 interestRate,
|
||||
std::uint32_t paymentTotal,
|
||||
LoanProperties const& properties,
|
||||
LoanState const& state,
|
||||
std::vector<OptionaledField<STNumber>> const& valueFields,
|
||||
beast::Journal j)
|
||||
{
|
||||
auto const vaultTotalProxy = vaultSle->at(sfAssetsTotal);
|
||||
auto const vaultMaximum = *vaultSle->at(sfAssetsMaximum);
|
||||
XRPL_ASSERT_PARTS(
|
||||
vaultMaximum == 0 || vaultMaximum > *vaultTotalProxy,
|
||||
"xrpl::checkLoanLimits",
|
||||
"Vault is below maximum limit");
|
||||
if (vaultMaximum != 0 && state.interestDue > vaultMaximum - vaultTotalProxy)
|
||||
{
|
||||
JLOG(j.warn()) << "Loan would exceed the maximum assets of the vault";
|
||||
return tecLIMIT_EXCEEDED;
|
||||
}
|
||||
// Check that relevant values won't lose precision. This is mostly only
|
||||
// relevant for IOU assets.
|
||||
for (auto const& field : valueFields)
|
||||
{
|
||||
if (auto const value = tx[field];
|
||||
value && !isRounded(vaultAsset, *value, properties.loanScale))
|
||||
{
|
||||
JLOG(j.warn()) << field.f->getName() << " (" << *value
|
||||
<< ") has too much precision. Total loan value is "
|
||||
<< properties.loanState.valueOutstanding << " with a scale of "
|
||||
<< properties.loanScale;
|
||||
return tecPRECISION_LOSS;
|
||||
}
|
||||
}
|
||||
|
||||
if (auto const ret = checkLoanGuards(
|
||||
vaultAsset,
|
||||
principalRequested,
|
||||
interestRate != beast::kZero,
|
||||
paymentTotal,
|
||||
properties,
|
||||
j))
|
||||
return ret;
|
||||
|
||||
// Check that the other computed values are valid
|
||||
if (properties.loanState.managementFeeDue < 0 || properties.loanState.valueOutstanding <= 0 ||
|
||||
properties.periodicPayment <= 0)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.warn()) << "Computed loan properties are invalid. Does not compute."
|
||||
<< " Management fee: " << properties.loanState.managementFeeDue
|
||||
<< ". Total Value: " << properties.loanState.valueOutstanding
|
||||
<< ". PeriodicPayment: " << properties.periodicPayment;
|
||||
return tecINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
auto const newDebtTotal = brokerSle->at(sfDebtTotal) + principalRequested + state.interestDue;
|
||||
if (auto const debtMaximum = brokerSle->at(sfDebtMaximum);
|
||||
debtMaximum != 0 && debtMaximum < newDebtTotal)
|
||||
{
|
||||
JLOG(j.warn()) << "Loan would exceed the maximum debt limit of the LoanBroker.";
|
||||
return tecLIMIT_EXCEEDED;
|
||||
}
|
||||
TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)};
|
||||
{
|
||||
auto const minCover = [&]() {
|
||||
if (view.rules().enabled(fixCleanup3_2_0))
|
||||
{
|
||||
return minimumBrokerCover(newDebtTotal, coverRateMinimum, vaultSle);
|
||||
}
|
||||
|
||||
// Round the minimum required cover up to be conservative. This ensures
|
||||
// CoverAvailable never drops below the theoretical minimum, protecting
|
||||
// the broker's solvency.
|
||||
NumberRoundModeGuard const mg(Number::RoundingMode::Upward);
|
||||
return tenthBipsOfValue(newDebtTotal, coverRateMinimum);
|
||||
}();
|
||||
if (brokerSle->at(sfCoverAvailable) < minCover)
|
||||
{
|
||||
JLOG(j.warn()) << "Insufficient first-loss capital to cover the loan.";
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
}
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
reserveLoanOwner(
|
||||
ApplyView& view,
|
||||
AccountID const& borrower,
|
||||
SLE::ref borrowerSle,
|
||||
AccountID const& signingAccount,
|
||||
XRPAmount preFeeBalance,
|
||||
beast::Journal j)
|
||||
{
|
||||
adjustOwnerCount(view, borrowerSle, 1, j);
|
||||
auto const ownerCount = borrowerSle->at(sfOwnerCount);
|
||||
auto const balance =
|
||||
signingAccount == borrower ? preFeeBalance : borrowerSle->at(sfBalance).value().xrp();
|
||||
if (balance < view.fees().accountReserve(ownerCount))
|
||||
return tecINSUFFICIENT_RESERVE;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
disburseLoan(
|
||||
ApplyView& view,
|
||||
AccountID const& borrower,
|
||||
SLE::ref borrowerSle,
|
||||
AccountID const& brokerOwner,
|
||||
SLE::ref brokerOwnerSle,
|
||||
AccountID const& vaultPseudo,
|
||||
Asset const& vaultAsset,
|
||||
Number const& loanAssetsToBorrower,
|
||||
Number const& originationFee,
|
||||
AccountID const& signingAccount,
|
||||
AccountID const& counterparty,
|
||||
beast::Journal j)
|
||||
{
|
||||
// Account for the origination fee using two payments
|
||||
//
|
||||
// 1. Transfer loanAssetsAvailable (principalRequested - originationFee)
|
||||
// from vault pseudo-account to the borrower.
|
||||
// Create a holding for the borrower if one does not already exist.
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
borrower == signingAccount || borrower == counterparty,
|
||||
"xrpl::disburseLoan",
|
||||
"borrower signed transaction");
|
||||
if (auto const ter = addEmptyHolding(
|
||||
view, borrower, borrowerSle->at(sfBalance).value().xrp(), vaultAsset, j);
|
||||
ter && ter != tecDUPLICATE)
|
||||
{
|
||||
// ignore tecDUPLICATE. That means the holding already exists, and
|
||||
// is fine here
|
||||
return ter;
|
||||
}
|
||||
|
||||
if (auto const ter = requireAuth(view, vaultAsset, borrower, AuthType::StrongAuth))
|
||||
return ter;
|
||||
|
||||
// 2. Transfer originationFee, if any, from vault pseudo-account to
|
||||
// LoanBroker owner.
|
||||
if (originationFee != beast::kZero)
|
||||
{
|
||||
// Create the holding if it doesn't already exist (necessary for MPTs).
|
||||
// The owner may have deleted their MPT / line at some point.
|
||||
XRPL_ASSERT_PARTS(
|
||||
brokerOwner == signingAccount || brokerOwner == counterparty,
|
||||
"xrpl::disburseLoan",
|
||||
"broker owner signed transaction");
|
||||
|
||||
if (auto const ter = addEmptyHolding(
|
||||
view, brokerOwner, brokerOwnerSle->at(sfBalance).value().xrp(), vaultAsset, j);
|
||||
ter && ter != tecDUPLICATE)
|
||||
{
|
||||
// ignore tecDUPLICATE. That means the holding already exists,
|
||||
// and is fine here
|
||||
return ter;
|
||||
}
|
||||
}
|
||||
|
||||
if (auto const ter = requireAuth(view, vaultAsset, brokerOwner, AuthType::StrongAuth))
|
||||
return ter;
|
||||
|
||||
if (auto const ter = accountSendMulti(
|
||||
view,
|
||||
vaultPseudo,
|
||||
vaultAsset,
|
||||
{{borrower, loanAssetsToBorrower}, {brokerOwner, originationFee}},
|
||||
j,
|
||||
WaiveTransferFee::Yes))
|
||||
return ter;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
void
|
||||
adjustBrokerDebtTotal(
|
||||
SLE::ref brokerSle,
|
||||
Number const& newDebtDelta,
|
||||
Asset const& vaultAsset,
|
||||
int vaultScale)
|
||||
{
|
||||
adjustImpreciseNumber(brokerSle->at(sfDebtTotal), newDebtDelta, vaultAsset, vaultScale);
|
||||
}
|
||||
|
||||
TER
|
||||
updateLoanBroker(
|
||||
ApplyView& view,
|
||||
SLE::ref brokerSle,
|
||||
Number const& newDebtDelta,
|
||||
Asset const& vaultAsset,
|
||||
int vaultScale,
|
||||
beast::Journal j)
|
||||
{
|
||||
// Update the balances in the loan broker
|
||||
adjustBrokerDebtTotal(brokerSle, newDebtDelta, vaultAsset, vaultScale);
|
||||
// The broker's owner count is solely for the number of outstanding loans,
|
||||
// and is distinct from the broker's pseudo-account's owner count
|
||||
adjustOwnerCount(view, brokerSle, 1, j);
|
||||
auto loanSequenceProxy = brokerSle->at(sfLoanSequence);
|
||||
loanSequenceProxy += 1;
|
||||
// The sequence should be extremely unlikely to roll over, but fail if it
|
||||
// does
|
||||
if (loanSequenceProxy == 0)
|
||||
return tecMAX_SEQUENCE_REACHED;
|
||||
view.update(brokerSle);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
linkLoanBroker(ApplyView& view, AccountID const& brokerPseudo, SLE::pointer& loan)
|
||||
{
|
||||
// Put the loan into the pseudo-account's directory
|
||||
return dirLink(view, brokerPseudo, loan, sfLoanBrokerNode);
|
||||
}
|
||||
|
||||
TER
|
||||
linkLoanBorrower(ApplyView& view, AccountID const& borrower, SLE::pointer& loan)
|
||||
{
|
||||
// Borrower is the owner of the loan
|
||||
return dirLink(view, borrower, loan, sfOwnerNode);
|
||||
}
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
#include <xrpl/tx/transactors/lending/LoanAccept.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/LendingHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STTakesAsset.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
@@ -7,16 +21,147 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
bool
|
||||
LoanAccept::checkExtraFeatures(PreflightContext const& ctx)
|
||||
{
|
||||
return checkLendingProtocolDependencies(ctx.rules, ctx.tx);
|
||||
}
|
||||
|
||||
NotTEC
|
||||
LoanAccept::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
if (ctx.tx[sfLoanID] == beast::kZero)
|
||||
return temINVALID;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
LoanAccept::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;
|
||||
}
|
||||
|
||||
if (!loanSle->isFlag(lsfLoanPending))
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Loan is not pending acceptance.";
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
if (loanSle->at(sfBorrower) != account)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "LoanAccept can only be submitted by the Borrower.";
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
if (hasExpired(ctx.view, loanSle->at(sfStartDate)))
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Loan proposal has expired.";
|
||||
return tecEXPIRED;
|
||||
}
|
||||
|
||||
auto const brokerSle = ctx.view.read(keylet::loanBroker(loanSle->at(sfLoanBrokerID)));
|
||||
if (!brokerSle)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
auto const brokerOwner = brokerSle->at(sfOwner);
|
||||
auto const brokerPseudo = brokerSle->at(sfAccount);
|
||||
|
||||
auto const vaultSle = ctx.view.read(keylet::vault(brokerSle->at(sfVaultID)));
|
||||
if (!vaultSle)
|
||||
return tefBAD_LEDGER; // LCOV_EXCL_LINE
|
||||
Asset const asset = vaultSle->at(sfAsset);
|
||||
auto const vaultPseudo = vaultSle->at(sfAccount);
|
||||
|
||||
if (auto const ter = checkLoanFreeze(
|
||||
ctx.view, asset, vaultPseudo, brokerPseudo, account, brokerOwner, ctx.j))
|
||||
return ter;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
LoanAccept::doApply()
|
||||
{
|
||||
// Intentionally does nothing for now.
|
||||
auto const& tx = ctx_.tx;
|
||||
auto& view = ctx_.view();
|
||||
|
||||
auto const loanID = tx[sfLoanID];
|
||||
auto loanSle = view.peek(keylet::loan(loanID));
|
||||
if (!loanSle)
|
||||
return tefBAD_LEDGER; // LCOV_EXCL_LINE
|
||||
|
||||
auto const brokerSle = view.peek(keylet::loanBroker(loanSle->at(sfLoanBrokerID)));
|
||||
if (!brokerSle)
|
||||
return tefBAD_LEDGER; // LCOV_EXCL_LINE
|
||||
auto const brokerOwner = brokerSle->at(sfOwner);
|
||||
auto const brokerOwnerSle = view.peek(keylet::account(brokerOwner));
|
||||
if (!brokerOwnerSle)
|
||||
return tefBAD_LEDGER; // LCOV_EXCL_LINE
|
||||
|
||||
auto const vaultSle = view.peek(keylet::vault(brokerSle->at(sfVaultID)));
|
||||
if (!vaultSle)
|
||||
return tefBAD_LEDGER; // LCOV_EXCL_LINE
|
||||
Asset const vaultAsset = vaultSle->at(sfAsset);
|
||||
auto const vaultPseudo = vaultSle->at(sfAccount);
|
||||
|
||||
auto const borrower = loanSle->at(sfBorrower);
|
||||
auto const borrowerSle = view.peek(keylet::account(borrower));
|
||||
if (!borrowerSle)
|
||||
return tefBAD_LEDGER; // LCOV_EXCL_LINE
|
||||
|
||||
Number const principalOutstanding = loanSle->at(sfPrincipalOutstanding);
|
||||
Number const originationFee = loanSle->at(sfLoanOriginationFee);
|
||||
auto const loanAssetsToBorrower = principalOutstanding - originationFee;
|
||||
|
||||
// The loan is no longer pending; it becomes active.
|
||||
loanSle->clearFlag(lsfLoanPending);
|
||||
|
||||
// Release the owner reserve that was charged to the LoanBroker.Owner when
|
||||
// the loan was proposed, and charge it to the borrower instead.
|
||||
adjustOwnerCount(view, brokerOwnerSle, -1, j_);
|
||||
if (auto const ter =
|
||||
reserveLoanOwner(view, borrower, borrowerSle, accountID_, preFeeBalance_, j_))
|
||||
return ter;
|
||||
|
||||
// Disburse the principal to the borrower and the origination fee, if any,
|
||||
// to the broker owner.
|
||||
if (auto const ter = disburseLoan(
|
||||
view,
|
||||
borrower,
|
||||
borrowerSle,
|
||||
brokerOwner,
|
||||
brokerOwnerSle,
|
||||
vaultPseudo,
|
||||
vaultAsset,
|
||||
loanAssetsToBorrower,
|
||||
originationFee,
|
||||
accountID_,
|
||||
brokerOwner,
|
||||
j_))
|
||||
return ter;
|
||||
|
||||
// Release the reserved principal now that it has been paid out.
|
||||
auto vaultAssetReservedProxy = vaultSle->at(sfAssetsReserved);
|
||||
vaultAssetReservedProxy -= principalOutstanding;
|
||||
view.update(vaultSle);
|
||||
|
||||
// Make the borrower the owner of the loan.
|
||||
if (auto const ter = linkLoanBorrower(view, borrower, loanSle))
|
||||
return ter;
|
||||
view.update(loanSle);
|
||||
|
||||
associateAsset(*loanSle, vaultAsset);
|
||||
associateAsset(*brokerSle, vaultAsset);
|
||||
associateAsset(*vaultSle, vaultAsset);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,10 @@ LoanDelete::preclaim(PreclaimContext const& ctx)
|
||||
JLOG(ctx.j.warn()) << "Loan does not exist.";
|
||||
return tecNO_ENTRY;
|
||||
}
|
||||
if (loanSle->at(sfPaymentRemaining) > 0)
|
||||
// A pending loan (created in the two-step flow) can be deleted at any time
|
||||
// by either the LoanBroker owner or the Borrower, regardless of remaining
|
||||
// payments. An active loan can only be deleted once it is fully paid.
|
||||
if (!loanSle->isFlag(lsfLoanPending) && loanSle->at(sfPaymentRemaining) > 0)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Active loan can not be deleted.";
|
||||
return tecHAS_OBLIGATIONS;
|
||||
@@ -79,10 +82,6 @@ LoanDelete::doApply()
|
||||
auto const loanSle = view.peek(keylet::loan(loanID));
|
||||
if (!loanSle)
|
||||
return tefBAD_LEDGER; // LCOV_EXCL_LINE
|
||||
auto const borrower = loanSle->at(sfBorrower);
|
||||
auto const borrowerSle = view.peek(keylet::account(borrower));
|
||||
if (!borrowerSle)
|
||||
return tefBAD_LEDGER; // LCOV_EXCL_LINE
|
||||
|
||||
auto const brokerID = loanSle->at(sfLoanBrokerID);
|
||||
auto const brokerSle = view.peek(keylet::loanBroker(brokerID));
|
||||
@@ -95,6 +94,61 @@ LoanDelete::doApply()
|
||||
return tefBAD_LEDGER; // LCOV_EXCL_LINE
|
||||
auto const vaultAsset = vaultSle->at(sfAsset);
|
||||
|
||||
// A pending loan reverses the bookkeeping performed by LoanSet at proposal
|
||||
// time and releases the owner reserve charged to the LoanBroker owner. It is
|
||||
// only linked into the broker pseudo-account's directory, and the borrower
|
||||
// was never charged a reserve.
|
||||
if (loanSle->isFlag(lsfLoanPending))
|
||||
{
|
||||
auto const brokerOwner = brokerSle->at(sfOwner);
|
||||
auto const brokerOwnerSle = view.peek(keylet::account(brokerOwner));
|
||||
if (!brokerOwnerSle)
|
||||
return tefBAD_LEDGER; // LCOV_EXCL_LINE
|
||||
|
||||
auto const vaultScale = getAssetsTotalScale(vaultSle);
|
||||
Number const principalOutstanding = loanSle->at(sfPrincipalOutstanding);
|
||||
auto const state = constructRoundedLoanState(loanSle);
|
||||
|
||||
// Remove LoanID from the broker pseudo-account's directory.
|
||||
if (!view.dirRemove(
|
||||
keylet::ownerDir(brokerPseudoAccount),
|
||||
loanSle->at(sfLoanBrokerNode),
|
||||
loanID,
|
||||
false))
|
||||
return tefBAD_LEDGER; // LCOV_EXCL_LINE
|
||||
|
||||
// Delete the Loan object
|
||||
view.erase(loanSle);
|
||||
|
||||
// Reverse the vault bookkeeping from the proposal.
|
||||
auto vaultAvailableProxy = vaultSle->at(sfAssetsAvailable);
|
||||
auto vaultReservedProxy = vaultSle->at(sfAssetsReserved);
|
||||
auto vaultTotalProxy = vaultSle->at(sfAssetsTotal);
|
||||
vaultAvailableProxy += principalOutstanding;
|
||||
vaultReservedProxy -= principalOutstanding;
|
||||
vaultTotalProxy -= state.interestDue;
|
||||
view.update(vaultSle);
|
||||
|
||||
// Reverse the broker debt and outstanding loan count.
|
||||
adjustBrokerDebtTotal(
|
||||
brokerSle, -(principalOutstanding + state.interestDue), vaultAsset, vaultScale);
|
||||
adjustOwnerCount(view, brokerSle, -1, j_);
|
||||
|
||||
// Release the owner reserve charged to the LoanBroker owner when the
|
||||
// loan was proposed.
|
||||
adjustOwnerCount(view, brokerOwnerSle, -1, j_);
|
||||
|
||||
associateAsset(*brokerSle, vaultAsset);
|
||||
associateAsset(*vaultSle, vaultAsset);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
auto const borrower = loanSle->at(sfBorrower);
|
||||
auto const borrowerSle = view.peek(keylet::account(borrower));
|
||||
if (!borrowerSle)
|
||||
return tefBAD_LEDGER; // LCOV_EXCL_LINE
|
||||
|
||||
// Remove LoanID from Directory of the LoanBroker pseudo-account.
|
||||
if (!view.dirRemove(
|
||||
keylet::ownerDir(brokerPseudoAccount), loanSle->at(sfLoanBrokerNode), loanID, false))
|
||||
|
||||
@@ -6,9 +6,7 @@
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/LendingHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
@@ -29,6 +27,7 @@
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
@@ -37,6 +36,22 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
static std::uint32_t
|
||||
currentLedgerCloseTime(ReadView const& view)
|
||||
{
|
||||
return view.header().closeTime.time_since_epoch().count();
|
||||
}
|
||||
|
||||
std::uint32_t
|
||||
LoanSet::getStartDate(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
if (isTwoStepFlow(tx, view.rules()))
|
||||
{
|
||||
return tx[sfStartDate];
|
||||
}
|
||||
return currentLedgerCloseTime(view);
|
||||
}
|
||||
|
||||
bool
|
||||
LoanSet::checkExtraFeatures(PreflightContext const& ctx)
|
||||
{
|
||||
@@ -62,6 +77,12 @@ LoanSet::isTwoStepFlow(STTx const& tx, Rules const& rules)
|
||||
!tx.isFieldPresent(sfCounterparty) && !tx.isFieldPresent(sfCounterpartySignature);
|
||||
}
|
||||
|
||||
bool
|
||||
LoanSet::isOneStepFlow(STTx const& tx)
|
||||
{
|
||||
return tx.isFieldPresent(sfCounterpartySignature);
|
||||
}
|
||||
|
||||
NotTEC
|
||||
LoanSet::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
@@ -87,6 +108,7 @@ LoanSet::preflight(PreflightContext const& ctx)
|
||||
}();
|
||||
|
||||
bool twoStepFlow = isTwoStepFlow(tx, ctx.rules);
|
||||
bool oneStepFlow = isOneStepFlow(tx);
|
||||
// In the two-step (Borrower) flow introduced by V1.1, a CounterpartySignature
|
||||
// is not required even for non-batch transactions. The immediate flow still
|
||||
// requires one.
|
||||
@@ -96,6 +118,33 @@ LoanSet::preflight(PreflightContext const& ctx)
|
||||
return temBAD_SIGNER;
|
||||
}
|
||||
|
||||
if (!twoStepFlow && !oneStepFlow)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "LoanSet transaction must specify either a Borrower with a "
|
||||
"StartDate or a CounterpartySignature.";
|
||||
return temINVALID;
|
||||
}
|
||||
|
||||
if (oneStepFlow)
|
||||
{
|
||||
if (tx.isFieldPresent(sfBorrower) || tx.isFieldPresent(sfStartDate))
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "LoanSet transaction cannot specify both a Borrower with a "
|
||||
"StartDate and a CounterpartySignature.";
|
||||
return temINVALID;
|
||||
}
|
||||
}
|
||||
|
||||
if (twoStepFlow)
|
||||
{
|
||||
if (tx.isFieldPresent(sfCounterpartySignature))
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "LoanSet transaction cannot specify both a Borrower with a "
|
||||
"StartDate and a CounterpartySignature.";
|
||||
return temINVALID;
|
||||
}
|
||||
}
|
||||
|
||||
if (counterPartySig)
|
||||
{
|
||||
if (auto const ret = xrpl::detail::preflightCheckSigningKey(*counterPartySig, ctx.j))
|
||||
@@ -230,12 +279,6 @@ LoanSet::getValueFields()
|
||||
return kValueFields;
|
||||
}
|
||||
|
||||
static std::uint32_t
|
||||
getStartDate(ReadView const& view)
|
||||
{
|
||||
return view.header().closeTime.time_since_epoch().count();
|
||||
}
|
||||
|
||||
TER
|
||||
LoanSet::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
@@ -252,7 +295,7 @@ LoanSet::preclaim(PreclaimContext const& ctx)
|
||||
constexpr timeType kMaxTime = std::numeric_limits<timeType>::max();
|
||||
static_assert(kMaxTime == 4'294'967'295);
|
||||
|
||||
auto const timeAvailable = kMaxTime - getStartDate(ctx.view);
|
||||
auto const timeAvailable = kMaxTime - getStartDate(ctx.view, tx);
|
||||
|
||||
auto const interval = ctx.tx.at(~sfPaymentInterval).value_or(kDefaultPaymentInterval);
|
||||
auto const total = ctx.tx.at(~sfPaymentTotal).value_or(kDefaultPaymentTotal);
|
||||
@@ -300,16 +343,37 @@ LoanSet::preclaim(PreclaimContext const& ctx)
|
||||
return tecNO_ENTRY;
|
||||
}
|
||||
auto const brokerOwner = brokerSle->at(sfOwner);
|
||||
auto const counterparty = tx[~sfCounterparty].value_or(brokerOwner);
|
||||
if (account != brokerOwner && counterparty != brokerOwner)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Neither Account nor Counterparty are the owner "
|
||||
"of the LoanBroker.";
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
auto const brokerPseudo = brokerSle->at(sfAccount);
|
||||
bool const twoStepFlow = isTwoStepFlow(tx, ctx.view.rules());
|
||||
|
||||
auto const borrower = counterparty == brokerOwner ? account : counterparty;
|
||||
// Determine the Borrower and validate the submitter's permission. In the
|
||||
// two-step flow the LoanBroker owner proposes the loan on behalf of the
|
||||
// named Borrower, so the submitter must be the owner. In the immediate flow
|
||||
// either the Borrower or the LoanBroker owner may submit, with the other
|
||||
// acting as the counterparty.
|
||||
std::expected<AccountID, TER> const maybeBorrower = [&]() -> std::expected<AccountID, TER> {
|
||||
if (twoStepFlow)
|
||||
{
|
||||
if (account != brokerOwner)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Account is not the owner of the LoanBroker.";
|
||||
return std::unexpected(tecNO_PERMISSION);
|
||||
}
|
||||
return tx[sfBorrower];
|
||||
}
|
||||
auto const counterparty = tx[~sfCounterparty].value_or(brokerOwner);
|
||||
if (account != brokerOwner && counterparty != brokerOwner)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Neither Account nor Counterparty are the owner "
|
||||
"of the LoanBroker.";
|
||||
return std::unexpected(tecNO_PERMISSION);
|
||||
}
|
||||
return counterparty == brokerOwner ? account : counterparty;
|
||||
}();
|
||||
if (!maybeBorrower)
|
||||
return maybeBorrower.error();
|
||||
|
||||
auto borrower = *maybeBorrower;
|
||||
auto const brokerPseudo = brokerSle->at(sfAccount);
|
||||
if (auto const borrowerSle = ctx.view.read(keylet::account(borrower)); !borrowerSle)
|
||||
{
|
||||
// It may not be possible to hit this case, because it'll fail the
|
||||
@@ -349,40 +413,17 @@ LoanSet::preclaim(PreclaimContext const& ctx)
|
||||
}
|
||||
}
|
||||
|
||||
if (auto const ter = canAddHolding(ctx.view, asset))
|
||||
if (auto const ter = checkLoanFreeze(
|
||||
ctx.view, asset, vaultPseudo, brokerPseudo, borrower, brokerOwner, ctx.j))
|
||||
return ter;
|
||||
|
||||
// vaultPseudo is going to send funds, so it can't be frozen.
|
||||
if (auto const ret = checkFrozen(ctx.view, vaultPseudo, asset))
|
||||
if (twoStepFlow)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Vault pseudo-account is frozen.";
|
||||
return ret;
|
||||
}
|
||||
|
||||
// brokerPseudo is the fallback account to receive LoanPay fees, even if the
|
||||
// broker owner is unable to accept them. Don't create the loan if it is
|
||||
// deep frozen.
|
||||
if (auto const ret = checkDeepFrozen(ctx.view, brokerPseudo, asset))
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Broker pseudo-account is frozen.";
|
||||
return ret;
|
||||
}
|
||||
|
||||
// borrower is eventually going to have to pay back the loan, so it can't be
|
||||
// frozen now. It is also going to receive funds, so it can't be deep
|
||||
// frozen, but being frozen is a prerequisite for being deep frozen, so
|
||||
// checking the one is sufficient.
|
||||
if (auto const ret = checkFrozen(ctx.view, borrower, asset))
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Borrower account is frozen.";
|
||||
return ret;
|
||||
}
|
||||
// brokerOwner is going to receive funds if there's an origination fee, so
|
||||
// it can't be deep frozen
|
||||
if (auto const ret = checkDeepFrozen(ctx.view, brokerOwner, asset))
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Broker owner account is frozen.";
|
||||
return ret;
|
||||
if (tx[sfStartDate] <= currentLedgerCloseTime(ctx.view))
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Start date is in the past.";
|
||||
return tecTOO_SOON;
|
||||
}
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
@@ -393,6 +434,7 @@ LoanSet::doApply()
|
||||
{
|
||||
auto const& tx = ctx_.tx;
|
||||
auto& view = ctx_.view();
|
||||
bool const twoStepFlow = isTwoStepFlow(tx, view.rules());
|
||||
|
||||
auto const brokerID = tx[sfLoanBrokerID];
|
||||
|
||||
@@ -411,7 +453,19 @@ LoanSet::doApply()
|
||||
Asset const vaultAsset = vaultSle->at(sfAsset);
|
||||
|
||||
auto const counterparty = tx[~sfCounterparty].value_or(brokerOwner);
|
||||
auto const borrower = counterparty == brokerOwner ? accountID_ : counterparty;
|
||||
auto const borrower = [&]() {
|
||||
if (twoStepFlow)
|
||||
{
|
||||
return tx[sfBorrower];
|
||||
}
|
||||
|
||||
if (counterparty == brokerOwner)
|
||||
{
|
||||
return accountID_;
|
||||
}
|
||||
return counterparty;
|
||||
}();
|
||||
|
||||
auto const borrowerSle = view.peek(keylet::account(borrower));
|
||||
if (!borrowerSle)
|
||||
{
|
||||
@@ -426,6 +480,7 @@ LoanSet::doApply()
|
||||
}
|
||||
auto const principalRequested = tx[sfPrincipalRequested];
|
||||
|
||||
auto vaultAssetReservedProxy = vaultSle->at(sfAssetsReserved);
|
||||
auto vaultAvailableProxy = vaultSle->at(sfAssetsAvailable);
|
||||
auto vaultTotalProxy = vaultSle->at(sfAssetsTotal);
|
||||
auto const vaultScale = getAssetsTotalScale(vaultSle);
|
||||
@@ -455,152 +510,59 @@ LoanSet::doApply()
|
||||
principalRequested,
|
||||
properties.loanState.managementFeeDue);
|
||||
|
||||
auto const vaultMaximum = *vaultSle->at(sfAssetsMaximum);
|
||||
XRPL_ASSERT_PARTS(
|
||||
vaultMaximum == 0 || vaultMaximum > *vaultTotalProxy,
|
||||
"xrpl::LoanSet::doApply",
|
||||
"Vault is below maximum limit");
|
||||
if (vaultMaximum != 0 && state.interestDue > vaultMaximum - vaultTotalProxy)
|
||||
{
|
||||
JLOG(j_.warn()) << "Loan would exceed the maximum assets of the vault";
|
||||
return tecLIMIT_EXCEEDED;
|
||||
}
|
||||
// Check that relevant values won't lose precision. This is mostly only
|
||||
// relevant for IOU assets.
|
||||
for (auto const& field : getValueFields())
|
||||
{
|
||||
if (auto const value = tx[field];
|
||||
value && !isRounded(vaultAsset, *value, properties.loanScale))
|
||||
{
|
||||
JLOG(j_.warn()) << field.f->getName() << " (" << *value
|
||||
<< ") has too much precision. Total loan value is "
|
||||
<< properties.loanState.valueOutstanding << " with a scale of "
|
||||
<< properties.loanScale;
|
||||
return tecPRECISION_LOSS;
|
||||
}
|
||||
}
|
||||
|
||||
if (auto const ret = checkLoanGuards(
|
||||
if (auto const ter = checkLoanLimits(
|
||||
view,
|
||||
tx,
|
||||
brokerSle,
|
||||
vaultSle,
|
||||
vaultAsset,
|
||||
principalRequested,
|
||||
interestRate != beast::kZero,
|
||||
interestRate,
|
||||
paymentTotal,
|
||||
properties,
|
||||
state,
|
||||
getValueFields(),
|
||||
j_))
|
||||
return ret;
|
||||
|
||||
// Check that the other computed values are valid
|
||||
if (properties.loanState.managementFeeDue < 0 || properties.loanState.valueOutstanding <= 0 ||
|
||||
properties.periodicPayment <= 0)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j_.warn()) << "Computed loan properties are invalid. Does not compute."
|
||||
<< " Management fee: " << properties.loanState.managementFeeDue
|
||||
<< ". Total Value: " << properties.loanState.valueOutstanding
|
||||
<< ". PeriodicPayment: " << properties.periodicPayment;
|
||||
return tecINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
return ter;
|
||||
|
||||
auto const originationFee = tx[~sfLoanOriginationFee].value_or(Number{});
|
||||
|
||||
auto const loanAssetsToBorrower = principalRequested - originationFee;
|
||||
|
||||
auto const newDebtDelta = principalRequested + state.interestDue;
|
||||
auto const newDebtTotal = brokerSle->at(sfDebtTotal) + newDebtDelta;
|
||||
if (auto const debtMaximum = brokerSle->at(sfDebtMaximum);
|
||||
debtMaximum != 0 && debtMaximum < newDebtTotal)
|
||||
|
||||
// In the two-step flow, the LoanBroker.Owner is charged the owner reserve
|
||||
// for the pending loan; the borrower is not charged and receives no funds
|
||||
// until the loan is accepted (see LoanAccept). In the immediate flow, the
|
||||
// borrower is charged the reserve and the funds are disbursed now.
|
||||
if (twoStepFlow)
|
||||
{
|
||||
JLOG(j_.warn()) << "Loan would exceed the maximum debt limit of the LoanBroker.";
|
||||
return tecLIMIT_EXCEEDED;
|
||||
}
|
||||
TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)};
|
||||
{
|
||||
auto const minCover = [&]() {
|
||||
if (ctx_.view().rules().enabled(fixCleanup3_2_0))
|
||||
{
|
||||
return minimumBrokerCover(newDebtTotal, coverRateMinimum, vaultSle);
|
||||
}
|
||||
|
||||
// Round the minimum required cover up to be conservative. This ensures
|
||||
// CoverAvailable never drops below the theoretical minimum, protecting
|
||||
// the broker's solvency.
|
||||
NumberRoundModeGuard const mg(Number::RoundingMode::Upward);
|
||||
return tenthBipsOfValue(newDebtTotal, coverRateMinimum);
|
||||
}();
|
||||
if (brokerSle->at(sfCoverAvailable) < minCover)
|
||||
{
|
||||
JLOG(j_.warn()) << "Insufficient first-loss capital to cover the loan.";
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
}
|
||||
}
|
||||
|
||||
adjustOwnerCount(view, borrowerSle, 1, j_);
|
||||
{
|
||||
auto const ownerCount = borrowerSle->at(sfOwnerCount);
|
||||
auto const balance =
|
||||
accountID_ == borrower ? preFeeBalance_ : borrowerSle->at(sfBalance).value().xrp();
|
||||
if (balance < view.fees().accountReserve(ownerCount))
|
||||
return tecINSUFFICIENT_RESERVE;
|
||||
}
|
||||
|
||||
// Account for the origination fee using two payments
|
||||
//
|
||||
// 1. Transfer loanAssetsAvailable (principalRequested - originationFee)
|
||||
// from vault pseudo-account to the borrower.
|
||||
// Create a holding for the borrower if one does not already exist.
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
borrower == accountID_ || borrower == counterparty,
|
||||
"xrpl::LoanSet::doApply",
|
||||
"borrower signed transaction");
|
||||
if (auto const ter = addEmptyHolding(
|
||||
view, borrower, borrowerSle->at(sfBalance).value().xrp(), vaultAsset, j_);
|
||||
ter && ter != tecDUPLICATE)
|
||||
{
|
||||
// ignore tecDUPLICATE. That means the holding already exists, and
|
||||
// is fine here
|
||||
return ter;
|
||||
}
|
||||
|
||||
if (auto const ter = requireAuth(view, vaultAsset, borrower, AuthType::StrongAuth))
|
||||
return ter;
|
||||
|
||||
// 2. Transfer originationFee, if any, from vault pseudo-account to
|
||||
// LoanBroker owner.
|
||||
if (originationFee != beast::kZero)
|
||||
{
|
||||
// Create the holding if it doesn't already exist (necessary for MPTs).
|
||||
// The owner may have deleted their MPT / line at some point.
|
||||
XRPL_ASSERT_PARTS(
|
||||
brokerOwner == accountID_ || brokerOwner == counterparty,
|
||||
"xrpl::LoanSet::doApply",
|
||||
"broker owner signed transaction");
|
||||
|
||||
if (auto const ter = addEmptyHolding(
|
||||
view, brokerOwner, brokerOwnerSle->at(sfBalance).value().xrp(), vaultAsset, j_);
|
||||
ter && ter != tecDUPLICATE)
|
||||
{
|
||||
// ignore tecDUPLICATE. That means the holding already exists,
|
||||
// and is fine here
|
||||
if (auto const ter =
|
||||
reserveLoanOwner(view, brokerOwner, brokerOwnerSle, accountID_, preFeeBalance_, j_))
|
||||
return ter;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (auto const ter =
|
||||
reserveLoanOwner(view, borrower, borrowerSle, accountID_, preFeeBalance_, j_))
|
||||
return ter;
|
||||
|
||||
if (auto const ter = requireAuth(view, vaultAsset, brokerOwner, AuthType::StrongAuth))
|
||||
return ter;
|
||||
|
||||
if (auto const ter = accountSendMulti(
|
||||
view,
|
||||
vaultPseudo,
|
||||
vaultAsset,
|
||||
{{borrower, loanAssetsToBorrower}, {brokerOwner, originationFee}},
|
||||
j_,
|
||||
WaiveTransferFee::Yes))
|
||||
return ter;
|
||||
if (auto const ter = disburseLoan(
|
||||
view,
|
||||
borrower,
|
||||
borrowerSle,
|
||||
brokerOwner,
|
||||
brokerOwnerSle,
|
||||
vaultPseudo,
|
||||
vaultAsset,
|
||||
loanAssetsToBorrower,
|
||||
originationFee,
|
||||
accountID_,
|
||||
counterparty,
|
||||
j_))
|
||||
return ter;
|
||||
}
|
||||
|
||||
// Get shortcuts to the loan property values
|
||||
auto const startDate = getStartDate(view);
|
||||
auto const startDate = getStartDate(view, tx);
|
||||
auto loanSequenceProxy = brokerSle->at(sfLoanSequence);
|
||||
|
||||
// Create the loan
|
||||
@@ -641,36 +603,38 @@ LoanSet::doApply()
|
||||
loan->at(sfPreviousPaymentDueDate) = 0;
|
||||
loan->at(sfNextPaymentDueDate) = startDate + paymentInterval;
|
||||
loan->at(sfPaymentRemaining) = paymentTotal;
|
||||
if (twoStepFlow)
|
||||
loan->setFlag(lsfLoanPending);
|
||||
view.insert(loan);
|
||||
|
||||
// Update the balances in the vault
|
||||
// Update the balances in the vault. Both flows decrement the available
|
||||
// assets and accrue the interest due. The two-step flow additionally moves
|
||||
// the principal into the reserved bucket until the borrower accepts.
|
||||
vaultAvailableProxy -= principalRequested;
|
||||
vaultTotalProxy += state.interestDue;
|
||||
if (twoStepFlow)
|
||||
vaultAssetReservedProxy += principalRequested;
|
||||
XRPL_ASSERT_PARTS(
|
||||
*vaultAvailableProxy <= *vaultTotalProxy,
|
||||
"xrpl::LoanSet::doApply",
|
||||
"assets available must not be greater than assets outstanding");
|
||||
view.update(vaultSle);
|
||||
|
||||
// Update the balances in the loan broker
|
||||
adjustImpreciseNumber(brokerSle->at(sfDebtTotal), newDebtDelta, vaultAsset, vaultScale);
|
||||
// The broker's owner count is solely for the number of outstanding loans,
|
||||
// and is distinct from the broker's pseudo-account's owner count
|
||||
adjustOwnerCount(view, brokerSle, 1, j_);
|
||||
loanSequenceProxy += 1;
|
||||
// The sequence should be extremely unlikely to roll over, but fail if it
|
||||
// does
|
||||
if (loanSequenceProxy == 0)
|
||||
return tecMAX_SEQUENCE_REACHED;
|
||||
view.update(brokerSle);
|
||||
if (auto const ter =
|
||||
updateLoanBroker(view, brokerSle, newDebtDelta, vaultAsset, vaultScale, j_))
|
||||
return ter;
|
||||
|
||||
// Put the loan into the pseudo-account's directory
|
||||
if (auto const ter = dirLink(view, brokerPseudo, loan, sfLoanBrokerNode))
|
||||
return ter;
|
||||
// Borrower is the owner of the loan
|
||||
if (auto const ter = dirLink(view, borrower, loan, sfOwnerNode))
|
||||
// Always link the loan into the broker's directory. The borrower directory
|
||||
// link is deferred to LoanAccept for the two-step (pending) flow.
|
||||
if (auto const ter = linkLoanBroker(view, brokerPseudo, loan))
|
||||
return ter;
|
||||
|
||||
if (!twoStepFlow)
|
||||
{
|
||||
if (auto const ter = linkLoanBorrower(view, borrower, loan))
|
||||
return ter;
|
||||
}
|
||||
|
||||
associateAsset(*vaultSle, vaultAsset);
|
||||
associateAsset(*brokerSle, vaultAsset);
|
||||
associateAsset(*loan, vaultAsset);
|
||||
|
||||
@@ -4049,7 +4049,9 @@ class Invariants_test : public beast::unit_test::Suite
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECTS(result == tesSUCCESS, sink.messages().str());
|
||||
BEAST_EXPECTS(
|
||||
result == tesSUCCESS,
|
||||
sink.messages().str() + " expected logs: " + expected.value_or("<none>"));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user