mirror of
https://github.com/XRPLF/rippled.git
synced 2026-07-25 08:00:22 +00:00
Address comments WIP
This commit is contained in:
@@ -15,70 +15,14 @@
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
class LoanSet : public Transactor
|
||||
{
|
||||
private:
|
||||
static std::uint32_t
|
||||
getStartDate(ReadView const& view, STTx const& tx);
|
||||
static bool
|
||||
isTwoStepFlowEnabled(Rules const& rules);
|
||||
/* Returns true if the transaction is using the two-step flow. */
|
||||
static bool
|
||||
isTwoStepFlow(STTx const& tx);
|
||||
/* Returns true if the transaction is using the one-step flow. */
|
||||
static bool
|
||||
isOneStepFlow(STTx const& tx);
|
||||
|
||||
/* Returns the counterparty account: the explicit Counterparty field if
|
||||
* present, otherwise the LoanBroker owner. */
|
||||
static AccountID
|
||||
getCounterparty(STTx const& tx, AccountID const& brokerOwner);
|
||||
/* Returns the borrower account. In the two-step flow this is the named
|
||||
* Borrower; in the immediate flow it is whichever of the signer /
|
||||
* counterparty is not the LoanBroker owner. */
|
||||
static AccountID
|
||||
getBorrower(STTx const& tx, AccountID const& brokerOwner, AccountID const& signingAccount);
|
||||
|
||||
/* Holds the values validated and computed by doApply() that the flow
|
||||
* functions need to create the loan and update the ledger. The ledger
|
||||
* entries themselves are fetched (and their existence verified) by the flow
|
||||
* functions that mutate them; the derived borrower / counterparty account
|
||||
* IDs and the pure computed scalars are carried here so they are resolved
|
||||
* once, in doApply(), rather than in each flow function. */
|
||||
struct LoanPlan
|
||||
{
|
||||
uint256 brokerID;
|
||||
AccountID borrower;
|
||||
AccountID counterparty;
|
||||
Number principalRequested;
|
||||
Number originationFee;
|
||||
Number interestDue;
|
||||
LoanProperties properties;
|
||||
std::uint32_t paymentInterval;
|
||||
std::uint32_t paymentTotal;
|
||||
};
|
||||
|
||||
/* Build the Loan ledger entry from the plan, setting the pending flag when
|
||||
* requested. Does not insert the entry into the view. */
|
||||
std::shared_ptr<SLE>
|
||||
buildLoan(LoanPlan const& plan, SLE::ref brokerSle, bool pending);
|
||||
|
||||
/* Create a pending loan for the two-step flow: charge the broker owner the
|
||||
* owner reserve, create the loan flagged pending, reserve the principal in
|
||||
* the vault, and link the loan into the broker directory only. */
|
||||
TER
|
||||
applyPendingLoan(LoanPlan const& plan);
|
||||
|
||||
/* Create an active loan for the immediate flow: charge the borrower the
|
||||
* owner reserve, disburse the funds, create the loan, update the vault, and
|
||||
* link the loan into both the broker and borrower directories. */
|
||||
TER
|
||||
applyImmediateLoan(LoanPlan const& plan);
|
||||
|
||||
public:
|
||||
static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal;
|
||||
|
||||
@@ -101,9 +45,6 @@ public:
|
||||
static XRPAmount
|
||||
calculateBaseFee(ReadView const& view, STTx const& tx);
|
||||
|
||||
static std::vector<OptionaledField<STNumber>> const&
|
||||
getValueFields();
|
||||
|
||||
static TER
|
||||
preclaim(PreclaimContext const& ctx);
|
||||
|
||||
|
||||
@@ -38,14 +38,91 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
static std::uint32_t
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* The borrower and counterparty accounts resolved for a LoanSet.
|
||||
*/
|
||||
struct Participants
|
||||
{
|
||||
AccountID borrower;
|
||||
AccountID counterparty;
|
||||
};
|
||||
|
||||
/**
|
||||
* Holds the values validated and computed by doApply() that the flow
|
||||
* functions need to create the loan and update the ledger.
|
||||
*
|
||||
* The ledger entries themselves are fetched (and their existence verified)
|
||||
* by the flow functions that mutate them; the derived borrower /
|
||||
* counterparty account IDs and the pure computed scalars are carried here so
|
||||
* they are resolved once, in doApply(), rather than in each flow function.
|
||||
*/
|
||||
struct LoanPlan
|
||||
{
|
||||
uint256 brokerID;
|
||||
AccountID borrower;
|
||||
AccountID counterparty;
|
||||
Number principalRequested;
|
||||
Number originationFee;
|
||||
Number interestDue;
|
||||
LoanProperties properties;
|
||||
std::uint32_t paymentInterval{};
|
||||
std::uint32_t paymentTotal{};
|
||||
};
|
||||
|
||||
/**
|
||||
* Holds the LoanBroker entry and the validated / computed scalars produced
|
||||
* by setupLoan(): everything doApply() needs to resolve the participants and
|
||||
* assemble the LoanPlan.
|
||||
*/
|
||||
struct LoanSetup
|
||||
{
|
||||
uint256 brokerID;
|
||||
std::shared_ptr<SLE> brokerSle;
|
||||
Number principalRequested;
|
||||
Number originationFee;
|
||||
Number interestDue;
|
||||
LoanProperties properties;
|
||||
std::uint32_t paymentInterval;
|
||||
std::uint32_t paymentTotal;
|
||||
};
|
||||
|
||||
std::uint32_t
|
||||
currentLedgerCloseTime(ReadView const& view)
|
||||
{
|
||||
return view.header().closeTime.time_since_epoch().count();
|
||||
}
|
||||
|
||||
bool
|
||||
isTwoStepFlowEnabled(Rules const& rules)
|
||||
{
|
||||
return rules.enabled(featureLendingProtocolV1_1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the transaction is using the two-step flow.
|
||||
*/
|
||||
bool
|
||||
isTwoStepFlow(STTx const& tx)
|
||||
{
|
||||
// The two-step (Borrower) flow is started when the LoanSet names a
|
||||
// Borrower and a StartDate but carries neither a Counterparty nor a
|
||||
// CounterpartySignature.
|
||||
return tx.isFieldPresent(sfBorrower) && tx.isFieldPresent(sfStartDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the transaction is using the one-step flow.
|
||||
*/
|
||||
bool
|
||||
isOneStepFlow(STTx const& tx)
|
||||
{
|
||||
return tx.isFieldPresent(sfCounterpartySignature);
|
||||
}
|
||||
|
||||
std::uint32_t
|
||||
LoanSet::getStartDate(ReadView const& view, STTx const& tx)
|
||||
getStartDate(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
if (isTwoStepFlow(tx) && isTwoStepFlowEnabled(view.rules()))
|
||||
{
|
||||
@@ -54,6 +131,456 @@ LoanSet::getStartDate(ReadView const& view, STTx const& tx)
|
||||
return currentLedgerCloseTime(view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the borrower and counterparty accounts, reading the LoanBroker
|
||||
* owner from the broker entry.
|
||||
*
|
||||
* The counterparty is the explicit Counterparty field if present, otherwise
|
||||
* the LoanBroker owner. In the two-step flow the borrower is the named
|
||||
* Borrower; in the immediate flow it is whichever of the signer /
|
||||
* counterparty is not the LoanBroker owner.
|
||||
*
|
||||
* @param tx The LoanSet transaction being applied.
|
||||
* @param brokerSle The LoanBroker ledger entry.
|
||||
* @param signingAccount The account that signed the transaction.
|
||||
*
|
||||
* @return The resolved borrower and counterparty accounts.
|
||||
*/
|
||||
Participants
|
||||
resolveParticipants(STTx const& tx, SLE::const_ref brokerSle, AccountID const& signingAccount)
|
||||
{
|
||||
AccountID const brokerOwner = brokerSle->at(sfOwner);
|
||||
auto const counterparty = tx[~sfCounterparty].value_or(brokerOwner);
|
||||
|
||||
// In the two-step (Borrower) flow the LoanBroker owner proposes the loan on
|
||||
// behalf of the named Borrower. In the immediate flow the Borrower is
|
||||
// whichever of the signer / counterparty is not the LoanBroker owner.
|
||||
if (isTwoStepFlow(tx))
|
||||
return Participants{.borrower = tx[sfBorrower], .counterparty = counterparty};
|
||||
|
||||
auto const borrower = counterparty == brokerOwner ? signingAccount : counterparty;
|
||||
return Participants{.borrower = borrower, .counterparty = counterparty};
|
||||
}
|
||||
|
||||
std::vector<OptionaledField<STNumber>> const&
|
||||
getValueFields()
|
||||
{
|
||||
static std::vector<OptionaledField<STNumber>> const kValueFields{
|
||||
~sfPrincipalRequested,
|
||||
~sfLoanOriginationFee,
|
||||
~sfLoanServiceFee,
|
||||
~sfLatePaymentFee,
|
||||
~sfClosePaymentFee
|
||||
// Overpayment fee is really a rate. Don't check it here.
|
||||
};
|
||||
|
||||
return kValueFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the LoanBroker and Vault entries, validates the requested loan
|
||||
* against them, and computes the loan properties and derived values.
|
||||
*
|
||||
* @param ctx The apply context for the transaction.
|
||||
* @param j Log.
|
||||
*
|
||||
* @return The validated and computed LoanSetup on success, or the TER
|
||||
* describing why the loan cannot be created on failure.
|
||||
*/
|
||||
std::expected<LoanSetup, TER>
|
||||
setupLoan(ApplyContext& ctx, beast::Journal const& j)
|
||||
{
|
||||
auto const& tx = ctx.tx;
|
||||
auto& view = ctx.view();
|
||||
|
||||
auto const brokerID = tx[sfLoanBrokerID];
|
||||
|
||||
// Only the LoanBroker and Vault entries are read here; setupLoan() validates
|
||||
// the loan against them and computes the plan inputs. The broker owner,
|
||||
// borrower, and broker pseudo-account entries are re-fetched (and their
|
||||
// existence re-verified) by the flow functions that actually mutate them, so
|
||||
// they are not peeked here. Borrower existence is already guaranteed by
|
||||
// preclaim().
|
||||
auto const brokerSle = view.peek(keylet::loanBroker(brokerID));
|
||||
if (!brokerSle)
|
||||
return std::unexpected(tefBAD_LEDGER); // LCOV_EXCL_LINE
|
||||
|
||||
auto const vaultSle = view.peek(keylet::vault(brokerSle->at(sfVaultID)));
|
||||
if (!vaultSle)
|
||||
return std::unexpected(tefBAD_LEDGER); // LCOV_EXCL_LINE
|
||||
Asset const vaultAsset = vaultSle->at(sfAsset);
|
||||
|
||||
auto const principalRequested = tx[sfPrincipalRequested];
|
||||
|
||||
auto vaultAvailableProxy = vaultSle->at(sfAssetsAvailable);
|
||||
auto vaultTotalProxy = vaultSle->at(sfAssetsTotal);
|
||||
auto const vaultScale = getAssetsTotalScale(vaultSle);
|
||||
if (vaultAvailableProxy < principalRequested)
|
||||
{
|
||||
JLOG(j.warn()) << "Insufficient assets available in the Vault to fund the loan.";
|
||||
return std::unexpected(tecINSUFFICIENT_FUNDS);
|
||||
}
|
||||
|
||||
TenthBips32 const interestRate{tx[~sfInterestRate].value_or(0)};
|
||||
|
||||
auto const paymentInterval = tx[~sfPaymentInterval].value_or(LoanSet::kDefaultPaymentInterval);
|
||||
auto const paymentTotal = tx[~sfPaymentTotal].value_or(LoanSet::kDefaultPaymentTotal);
|
||||
|
||||
auto const properties = computeLoanProperties(
|
||||
view.rules(),
|
||||
vaultAsset,
|
||||
principalRequested,
|
||||
interestRate,
|
||||
paymentInterval,
|
||||
paymentTotal,
|
||||
TenthBips16{brokerSle->at(sfManagementFeeRate)},
|
||||
vaultScale);
|
||||
|
||||
LoanState const state = constructLoanState(
|
||||
properties.loanState.valueOutstanding,
|
||||
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 std::unexpected(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 std::unexpected(tecPRECISION_LOSS);
|
||||
}
|
||||
}
|
||||
|
||||
if (auto const ret = checkLoanGuards(
|
||||
vaultAsset,
|
||||
principalRequested,
|
||||
interestRate != beast::kZero,
|
||||
paymentTotal,
|
||||
properties,
|
||||
j))
|
||||
return std::unexpected(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 std::unexpected(tecINTERNAL);
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
auto const originationFee = tx[~sfLoanOriginationFee].value_or(Number{});
|
||||
|
||||
auto const newDebtDelta = principalRequested + state.interestDue;
|
||||
auto const newDebtTotal = brokerSle->at(sfDebtTotal) + newDebtDelta;
|
||||
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 std::unexpected(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 std::unexpected(tecINSUFFICIENT_FUNDS);
|
||||
}
|
||||
}
|
||||
|
||||
return LoanSetup{
|
||||
.brokerID = brokerID,
|
||||
.brokerSle = brokerSle,
|
||||
.principalRequested = principalRequested,
|
||||
.originationFee = originationFee,
|
||||
.interestDue = state.interestDue,
|
||||
.properties = properties,
|
||||
.paymentInterval = paymentInterval,
|
||||
.paymentTotal = paymentTotal};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the Loan ledger entry from the plan, setting the pending flag when
|
||||
* requested. Does not insert the entry into the view.
|
||||
*
|
||||
* @param ctx The apply context for the transaction.
|
||||
* @param plan The validated and computed values for the loan.
|
||||
* @param brokerSle The LoanBroker ledger entry.
|
||||
* @param pending Whether the loan should be flagged as pending.
|
||||
*
|
||||
* @return The newly built Loan ledger entry.
|
||||
*/
|
||||
std::shared_ptr<SLE>
|
||||
buildLoan(ApplyContext& ctx, LoanPlan const& plan, SLE::ref brokerSle, bool pending)
|
||||
{
|
||||
auto const& tx = ctx.tx;
|
||||
|
||||
// Get shortcuts to the loan property values
|
||||
auto const startDate = getStartDate(ctx.view(), tx);
|
||||
auto const loanSequence = *brokerSle->at(sfLoanSequence);
|
||||
|
||||
// Create the loan
|
||||
auto loan = std::make_shared<SLE>(keylet::loan(plan.brokerID, loanSequence));
|
||||
|
||||
// Prevent copy/paste errors
|
||||
auto setLoanField = [&loan, &tx](auto const& field, std::uint32_t const defValue = 0) {
|
||||
// at() is smart enough to unseat a default field set to the default
|
||||
// value
|
||||
loan->at(field) = tx[field].value_or(defValue);
|
||||
};
|
||||
|
||||
// Set required and fixed tx fields
|
||||
loan->at(sfLoanScale) = plan.properties.loanScale;
|
||||
loan->at(sfStartDate) = startDate;
|
||||
loan->at(sfPaymentInterval) = plan.paymentInterval;
|
||||
loan->at(sfLoanSequence) = loanSequence;
|
||||
loan->at(sfLoanBrokerID) = plan.brokerID;
|
||||
loan->at(sfBorrower) = plan.borrower;
|
||||
// Set all other transaction fields directly from the transaction
|
||||
if (tx.isFlag(tfLoanOverpayment))
|
||||
loan->setFlag(lsfLoanOverpayment);
|
||||
setLoanField(~sfLoanOriginationFee);
|
||||
setLoanField(~sfLoanServiceFee);
|
||||
setLoanField(~sfLatePaymentFee);
|
||||
setLoanField(~sfClosePaymentFee);
|
||||
setLoanField(~sfOverpaymentFee);
|
||||
setLoanField(~sfInterestRate);
|
||||
setLoanField(~sfLateInterestRate);
|
||||
setLoanField(~sfCloseInterestRate);
|
||||
setLoanField(~sfOverpaymentInterestRate);
|
||||
setLoanField(~sfGracePeriod, LoanSet::kDefaultGracePeriod);
|
||||
// Set dynamic / computed fields to their initial values
|
||||
loan->at(sfPrincipalOutstanding) = plan.principalRequested;
|
||||
loan->at(sfPeriodicPayment) = plan.properties.periodicPayment;
|
||||
loan->at(sfTotalValueOutstanding) = plan.properties.loanState.valueOutstanding;
|
||||
loan->at(sfManagementFeeOutstanding) = plan.properties.loanState.managementFeeDue;
|
||||
loan->at(sfPreviousPaymentDueDate) = 0;
|
||||
loan->at(sfNextPaymentDueDate) = startDate + plan.paymentInterval;
|
||||
loan->at(sfPaymentRemaining) = plan.paymentTotal;
|
||||
if (pending)
|
||||
loan->setFlag(lsfLoanPending);
|
||||
|
||||
return loan;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a pending loan for the two-step flow: charge the broker owner the
|
||||
* owner reserve, create the loan flagged pending, reserve the principal in
|
||||
* the vault, and link the loan into the broker directory only.
|
||||
*
|
||||
* @param ctx The apply context for the transaction.
|
||||
* @param accountID The account that submitted the transaction.
|
||||
* @param preFeeBalance The account balance before the transaction fee.
|
||||
* @param plan The validated and computed values for the loan.
|
||||
* @param j Log.
|
||||
*
|
||||
* @return tesSUCCESS on success, otherwise the error code describing the
|
||||
* failure.
|
||||
*/
|
||||
TER
|
||||
applyPendingLoan(
|
||||
ApplyContext& ctx,
|
||||
AccountID accountID,
|
||||
XRPAmount preFeeBalance,
|
||||
LoanPlan const& plan,
|
||||
beast::Journal const& j)
|
||||
{
|
||||
auto& view = ctx.view();
|
||||
|
||||
// Re-fetch the ledger entries doApply() already verified exist.
|
||||
auto const brokerSle = view.peek(keylet::loanBroker(plan.brokerID));
|
||||
if (!brokerSle)
|
||||
return tefBAD_LEDGER; // LCOV_EXCL_LINE
|
||||
AccountID 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
|
||||
|
||||
// Values derived from the ledger entries and the plan's scalars.
|
||||
AccountID const brokerPseudo = brokerSle->at(sfAccount);
|
||||
Asset const vaultAsset = vaultSle->at(sfAsset);
|
||||
auto const vaultScale = getAssetsTotalScale(vaultSle);
|
||||
auto const newDebtDelta = plan.principalRequested + plan.interestDue;
|
||||
|
||||
// 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).
|
||||
if (auto const ter =
|
||||
reserveLoanOwner(view, brokerOwner, brokerOwnerSle, accountID, preFeeBalance, j))
|
||||
return ter;
|
||||
|
||||
auto loan = buildLoan(ctx, plan, brokerSle, /*pending=*/true);
|
||||
view.insert(loan);
|
||||
|
||||
// Update the balances in the vault. Decrement the available assets, accrue
|
||||
// the interest due, and move the principal into the reserved bucket until
|
||||
// the borrower accepts.
|
||||
auto vaultAssetReservedProxy = vaultSle->at(sfAssetsReserved);
|
||||
auto vaultAvailableProxy = vaultSle->at(sfAssetsAvailable);
|
||||
auto vaultTotalProxy = vaultSle->at(sfAssetsTotal);
|
||||
vaultAvailableProxy -= plan.principalRequested;
|
||||
vaultTotalProxy += plan.interestDue;
|
||||
vaultAssetReservedProxy += plan.principalRequested;
|
||||
XRPL_ASSERT_PARTS(
|
||||
*vaultAvailableProxy <= *vaultTotalProxy,
|
||||
"xrpl::LoanSet::applyPendingLoan",
|
||||
"assets available must not be greater than assets outstanding");
|
||||
view.update(vaultSle);
|
||||
|
||||
if (auto const ter = updateLoanBroker(view, brokerSle, newDebtDelta, vaultAsset, vaultScale, j))
|
||||
return ter;
|
||||
|
||||
// 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;
|
||||
|
||||
associateAsset(*vaultSle, vaultAsset);
|
||||
associateAsset(*brokerSle, vaultAsset);
|
||||
associateAsset(*loan, vaultAsset);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an active loan for the immediate flow: charge the borrower the
|
||||
* owner reserve, disburse the funds, create the loan, update the vault, and
|
||||
* link the loan into both the broker and borrower directories.
|
||||
*
|
||||
* @param ctx The apply context for the transaction.
|
||||
* @param accountID The account that submitted the transaction.
|
||||
* @param preFeeBalance The account balance before the transaction fee.
|
||||
* @param plan The validated and computed values for the loan.
|
||||
* @param j Log.
|
||||
*
|
||||
* @return tesSUCCESS on success, otherwise the error code describing the
|
||||
* failure.
|
||||
*/
|
||||
TER
|
||||
applyImmediateLoan(
|
||||
ApplyContext& ctx,
|
||||
AccountID accountID,
|
||||
XRPAmount preFeeBalance,
|
||||
LoanPlan const& plan,
|
||||
beast::Journal const& j)
|
||||
{
|
||||
auto& view = ctx.view();
|
||||
|
||||
// Re-fetch the ledger entries doApply() already verified exist.
|
||||
auto const brokerSle = view.peek(keylet::loanBroker(plan.brokerID));
|
||||
if (!brokerSle)
|
||||
return tefBAD_LEDGER; // LCOV_EXCL_LINE
|
||||
AccountID 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
|
||||
auto const borrowerSle = view.peek(keylet::account(plan.borrower));
|
||||
if (!borrowerSle)
|
||||
return tefBAD_LEDGER; // LCOV_EXCL_LINE
|
||||
|
||||
// Values derived from the ledger entries and the plan's scalars.
|
||||
AccountID const brokerPseudo = brokerSle->at(sfAccount);
|
||||
AccountID const vaultPseudo = vaultSle->at(sfAccount);
|
||||
Asset const vaultAsset = vaultSle->at(sfAsset);
|
||||
auto const vaultScale = getAssetsTotalScale(vaultSle);
|
||||
auto const loanAssetsToBorrower = plan.principalRequested - plan.originationFee;
|
||||
auto const newDebtDelta = plan.principalRequested + plan.interestDue;
|
||||
|
||||
// In the immediate flow, the borrower is charged the owner reserve and the
|
||||
// funds are disbursed now.
|
||||
if (auto const ter =
|
||||
reserveLoanOwner(view, plan.borrower, borrowerSle, accountID, preFeeBalance, j))
|
||||
return ter;
|
||||
|
||||
// Disburse the principal to the borrower and the origination fee, if any,
|
||||
// to the broker owner, creating holdings as necessary.
|
||||
auto applyViewContext = ctx.getApplyViewContext();
|
||||
if (auto const ter = disburseLoan(
|
||||
applyViewContext,
|
||||
plan.borrower,
|
||||
borrowerSle,
|
||||
brokerOwner,
|
||||
brokerOwnerSle,
|
||||
vaultPseudo,
|
||||
vaultAsset,
|
||||
loanAssetsToBorrower,
|
||||
plan.originationFee,
|
||||
accountID,
|
||||
plan.counterparty,
|
||||
j))
|
||||
return ter;
|
||||
|
||||
auto loan = buildLoan(ctx, plan, brokerSle, /*pending=*/false);
|
||||
view.insert(loan);
|
||||
|
||||
// Update the balances in the vault. Decrement the available assets and
|
||||
// accrue the interest due.
|
||||
auto vaultAvailableProxy = vaultSle->at(sfAssetsAvailable);
|
||||
auto vaultTotalProxy = vaultSle->at(sfAssetsTotal);
|
||||
vaultAvailableProxy -= plan.principalRequested;
|
||||
vaultTotalProxy += plan.interestDue;
|
||||
XRPL_ASSERT_PARTS(
|
||||
*vaultAvailableProxy <= *vaultTotalProxy,
|
||||
"xrpl::LoanSet::applyImmediateLoan",
|
||||
"assets available must not be greater than assets outstanding");
|
||||
view.update(vaultSle);
|
||||
|
||||
if (auto const ter = updateLoanBroker(view, brokerSle, newDebtDelta, vaultAsset, vaultScale, j))
|
||||
return ter;
|
||||
|
||||
// Link the loan into the broker's directory, then make the borrower the
|
||||
// owner of the loan by linking it into the borrower's directory.
|
||||
if (auto const ter = linkLoanBroker(view, brokerPseudo, loan))
|
||||
return ter;
|
||||
|
||||
if (auto const ter = linkLoanBorrower(view, plan.borrower, loan))
|
||||
return ter;
|
||||
|
||||
associateAsset(*vaultSle, vaultAsset);
|
||||
associateAsset(*brokerSle, vaultAsset);
|
||||
associateAsset(*loan, vaultAsset);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool
|
||||
LoanSet::checkExtraFeatures(PreflightContext const& ctx)
|
||||
{
|
||||
@@ -66,46 +593,6 @@ LoanSet::getFlagsMask(PreflightContext const& ctx)
|
||||
return tfLoanSetMask;
|
||||
}
|
||||
|
||||
bool
|
||||
LoanSet::isTwoStepFlowEnabled(Rules const& rules)
|
||||
{
|
||||
return rules.enabled(featureLendingProtocolV1_1);
|
||||
}
|
||||
|
||||
bool
|
||||
LoanSet::isTwoStepFlow(STTx const& tx)
|
||||
{
|
||||
// The two-step (Borrower) flow is started when the LoanSet names a
|
||||
// Borrower and a StartDate but carries neither a Counterparty nor a
|
||||
// CounterpartySignature.
|
||||
return tx.isFieldPresent(sfBorrower) && tx.isFieldPresent(sfStartDate);
|
||||
}
|
||||
|
||||
bool
|
||||
LoanSet::isOneStepFlow(STTx const& tx)
|
||||
{
|
||||
return tx.isFieldPresent(sfCounterpartySignature);
|
||||
}
|
||||
|
||||
AccountID
|
||||
LoanSet::getCounterparty(STTx const& tx, AccountID const& brokerOwner)
|
||||
{
|
||||
return tx[~sfCounterparty].value_or(brokerOwner);
|
||||
}
|
||||
|
||||
AccountID
|
||||
LoanSet::getBorrower(STTx const& tx, AccountID const& brokerOwner, AccountID const& signingAccount)
|
||||
{
|
||||
// In the two-step (Borrower) flow the LoanBroker owner proposes the loan on
|
||||
// behalf of the named Borrower. In the immediate flow the Borrower is
|
||||
// whichever of the signer / counterparty is not the LoanBroker owner.
|
||||
if (isTwoStepFlow(tx))
|
||||
return tx[sfBorrower];
|
||||
|
||||
auto const counterparty = getCounterparty(tx, brokerOwner);
|
||||
return counterparty == brokerOwner ? signingAccount : counterparty;
|
||||
}
|
||||
|
||||
NotTEC
|
||||
LoanSet::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
@@ -306,21 +793,6 @@ LoanSet::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
return normalCost + (signerCount * baseFee);
|
||||
}
|
||||
|
||||
std::vector<OptionaledField<STNumber>> const&
|
||||
LoanSet::getValueFields()
|
||||
{
|
||||
static std::vector<OptionaledField<STNumber>> const kValueFields{
|
||||
~sfPrincipalRequested,
|
||||
~sfLoanOriginationFee,
|
||||
~sfLoanServiceFee,
|
||||
~sfLatePaymentFee,
|
||||
~sfClosePaymentFee
|
||||
// Overpayment fee is really a rate. Don't check it here.
|
||||
};
|
||||
|
||||
return kValueFields;
|
||||
}
|
||||
|
||||
TER
|
||||
LoanSet::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
@@ -486,354 +958,28 @@ LoanSet::preclaim(PreclaimContext const& ctx)
|
||||
TER
|
||||
LoanSet::doApply()
|
||||
{
|
||||
auto const& tx = ctx_.tx;
|
||||
auto& view = ctx_.view();
|
||||
bool const twoStepFlow = isTwoStepFlow(tx);
|
||||
auto const setup = setupLoan();
|
||||
if (!setup)
|
||||
return setup.error();
|
||||
|
||||
auto const brokerID = tx[sfLoanBrokerID];
|
||||
|
||||
// Only the LoanBroker and Vault entries are read here; doApply() validates
|
||||
// the loan against them and computes the plan. The broker owner, borrower,
|
||||
// and broker pseudo-account entries are re-fetched (and their existence
|
||||
// re-verified) by the flow functions that actually mutate them, so they are
|
||||
// not peeked here. Borrower existence is already guaranteed by preclaim().
|
||||
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
|
||||
Asset const vaultAsset = vaultSle->at(sfAsset);
|
||||
|
||||
auto const principalRequested = tx[sfPrincipalRequested];
|
||||
|
||||
auto vaultAvailableProxy = vaultSle->at(sfAssetsAvailable);
|
||||
auto vaultTotalProxy = vaultSle->at(sfAssetsTotal);
|
||||
auto const vaultScale = getAssetsTotalScale(vaultSle);
|
||||
if (vaultAvailableProxy < principalRequested)
|
||||
{
|
||||
JLOG(j_.warn()) << "Insufficient assets available in the Vault to fund the loan.";
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
}
|
||||
|
||||
TenthBips32 const interestRate{tx[~sfInterestRate].value_or(0)};
|
||||
|
||||
auto const paymentInterval = tx[~sfPaymentInterval].value_or(kDefaultPaymentInterval);
|
||||
auto const paymentTotal = tx[~sfPaymentTotal].value_or(kDefaultPaymentTotal);
|
||||
|
||||
auto const properties = computeLoanProperties(
|
||||
view.rules(),
|
||||
vaultAsset,
|
||||
principalRequested,
|
||||
interestRate,
|
||||
paymentInterval,
|
||||
paymentTotal,
|
||||
TenthBips16{brokerSle->at(sfManagementFeeRate)},
|
||||
vaultScale);
|
||||
|
||||
LoanState const state = constructLoanState(
|
||||
properties.loanState.valueOutstanding,
|
||||
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(
|
||||
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 originationFee = tx[~sfLoanOriginationFee].value_or(Number{});
|
||||
|
||||
auto const newDebtDelta = principalRequested + state.interestDue;
|
||||
auto const newDebtTotal = brokerSle->at(sfDebtTotal) + newDebtDelta;
|
||||
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 (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;
|
||||
}
|
||||
}
|
||||
// Bundle the validated and computed values for the flow functions. The
|
||||
// pending (two-step) and immediate flows each own their full sequence of
|
||||
// ledger mutations; nothing below is reordered relative to the prior
|
||||
// ledger mutations; nothing here is reordered relative to the prior
|
||||
// implementation.
|
||||
auto const brokerOwner = brokerSle->at(sfOwner);
|
||||
auto const participants = resolveParticipants(ctx_.tx, setup->brokerSle, accountID_);
|
||||
LoanPlan const plan{
|
||||
.brokerID = brokerID,
|
||||
.borrower = getBorrower(tx, brokerOwner, accountID_),
|
||||
.counterparty = getCounterparty(tx, brokerOwner),
|
||||
.principalRequested = principalRequested,
|
||||
.originationFee = originationFee,
|
||||
.interestDue = state.interestDue,
|
||||
.properties = properties,
|
||||
.paymentInterval = paymentInterval,
|
||||
.paymentTotal = paymentTotal};
|
||||
.brokerID = setup->brokerID,
|
||||
.borrower = participants.borrower,
|
||||
.counterparty = participants.counterparty,
|
||||
.principalRequested = setup->principalRequested,
|
||||
.originationFee = setup->originationFee,
|
||||
.interestDue = setup->interestDue,
|
||||
.properties = setup->properties,
|
||||
.paymentInterval = setup->paymentInterval,
|
||||
.paymentTotal = setup->paymentTotal};
|
||||
|
||||
return twoStepFlow ? applyPendingLoan(plan) : applyImmediateLoan(plan);
|
||||
}
|
||||
|
||||
std::shared_ptr<SLE>
|
||||
LoanSet::buildLoan(LoanPlan const& plan, SLE::ref brokerSle, bool pending)
|
||||
{
|
||||
auto const& tx = ctx_.tx;
|
||||
|
||||
// Get shortcuts to the loan property values
|
||||
auto const startDate = getStartDate(ctx_.view(), tx);
|
||||
auto const loanSequence = *brokerSle->at(sfLoanSequence);
|
||||
|
||||
// Create the loan
|
||||
auto loan = std::make_shared<SLE>(keylet::loan(plan.brokerID, loanSequence));
|
||||
|
||||
// Prevent copy/paste errors
|
||||
auto setLoanField = [&loan, &tx](auto const& field, std::uint32_t const defValue = 0) {
|
||||
// at() is smart enough to unseat a default field set to the default
|
||||
// value
|
||||
loan->at(field) = tx[field].value_or(defValue);
|
||||
};
|
||||
|
||||
// Set required and fixed tx fields
|
||||
loan->at(sfLoanScale) = plan.properties.loanScale;
|
||||
loan->at(sfStartDate) = startDate;
|
||||
loan->at(sfPaymentInterval) = plan.paymentInterval;
|
||||
loan->at(sfLoanSequence) = loanSequence;
|
||||
loan->at(sfLoanBrokerID) = plan.brokerID;
|
||||
loan->at(sfBorrower) = plan.borrower;
|
||||
// Set all other transaction fields directly from the transaction
|
||||
if (tx.isFlag(tfLoanOverpayment))
|
||||
loan->setFlag(lsfLoanOverpayment);
|
||||
setLoanField(~sfLoanOriginationFee);
|
||||
setLoanField(~sfLoanServiceFee);
|
||||
setLoanField(~sfLatePaymentFee);
|
||||
setLoanField(~sfClosePaymentFee);
|
||||
setLoanField(~sfOverpaymentFee);
|
||||
setLoanField(~sfInterestRate);
|
||||
setLoanField(~sfLateInterestRate);
|
||||
setLoanField(~sfCloseInterestRate);
|
||||
setLoanField(~sfOverpaymentInterestRate);
|
||||
setLoanField(~sfGracePeriod, kDefaultGracePeriod);
|
||||
// Set dynamic / computed fields to their initial values
|
||||
loan->at(sfPrincipalOutstanding) = plan.principalRequested;
|
||||
loan->at(sfPeriodicPayment) = plan.properties.periodicPayment;
|
||||
loan->at(sfTotalValueOutstanding) = plan.properties.loanState.valueOutstanding;
|
||||
loan->at(sfManagementFeeOutstanding) = plan.properties.loanState.managementFeeDue;
|
||||
loan->at(sfPreviousPaymentDueDate) = 0;
|
||||
loan->at(sfNextPaymentDueDate) = startDate + plan.paymentInterval;
|
||||
loan->at(sfPaymentRemaining) = plan.paymentTotal;
|
||||
if (pending)
|
||||
loan->setFlag(lsfLoanPending);
|
||||
|
||||
return loan;
|
||||
}
|
||||
|
||||
TER
|
||||
LoanSet::applyPendingLoan(LoanPlan const& plan)
|
||||
{
|
||||
auto& view = ctx_.view();
|
||||
|
||||
// Re-fetch the ledger entries doApply() already verified exist.
|
||||
auto const brokerSle = view.peek(keylet::loanBroker(plan.brokerID));
|
||||
if (!brokerSle)
|
||||
return tefBAD_LEDGER; // LCOV_EXCL_LINE
|
||||
AccountID 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
|
||||
|
||||
// Values derived from the ledger entries and the plan's scalars.
|
||||
AccountID const brokerPseudo = brokerSle->at(sfAccount);
|
||||
Asset const vaultAsset = vaultSle->at(sfAsset);
|
||||
auto const vaultScale = getAssetsTotalScale(vaultSle);
|
||||
auto const newDebtDelta = plan.principalRequested + plan.interestDue;
|
||||
|
||||
// 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).
|
||||
if (auto const ter =
|
||||
reserveLoanOwner(view, brokerOwner, brokerOwnerSle, accountID_, preFeeBalance_, j_))
|
||||
return ter;
|
||||
|
||||
auto loan = buildLoan(plan, brokerSle, /*pending=*/true);
|
||||
view.insert(loan);
|
||||
|
||||
// Update the balances in the vault. Decrement the available assets, accrue
|
||||
// the interest due, and move the principal into the reserved bucket until
|
||||
// the borrower accepts.
|
||||
auto vaultAssetReservedProxy = vaultSle->at(sfAssetsReserved);
|
||||
auto vaultAvailableProxy = vaultSle->at(sfAssetsAvailable);
|
||||
auto vaultTotalProxy = vaultSle->at(sfAssetsTotal);
|
||||
vaultAvailableProxy -= plan.principalRequested;
|
||||
vaultTotalProxy += plan.interestDue;
|
||||
vaultAssetReservedProxy += plan.principalRequested;
|
||||
XRPL_ASSERT_PARTS(
|
||||
*vaultAvailableProxy <= *vaultTotalProxy,
|
||||
"xrpl::LoanSet::applyPendingLoan",
|
||||
"assets available must not be greater than assets outstanding");
|
||||
view.update(vaultSle);
|
||||
|
||||
if (auto const ter =
|
||||
updateLoanBroker(view, brokerSle, newDebtDelta, vaultAsset, vaultScale, j_))
|
||||
return ter;
|
||||
|
||||
// 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;
|
||||
|
||||
associateAsset(*vaultSle, vaultAsset);
|
||||
associateAsset(*brokerSle, vaultAsset);
|
||||
associateAsset(*loan, vaultAsset);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
LoanSet::applyImmediateLoan(LoanPlan const& plan)
|
||||
{
|
||||
auto& view = ctx_.view();
|
||||
|
||||
// Re-fetch the ledger entries doApply() already verified exist.
|
||||
auto const brokerSle = view.peek(keylet::loanBroker(plan.brokerID));
|
||||
if (!brokerSle)
|
||||
return tefBAD_LEDGER; // LCOV_EXCL_LINE
|
||||
AccountID 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
|
||||
auto const borrowerSle = view.peek(keylet::account(plan.borrower));
|
||||
if (!borrowerSle)
|
||||
return tefBAD_LEDGER; // LCOV_EXCL_LINE
|
||||
|
||||
// Values derived from the ledger entries and the plan's scalars.
|
||||
AccountID const brokerPseudo = brokerSle->at(sfAccount);
|
||||
AccountID const vaultPseudo = vaultSle->at(sfAccount);
|
||||
Asset const vaultAsset = vaultSle->at(sfAsset);
|
||||
auto const vaultScale = getAssetsTotalScale(vaultSle);
|
||||
auto const loanAssetsToBorrower = plan.principalRequested - plan.originationFee;
|
||||
auto const newDebtDelta = plan.principalRequested + plan.interestDue;
|
||||
|
||||
// In the immediate flow, the borrower is charged the owner reserve and the
|
||||
// funds are disbursed now.
|
||||
if (auto const ter =
|
||||
reserveLoanOwner(view, plan.borrower, borrowerSle, accountID_, preFeeBalance_, j_))
|
||||
return ter;
|
||||
|
||||
// Disburse the principal to the borrower and the origination fee, if any,
|
||||
// to the broker owner, creating holdings as necessary.
|
||||
auto applyViewContext = ctx_.getApplyViewContext();
|
||||
if (auto const ter = disburseLoan(
|
||||
applyViewContext,
|
||||
plan.borrower,
|
||||
borrowerSle,
|
||||
brokerOwner,
|
||||
brokerOwnerSle,
|
||||
vaultPseudo,
|
||||
vaultAsset,
|
||||
loanAssetsToBorrower,
|
||||
plan.originationFee,
|
||||
accountID_,
|
||||
plan.counterparty,
|
||||
j_))
|
||||
return ter;
|
||||
|
||||
auto loan = buildLoan(plan, brokerSle, /*pending=*/false);
|
||||
view.insert(loan);
|
||||
|
||||
// Update the balances in the vault. Decrement the available assets and
|
||||
// accrue the interest due.
|
||||
auto vaultAvailableProxy = vaultSle->at(sfAssetsAvailable);
|
||||
auto vaultTotalProxy = vaultSle->at(sfAssetsTotal);
|
||||
vaultAvailableProxy -= plan.principalRequested;
|
||||
vaultTotalProxy += plan.interestDue;
|
||||
XRPL_ASSERT_PARTS(
|
||||
*vaultAvailableProxy <= *vaultTotalProxy,
|
||||
"xrpl::LoanSet::applyImmediateLoan",
|
||||
"assets available must not be greater than assets outstanding");
|
||||
view.update(vaultSle);
|
||||
|
||||
if (auto const ter =
|
||||
updateLoanBroker(view, brokerSle, newDebtDelta, vaultAsset, vaultScale, j_))
|
||||
return ter;
|
||||
|
||||
// Link the loan into the broker's directory, then make the borrower the
|
||||
// owner of the loan by linking it into the borrower's directory.
|
||||
if (auto const ter = linkLoanBroker(view, brokerPseudo, loan))
|
||||
return ter;
|
||||
|
||||
if (auto const ter = linkLoanBorrower(view, plan.borrower, loan))
|
||||
return ter;
|
||||
|
||||
associateAsset(*vaultSle, vaultAsset);
|
||||
associateAsset(*brokerSle, vaultAsset);
|
||||
associateAsset(*loan, vaultAsset);
|
||||
|
||||
return tesSUCCESS;
|
||||
return isTwoStepFlow(ctx_.tx) ? applyPendingLoan(ctx_, accountID_, preFeeBalance_, plan, j_)
|
||||
: applyImmediateLoan(ctx_, accountID_, preFeeBalance_, plan, j_);
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
Reference in New Issue
Block a user