mirror of
https://github.com/XRPLF/rippled.git
synced 2026-07-25 08:00:22 +00:00
Address issues
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.";
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user