Add new invariants

This commit is contained in:
JCW
2026-07-13 15:46:43 +01:00
parent 7f3a0cbb8a
commit 23e19827d7
3 changed files with 58 additions and 2 deletions

View File

@@ -37,9 +37,13 @@ namespace xrpl {
* must be absent.
* c. Either `Borrower`, or both `Counterparty` and `CounterpartySignature`,
* must be present.
* d. If `Borrower` is present, it must differ from the submitting
* `Account`.
* 6. A `LoanSet` that creates a loan must set `lsfLoanPending` if and only if
* `Borrower` is present and `CounterpartySignature` is absent.
* 7. A `LoanSet` that creates a loan must set the loan's `Borrower`.
* Additionally, a pending loan's `StartDate` must be in the future (so it is
* still in the future when `LoanAccept` finalizes it).
* 8. A pending loan (`lsfLoanPending` set) must not be linked into the
* borrower's directory (`OwnerNode` absent), and a non-pending loan must
* be linked (`OwnerNode` present).

View File

@@ -182,6 +182,14 @@ ValidLoan::finalize(
"CounterpartySignature";
return false;
}
// In the two-step flow the named Borrower must be a different
// account from the one submitting the LoanSet.
if (hasBorrower && tx.getAccountID(sfBorrower) == tx.getAccountID(sfAccount))
{
JLOG(j.fatal()) << "Invariant failed: LoanSet Borrower is the "
"submitting account";
return false;
}
bool const shouldPend = hasBorrower && !hasCounterpartySig;
if (shouldPend != after->isFlag(lsfLoanPending))
@@ -191,6 +199,18 @@ ValidLoan::finalize(
return false;
}
// A pending loan (the two-step flow) is accepted in a later ledger,
// so its StartDate must be strictly in the future at creation to
// remain in the future when LoanAccept finalizes it.
if (shouldPend &&
after->getFieldU32(sfStartDate) <=
view.parentCloseTime().time_since_epoch().count())
{
JLOG(j.fatal()) << "Invariant failed: LoanSet created a pending "
"Loan whose StartDate is not in the future";
return false;
}
// A created loan must always record its Borrower.
if (!after->isFieldPresent(sfBorrower))
{

View File

@@ -4123,8 +4123,9 @@ class Invariants_test : public beast::unit_test::Suite
// Creates a fully paid-off loan, optionally flagged pending, so the
// earlier loan checks pass and only the LoanSet creation checks
// fire.
auto const createLoan = [](bool pending) {
return [pending](Account const& a1, Account const&, ApplyContext& ac) {
auto const createLoan = [](bool pending,
std::optional<std::uint32_t> startDate = std::nullopt) {
return [pending, startDate](Account const& a1, Account const&, ApplyContext& ac) {
auto const vaultKeylet = keylet::vault(a1.id(), ac.view().seq());
auto const loanKeylet = keylet::loan(vaultKeylet.key, 1);
auto sleLoan = std::make_shared<SLE>(loanKeylet);
@@ -4135,6 +4136,8 @@ class Invariants_test : public beast::unit_test::Suite
sleLoan->setFieldU32(sfPaymentRemaining, 0);
if (pending)
sleLoan->setFlag(lsfLoanPending);
if (startDate)
sleLoan->setFieldU32(sfStartDate, *startDate);
ac.view().insert(sleLoan);
return true;
};
@@ -4228,6 +4231,35 @@ class Invariants_test : public beast::unit_test::Suite
tx.makeFieldPresent(sfCounterpartySignature);
}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED});
// In the two-step flow the named Borrower must differ from the
// submitting account; a LoanSet where they are equal is a violation.
doInvariantCheck(
Env{*this, defaultAmendments() | featureLendingProtocolV1_1},
{"LoanSet Borrower is the submitting account"},
createLoan(false),
XRPAmount{},
STTx{
ttLOAN_SET,
[&](STObject& tx) {
tx.setAccountID(sfAccount, borrower.id());
tx.setAccountID(sfBorrower, borrower.id());
}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED});
// A pending loan created by the two-step flow must have a StartDate
// in the future; creating one with a past StartDate is a violation.
// createLoan(true, 1) makes a pending loan whose StartDate (1) is in
// the past, and the Borrower-only LoanSet keeps the pending flag
// consistent so this check is reached.
doInvariantCheck(
Env{*this, defaultAmendments() | featureLendingProtocolV1_1},
{"LoanSet created a pending Loan whose StartDate is not in the "
"future"},
createLoan(true, 1u),
XRPAmount{},
STTx{ttLOAN_SET, [&](STObject& tx) { tx.setAccountID(sfBorrower, borrower.id()); }},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED});
}
// Loan interest due (total value less principal and management fee)