diff --git a/include/xrpl/tx/invariants/LoanInvariant.h b/include/xrpl/tx/invariants/LoanInvariant.h index 3f7d33f13e..474a2abb71 100644 --- a/include/xrpl/tx/invariants/LoanInvariant.h +++ b/include/xrpl/tx/invariants/LoanInvariant.h @@ -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). diff --git a/src/libxrpl/tx/invariants/LoanInvariant.cpp b/src/libxrpl/tx/invariants/LoanInvariant.cpp index 4fd0e28949..76aac774c9 100644 --- a/src/libxrpl/tx/invariants/LoanInvariant.cpp +++ b/src/libxrpl/tx/invariants/LoanInvariant.cpp @@ -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)) { diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index 9d9f36456b..cd7e16c6a3 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -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 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(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)