mirror of
https://github.com/XRPLF/rippled.git
synced 2026-08-23 15:20:54 +00:00
WIP
This commit is contained in:
@@ -474,6 +474,11 @@ LoanPay::doApply()
|
||||
|
||||
Number const assetsAvailableBefore = *assetsAvailableProxy;
|
||||
Number const assetsTotalBefore = *assetsTotalProxy;
|
||||
// AssetsReserved holds funds still in the pseudo-account that are earmarked
|
||||
// for pending loans awaiting acceptance. LoanPay does not touch it, so the
|
||||
// invariant is pseudo_balance == AssetsAvailable + AssetsReserved both
|
||||
// before and after the payment.
|
||||
Number const assetsReserved = *vaultSle->at(sfAssetsReserved);
|
||||
#if !NDEBUG
|
||||
{
|
||||
Number const pseudoAccountBalanceBefore = accountHolds(
|
||||
@@ -485,7 +490,7 @@ LoanPay::doApply()
|
||||
j_);
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
assetsAvailableBefore == pseudoAccountBalanceBefore,
|
||||
assetsAvailableBefore + assetsReserved == pseudoAccountBalanceBefore,
|
||||
"xrpl::LoanPay::doApply",
|
||||
"vault pseudo balance agrees before");
|
||||
}
|
||||
@@ -663,7 +668,7 @@ LoanPay::doApply()
|
||||
AuthHandling::IgnoreAuth,
|
||||
j_);
|
||||
XRPL_ASSERT_PARTS(
|
||||
assetsAvailableAfter == pseudoAccountBalanceAfter,
|
||||
assetsAvailableAfter + assetsReserved == pseudoAccountBalanceAfter,
|
||||
"xrpl::LoanPay::doApply",
|
||||
"vault pseudo balance agrees after");
|
||||
}
|
||||
|
||||
@@ -628,13 +628,17 @@ LoanSet::preflight(PreflightContext const& ctx)
|
||||
return temINVALID_FLAG;
|
||||
}
|
||||
|
||||
// Special case for Batch inner transactions
|
||||
// Special case for Batch inner transactions. A Batch inner LoanSet
|
||||
// must identify the borrower explicitly, since the inner transaction
|
||||
// cannot carry a CounterpartySignature. That means either a Counterparty
|
||||
// (immediate flow) or, once V1.1 enables it, a Borrower (two-step flow).
|
||||
if (tx.isFlag(tfInnerBatchTxn) && ctx.rules.enabled(featureBatchV1_1) &&
|
||||
!tx.isFieldPresent(sfCounterparty))
|
||||
!tx.isFieldPresent(sfCounterparty) &&
|
||||
!(isTwoStepFlowEnabled(ctx.rules) && tx.isFieldPresent(sfBorrower)))
|
||||
{
|
||||
auto const parentBatchId = ctx.parentBatchId.value_or(uint256{0});
|
||||
JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
|
||||
<< "no Counterparty for inner LoanSet transaction.";
|
||||
<< "no Counterparty or Borrower for inner LoanSet transaction.";
|
||||
return temBAD_SIGNER;
|
||||
}
|
||||
|
||||
|
||||
@@ -5266,6 +5266,7 @@ class Vault_test : public beast::unit_test::Suite
|
||||
BEAST_EXPECT(kCheckString(vault, sfAssetsAvailable, "50"));
|
||||
BEAST_EXPECT(kCheckString(vault, sfAssetsMaximum, "1000"));
|
||||
BEAST_EXPECT(kCheckString(vault, sfAssetsTotal, "50"));
|
||||
BEAST_EXPECT(!vault.isMember(sfAssetsReserved.getJsonName()));
|
||||
BEAST_EXPECT(!vault.isMember(sfLossUnrealized.getJsonName()));
|
||||
|
||||
auto const strShareID = strHex(sle->at(sfShareMPTID));
|
||||
@@ -5606,6 +5607,36 @@ class Vault_test : public beast::unit_test::Suite
|
||||
json::Value jv = env.rpc("vault_info", strHex(keylet.key), "0");
|
||||
BEAST_EXPECT(jv[jss::result][jss::error].asString() == "lgrNotFound");
|
||||
}
|
||||
|
||||
// vault_info reflects AssetsReserved when the vault holds reserved
|
||||
// assets. The field is a SoeDefault Number that is elided from the JSON
|
||||
// when zero (asserted in `check(...)` above); after mutating the SLE to
|
||||
// a non-zero value the response must expose it as a string matching
|
||||
// the ledger.
|
||||
{
|
||||
testcase("RPC vault_info reflects AssetsReserved when non-zero");
|
||||
Number const reserved{25};
|
||||
|
||||
auto const changed = env.app().getOpenLedger().modify(
|
||||
[&](OpenView& view, beast::Journal) -> bool {
|
||||
Sandbox sb(&view, TapNone);
|
||||
auto v = sb.peek(keylet);
|
||||
if (!v)
|
||||
return false;
|
||||
v->at(sfAssetsReserved) = reserved;
|
||||
sb.update(v);
|
||||
sb.apply(view);
|
||||
return true;
|
||||
});
|
||||
BEAST_EXPECT(changed);
|
||||
|
||||
json::Value jv = env.rpc("vault_info", strHex(keylet.key));
|
||||
BEAST_EXPECT(!jv[jss::result].isMember(jss::error));
|
||||
auto const& vaultJv = jv[jss::result][jss::vault];
|
||||
BEAST_EXPECT(vaultJv.isMember(sfAssetsReserved.getJsonName()));
|
||||
BEAST_EXPECT(
|
||||
vaultJv[sfAssetsReserved.getJsonName()].asString() == to_string(reserved));
|
||||
}
|
||||
}
|
||||
|
||||
// RPC coverage: closed-ended vaults must return VaultKind, SubscriptionDate and RedemptionDate
|
||||
|
||||
@@ -609,6 +609,45 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
// Once V1.1 enables the two-step flow, a Batch inner LoanSet may
|
||||
// name a Borrower (with a StartDate) instead of a Counterparty:
|
||||
// the borrower is identified explicitly on the inner tx and no
|
||||
// CounterpartySignature is required. Preflight must accept it.
|
||||
if (features[featureLendingProtocolV1_1])
|
||||
{
|
||||
auto const jtx = env.jt(
|
||||
set(lender, broker.brokerID, principalRequest),
|
||||
Txflags(tfInnerBatchTxn),
|
||||
kBorrower(borrower),
|
||||
kStartDate((env.now() + 1h).time_since_epoch().count()));
|
||||
if (BEAST_EXPECT(jtx.stx))
|
||||
{
|
||||
PreflightContext const pfCtx(
|
||||
env.app(), *jtx.stx, uint256{1}, env.current()->rules(), TapBatch, env.journal);
|
||||
BEAST_EXPECT(Transactor::invokePreflight<LoanSet>(pfCtx) == tesSUCCESS);
|
||||
}
|
||||
|
||||
// A Batch inner LoanSet with Borrower but no StartDate is not a
|
||||
// valid two-step proposal and no longer masquerades as a
|
||||
// missing-Counterparty error: it is rejected as temINVALID by
|
||||
// getLoanFlow, past the Batch-specific check.
|
||||
auto const jtxNoStart = env.jt(
|
||||
set(lender, broker.brokerID, principalRequest),
|
||||
Txflags(tfInnerBatchTxn),
|
||||
kBorrower(borrower));
|
||||
if (BEAST_EXPECT(jtxNoStart.stx))
|
||||
{
|
||||
PreflightContext const pfCtx(
|
||||
env.app(),
|
||||
*jtxNoStart.stx,
|
||||
uint256{1},
|
||||
env.current()->rules(),
|
||||
TapBatch,
|
||||
env.journal);
|
||||
BEAST_EXPECT(Transactor::invokePreflight<LoanSet>(pfCtx) == temINVALID);
|
||||
}
|
||||
}
|
||||
|
||||
// Success: a Batch containing an inner LoanSet that names a
|
||||
// Counterparty (but carries no CounterpartySignature) is accepted
|
||||
// when the counterparty signs the outer Batch. The immediate flow's
|
||||
|
||||
@@ -236,7 +236,7 @@ protected:
|
||||
if (twoStep)
|
||||
{
|
||||
kBorrower(account)(env, jt);
|
||||
kStartDate (*startDate)(env, jt);
|
||||
kStartDate(startDate.value())(env, jt);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -106,6 +106,21 @@ private:
|
||||
.total = v->at(sfAssetsTotal)};
|
||||
};
|
||||
|
||||
// Snapshot of the LoanBroker's own bookkeeping.
|
||||
struct BrokerAmounts
|
||||
{
|
||||
Number debtTotal;
|
||||
Number coverAvailable;
|
||||
std::uint32_t ownerCount{};
|
||||
};
|
||||
auto const readBroker = [&](Env& env, BrokerInfo const& broker) -> BrokerAmounts {
|
||||
auto const b = env.le(broker.brokerKeylet());
|
||||
return {
|
||||
.debtTotal = b->at(sfDebtTotal),
|
||||
.coverAvailable = b->at(sfCoverAvailable),
|
||||
.ownerCount = b->at(sfOwnerCount)};
|
||||
};
|
||||
|
||||
// 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,
|
||||
@@ -123,6 +138,14 @@ private:
|
||||
extra...);
|
||||
};
|
||||
|
||||
// Per spec §4.3, a failed LoanAccept must leave the pending Loan
|
||||
// intact so the borrower can rectify the issue and retry until the
|
||||
// StartDate expires.
|
||||
auto const expectStillPending = [this](Env& env, Keylet const& k) {
|
||||
if (auto const loan = env.le(k); BEAST_EXPECT(loan))
|
||||
BEAST_EXPECT(loan->isFlag(lsfLoanPending));
|
||||
};
|
||||
|
||||
auto const featureEnabled = (features & featureLendingProtocolV1_1).any();
|
||||
|
||||
if (!featureEnabled)
|
||||
@@ -166,6 +189,7 @@ private:
|
||||
Number const principal = broker.asset(200).number();
|
||||
|
||||
auto const vault0 = readVault(env, broker);
|
||||
auto const broker0 = readBroker(env, broker);
|
||||
auto const lenderOwners0 = env.ownerCount(lender);
|
||||
auto const borrowerOwners0 = env.ownerCount(borrower);
|
||||
|
||||
@@ -197,6 +221,13 @@ private:
|
||||
BEAST_EXPECT(vault1.total > vault0.total);
|
||||
Number const interestDue = vault1.total - vault0.total;
|
||||
|
||||
// Broker bookkeeping: DebtTotal += P + InterestDue, OwnerCount +=
|
||||
// 1, CoverAvailable is untouched by the proposal.
|
||||
auto const broker1 = readBroker(env, broker);
|
||||
BEAST_EXPECT(broker1.debtTotal == broker0.debtTotal + principal + interestDue);
|
||||
BEAST_EXPECT(broker1.ownerCount == broker0.ownerCount + 1);
|
||||
BEAST_EXPECT(broker1.coverAvailable == broker0.coverAvailable);
|
||||
|
||||
// Capture pre-acceptance balances to verify disbursement.
|
||||
auto const vaultPseudo = [&]() {
|
||||
auto const v = env.le(broker.vaultKeylet());
|
||||
@@ -227,6 +258,13 @@ private:
|
||||
BEAST_EXPECT(vault2.available == vault0.available - principal);
|
||||
BEAST_EXPECT(vault2.total == vault0.total + interestDue);
|
||||
|
||||
// Broker bookkeeping: acceptance leaves DebtTotal, OwnerCount, and
|
||||
// CoverAvailable unchanged from the pending snapshot.
|
||||
auto const broker2 = readBroker(env, broker);
|
||||
BEAST_EXPECT(broker2.debtTotal == broker1.debtTotal);
|
||||
BEAST_EXPECT(broker2.ownerCount == broker1.ownerCount);
|
||||
BEAST_EXPECT(broker2.coverAvailable == broker1.coverAvailable);
|
||||
|
||||
// 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).
|
||||
@@ -236,13 +274,18 @@ private:
|
||||
BEAST_EXPECT(env.balance(borrower, broker.asset).value() > borrowerBal0);
|
||||
}
|
||||
|
||||
// Exercise a proposal with a non-zero origination fee, then verify at
|
||||
// acceptance that the principal leaves the vault pseudo-account, the
|
||||
// borrower receives the net, and the broker owner receives the fee.
|
||||
// XRP is excluded because the borrower's LoanAccept fee would perturb
|
||||
// the exact borrower balance assertion.
|
||||
for (auto const assetType : {AssetType::IOU, AssetType::MPT})
|
||||
{
|
||||
testcase("Two-step: propose then accept with origination fee");
|
||||
testcase << "Two-step: propose then accept with origination fee ("
|
||||
<< assetTypeName(assetType) << ")";
|
||||
|
||||
// Use an IOU so the disbursed amounts can be checked exactly,
|
||||
// without the borrower's XRP transaction fee getting in the way.
|
||||
Env env(*this, features);
|
||||
auto const broker = makeBroker(env, AssetType::IOU);
|
||||
auto const broker = makeBroker(env, assetType);
|
||||
Number const principal = broker.asset(200).number();
|
||||
Number const originationFee = broker.asset(5).number();
|
||||
|
||||
@@ -288,6 +331,97 @@ private:
|
||||
BEAST_EXPECT(env.balance(lender, broker.asset).value() == lenderBal0 + feeToOwner);
|
||||
}
|
||||
|
||||
{
|
||||
testcase("Two-step: accepted loan behaves as a normal loan");
|
||||
|
||||
// Once accepted, a two-step loan is indistinguishable from a
|
||||
// one-step loan for the rest of its lifecycle: it can be
|
||||
// impaired, unimpaired, paid, and finally deleted.
|
||||
Env env(*this, features);
|
||||
auto const broker = makeBroker(env, AssetType::XRP);
|
||||
|
||||
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();
|
||||
|
||||
env(accept(borrower, loanKeylet.key));
|
||||
env.close();
|
||||
|
||||
if (auto const loan = env.le(loanKeylet); BEAST_EXPECT(loan))
|
||||
{
|
||||
BEAST_EXPECT(!loan->isFlag(lsfLoanPending));
|
||||
BEAST_EXPECT(loan->at(sfPaymentRemaining) == payTotal);
|
||||
}
|
||||
|
||||
// LoanManage: impair then unimpair.
|
||||
env(manage(lender, loanKeylet.key, tfLoanImpair));
|
||||
env.close();
|
||||
if (auto const loan = env.le(loanKeylet); BEAST_EXPECT(loan))
|
||||
BEAST_EXPECT(loan->isFlag(lsfLoanImpaired));
|
||||
|
||||
env(manage(lender, loanKeylet.key, tfLoanUnimpair));
|
||||
env.close();
|
||||
if (auto const loan = env.le(loanKeylet); BEAST_EXPECT(loan))
|
||||
BEAST_EXPECT(!loan->isFlag(lsfLoanImpaired));
|
||||
|
||||
// LoanPay: a regular periodic payment succeeds, then the borrower
|
||||
// clears the remainder with tfLoanFullPayment.
|
||||
env.close(NetClock::time_point{NetClock::duration{startDate}} + 1h);
|
||||
env(pay(borrower, loanKeylet.key, broker.asset(30)));
|
||||
env.close();
|
||||
if (auto const loan = env.le(loanKeylet); BEAST_EXPECT(loan))
|
||||
BEAST_EXPECT(loan->at(sfPaymentRemaining) < payTotal);
|
||||
|
||||
// A generous upper bound (2x principal) clears principal + interest.
|
||||
env(pay(borrower, loanKeylet.key, broker.asset(400), tfLoanFullPayment));
|
||||
env.close();
|
||||
if (auto const loan = env.le(loanKeylet); BEAST_EXPECT(loan))
|
||||
BEAST_EXPECT(loan->at(sfPaymentRemaining) == 0);
|
||||
|
||||
// LoanDelete succeeds once the loan is fully paid.
|
||||
env(del(borrower, loanKeylet.key));
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(loanKeylet));
|
||||
}
|
||||
|
||||
{
|
||||
testcase("Two-step: LoanPay on accepted loan while another loan is pending");
|
||||
|
||||
// Regression: LoanPay::doApply's vault-balance invariant used to
|
||||
// assert AssetsAvailable == pseudo_balance, ignoring
|
||||
// AssetsReserved. Whenever a pending loan bumped AssetsReserved,
|
||||
// any LoanPay on an accepted loan would fire the debug assertion.
|
||||
// The correct invariant is
|
||||
// pseudo_balance == AssetsAvailable + AssetsReserved,
|
||||
// and this test locks that in.
|
||||
Env env(*this, features);
|
||||
auto const broker = makeBroker(env, AssetType::XRP);
|
||||
|
||||
// L1: accepted (borrower) — disburses principal, drains
|
||||
// AssetsReserved back to 0.
|
||||
auto const l1Keylet = nextLoanKeylet(env, broker);
|
||||
propose(env, broker, lender, borrower, (env.now() + 1h).time_since_epoch().count());
|
||||
env.close();
|
||||
env(accept(borrower, l1Keylet.key));
|
||||
env.close();
|
||||
if (auto const l1 = env.le(l1Keylet); BEAST_EXPECT(l1))
|
||||
BEAST_EXPECT(!l1->isFlag(lsfLoanPending));
|
||||
|
||||
// L2: still pending (evan) — leaves AssetsReserved > 0.
|
||||
propose(env, broker, lender, evan, (env.now() + 1h).time_since_epoch().count());
|
||||
env.close();
|
||||
if (auto const v = env.le(broker.vaultKeylet()); BEAST_EXPECT(v))
|
||||
BEAST_EXPECT(v->at(sfAssetsReserved) > beast::kZero);
|
||||
|
||||
// A payment on L1 must succeed with L2 still pending. Before the
|
||||
// fix, LoanPay's debug invariant tripped here.
|
||||
env(pay(borrower, l1Keylet.key, broker.asset(30)));
|
||||
env.close();
|
||||
if (auto const l1 = env.le(l1Keylet); BEAST_EXPECT(l1))
|
||||
BEAST_EXPECT(l1->at(sfPaymentRemaining) < payTotal);
|
||||
}
|
||||
|
||||
{
|
||||
testcase("Two-step: proposal failures");
|
||||
|
||||
@@ -325,6 +459,20 @@ private:
|
||||
env(set(lender, broker.brokerID, broker.asset(200).number()),
|
||||
kStartDate((env.now() + 1h).time_since_epoch().count()),
|
||||
Ter(temINVALID));
|
||||
|
||||
// A LoanSet with Borrower and Counterparty is ambiguous.
|
||||
env(set(lender, broker.brokerID, broker.asset(200).number()),
|
||||
kBorrower(borrower),
|
||||
kStartDate((env.now() + 1h).time_since_epoch().count()),
|
||||
kCounterparty(borrower),
|
||||
Ter(temINVALID));
|
||||
|
||||
// A LoanSet with Borrower and CounterpartySignature is ambiguous.
|
||||
env(set(lender, broker.brokerID, broker.asset(200).number()),
|
||||
kBorrower(borrower),
|
||||
kStartDate((env.now() + 1h).time_since_epoch().count()),
|
||||
Sig(sfCounterpartySignature, borrower),
|
||||
Ter(temINVALID));
|
||||
}
|
||||
|
||||
{
|
||||
@@ -348,6 +496,7 @@ private:
|
||||
// Only the borrower may accept.
|
||||
env(accept(evan, loanKeylet.key), Ter(tecNO_PERMISSION));
|
||||
env(accept(lender, loanKeylet.key), Ter(tecNO_PERMISSION));
|
||||
expectStillPending(env, loanKeylet);
|
||||
|
||||
// The borrower accepts successfully.
|
||||
env(accept(borrower, loanKeylet.key));
|
||||
@@ -391,6 +540,42 @@ private:
|
||||
BEAST_EXPECT(!loan->isFlag(lsfLoanPending));
|
||||
}
|
||||
|
||||
// LoanManage::preclaim rejects pending loans before it inspects the
|
||||
// payment schedule. Guard that ordering by advancing the ledger past
|
||||
// NextPaymentDueDate + GracePeriod on a still-pending loan: the tx
|
||||
// must still return tecNO_PERMISSION, never tecTOO_SOON or success.
|
||||
for (auto const assetType : {AssetType::XRP, AssetType::IOU, AssetType::MPT})
|
||||
{
|
||||
testcase << "Two-step: pending loan rejects LoanManage after due date ("
|
||||
<< assetTypeName(assetType) << ")";
|
||||
|
||||
Env env(*this, features);
|
||||
auto const broker = makeBroker(env, assetType);
|
||||
|
||||
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 past StartDate + PaymentInterval + GracePeriod. payInterval
|
||||
// is 200s and the default GracePeriod is 60s, so +2h from StartDate
|
||||
// is comfortably past both.
|
||||
env.close(NetClock::time_point{NetClock::duration{startDate}} + 2h);
|
||||
|
||||
if (auto const loan = env.le(loanKeylet); BEAST_EXPECT(loan))
|
||||
{
|
||||
BEAST_EXPECT(loan->isFlag(lsfLoanPending));
|
||||
BEAST_EXPECT(
|
||||
env.now() >
|
||||
NetClock::time_point{NetClock::duration{
|
||||
loan->at(sfNextPaymentDueDate) + loan->at(sfGracePeriod)}});
|
||||
}
|
||||
|
||||
env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tecNO_PERMISSION));
|
||||
env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tecNO_PERMISSION));
|
||||
env(manage(lender, loanKeylet.key, tfLoanUnimpair), Ter(tecNO_PERMISSION));
|
||||
}
|
||||
|
||||
{
|
||||
testcase("Two-step: LoanAccept after expiry");
|
||||
|
||||
@@ -406,6 +591,7 @@ private:
|
||||
env.close(NetClock::time_point{NetClock::duration{startDate}} + 1h);
|
||||
|
||||
env(accept(borrower, loanKeylet.key), Ter(tecEXPIRED));
|
||||
expectStillPending(env, loanKeylet);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -435,6 +621,7 @@ private:
|
||||
|
||||
// The proposal has expired, so it can no longer be accepted.
|
||||
env(accept(borrower, loanKeylet.key), Ter(tecEXPIRED));
|
||||
expectStillPending(env, loanKeylet);
|
||||
|
||||
// But it can still be deleted.
|
||||
env(del(lender, loanKeylet.key));
|
||||
@@ -477,6 +664,159 @@ private:
|
||||
Ter(tecINSUFFICIENT_RESERVE));
|
||||
}
|
||||
|
||||
// The issuer freezes the trust line (IOU) or locks the MPToken (MPT)
|
||||
// on the vault pseudo-account before LoanSet is submitted. The
|
||||
// proposal must be rejected by checkLoanFreeze in preclaim, and no
|
||||
// pending Loan is created. XRP cannot be frozen, so it is excluded.
|
||||
for (auto const assetType : {AssetType::IOU, AssetType::MPT})
|
||||
{
|
||||
testcase << "Two-step: LoanSet with frozen vault pseudo-account ("
|
||||
<< assetTypeName(assetType) << ")";
|
||||
|
||||
Env env(*this, features);
|
||||
auto const broker = makeBroker(env, assetType);
|
||||
auto const loanKeylet = nextLoanKeylet(env, broker);
|
||||
|
||||
auto const vaultPseudo = [&]() {
|
||||
auto const v = env.le(broker.vaultKeylet());
|
||||
return Account("vault pseudo-account", v->at(sfAccount));
|
||||
}();
|
||||
|
||||
TER expected = tesSUCCESS;
|
||||
if (assetType == AssetType::IOU)
|
||||
{
|
||||
env(trust(issuer, vaultPseudo[iouCurrency_](0), tfSetFreeze));
|
||||
env.close();
|
||||
expected = TER{tecFROZEN};
|
||||
}
|
||||
else
|
||||
{
|
||||
MPTTester mptt{env, issuer, broker.asset.raw().get<MPTIssue>().getMptID()};
|
||||
mptt.set({.account = issuer, .holder = vaultPseudo, .flags = tfMPTLock});
|
||||
env.close();
|
||||
expected = TER{tecLOCKED};
|
||||
}
|
||||
|
||||
propose(
|
||||
env,
|
||||
broker,
|
||||
lender,
|
||||
borrower,
|
||||
(env.now() + 1h).time_since_epoch().count(),
|
||||
Ter(expected));
|
||||
BEAST_EXPECT(!env.le(loanKeylet));
|
||||
}
|
||||
|
||||
// Same as above, but for the LoanBroker pseudo-account (deep freeze).
|
||||
for (auto const assetType : {AssetType::IOU, AssetType::MPT})
|
||||
{
|
||||
testcase << "Two-step: LoanSet with deep frozen broker pseudo-account ("
|
||||
<< assetTypeName(assetType) << ")";
|
||||
|
||||
Env env(*this, features);
|
||||
auto const broker = makeBroker(env, assetType);
|
||||
auto const loanKeylet = nextLoanKeylet(env, broker);
|
||||
|
||||
auto const brokerPseudo = [&]() {
|
||||
auto const b = env.le(broker.brokerKeylet());
|
||||
return Account("broker pseudo-account", b->at(sfAccount));
|
||||
}();
|
||||
|
||||
TER expected = tesSUCCESS;
|
||||
if (assetType == AssetType::IOU)
|
||||
{
|
||||
env(trust(issuer, brokerPseudo[iouCurrency_](0), tfSetFreeze | tfSetDeepFreeze));
|
||||
env.close();
|
||||
expected = TER{tecFROZEN};
|
||||
}
|
||||
else
|
||||
{
|
||||
MPTTester mptt{env, issuer, broker.asset.raw().get<MPTIssue>().getMptID()};
|
||||
mptt.set({.account = issuer, .holder = brokerPseudo, .flags = tfMPTLock});
|
||||
env.close();
|
||||
expected = TER{tecLOCKED};
|
||||
}
|
||||
|
||||
propose(
|
||||
env,
|
||||
broker,
|
||||
lender,
|
||||
borrower,
|
||||
(env.now() + 1h).time_since_epoch().count(),
|
||||
Ter(expected));
|
||||
BEAST_EXPECT(!env.le(loanKeylet));
|
||||
}
|
||||
|
||||
// Same as above, but for the Borrower.
|
||||
for (auto const assetType : {AssetType::IOU, AssetType::MPT})
|
||||
{
|
||||
testcase << "Two-step: LoanSet with frozen borrower (" << assetTypeName(assetType)
|
||||
<< ")";
|
||||
|
||||
Env env(*this, features);
|
||||
auto const broker = makeBroker(env, assetType);
|
||||
auto const loanKeylet = nextLoanKeylet(env, broker);
|
||||
|
||||
TER expected = tesSUCCESS;
|
||||
if (assetType == AssetType::IOU)
|
||||
{
|
||||
env(trust(issuer, borrower[iouCurrency_](0), tfSetFreeze));
|
||||
env.close();
|
||||
expected = TER{tecFROZEN};
|
||||
}
|
||||
else
|
||||
{
|
||||
MPTTester mptt{env, issuer, broker.asset.raw().get<MPTIssue>().getMptID()};
|
||||
mptt.set({.account = issuer, .holder = borrower, .flags = tfMPTLock});
|
||||
env.close();
|
||||
expected = TER{tecLOCKED};
|
||||
}
|
||||
|
||||
propose(
|
||||
env,
|
||||
broker,
|
||||
lender,
|
||||
borrower,
|
||||
(env.now() + 1h).time_since_epoch().count(),
|
||||
Ter(expected));
|
||||
BEAST_EXPECT(!env.le(loanKeylet));
|
||||
}
|
||||
|
||||
// Same as above, but for the LoanBroker owner (deep freeze).
|
||||
for (auto const assetType : {AssetType::IOU, AssetType::MPT})
|
||||
{
|
||||
testcase << "Two-step: LoanSet with deep frozen broker owner ("
|
||||
<< assetTypeName(assetType) << ")";
|
||||
|
||||
Env env(*this, features);
|
||||
auto const broker = makeBroker(env, assetType);
|
||||
auto const loanKeylet = nextLoanKeylet(env, broker);
|
||||
|
||||
TER expected = tesSUCCESS;
|
||||
if (assetType == AssetType::IOU)
|
||||
{
|
||||
env(trust(issuer, lender[iouCurrency_](0), tfSetFreeze | tfSetDeepFreeze));
|
||||
env.close();
|
||||
expected = TER{tecFROZEN};
|
||||
}
|
||||
else
|
||||
{
|
||||
MPTTester mptt{env, issuer, broker.asset.raw().get<MPTIssue>().getMptID()};
|
||||
mptt.set({.account = issuer, .holder = lender, .flags = tfMPTLock});
|
||||
env.close();
|
||||
expected = TER{tecLOCKED};
|
||||
}
|
||||
|
||||
propose(
|
||||
env,
|
||||
broker,
|
||||
lender,
|
||||
borrower,
|
||||
(env.now() + 1h).time_since_epoch().count(),
|
||||
Ter(expected));
|
||||
BEAST_EXPECT(!env.le(loanKeylet));
|
||||
}
|
||||
|
||||
{
|
||||
testcase("Two-step: LoanAccept with insufficient reserve");
|
||||
|
||||
@@ -498,6 +838,7 @@ private:
|
||||
env.close();
|
||||
|
||||
env(accept(borrower, loanKeylet.key), Ter(tecINSUFFICIENT_RESERVE));
|
||||
expectStillPending(env, loanKeylet);
|
||||
}
|
||||
|
||||
// Between the LoanSet proposal and the LoanAccept, the issuer
|
||||
@@ -538,6 +879,7 @@ private:
|
||||
}
|
||||
|
||||
env(accept(borrower, loanKeylet.key), Ter(expected));
|
||||
expectStillPending(env, loanKeylet);
|
||||
}
|
||||
|
||||
// Between the LoanSet proposal and the LoanAccept, the issuer deep
|
||||
@@ -578,6 +920,7 @@ private:
|
||||
}
|
||||
|
||||
env(accept(borrower, loanKeylet.key), Ter(expected));
|
||||
expectStillPending(env, loanKeylet);
|
||||
}
|
||||
|
||||
// Between the LoanSet proposal and the LoanAccept, the issuer
|
||||
@@ -612,6 +955,7 @@ private:
|
||||
}
|
||||
|
||||
env(accept(borrower, loanKeylet.key), Ter(expected));
|
||||
expectStillPending(env, loanKeylet);
|
||||
}
|
||||
|
||||
// Between the LoanSet proposal and the LoanAccept, the issuer deep
|
||||
@@ -646,6 +990,7 @@ private:
|
||||
}
|
||||
|
||||
env(accept(borrower, loanKeylet.key), Ter(expected));
|
||||
expectStillPending(env, loanKeylet);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -668,6 +1013,7 @@ private:
|
||||
env.close();
|
||||
|
||||
env(accept(borrower, loanKeylet.key), Ter(terNO_RIPPLE));
|
||||
expectStillPending(env, loanKeylet);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -706,6 +1052,7 @@ private:
|
||||
env.close();
|
||||
|
||||
env(accept(borrower, loanKeylet.key), Ter(tecNO_AUTH));
|
||||
expectStillPending(env, loanKeylet);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -741,6 +1088,7 @@ private:
|
||||
env.close();
|
||||
|
||||
env(accept(borrower, loanKeylet.key), Ter(tecNO_AUTH));
|
||||
expectStillPending(env, loanKeylet);
|
||||
}
|
||||
|
||||
// Deleting a pending loan reverses the proposal-time bookkeeping and
|
||||
@@ -751,6 +1099,7 @@ private:
|
||||
auto const broker = makeBroker(env, assetType);
|
||||
|
||||
auto const vault0 = readVault(env, broker);
|
||||
auto const broker0 = readBroker(env, broker);
|
||||
auto const lenderOwners0 = env.ownerCount(lender);
|
||||
auto const borrowerOwners0 = env.ownerCount(borrower);
|
||||
|
||||
@@ -778,6 +1127,14 @@ private:
|
||||
BEAST_EXPECT(vault1.available == vault0.available);
|
||||
BEAST_EXPECT(vault1.reserved == vault0.reserved);
|
||||
BEAST_EXPECT(vault1.total == vault0.total);
|
||||
|
||||
// Broker bookkeeping is also fully reversed: DebtTotal and
|
||||
// OwnerCount return to their pre-proposal values, CoverAvailable
|
||||
// is untouched throughout.
|
||||
auto const broker1 = readBroker(env, broker);
|
||||
BEAST_EXPECT(broker1.debtTotal == broker0.debtTotal);
|
||||
BEAST_EXPECT(broker1.ownerCount == broker0.ownerCount);
|
||||
BEAST_EXPECT(broker1.coverAvailable == broker0.coverAvailable);
|
||||
};
|
||||
|
||||
for (auto const assetType : {AssetType::XRP, AssetType::IOU, AssetType::MPT})
|
||||
@@ -790,6 +1147,40 @@ private:
|
||||
<< assetTypeName(assetType) << ")";
|
||||
testDeletePending(assetType, borrower);
|
||||
}
|
||||
|
||||
{
|
||||
testcase("Two-step: LoanBrokerDelete blocked by pending loan");
|
||||
|
||||
// A pending loan bumps the LoanBroker's OwnerCount, so
|
||||
// LoanBrokerDelete must fail with tecHAS_OBLIGATIONS while the
|
||||
// pending loan is outstanding, just as it does for an active
|
||||
// (accepted) loan. Once the pending loan is deleted, the broker
|
||||
// can be deleted too.
|
||||
Env env(*this, features);
|
||||
auto const broker = makeBroker(env, AssetType::XRP);
|
||||
|
||||
auto const loanKeylet = nextLoanKeylet(env, broker);
|
||||
propose(env, broker, lender, borrower, (env.now() + 1h).time_since_epoch().count());
|
||||
env.close();
|
||||
|
||||
// The loan is pending; the broker's OwnerCount is non-zero.
|
||||
if (auto const b = env.le(broker.brokerKeylet()); BEAST_EXPECT(b))
|
||||
BEAST_EXPECT(b->at(sfOwnerCount) != 0u);
|
||||
|
||||
env(jtx::loan_broker::del(lender, broker.brokerID), Ter(tecHAS_OBLIGATIONS));
|
||||
env.close();
|
||||
|
||||
// Broker and loan are both still present.
|
||||
BEAST_EXPECT(env.le(broker.brokerKeylet()));
|
||||
BEAST_EXPECT(env.le(loanKeylet));
|
||||
|
||||
// Delete the pending loan, then the broker can be deleted.
|
||||
env(del(lender, loanKeylet.key));
|
||||
env.close();
|
||||
env(jtx::loan_broker::del(lender, broker.brokerID));
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(broker.brokerKeylet()));
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
Reference in New Issue
Block a user