fix: Exempt loan default from asset freeze (FN-23) - #7932
Conversation
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
09d7989 to
0e8ed1f
Compare
There was a problem hiding this comment.
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); | ||
|
|
There was a problem hiding this comment.
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.
0e8ed1f to
87f9505
Compare
There was a problem hiding this comment.
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.
|
This PR has conflicts, please resolve them in order for the PR to be reviewed. |
High Level Overview of Change
LoanManage::defaultLoanmoves First-Loss Capital through the issuer's trust lines with the broker and vault pseudo-accounts. When the vault's asset was frozen andfeatureDeepFreezewas active, this tripped theTransfersNotFrozen(IOU) andValidMPTTransfer(MPT) invariants, rejectingtfLoanDefaultwithtecINVARIANT_FAILEDand 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'stfLoanDefaultfrom both invariants, gated behindfixCleanup3_4_0so ledgers without the amendment keep the current behavior. AddedtestLoanDefaultBypassesFreeze, which locks in both the pre- and post-amendment behavior.Ref: FN-23.
Context of Change
The bug report assumed
accountSenditself rejects transfers on a frozen line. It doesn't:directSendNoFeeIOU/directSendNoFeeMPTnever check freeze state. The actual rejection comes from the post-apply ledger invariants, which is why the fix lives there instead of inLoanManage's preclaim.API Impact
libxrplchange (any change that may affectlibxrplor dependents oflibxrpl)Test Plan
testLoanDefaultBypassesFreeze(src/test/app/Loan_test.cpp) freezes the issuer, then submitstfLoanDefaultwithfixCleanup3_4_0disabled (expectstecINVARIANT_FAILED, confirming the old behavior) and again with it enabled (expectstesSUCCESS).--unittest=Loan: 1116 cases, 121874 tests, 0 failures--unittest=Invariants: 85 cases, 13978 tests, 0 failuresctest: 891 tests, 100% passed