mirror of
https://github.com/XRPLF/rippled.git
synced 2026-07-16 11:40:24 +00:00
Fix issues
This commit is contained in:
@@ -1052,7 +1052,7 @@ TRANSACTION(ttLOAN_SET, 80, LoanSet,
|
||||
TRANSACTION(ttLOAN_DELETE, 81, LoanDelete,
|
||||
Delegation::NotDelegable,
|
||||
featureLendingProtocol,
|
||||
NoPriv, ({
|
||||
MayModifyVault, ({
|
||||
{sfLoanID, SoeRequired},
|
||||
}))
|
||||
|
||||
|
||||
@@ -96,6 +96,7 @@ class ValidVault
|
||||
Number principalOutstanding = 0;
|
||||
Number totalValueOutstanding = 0;
|
||||
Number managementFeeOutstanding = 0;
|
||||
uint32_t flags = 0;
|
||||
|
||||
// Interest booked to the vault at loan creation: the portion of the
|
||||
// total value owed that is neither principal nor broker management fee.
|
||||
|
||||
@@ -24,9 +24,11 @@ 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, Rules const& rules);
|
||||
isTwoStepFlow(STTx const& tx);
|
||||
/* Returns true if the transaction is using the one-step flow. */
|
||||
static bool
|
||||
isOneStepFlow(STTx const& tx);
|
||||
|
||||
@@ -87,6 +87,7 @@ ValidVault::Loan::make(SLE const& from)
|
||||
self.principalOutstanding = from.at(sfPrincipalOutstanding);
|
||||
self.totalValueOutstanding = from.at(sfTotalValueOutstanding);
|
||||
self.managementFeeOutstanding = from.at(sfManagementFeeOutstanding);
|
||||
self.flags = from.getFlags();
|
||||
return self;
|
||||
}
|
||||
|
||||
@@ -1110,11 +1111,22 @@ ValidVault::finalize(
|
||||
!beforeVault_.empty(), "xrpl::ValidVault::finalize : loan set updated a vault");
|
||||
auto const& beforeVault = beforeVault_[0];
|
||||
|
||||
// A loan set must create exactly one loan object; the interest
|
||||
// it books is the only permitted change to assets outstanding.
|
||||
if (afterLoan_.size() != 1 || !beforeLoan_.empty())
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: loan set must create exactly one loan";
|
||||
return false; // That's all we can do
|
||||
}
|
||||
auto const& loan = afterLoan_[0];
|
||||
auto const isPendingLoan = (loan.flags & lsfLoanPending) != 0;
|
||||
|
||||
// Funding a loan moves the requested principal out of the vault
|
||||
// pseudo-account to the borrower (and, if any, the origination
|
||||
// fee to the broker owner).
|
||||
auto const maybeVaultDeltaAssets = deltaAssets(afterVault.pseudoId);
|
||||
if (!maybeVaultDeltaAssets)
|
||||
if (!maybeVaultDeltaAssets && !isPendingLoan)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: loan set must change vault balance";
|
||||
@@ -1132,7 +1144,7 @@ ValidVault::finalize(
|
||||
|
||||
auto const vaultDeltaAssets =
|
||||
roundToAsset(vaultAsset, maybeVaultDeltaAssets->delta, minScale);
|
||||
if (vaultDeltaAssets != principalDelta)
|
||||
if (vaultDeltaAssets != principalDelta && !isPendingLoan)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: loan set must decrease vault balance "
|
||||
@@ -1150,16 +1162,6 @@ ValidVault::finalize(
|
||||
result = false;
|
||||
}
|
||||
|
||||
// A loan set must create exactly one loan object; the interest
|
||||
// it books is the only permitted change to assets outstanding.
|
||||
if (afterLoan_.size() != 1 || !beforeLoan_.empty())
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: loan set must create exactly one loan";
|
||||
return false; // That's all we can do
|
||||
}
|
||||
auto const& loan = afterLoan_[0];
|
||||
|
||||
// The created loan must record exactly the principal the vault
|
||||
// released. Otherwise the borrower's claim (and thus the assets
|
||||
// booked back to the vault on repayment) is decoupled from the
|
||||
@@ -1536,6 +1538,101 @@ ValidVault::finalize(
|
||||
return result;
|
||||
}
|
||||
|
||||
case ttLOAN_DELETE: {
|
||||
bool result = true;
|
||||
|
||||
XRPL_ASSERT(
|
||||
!beforeVault_.empty(),
|
||||
"xrpl::ValidVault::finalize : loan delete updated a vault");
|
||||
auto const& beforeVault = beforeVault_[0];
|
||||
|
||||
// Only the deletion of a pending loan touches the vault: it
|
||||
// reverses the bookkeeping LoanSet performed at proposal time.
|
||||
// The reserved principal returns to the available pool and the
|
||||
// booked interest is removed from assets outstanding. No funds
|
||||
// move, so the vault (pseudo-account) balance must not change.
|
||||
// (Deleting an active loan never modifies the vault, so it does
|
||||
// not reach this switch.)
|
||||
auto const maybeVaultDeltaAssets = deltaAssets(afterVault.pseudoId);
|
||||
auto const vaultDelta = maybeVaultDeltaAssets.value_or(
|
||||
DeltaInfo{.delta = kZero, .scale = scale(afterVault.assetsTotal, vaultAsset)});
|
||||
|
||||
// Get the posterior scale to round calculations to
|
||||
auto const minScale = computeVaultMinScale(vaultDelta, view.rules());
|
||||
|
||||
auto const vaultDeltaAssets = roundToAsset(vaultAsset, vaultDelta.delta, minScale);
|
||||
if (vaultDeltaAssets != kZero)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: loan delete must not change vault "
|
||||
"balance";
|
||||
result = false;
|
||||
}
|
||||
|
||||
// A pending loan delete removes exactly the loan being deleted;
|
||||
// its principal outstanding is returned to the available pool.
|
||||
if (beforeLoan_.size() != 1 || !afterLoan_.empty())
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: loan delete must delete exactly one "
|
||||
"loan";
|
||||
return false; // That's all we can do
|
||||
}
|
||||
auto const& loan = beforeLoan_[0];
|
||||
|
||||
auto const principalDelta =
|
||||
roundToAsset(vaultAsset, loan.principalOutstanding, minScale);
|
||||
|
||||
// The reserved principal (held back when the pending loan was
|
||||
// created) must be released by exactly the loan's principal.
|
||||
auto const assetsReservedDelta = roundToAsset(
|
||||
vaultAsset, afterVault.assetsReserved - beforeVault.assetsReserved, minScale);
|
||||
if (assetsReservedDelta != -principalDelta)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: loan delete must decrease assets "
|
||||
"reserved by the principal outstanding";
|
||||
result = false;
|
||||
}
|
||||
|
||||
// That same principal returns to the available pool.
|
||||
auto const assetAvailableDelta = roundToAsset(
|
||||
vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale);
|
||||
if (assetAvailableDelta != principalDelta)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: loan delete must increase assets "
|
||||
"available by the principal outstanding";
|
||||
result = false;
|
||||
}
|
||||
|
||||
// The interest booked at loan creation is reversed: assets
|
||||
// outstanding fall by exactly the interest due removed with the
|
||||
// loan.
|
||||
auto const assetsTotalDelta = roundToAsset(
|
||||
vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
|
||||
auto const interestDue = roundToAsset(vaultAsset, loan.interestDue(), minScale);
|
||||
if (assetsTotalDelta != -interestDue)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: loan delete must decrease assets "
|
||||
"outstanding by the interest due";
|
||||
result = false;
|
||||
}
|
||||
|
||||
// A loan delete neither mints nor burns vault shares.
|
||||
if (beforeShares && updatedShares &&
|
||||
beforeShares->sharesTotal != updatedShares->sharesTotal)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: loan delete must not change shares "
|
||||
"outstanding";
|
||||
result = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// LCOV_EXCL_START
|
||||
default:
|
||||
UNREACHABLE("xrpl::ValidVault::finalize : unknown transaction type");
|
||||
|
||||
@@ -45,7 +45,7 @@ currentLedgerCloseTime(ReadView const& view)
|
||||
std::uint32_t
|
||||
LoanSet::getStartDate(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
if (isTwoStepFlow(tx, view.rules()))
|
||||
if (isTwoStepFlow(tx) && isTwoStepFlowEnabled(view.rules()))
|
||||
{
|
||||
return tx[sfStartDate];
|
||||
}
|
||||
@@ -65,11 +65,14 @@ LoanSet::getFlagsMask(PreflightContext const& ctx)
|
||||
}
|
||||
|
||||
bool
|
||||
LoanSet::isTwoStepFlow(STTx const& tx, Rules const& rules)
|
||||
LoanSet::isTwoStepFlowEnabled(Rules const& rules)
|
||||
{
|
||||
if (!rules.enabled(featureLendingProtocolV1_1))
|
||||
return false;
|
||||
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.
|
||||
@@ -107,41 +110,54 @@ LoanSet::preflight(PreflightContext const& ctx)
|
||||
return std::nullopt;
|
||||
}();
|
||||
|
||||
bool twoStepFlow = isTwoStepFlow(tx, ctx.rules);
|
||||
bool oneStepFlow = isOneStepFlow(tx);
|
||||
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 && !twoStepFlow)
|
||||
if (!tx.isFlag(tfInnerBatchTxn) && !counterPartySig && !twoStepFlowEnabled)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "LoanSet transaction must have a CounterpartySignature.";
|
||||
return temBAD_SIGNER;
|
||||
}
|
||||
|
||||
if (!twoStepFlow && !oneStepFlow)
|
||||
if (twoStepFlowEnabled)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "LoanSet transaction must specify either a Borrower with a "
|
||||
"StartDate or a CounterpartySignature.";
|
||||
return temINVALID;
|
||||
}
|
||||
if (!twoStepFlow && !oneStepFlow)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "LoanSet transaction must specify either a Borrower with a "
|
||||
"StartDate or a CounterpartySignature.";
|
||||
return temINVALID;
|
||||
}
|
||||
|
||||
if (oneStepFlow)
|
||||
if (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 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;
|
||||
JLOG(ctx.j.warn()) << "LoanSet transaction cannot specify a Borrower with a "
|
||||
"StartDate without the two-step flow being enabled.";
|
||||
return temDISABLED;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,7 +228,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 (isTwoStepFlow(ctx.tx, ctx.view.rules()))
|
||||
if (isTwoStepFlowEnabled(ctx.view.rules()) && isTwoStepFlow(ctx.tx))
|
||||
return tesSUCCESS;
|
||||
|
||||
// Counter signer is optional. If it's not specified, it's assumed to be
|
||||
@@ -343,7 +359,7 @@ LoanSet::preclaim(PreclaimContext const& ctx)
|
||||
return tecNO_ENTRY;
|
||||
}
|
||||
auto const brokerOwner = brokerSle->at(sfOwner);
|
||||
bool const twoStepFlow = isTwoStepFlow(tx, ctx.view.rules());
|
||||
bool const twoStepFlow = isTwoStepFlow(tx);
|
||||
|
||||
// Determine the Borrower and validate the submitter's permission. In the
|
||||
// two-step flow the LoanBroker owner proposes the loan on behalf of the
|
||||
@@ -422,7 +438,7 @@ LoanSet::preclaim(PreclaimContext const& ctx)
|
||||
if (tx[sfStartDate] <= currentLedgerCloseTime(ctx.view))
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Start date is in the past.";
|
||||
return tecTOO_SOON;
|
||||
return tecEXPIRED;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -434,7 +450,7 @@ LoanSet::doApply()
|
||||
{
|
||||
auto const& tx = ctx_.tx;
|
||||
auto& view = ctx_.view();
|
||||
bool const twoStepFlow = isTwoStepFlow(tx, view.rules());
|
||||
bool const twoStepFlow = isTwoStepFlow(tx);
|
||||
|
||||
auto const brokerID = tx[sfLoanBrokerID];
|
||||
|
||||
|
||||
@@ -3461,6 +3461,277 @@ protected:
|
||||
nullptr);
|
||||
}
|
||||
|
||||
// Exercises the two-step (LendingProtocolV1_1) flow, where the LoanBroker
|
||||
// owner proposes a pending Loan (LoanSet with a Borrower and StartDate) that
|
||||
// the Borrower later accepts (LoanAccept) or that either party cancels
|
||||
// (LoanDelete). Requires the LendingProtocolV1_1 amendment.
|
||||
void
|
||||
testTwoStep(FeatureBitset features)
|
||||
{
|
||||
using namespace jtx;
|
||||
using namespace jtx::loan;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
Account const lender{"lender"}; // Vault + LoanBroker owner
|
||||
Account const borrower{"borrower"};
|
||||
Account const evan{"evan"}; // unrelated third party
|
||||
|
||||
PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
|
||||
|
||||
// Loan terms shared across the scenarios.
|
||||
Number const principal = xrpAsset(200).number();
|
||||
auto const interest = TenthBips32{50'000};
|
||||
std::uint32_t const payTotal = 10;
|
||||
std::uint32_t const payInterval = 200;
|
||||
|
||||
// Build a funded environment with a Vault + LoanBroker owned by
|
||||
// `lender`, and return the broker.
|
||||
auto const makeBroker = [&](Env& env) -> BrokerInfo {
|
||||
env.fund(XRP(100'000'000), noripple(lender));
|
||||
env.fund(XRP(1'000'000), borrower, evan);
|
||||
env.close();
|
||||
return createVaultAndBroker(env, xrpAsset, lender);
|
||||
};
|
||||
|
||||
// The keylet of the next loan the broker will create.
|
||||
auto const nextLoanKeylet = [&](Env& env, BrokerInfo const& broker) -> Keylet {
|
||||
auto const brokerSle = env.le(broker.brokerKeylet());
|
||||
return keylet::loan(broker.brokerID, brokerSle->at(sfLoanSequence));
|
||||
};
|
||||
|
||||
// A StartDate comfortably in the future.
|
||||
auto const futureStart = [&](Env& env) -> std::uint32_t {
|
||||
return (env.now() + 1h).time_since_epoch().count();
|
||||
};
|
||||
|
||||
// Snapshot of the vault's asset accounting.
|
||||
struct VaultAmounts
|
||||
{
|
||||
Number available;
|
||||
Number reserved;
|
||||
Number total;
|
||||
};
|
||||
auto const readVault = [&](Env& env, BrokerInfo const& broker) -> VaultAmounts {
|
||||
auto const v = env.le(broker.vaultKeylet());
|
||||
return {
|
||||
.available = v->at(sfAssetsAvailable),
|
||||
.reserved = v->at(sfAssetsReserved),
|
||||
.total = v->at(sfAssetsTotal)};
|
||||
};
|
||||
|
||||
// Submit a valid two-step proposal from `proposer` on behalf of
|
||||
// `theBorrower`, with the supplied StartDate and any extra functors.
|
||||
auto const propose = [&](Env& env,
|
||||
BrokerInfo const& broker,
|
||||
Account const& proposer,
|
||||
Account const& theBorrower,
|
||||
std::uint32_t startDate,
|
||||
auto const&... extra) {
|
||||
env(set(proposer, broker.brokerID, principal),
|
||||
kBorrower(theBorrower),
|
||||
kStartDate(startDate),
|
||||
kInterestRate(interest),
|
||||
kPaymentTotal(payTotal),
|
||||
kPaymentInterval(payInterval),
|
||||
extra...);
|
||||
};
|
||||
|
||||
auto const featureEnabled = [&]() -> bool {
|
||||
return (features & featureLendingProtocolV1_1).any();
|
||||
}();
|
||||
|
||||
if (!featureEnabled)
|
||||
{
|
||||
testcase("Two-step: rejected as before");
|
||||
|
||||
Env env(*this, features);
|
||||
auto const broker = makeBroker(env);
|
||||
propose(env, broker, lender, borrower, futureStart(env), Ter(temBAD_SIGNER));
|
||||
|
||||
// Rest of the tests are not applicable
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
testcase("Two-step: propose then accept");
|
||||
|
||||
Env env(*this, features);
|
||||
auto const broker = makeBroker(env);
|
||||
|
||||
auto const vault0 = readVault(env, broker);
|
||||
auto const lenderOwners0 = env.ownerCount(lender);
|
||||
auto const borrowerOwners0 = env.ownerCount(borrower);
|
||||
|
||||
auto const loanKeylet = nextLoanKeylet(env, broker);
|
||||
propose(env, broker, lender, borrower, futureStart(env));
|
||||
env.close();
|
||||
|
||||
// The proposal creates a pending Loan, linked only into the broker
|
||||
// pseudo-account's directory.
|
||||
if (auto const loan = env.le(loanKeylet); BEAST_EXPECT(loan))
|
||||
{
|
||||
BEAST_EXPECT(loan->isFlag(lsfLoanPending));
|
||||
BEAST_EXPECT(loan->at(sfBorrower) == borrower.id());
|
||||
BEAST_EXPECT(loan->isFieldPresent(sfLoanBrokerNode));
|
||||
BEAST_EXPECT(!loan->isFieldPresent(sfOwnerNode));
|
||||
}
|
||||
|
||||
// The owner reserve is charged to the broker owner, not the
|
||||
// borrower.
|
||||
BEAST_EXPECT(env.ownerCount(lender) == lenderOwners0 + 1);
|
||||
BEAST_EXPECT(env.ownerCount(borrower) == borrowerOwners0);
|
||||
|
||||
// Vault bookkeeping: Available -= P, Reserved += P, Total +=
|
||||
// InterestDue.
|
||||
auto const vault1 = readVault(env, broker);
|
||||
BEAST_EXPECT(vault1.available == vault0.available - principal);
|
||||
BEAST_EXPECT(vault1.reserved == vault0.reserved + principal);
|
||||
BEAST_EXPECT(vault1.total > vault0.total);
|
||||
Number const interestDue = vault1.total - vault0.total;
|
||||
|
||||
// Capture pre-acceptance balances to verify disbursement.
|
||||
auto const vaultPseudo = [&]() {
|
||||
auto const v = env.le(broker.vaultKeylet());
|
||||
return Account("vault pseudo-account", v->at(sfAccount));
|
||||
}();
|
||||
STAmount const pseudoBal0 = env.balance(vaultPseudo).value();
|
||||
STAmount const borrowerBal0 = env.balance(borrower).value();
|
||||
|
||||
env(accept(borrower, loanKeylet.key));
|
||||
env.close();
|
||||
|
||||
// The loan is now active and linked into the borrower's directory.
|
||||
if (auto const loan = env.le(loanKeylet); BEAST_EXPECT(loan))
|
||||
{
|
||||
BEAST_EXPECT(!loan->isFlag(lsfLoanPending));
|
||||
BEAST_EXPECT(loan->isFieldPresent(sfLoanBrokerNode));
|
||||
BEAST_EXPECT(loan->isFieldPresent(sfOwnerNode));
|
||||
}
|
||||
|
||||
// The reserve is swapped from the broker owner to the borrower.
|
||||
BEAST_EXPECT(env.ownerCount(lender) == lenderOwners0);
|
||||
BEAST_EXPECT(env.ownerCount(borrower) == borrowerOwners0 + 1);
|
||||
|
||||
// Reserved principal is released; Available and Total are unchanged
|
||||
// from the proposal.
|
||||
auto const vault2 = readVault(env, broker);
|
||||
BEAST_EXPECT(vault2.reserved == vault0.reserved);
|
||||
BEAST_EXPECT(vault2.available == vault0.available - principal);
|
||||
BEAST_EXPECT(vault2.total == vault0.total + interestDue);
|
||||
|
||||
// The principal is disbursed from the vault pseudo-account to the
|
||||
// borrower (origination fee is zero, so the borrower receives it
|
||||
// all, less the transaction fee it paid).
|
||||
BEAST_EXPECT(env.balance(vaultPseudo).value() == pseudoBal0 - xrpAsset(200).value());
|
||||
BEAST_EXPECT(env.balance(borrower).value() > borrowerBal0);
|
||||
}
|
||||
|
||||
{
|
||||
testcase("Two-step: proposal failures");
|
||||
|
||||
Env env(*this, features);
|
||||
auto const epoch = env.now();
|
||||
auto const broker = makeBroker(env);
|
||||
|
||||
// The submitter must be the LoanBroker owner
|
||||
propose(env, broker, evan, borrower, futureStart(env), Ter(tecNO_PERMISSION));
|
||||
|
||||
// The StartDate must be in the future.
|
||||
std::uint32_t const pastDate = epoch.time_since_epoch().count();
|
||||
propose(env, broker, lender, borrower, pastDate, Ter(tecEXPIRED));
|
||||
}
|
||||
|
||||
{
|
||||
testcase("Two-step: LoanAccept validation");
|
||||
|
||||
Env env(*this, features);
|
||||
auto const broker = makeBroker(env);
|
||||
|
||||
// Zero LoanID fails preflight.
|
||||
env(accept(borrower, uint256{}), Ter(temINVALID));
|
||||
|
||||
// A LoanID that does not resolve to a Loan object.
|
||||
env(accept(borrower, keylet::loan(broker.brokerID, 999).key), Ter(tecNO_ENTRY));
|
||||
|
||||
auto const loanKeylet = nextLoanKeylet(env, broker);
|
||||
propose(env, broker, lender, borrower, futureStart(env));
|
||||
env.close();
|
||||
|
||||
// Only the borrower may accept.
|
||||
env(accept(evan, loanKeylet.key), Ter(tecNO_PERMISSION));
|
||||
env(accept(lender, loanKeylet.key), Ter(tecNO_PERMISSION));
|
||||
|
||||
// The borrower accepts successfully.
|
||||
env(accept(borrower, loanKeylet.key));
|
||||
env.close();
|
||||
|
||||
// The loan is no longer pending, so it cannot be accepted again.
|
||||
env(accept(borrower, loanKeylet.key), Ter(tecNO_PERMISSION));
|
||||
}
|
||||
|
||||
{
|
||||
testcase("Two-step: LoanAccept after expiry");
|
||||
|
||||
Env env(*this, features);
|
||||
auto const broker = makeBroker(env);
|
||||
|
||||
auto const loanKeylet = nextLoanKeylet(env, broker);
|
||||
std::uint32_t const startDate = (env.now() + 1h).time_since_epoch().count();
|
||||
propose(env, broker, lender, borrower, startDate);
|
||||
env.close();
|
||||
|
||||
// Advance the ledger beyond the StartDate.
|
||||
env.close(NetClock::time_point{NetClock::duration{startDate}} + 1h);
|
||||
|
||||
env(accept(borrower, loanKeylet.key), Ter(tecEXPIRED));
|
||||
}
|
||||
|
||||
// Deleting a pending loan reverses the proposal-time bookkeeping and
|
||||
// releases the broker owner's reserve. It can be done by either the
|
||||
// broker owner or the borrower.
|
||||
auto const testDeletePending = [&](Account const& deleter) {
|
||||
Env env(*this, features);
|
||||
auto const broker = makeBroker(env);
|
||||
|
||||
auto const vault0 = readVault(env, broker);
|
||||
auto const lenderOwners0 = env.ownerCount(lender);
|
||||
auto const borrowerOwners0 = env.ownerCount(borrower);
|
||||
|
||||
auto const loanKeylet = nextLoanKeylet(env, broker);
|
||||
propose(env, broker, lender, borrower, futureStart(env));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.le(loanKeylet));
|
||||
BEAST_EXPECT(env.ownerCount(lender) == lenderOwners0 + 1);
|
||||
|
||||
// An unrelated account cannot delete the loan.
|
||||
env(del(evan, loanKeylet.key), Ter(tecNO_PERMISSION));
|
||||
|
||||
env(del(deleter, loanKeylet.key));
|
||||
env.close();
|
||||
|
||||
// The loan is gone, the reserve is released, and the vault
|
||||
// bookkeeping is fully reversed.
|
||||
BEAST_EXPECT(!env.le(loanKeylet));
|
||||
BEAST_EXPECT(env.ownerCount(lender) == lenderOwners0);
|
||||
BEAST_EXPECT(env.ownerCount(borrower) == borrowerOwners0);
|
||||
|
||||
auto const vault1 = readVault(env, broker);
|
||||
BEAST_EXPECT(vault1.available == vault0.available);
|
||||
BEAST_EXPECT(vault1.reserved == vault0.reserved);
|
||||
BEAST_EXPECT(vault1.total == vault0.total);
|
||||
};
|
||||
|
||||
{
|
||||
testcase("Two-step: LoanDelete of pending loan by broker owner");
|
||||
testDeletePending(lender);
|
||||
}
|
||||
{
|
||||
testcase("Two-step: LoanDelete of pending loan by borrower");
|
||||
testDeletePending(borrower);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testLifecycle(FeatureBitset features)
|
||||
{
|
||||
@@ -8560,6 +8831,7 @@ protected:
|
||||
// Lifecycle
|
||||
testLifecycle(features);
|
||||
testLoanSet(features);
|
||||
testTwoStep(features);
|
||||
testDosLoanPay(features);
|
||||
testSelfLoan(features);
|
||||
|
||||
@@ -8608,7 +8880,8 @@ public:
|
||||
{
|
||||
runAmendmentIndependent();
|
||||
for (auto const& features : jtx::amendmentCombinations(
|
||||
{fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2}, all_))
|
||||
{fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2, featureLendingProtocolV1_1},
|
||||
all_))
|
||||
runAmendmentSensitive(features);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -919,6 +919,11 @@ set(AccountID const& account,
|
||||
|
||||
auto const kCounterparty = JTxFieldWrapper<AccountIdField>(sfCounterparty);
|
||||
|
||||
// Two-step (LendingProtocolV1_1) proposal fields.
|
||||
auto const kBorrower = JTxFieldWrapper<AccountIdField>(sfBorrower);
|
||||
|
||||
auto const kStartDate = simpleField<SF_UINT32>(sfStartDate);
|
||||
|
||||
// For `CounterPartySignature`, use `Sig(sfCounterpartySignature, ...)`
|
||||
|
||||
auto const kLoanOriginationFee = simpleField<SF_NUMBER>(sfLoanOriginationFee);
|
||||
@@ -950,6 +955,10 @@ auto const kGracePeriod = simpleField<SF_UINT32>(sfGracePeriod);
|
||||
json::Value
|
||||
manage(AccountID const& account, uint256 const& loanID, std::uint32_t flags);
|
||||
|
||||
// Two-step (LendingProtocolV1_1) acceptance of a pending loan proposal.
|
||||
json::Value
|
||||
accept(AccountID const& account, uint256 const& loanID, std::uint32_t flags = 0);
|
||||
|
||||
json::Value
|
||||
del(AccountID const& account, uint256 const& loanID, std::uint32_t flags = 0);
|
||||
|
||||
|
||||
@@ -823,6 +823,17 @@ manage(AccountID const& account, uint256 const& loanID, std::uint32_t flags)
|
||||
return jv;
|
||||
}
|
||||
|
||||
json::Value
|
||||
accept(AccountID const& account, uint256 const& loanID, std::uint32_t flags)
|
||||
{
|
||||
json::Value jv;
|
||||
jv[sfTransactionType] = jss::LoanAccept;
|
||||
jv[sfAccount] = to_string(account);
|
||||
jv[sfLoanID] = to_string(loanID);
|
||||
jv[sfFlags] = flags;
|
||||
return jv;
|
||||
}
|
||||
|
||||
json::Value
|
||||
del(AccountID const& account, uint256 const& loanID, std::uint32_t flags)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user