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.
This commit is contained in:
Vito
2026-08-18 16:04:02 +02:00
parent 5d3e957576
commit 5cb6dbadfd
4 changed files with 65 additions and 20 deletions

View File

@@ -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);

View File

@@ -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();

View File

@@ -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()),

View File

@@ -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));