Skip to content

fix: Conserve funds correctly when LoanPay fee payee is below reserve - #7843

Open
Tapanito wants to merge 5 commits into
developfrom
tapanito/bugfix-loan-pay-fn-67
Open

fix: Conserve funds correctly when LoanPay fee payee is below reserve#7843
Tapanito wants to merge 5 commits into
developfrom
tapanito/bugfix-loan-pay-fn-67

Conversation

@Tapanito

Copy link
Copy Markdown
Contributor

LoanPay's fund-conservation check read XRP balances via accountHolds(SpendableHandling::FullBalance), which for XRP always defers to xrpLiquid (balance minus reserve, clamped at zero). When the broker fee landed on a payee sitting below its own reserve, the payee's clamped balance stayed zero and the fee vanished from the conservation sum, tripping the "funds are conserved" assert on an otherwise valid payment.

High Level Overview of Change

Context of Change

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)

LoanPay's fund-conservation check read XRP balances via
accountHolds(SpendableHandling::FullBalance), which for XRP always
defers to xrpLiquid (balance minus reserve, clamped at zero). When
the broker fee landed on a payee sitting below its own reserve, the
payee's clamped balance stayed zero and the fee vanished from the
conservation sum, tripping the "funds are conserved" assert on an
otherwise valid payment.
@Tapanito
Tapanito requested review from a1q123456 and gregtatcam July 22, 2026 15:53

@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.

Good to go

Review by Claude Sonnet 4.6 · Prompt: V15

Comment thread src/libxrpl/tx/transactors/lending/LoanPay.cpp

@gregtatcam gregtatcam 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.

LGTM

@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.

Looks good.

@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

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

Files with missing lines Patch % Lines
src/libxrpl/tx/transactors/lending/LoanPay.cpp 93.3% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@a1q123456 a1q123456 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.

all good

{
auto const sle = view.read(keylet::account(id));
if (!sle)
return STAmount{};

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 can be easily tested. I think it's worth adding a test case to cover this line.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I looked at whether this branch (missing account SLE) is actually reachable, and I don't think it is given the invariants around the three callers:

  • accountID_ is the borrower making the payment — it signed the transaction, so it always exists.
  • vaultPseudoAccount exists for as long as the vault/loan does.
  • brokerPayee is either the broker's pseudo-account, or brokerOwner (sfOwner on the broker SLE) — and an account can't go through AccountDelete while it still owns objects, so as long as the broker object exists, its owner account does too.

So hitting the !sle path here would require ledger state that violates one of those invariants (e.g. an owner account deleted while it still owns a broker) — not something reachable through normal transaction processing. I'd rather not fabricate a test that constructs invalid ledger state just to exercise this line; it's a defensive guard, not a real scenario.

If you see a path I'm missing where this is actually reachable, let me know and I'll add a test for it.

@Tapanito Tapanito added this to the 3.4.0 milestone Jul 29, 2026
@Tapanito
Tapanito requested a review from a1q123456 July 29, 2026 11:26
Tapanito added 2 commits July 29, 2026 13:31
- Normalize includes in LoanPay.cpp to angle-bracket form and sort order
- Use init-statement guard in Loan_test.cpp so bugprone-unchecked-optional-access can verify the check before dereferencing loanOpt
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants