From 5cb6dbadfd5a857e5ced1451d03604cdbe8dc980 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Tue, 18 Aug 2026 16:04:02 +0200 Subject: [PATCH] test: Propagate fixCleanup3_4_0 impair gate into shared lending helpers Gating impair on `fixCleanup3_4_0` switched the "reject not-yet-late impair with tecTOO_SOON" behaviour from an amendment excluded by `LoanTestBase::all_` to one that is included. Every shared helper that called `manage(tfLoanImpair)` immediately after loan creation started returning tecTOO_SOON, cascading into ~7.8k assertion failures across LoanLifecycle, LoanSet, LoanMisc, LoanPay, LoanRounding, LoanInvariants and LoanCoverFreezeAuth. Fixes: - Add `advancePastDueDate(env, loanKeylet)` helper to LoanTestBase.h. Advances the ledger past `sfNextPaymentDueDate` under the fix and is a no-op otherwise. - `defaultImmediately`: advance past due date before impair and gate the legacy `state.nextPaymentDate = env.now()` mutation on the fix state (the fix preserves `sfNextPaymentDueDate`). - `combineAllPayments` payment loop: only impair when the loan is actually late so periodic-payment iterations are not perturbed. - `lifecycle` inline impair/unimpair sub-block: keep the pre-amendment `tesSUCCESS`/`tecLIMIT_EXCEEDED` path when the fix is off; assert the new `tecTOO_SOON` (impair) + `tecNO_PERMISSION` (unimpair-on-unimpaired) contract when the fix is on. Advancing here would push the loan into a late state and break the downstream `toEndOfLife` payment flows with `tecEXPIRED`. The tesSUCCESS impair branch is separately covered by LoanSecurity_test.cpp and LoanCashBasis_test.cpp under the fix. - LoanCoverFreezeAuth_test.cpp `testWithdrawReflectsUnrealizedLoss`: advance past due date before the direct impair call. - LoanCashBasis_test.cpp: drop the now-duplicated local `advancePastDueDate` lambda in favour of the base helper. --- src/test/app/lending/LoanCashBasis_test.cpp | 11 --- .../app/lending/LoanCoverFreezeAuth_test.cpp | 3 + src/test/app/lending/LoanSecurity_test.cpp | 3 +- src/test/app/lending/LoanTestBase.h | 68 ++++++++++++++++--- 4 files changed, 65 insertions(+), 20 deletions(-) diff --git a/src/test/app/lending/LoanCashBasis_test.cpp b/src/test/app/lending/LoanCashBasis_test.cpp index d00f7a3989..a3ca28437d 100644 --- a/src/test/app/lending/LoanCashBasis_test.cpp +++ b/src/test/app/lending/LoanCashBasis_test.cpp @@ -542,17 +542,6 @@ private: return std::make_tuple(broker, loanKeylet, lender, borrower); }; - // Under fixCleanup3_4_0 impairment is only allowed once the - // payment is late. Advance past the first due date before impairing. - auto advancePastDueDate = [&](Env& env, Keylet const& loanKeylet) { - if (!env.current()->rules().enabled(fixCleanup3_4_0)) - return; - auto const loan = env.le(loanKeylet); - BEAST_EXPECT(loan); - std::uint32_t const dueDate = loan->at(sfNextPaymentDueDate); - env.close(NetClock::time_point{NetClock::duration{dueDate}} + 1s); - }; - // ---- impair / unimpair ---- auto runImpairUnimpair = [&](FeatureBitset features) { Env env(*this, features); diff --git a/src/test/app/lending/LoanCoverFreezeAuth_test.cpp b/src/test/app/lending/LoanCoverFreezeAuth_test.cpp index a9b3542c4e..7ec0f83355 100644 --- a/src/test/app/lending/LoanCoverFreezeAuth_test.cpp +++ b/src/test/app/lending/LoanCoverFreezeAuth_test.cpp @@ -236,6 +236,9 @@ private: Ter(tesSUCCESS)); env.close(); + // Under fixCleanup3_4_0 impair requires the payment to be late. + advancePastDueDate(env, loanKeylet); + // Impair the loan to create unrealized loss env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS)); env.close(); diff --git a/src/test/app/lending/LoanSecurity_test.cpp b/src/test/app/lending/LoanSecurity_test.cpp index 0708c8ad36..a425994ac4 100644 --- a/src/test/app/lending/LoanSecurity_test.cpp +++ b/src/test/app/lending/LoanSecurity_test.cpp @@ -622,7 +622,8 @@ private: Number const principalRequest{1, 3}; auto createNewLoan = [&]() { auto const sleBroker = env.le(keylet::loanBroker(broker.brokerID)); - BEAST_EXPECT(sleBroker); + if (!BEAST_EXPECT(sleBroker)) + return keylet::loan(uint256{}); auto const lk = keylet::loan(broker.brokerID, SeqProxy::rawSequence(sleBroker->at(sfLoanSequence))); env(set(borrower, broker.brokerID, broker.asset(principalRequest).value()), diff --git a/src/test/app/lending/LoanTestBase.h b/src/test/app/lending/LoanTestBase.h index 950b196043..9f28d17a7a 100644 --- a/src/test/app/lending/LoanTestBase.h +++ b/src/test/app/lending/LoanTestBase.h @@ -646,6 +646,23 @@ protected: return true; } + // Under fixCleanup3_4_0, LoanManage rejects tfLoanImpair with tecTOO_SOON + // unless the loan payment is already late. Advance the ledger past the + // loan's sfNextPaymentDueDate so shared lifecycle flows still exercise + // the tesSUCCESS branch when the amendment is active. No-op when the + // amendment is disabled. + void + advancePastDueDate(jtx::Env& env, Keylet const& loanKeylet) + { + if (!env.current()->rules().enabled(fixCleanup3_4_0)) + return; + auto const loan = env.le(loanKeylet); + if (!BEAST_EXPECT(loan)) + return; + std::uint32_t const dueDate = loan->at(sfNextPaymentDueDate); + env.close(NetClock::time_point{NetClock::duration{dueDate}} + std::chrono::seconds{1}); + } + enum class AssetType { XRP = 0, IOU = 1, MPT = 2 }; // Specify the accounts as params to allow other accounts to be used @@ -1564,12 +1581,30 @@ protected: // Check the vault bool const canImpair = canImpairLoan(env, broker, state); - // Impair the loan, if possible - env(manage(lender, keylet.key, tfLoanImpair), - canImpair ? Ter(tesSUCCESS) : Ter(tecLIMIT_EXCEEDED)); - // Unimpair the loan - env(manage(lender, keylet.key, tfLoanUnimpair), - canImpair ? Ter(tesSUCCESS) : Ter(tecNO_PERMISSION)); + // Under fixCleanup3_4_0, impair rejects a not-yet-late loan with + // tecTOO_SOON. Advancing time to satisfy the gate here would push + // the loan into a "late" state and break the toEndOfLife flows + // (singlePayment/fullPayment) that expect a fresh loan without the + // tfLoanLatePayment flag. The tesSUCCESS/tecLIMIT_EXCEEDED impair + // path is already covered under fixCleanup3_4_0 by dedicated tests + // in LoanSecurity_test.cpp and LoanCashBasis_test.cpp. + if (!env.current()->rules().enabled(fixCleanup3_4_0)) + { + // Impair the loan, if possible + env(manage(lender, keylet.key, tfLoanImpair), + canImpair ? Ter(tesSUCCESS) : Ter(tecLIMIT_EXCEEDED)); + // Unimpair the loan + env(manage(lender, keylet.key, tfLoanUnimpair), + canImpair ? Ter(tesSUCCESS) : Ter(tecNO_PERMISSION)); + } + else + { + // With the fix on, a not-yet-late loan can never be impaired + // (tecTOO_SOON) and the follow-up unimpair on an unimpaired + // loan is still tecNO_PERMISSION. + env(manage(lender, keylet.key, tfLoanImpair), Ter(tecTOO_SOON)); + env(manage(lender, keylet.key, tfLoanUnimpair), Ter(tecNO_PERMISSION)); + } auto const nextDueDate = startDate + *loanParams.payInterval; @@ -2160,6 +2195,11 @@ protected: { // Check the vault bool const canImpair = canImpairLoan(env, broker, state); + // Under fixCleanup3_4_0 impair requires the payment to + // already be late. Advance past the loan's next due + // date so this exercises the tesSUCCESS branch. No-op + // when the fix is disabled. + advancePastDueDate(env, loanKeylet); // Impair the loan, if possible env(manage(lender, loanKeylet.key, tfLoanImpair), canImpair ? Ter(tesSUCCESS) : Ter(tecLIMIT_EXCEEDED)); @@ -2167,7 +2207,11 @@ protected: if (canImpair) { state.flags |= tfLoanImpair; - state.nextPaymentDate = env.now().time_since_epoch().count(); + // Prior to fixCleanup3_4_0 impair rewrote + // sfNextPaymentDueDate to parentCloseTime. Under the + // fix, the due date is preserved. + if (!env.current()->rules().enabled(fixCleanup3_4_0)) + state.nextPaymentDate = env.now().time_since_epoch().count(); // Once the loan is impaired, it can't be impaired again env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tecNO_PERMISSION)); @@ -2787,7 +2831,15 @@ protected: auto const borrowerBalanceBeforePayment = env.balance(borrower, broker.asset); - if (canImpairLoan(env, broker, state)) + // Under fixCleanup3_4_0 impair requires the payment to + // already be late. This periodic-payment loop stays + // within each payment interval, so the loan is never + // late here; skip the impair rather than perturb the + // payment schedule. + bool const impairAllowed = canImpairLoan(env, broker, state) && + (!env.current()->rules().enabled(fixCleanup3_4_0) || + isPaymentLate(*env.current(), env.le(loanKeylet))); + if (impairAllowed) { // Making a payment will unimpair the loan env(manage(lender, loanKeylet.key, tfLoanImpair));