Address issues

This commit is contained in:
JCW
2026-07-24 13:08:51 +01:00
parent 3c9255a3df
commit 2ebfd8f5dc
8 changed files with 1185 additions and 470 deletions

View File

@@ -25,10 +25,19 @@
#include <expected>
#include <string_view>
#include <utility>
#include <vector>
namespace xrpl {
/**
* The flow requested by a LoanSet transaction, determined from its fields.
*
* OneStep is the immediate flow, where the loan is created and disbursed in
* a single transaction. TwoStep is the pending (Borrower) flow, where the
* LoanBroker owner proposes a loan that the named Borrower must later accept.
* Invalid indicates that the fields do not match either flow shape.
*/
enum class LoanFlow { Invalid, OneStep, TwoStep };
/**
* Broker cover preclaim precision guard (fixCleanup3_2_0).
*
@@ -595,7 +604,7 @@ checkLoanFreeze(
reserveLoanOwner(
ApplyView& view,
AccountID const& borrower,
SLE::ref borrowerSle,
SLE::ref loanOwnerSle,
AccountID const& signingAccount,
XRPAmount preFeeBalance,
beast::Journal j);
@@ -607,9 +616,7 @@ reserveLoanOwner(
[[nodiscard]] TER
disburseLoan(
ApplyViewContext& viewContext,
AccountID const& borrower,
SLE::ref borrowerSle,
AccountID const& brokerOwner,
SLE::ref brokerOwnerSle,
AccountID const& vaultPseudo,
Asset const& vaultAsset,
@@ -619,37 +626,4 @@ disburseLoan(
AccountID const& counterparty,
beast::Journal j);
/**
* 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

View File

@@ -15,9 +15,6 @@
#include <xrpl/tx/Transactor.h>
#include <cstdint>
#include <expected>
#include <memory>
#include <vector>
namespace xrpl {
@@ -45,6 +42,9 @@ public:
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
static std::vector<OptionaledField<STNumber>> const&
getValueFields();
static TER
preclaim(PreclaimContext const& ctx);

View File

@@ -32,7 +32,6 @@
#include <memory>
#include <string_view>
#include <utility>
#include <vector>
namespace xrpl {
@@ -2199,15 +2198,18 @@ TER
reserveLoanOwner(
ApplyView& view,
AccountID const& borrower,
SLE::ref borrowerSle,
SLE::ref loanOwnerSle,
AccountID const& signingAccount,
XRPAmount preFeeBalance,
beast::Journal j)
{
increaseOwnerCount(view, borrowerSle, {}, 1, j);
XRPL_ASSERT(
loanOwnerSle && loanOwnerSle->getType() == ltACCOUNT_ROOT,
"xrpl::reserveLoanOwner : valid AccountRoot");
increaseOwnerCount(view, loanOwnerSle, {}, 1, j);
auto const balance =
signingAccount == borrower ? preFeeBalance : borrowerSle->at(sfBalance).value().xrp();
if (balance < accountReserve(view, borrowerSle, j))
signingAccount == borrower ? preFeeBalance : loanOwnerSle->at(sfBalance).value().xrp();
if (balance < accountReserve(view, loanOwnerSle, j))
return tecINSUFFICIENT_RESERVE;
return tesSUCCESS;
}
@@ -2215,9 +2217,7 @@ reserveLoanOwner(
TER
disburseLoan(
ApplyViewContext& viewContext,
AccountID const& borrower,
SLE::ref borrowerSle,
AccountID const& brokerOwner,
SLE::ref brokerOwnerSle,
AccountID const& vaultPseudo,
Asset const& vaultAsset,
@@ -2227,6 +2227,15 @@ disburseLoan(
AccountID const& counterparty,
beast::Journal j)
{
XRPL_ASSERT(
borrowerSle && borrowerSle->getType() == ltACCOUNT_ROOT,
"xrpl::disburseLoan : valid borrower AccountRoot");
XRPL_ASSERT(
brokerOwnerSle && brokerOwnerSle->getType() == ltACCOUNT_ROOT,
"xrpl::disburseLoan : valid broker owner AccountRoot");
AccountID const borrower = borrowerSle->at(sfAccount);
AccountID const brokerOwner = brokerOwnerSle->at(sfAccount);
// Account for the origination fee using two payments
//
// 1. Transfer loanAssetsAvailable (principalRequested - originationFee)
@@ -2290,40 +2299,4 @@ disburseLoan(
return tesSUCCESS;
}
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
adjustImpreciseNumber(brokerSle->at(sfDebtTotal), newDebtDelta, vaultAsset, vaultScale);
adjustLoanBrokerOwnerCount(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

View File

@@ -136,9 +136,7 @@ LoanAccept::doApply()
// to the broker owner.
if (auto const ter = disburseLoan(
applyViewContext,
borrower,
borrowerSle,
brokerOwner,
brokerOwnerSle,
vaultPseudo,
vaultAsset,
@@ -155,7 +153,7 @@ LoanAccept::doApply()
view.update(vaultSle);
// Make the borrower the owner of the loan.
if (auto const ter = linkLoanBorrower(view, borrower, loanSle))
if (auto const ter = dirLink(view, borrower, loanSle, sfOwnerNode))
return ter;
view.update(loanSle);

View File

@@ -18,6 +18,126 @@
namespace xrpl {
namespace {
TER
deletePendingLoan(
ApplyContext& ctx,
SLE::ref loanSle,
SLE::ref brokerSle,
SLE::ref vaultSle,
beast::Journal const& j)
{
auto& view = ctx.view();
auto const loanID = loanSle->key();
auto const brokerPseudoAccount = brokerSle->at(sfAccount);
auto const vaultAsset = vaultSle->at(sfAsset);
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 = constructLoanState(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.
vaultSle->at(sfAssetsAvailable) += principalOutstanding;
vaultSle->at(sfAssetsReserved) -= principalOutstanding;
vaultSle->at(sfAssetsTotal) -= state.interestDue;
view.update(vaultSle);
// Reverse the broker debt and outstanding loan count.
adjustImpreciseNumber(
brokerSle->at(sfDebtTotal),
-(principalOutstanding + state.interestDue),
vaultAsset,
vaultScale);
adjustLoanBrokerOwnerCount(view, brokerSle, -1, j);
// Release the owner reserve charged to the LoanBroker owner when the
// loan was proposed.
decreaseOwnerCount(view, brokerOwnerSle, {}, 1, j);
associateAsset(*brokerSle, vaultAsset);
associateAsset(*vaultSle, vaultAsset);
return tesSUCCESS;
}
TER
deleteActiveLoan(
ApplyContext& ctx,
SLE::ref loanSle,
SLE::ref brokerSle,
SLE::ref vaultSle,
beast::Journal const& j)
{
auto& view = ctx.view();
auto const loanID = loanSle->key();
auto const brokerPseudoAccount = brokerSle->at(sfAccount);
auto const vaultAsset = vaultSle->at(sfAsset);
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))
return tefBAD_LEDGER; // LCOV_EXCL_LINE
// Remove LoanID from Directory of the Borrower.
if (!view.dirRemove(keylet::ownerDir(borrower), loanSle->at(sfOwnerNode), loanID, false))
return tefBAD_LEDGER; // LCOV_EXCL_LINE
// Delete the Loan object
view.erase(loanSle);
// Decrement the LoanBroker's owner count.
adjustLoanBrokerOwnerCount(view, brokerSle, -1, j);
// If there are no loans left, then any remaining debt must be forgiven,
// because there is no other way to pay it back.
if (brokerSle->at(sfOwnerCount) == 0)
{
auto debtTotalProxy = brokerSle->at(sfDebtTotal);
if (*debtTotalProxy != beast::kZero)
{
XRPL_ASSERT_PARTS(
roundToAsset(
vaultSle->at(sfAsset),
debtTotalProxy,
getAssetsTotalScale(vaultSle),
Number::RoundingMode::TowardsZero) == beast::kZero,
"xrpl::LoanDelete::deleteActiveLoan",
"last loan, remaining debt rounds to zero");
debtTotalProxy = 0;
}
}
// Decrement the borrower's owner count
decreaseOwnerCountForObject(view, borrowerSle, loanSle, 1, j);
// These associations shouldn't do anything, but do them just to be safe
associateAsset(*loanSle, vaultAsset);
associateAsset(*brokerSle, vaultAsset);
associateAsset(*vaultSle, vaultAsset);
return tesSUCCESS;
}
} // namespace
bool
LoanDelete::checkExtraFeatures(PreflightContext const& ctx)
{
@@ -87,112 +207,18 @@ LoanDelete::doApply()
auto const brokerSle = view.peek(keylet::loanBroker(brokerID));
if (!brokerSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const brokerPseudoAccount = brokerSle->at(sfAccount);
auto const vaultSle = view.peek(keylet::vault(brokerSle->at(sfVaultID)));
if (!vaultSle)
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 = constructLoanState(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.
adjustImpreciseNumber(
brokerSle->at(sfDebtTotal),
-(principalOutstanding + state.interestDue),
vaultAsset,
vaultScale);
adjustLoanBrokerOwnerCount(view, brokerSle, -1, j_);
// Release the owner reserve charged to the LoanBroker owner when the
// loan was proposed.
decreaseOwnerCount(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))
return tefBAD_LEDGER; // LCOV_EXCL_LINE
// Remove LoanID from Directory of the Borrower.
if (!view.dirRemove(keylet::ownerDir(borrower), loanSle->at(sfOwnerNode), loanID, false))
return tefBAD_LEDGER; // LCOV_EXCL_LINE
// Delete the Loan object
view.erase(loanSle);
// Decrement the LoanBroker's owner count.
adjustLoanBrokerOwnerCount(view, brokerSle, -1, j_);
// If there are no loans left, then any remaining debt must be forgiven,
// because there is no other way to pay it back.
if (brokerSle->at(sfOwnerCount) == 0)
{
auto debtTotalProxy = brokerSle->at(sfDebtTotal);
if (*debtTotalProxy != beast::kZero)
{
XRPL_ASSERT_PARTS(
roundToAsset(
vaultSle->at(sfAsset),
debtTotalProxy,
getAssetsTotalScale(vaultSle),
Number::RoundingMode::TowardsZero) == beast::kZero,
"xrpl::LoanDelete::doApply",
"last loan, remaining debt rounds to zero");
debtTotalProxy = 0;
}
}
// Decrement the borrower's owner count
decreaseOwnerCountForObject(view, borrowerSle, loanSle, 1, j_);
// These associations shouldn't do anything, but do them just to be safe
associateAsset(*loanSle, vaultAsset);
associateAsset(*brokerSle, vaultAsset);
associateAsset(*vaultSle, vaultAsset);
return tesSUCCESS;
return loanSle->isFlag(lsfLoanPending)
? deletePendingLoan(ctx_, loanSle, brokerSle, vaultSle, j_)
: deleteActiveLoan(ctx_, loanSle, brokerSle, vaultSle, j_);
}
void

View File

@@ -191,6 +191,12 @@ LoanPay::preclaim(PreclaimContext const& ctx)
return tecNO_ENTRY;
}
if (loanSle->isFlag(lsfLoanPending))
{
JLOG(ctx.j.warn()) << "Loan is pending acceptance. A pending loan can not be paid.";
return tecNO_PERMISSION;
}
if (loanSle->at(sfBorrower) != account)
{
JLOG(ctx.j.warn()) << "Loan does not belong to the account.";

View File

@@ -6,6 +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/SponsorHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
@@ -101,30 +102,33 @@ isTwoStepFlowEnabled(Rules const& rules)
}
/**
* Returns true if the transaction is using the two-step flow.
* Determines which LoanFlow a LoanSet transaction is requesting from its
* fields. The two-step flow is only available when the corresponding
* amendment is enabled; when it is not, transactions carrying two-step
* fields are reported as Invalid.
*/
bool
isTwoStepFlow(STTx const& tx)
LoanFlow
getLoanFlow(STTx const& tx, bool twoStepFlowEnabled)
{
// 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 const isBatch = tx.isFlag(tfInnerBatchTxn);
bool const hasCounterparty = tx.isFieldPresent(sfCounterparty);
bool const hasCounterpartySignature = tx.isFieldPresent(sfCounterpartySignature);
bool const hasBorrower = tx.isFieldPresent(sfBorrower);
bool const hasStartDate = tx.isFieldPresent(sfStartDate);
bool const hasBorrowerOrStartDate = hasBorrower || hasStartDate;
/**
* Returns true if the transaction is using the one-step flow.
*/
bool
isOneStepFlow(STTx const& tx)
{
return tx.isFieldPresent(sfCounterpartySignature);
if (twoStepFlowEnabled && hasBorrower && hasStartDate && !hasCounterparty &&
!hasCounterpartySignature)
return LoanFlow::TwoStep;
if ((hasCounterpartySignature || isBatch) && !hasBorrowerOrStartDate)
return LoanFlow::OneStep;
return LoanFlow::Invalid;
}
std::uint32_t
getStartDate(ReadView const& view, STTx const& tx)
{
if (isTwoStepFlow(tx) && isTwoStepFlowEnabled(view.rules()))
if (getLoanFlow(tx, isTwoStepFlowEnabled(view.rules())) == LoanFlow::TwoStep)
{
return tx[sfStartDate];
}
@@ -132,51 +136,39 @@ getStartDate(ReadView const& view, STTx const& tx)
}
/**
* Resolves the borrower and counterparty accounts, reading the LoanBroker
* owner from the broker entry.
* Resolves the borrower and counterparty accounts for a LoanSet, 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.
* the LoanBroker owner. In the two-step (Borrower) flow the borrower is the
* named Borrower; in the immediate flow the borrower 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.
* @param flow The flow the transaction is exercising.
*
* @return The resolved borrower and counterparty accounts.
*/
Participants
resolveParticipants(STTx const& tx, SLE::const_ref brokerSle, AccountID const& signingAccount)
resolveParticipants(
STTx const& tx,
SLE::const_ref brokerSle,
AccountID const& signingAccount,
LoanFlow flow)
{
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;
AccountID const borrower = [&]() -> AccountID {
if (flow == LoanFlow::TwoStep)
return tx[sfBorrower];
return 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.
@@ -253,7 +245,7 @@ setupLoan(ApplyContext& ctx, beast::Journal const& j)
}
// Check that relevant values won't lose precision. This is mostly only
// relevant for IOU assets.
for (auto const& field : getValueFields())
for (auto const& field : LoanSet::getValueFields())
{
if (auto const value = tx[field];
value && !isRounded(vaultAsset, *value, properties.loanScale))
@@ -461,12 +453,20 @@ 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;
// Update the balances in the loan broker
adjustImpreciseNumber(brokerSle->at(sfDebtTotal), newDebtDelta, vaultAsset, vaultScale);
adjustLoanBrokerOwnerCount(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);
// 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))
if (auto const ter = dirLink(view, brokerPseudo, loan, sfLoanBrokerNode))
return ter;
associateAsset(*vaultSle, vaultAsset);
@@ -534,9 +534,7 @@ applyImmediateLoan(
auto applyViewContext = ctx.getApplyViewContext();
if (auto const ter = disburseLoan(
applyViewContext,
plan.borrower,
borrowerSle,
brokerOwner,
brokerOwnerSle,
vaultPseudo,
vaultAsset,
@@ -562,15 +560,23 @@ 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;
// Update the balances in the loan broker
adjustImpreciseNumber(brokerSle->at(sfDebtTotal), newDebtDelta, vaultAsset, vaultScale);
adjustLoanBrokerOwnerCount(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);
// 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))
if (auto const ter = dirLink(view, brokerPseudo, loan, sfLoanBrokerNode))
return ter;
if (auto const ter = linkLoanBorrower(view, plan.borrower, loan))
if (auto const ter = dirLink(view, plan.borrower, loan, sfOwnerNode))
return ter;
associateAsset(*vaultSle, vaultAsset);
@@ -584,7 +590,14 @@ applyImmediateLoan(
bool
LoanSet::checkExtraFeatures(PreflightContext const& ctx)
{
return checkLendingProtocolDependencies(ctx.rules, ctx.tx);
if (!checkLendingProtocolDependencies(ctx.rules, ctx.tx))
return false;
// The two-step (Borrower) flow fields (Borrower / StartDate) require the
// two-step flow to be enabled.
bool const hasBorrowerOrStartDate =
ctx.tx.isFieldPresent(sfBorrower) || ctx.tx.isFieldPresent(sfStartDate);
return isTwoStepFlowEnabled(ctx.rules) || !hasBorrowerOrStartDate;
}
std::uint32_t
@@ -624,54 +637,18 @@ LoanSet::preflight(PreflightContext const& ctx)
}();
bool const twoStepFlowEnabled = isTwoStepFlowEnabled(ctx.rules);
bool const twoStepFlow = isTwoStepFlow(tx);
bool const 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.
if (!tx.isFlag(tfInnerBatchTxn) && !counterPartySig && !twoStepFlowEnabled)
if (getLoanFlow(tx, twoStepFlowEnabled) == LoanFlow::Invalid)
{
JLOG(ctx.j.warn()) << "LoanSet transaction must have a CounterpartySignature.";
return temBAD_SIGNER;
}
if (twoStepFlowEnabled)
{
if (!twoStepFlow && !oneStepFlow)
// Before the two-step (Borrower) flow was introduced by V1.1, a
// CounterpartySignature was mandatory for every non-batch transaction.
if (!twoStepFlowEnabled)
{
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;
}
}
}
else
{
if (tx.isFieldPresent(sfBorrower) || tx.isFieldPresent(sfStartDate))
{
JLOG(ctx.j.warn()) << "LoanSet transaction cannot specify a Borrower with a "
"StartDate without the two-step flow being enabled.";
return temDISABLED;
JLOG(ctx.j.warn()) << "LoanSet transaction must have a CounterpartySignature.";
return temBAD_SIGNER;
}
JLOG(ctx.j.warn()) << "LoanSet transaction must specify either a Borrower with a "
"StartDate or a CounterpartySignature.";
return temINVALID;
}
if (counterPartySig)
@@ -741,7 +718,7 @@ LoanSet::checkSign(PreclaimContext const& ctx)
// In the two-step (Borrower) flow introduced by V1.1 there is no
// counterparty, so there is no CounterpartySignature to check.
if (isTwoStepFlowEnabled(ctx.view.rules()) && isTwoStepFlow(ctx.tx))
if (getLoanFlow(ctx.tx, isTwoStepFlowEnabled(ctx.view.rules())) == LoanFlow::TwoStep)
return tesSUCCESS;
// Counter signer is optional. If it's not specified, it's assumed to be
@@ -793,6 +770,21 @@ 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)
{
@@ -857,36 +849,31 @@ LoanSet::preclaim(PreclaimContext const& ctx)
return tecNO_ENTRY;
}
auto const brokerOwner = brokerSle->at(sfOwner);
bool const twoStepFlow = isTwoStepFlow(tx);
auto const flow = getLoanFlow(tx, isTwoStepFlowEnabled(ctx.view.rules()));
bool const twoStepFlow = flow == LoanFlow::TwoStep;
auto const participants = resolveParticipants(tx, brokerSle, account, flow);
// 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> {
// 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.
if (account != brokerOwner)
{
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];
JLOG(ctx.j.warn()) << "Account is not the owner of the LoanBroker.";
return tecNO_PERMISSION;
}
auto const counterparty = tx[~sfCounterparty].value_or(brokerOwner);
if (account != brokerOwner && counterparty != brokerOwner)
if (participants.counterparty != brokerOwner)
{
JLOG(ctx.j.warn()) << "Neither Account nor Counterparty are the owner "
"of the LoanBroker.";
return std::unexpected(tecNO_PERMISSION);
return tecNO_PERMISSION;
}
return counterparty == brokerOwner ? account : counterparty;
}();
if (!maybeBorrower)
return maybeBorrower.error();
}
auto borrower = *maybeBorrower;
auto const borrower = participants.borrower;
auto const brokerPseudo = brokerSle->at(sfAccount);
if (auto const borrowerSle = ctx.view.read(keylet::account(borrower)); !borrowerSle)
{
@@ -945,7 +932,7 @@ LoanSet::preclaim(PreclaimContext const& ctx)
if (auto const ter = requireAuth(ctx.view, asset, brokerOwner, AuthType::WeakAuth))
return ter;
if (tx[sfStartDate] <= currentLedgerCloseTime(ctx.view))
if (hasExpired(ctx.view, tx[~sfStartDate]))
{
JLOG(ctx.j.warn()) << "Start date is in the past.";
return tecEXPIRED;
@@ -958,7 +945,7 @@ LoanSet::preclaim(PreclaimContext const& ctx)
TER
LoanSet::doApply()
{
auto const setup = setupLoan();
auto const setup = setupLoan(ctx_, j_);
if (!setup)
return setup.error();
@@ -966,7 +953,9 @@ LoanSet::doApply()
// pending (two-step) and immediate flows each own their full sequence of
// ledger mutations; nothing here is reordered relative to the prior
// implementation.
auto const participants = resolveParticipants(ctx_.tx, setup->brokerSle, accountID_);
auto const flow = getLoanFlow(ctx_.tx, isTwoStepFlowEnabled(ctx_.view().rules()));
bool const twoStepFlow = flow == LoanFlow::TwoStep;
auto const participants = resolveParticipants(ctx_.tx, setup->brokerSle, accountID_, flow);
LoanPlan const plan{
.brokerID = setup->brokerID,
.borrower = participants.borrower,
@@ -978,8 +967,8 @@ LoanSet::doApply()
.paymentInterval = setup->paymentInterval,
.paymentTotal = setup->paymentTotal};
return isTwoStepFlow(ctx_.tx) ? applyPendingLoan(ctx_, accountID_, preFeeBalance_, plan, j_)
: applyImmediateLoan(ctx_, accountID_, preFeeBalance_, plan, j_);
return twoStepFlow ? applyPendingLoan(ctx_, accountID_, preFeeBalance_, plan, j_)
: applyImmediateLoan(ctx_, accountID_, preFeeBalance_, plan, j_);
}
void

File diff suppressed because it is too large Load Diff