Skip to content

fix: Reject VaultWithdraw fixed-share amounts that round to zero - #7950

Open
Tapanito wants to merge 2 commits into
developfrom
tapanito/lending-bugfix
Open

fix: Reject VaultWithdraw fixed-share amounts that round to zero#7950
Tapanito wants to merge 2 commits into
developfrom
tapanito/lending-bugfix

Conversation

@Tapanito

@Tapanito Tapanito commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

  • VaultWithdraw::doApply()'s fixed-shares branch (withdraw a fixed number of shares, variable assets) had no guard against a requested share amount whose true value rounds down to zero in the vault asset's native (integral) representation — unlike the fixed-assets branch, which already returns tecPRECISION_LOSS in the analogous case. This let the transaction proceed to burn shares for zero value, tripping a VaultInvariant check (tecINVARIANT_FAILED) instead of failing cleanly.
  • Conversely, when the pool's effective value is genuinely zero (e.g. a fully impaired/insolvent vault), a zero-value withdrawal is legitimate — the invariant now allows it instead of failing.
  • Both behaviors are gated behind fixCleanup3_4_0.
  • Added a shared effectiveAssetsTotalWithdraw() helper in VaultHelpers to avoid duplicating the AssetsTotal - LossUnrealized computation between the withdraw conversion helpers and the new transactor check.

Test plan

  • testBugVaultWithdrawFixedSharesRoundsToZero (Loan_test.cpp) exercises both scenarios (XRP: real precision loss; IOU: genuinely zero pool) under both pre- and post-fixCleanup3_4_0 amendment states.
  • Full Loan suite: 1072 cases / 117,230 tests, 0 failures.
  • Full Vault suite: 411 cases / 11,373 tests, 0 failures.
  • Full Invariants suite: 85 cases / 14,090 tests, 0 failures.

The fixed-shares withdrawal branch had no guard against a requested
share amount whose true value rounds down to zero in the vault
asset's native (integral) representation, unlike the fixed-assets
branch's existing check. This let the transaction proceed to burn
shares for zero value, tripping a VaultInvariant check instead of
failing cleanly with tecPRECISION_LOSS.

Conversely, when the pool's effective value is genuinely zero (e.g. a
fully impaired/insolvent vault), a zero-value withdrawal is legitimate
and the invariant now allows it instead of failing.

Both behaviors are gated behind fixCleanup3_4_0.

@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 tested fix. I traced both new gating conditions end-to-end: (1) the VaultWithdraw.cpp precision-loss guard for the fixed-share branch correctly uses the same effectiveAssetsTotalWithdraw(vault, waiveUnrealizedLoss) computation that the actual conversion helper uses, so it can't diverge from the real payout calculation, and it correctly exempts the final-withdrawal case which is handled elsewhere; (2) the VaultInvariant.cpp zeroDeltaIsLegitimate exception is gated on !maybeVaultDeltaAssets, so it only ever relaxes the check when there truly was no recorded delta, and it still fires the original fatal checks whenever a delta exists or the pool had genuine effective value — preserving detection of real accounting bugs. The DeltaInfo default-value fallback and the skip of the account-delta reconciliation when zeroDeltaIsLegitimate are consistent with a genuinely zero-value transfer needing no reconciliation. I did not find a confident correctness, security, or resource-handling issue in the added lines.

@codecov

codecov Bot commented Aug 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

A recovered or withdrawn amount can be genuinely non-zero yet still be
dust relative to a sfAssetsTotal/sfAssetsAvailable large enough to
exceed STAmount's significant-digit precision: subtracting it rounds
the stored total right back to where it started. The shares still
move, so ValidVault previously failed after the fact with
"must decrease vault balance" instead of a clean upfront rejection.
@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.

1 participant