Skip to content

fix: Exempt loan default from asset freeze (FN-23) - #7932

Open
tyalymov wants to merge 1 commit into
developfrom
tialymov/FN-23-loan_default_freeze_guard
Open

fix: Exempt loan default from asset freeze (FN-23)#7932
tyalymov wants to merge 1 commit into
developfrom
tialymov/FN-23-loan_default_freeze_guard

Conversation

@tyalymov

@tyalymov tyalymov commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

High Level Overview of Change

LoanManage::defaultLoan moves First-Loss Capital through the issuer's trust lines with the broker and vault pseudo-accounts. When the vault's asset was frozen and featureDeepFreeze was active, this tripped the TransfersNotFrozen (IOU) and ValidMPTTransfer (MPT) invariants, rejecting tfLoanDefault with tecINVARIANT_FAILED and burning the broker's fee.

Per XLS-0066, a broker must be able to default an already-late loan regardless of freeze state. This exempts LoanManage's tfLoanDefault from both invariants, gated behind fixCleanup3_4_0 so ledgers without the amendment keep the current behavior. Added testLoanDefaultBypassesFreeze, which locks in both the pre- and post-amendment behavior.

Ref: FN-23.

Context of Change

The bug report assumed accountSend itself rejects transfers on a frozen line. It doesn't: directSendNoFeeIOU/directSendNoFeeMPT never check freeze state. The actual rejection comes from the post-apply ledger invariants, which is why the fix lives there instead of in LoanManage's preclaim.

API Impact

  • Public API: New feature (new methods and/or new fields)
  • Public API: Breaking change (in general, breaking changes should only impact the next api_version)
  • libxrpl change (any change that may affect libxrpl or dependents of libxrpl)
  • Peer protocol change (must be backward compatible or bump the peer protocol version)

Test Plan

testLoanDefaultBypassesFreeze (src/test/app/Loan_test.cpp) freezes the issuer, then submits tfLoanDefault with fixCleanup3_4_0 disabled (expects tecINVARIANT_FAILED, confirming the old behavior) and again with it enabled (expects tesSUCCESS).

  • --unittest=Loan: 1116 cases, 121874 tests, 0 failures
  • --unittest=Invariants: 85 cases, 13978 tests, 0 failures
  • Full gtest suite via ctest: 891 tests, 100% passed

@codecov

codecov Bot commented Aug 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.22222% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/libxrpl/ledger/helpers/LendingHelpers.cpp 93.3% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@xrplf-ai-reviewer xrplf-ai-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two identical security scoping issues in invariant exemptions—see inline.

Comment thread src/libxrpl/tx/invariants/FreezeInvariant.cpp Outdated
Comment thread src/libxrpl/tx/invariants/MPTInvariant.cpp Outdated
@tyalymov
tyalymov force-pushed the tialymov/FN-23-loan_default_freeze_guard branch 3 times, most recently from 09d7989 to 0e8ed1f Compare August 4, 2026 10:52

@xrplf-ai-reviewer xrplf-ai-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a well-scoped, carefully reasoned fix. The new loanDefaultFreezeExemptAccounts helper derives the broker/vault/issuer triple entirely from ledger-resolved chains (Loan -> LoanBroker -> Vault) rather than trusting independently-supplied IDs, so there's no IDOR-style mismatch risk. The freeze exemption in FreezeInvariant.cpp is placed correctly as a last-resort check right before the invariant-failure path (after the normal freeze/enforce short-circuits), and the account-pair matching reuses the same low/high-limit-issuer pattern already used elsewhere in the file. Because balanceChanges_ is only populated for Issues actually touched by the transaction, the exemption can't leak onto unrelated currencies even though the pairing check doesn't itself re-verify the currency. The MPT-side change correctly keeps the authorization check active while only waiving the freeze/lock check, and only for the specific broker/vault accounts. Test coverage (unit test for the helper's early-return paths, plus an end-to-end pre/post-amendment invariant test) matches the logic well. I didn't find any correctness or security issues in the changed lines that I'm confident about.


[[nodiscard]] std::optional<LoanDefaultFreezeExemptAccounts>
loanDefaultFreezeExemptAccounts(ReadView const& view, STTx const& tx);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it worth making freeze exempt general, not just the loan?

Then loanDefaultFreezeExemptAccounts() -> freezeExemptAccounts()

  struct LoanDefaultFreezeExemptAccounts
  {
      AccountID issuer;
      AccountID broker;
      AccountID vault;
  };
  ->
  struct FreezeExemptAccounts
  {
      AccountID issuer;
      vector<AccountID> accounts;
  };

Just a thought

LoanManage::defaultLoan moves First-Loss Capital through the issuer's
trust lines with the broker and vault pseudo-accounts, so a frozen
asset tripped the TransfersNotFrozen/ValidMPTTransfer invariants and
rejected tfLoanDefault with tecINVARIANT_FAILED. XLS-0066 exempts
defaults from freeze, so this exempts LoanManage's tfLoanDefault from
both invariants, gated behind fixCleanup3_4_0 to keep pre-amendment
ledgers unchanged.
@tyalymov
tyalymov force-pushed the tialymov/FN-23-loan_default_freeze_guard branch from 0e8ed1f to 87f9505 Compare August 4, 2026 13:28

@xrplf-ai-reviewer xrplf-ai-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exemption plumbing (loanDefaultFreezeExemptAccounts + the two invariant call sites) is well-reasoned and correctly gated behind fixCleanup3_4_0, with good test coverage for the intended IOU/MPT default-bypasses-freeze scenarios. The one real gap: in both FreezeInvariant.cpp and MPTInvariant.cpp, the exemption matches purely on account identity (issuer/broker/vault), never checking that the frozen/locked balance change is actually in the vault's own asset/currency or MPT ID. Since the same issuer AccountID can issue multiple currencies (IOU case) or broker/vault could hold unrelated MPT issuances (MPT case), the exemption is broader than the code's own comments claim ('an unrelated frozen line touched by the same transaction is still caught') — it's currently inert only because defaultLoan happens to move a single asset per tx.

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

This PR has conflicts, please resolve them in order for the PR to be reviewed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants