Skip to content

fix: Add VaultInvariant check that lossUnrealized is non-negative (FN-32) - #7863

Open
tyalymov wants to merge 1 commit into
developfrom
FN-32-vault-invariant-check-loss-unrealized-non-negative
Open

fix: Add VaultInvariant check that lossUnrealized is non-negative (FN-32)#7863
tyalymov wants to merge 1 commit into
developfrom
FN-32-vault-invariant-check-loss-unrealized-non-negative

Conversation

@tyalymov

@tyalymov tyalymov commented Jul 24, 2026

Copy link
Copy Markdown

High Level Overview of Change

ValidVault::finalize (the VaultInvariant) validated the upper bound on the vault's
sfLossUnrealized field but never the lower bound. This adds a defense-in-depth check
that lossUnrealized must be non-negative, plus a unit test.

Ref: FN-32.

Context of Change

The invariant already checks lossUnrealized <= assetsTotal - assetsAvailable,
assetsTotal >= 0, and assetsAvailable >= 0, but not lossUnrealized >= 0.

A negative lossUnrealized would pass all existing checks. Downstream, the withdraw
math in VaultHelpers computes assetTotal -= lossUnrealized; with a negative loss this
inflates the effective asset total above assetsTotal, which would let withdrawers
extract more assets than the vault tracks.

Today the only writers of sfLossUnrealized (the LoanManage impair / unimpair / default
paths) already guard against underflow, so lossUnrealized cannot currently go negative —
there is no mainnet exposure. This change is purely defense-in-depth: if a future lending
bug drove the field negative, the invariant would now catch it instead of allowing a
silent over-withdrawal.

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

Added a unit test in Invariants_test.cpp that sets lossUnrealized negative on a vault
and confirms the invariant fails with "loss unrealized must be positive". Verified the test
fails without the fix and passes with it; both test sets are green (xrpld --unittest and
ctest --preset conan-debug).

@tyalymov
tyalymov requested a review from Tapanito July 24, 2026 14:53
@codecov

codecov Bot commented Jul 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

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

Looks good.

Comment thread src/libxrpl/tx/invariants/VaultInvariant.cpp
@Tapanito
Tapanito requested a review from a1q123456 July 27, 2026 13:31
@tyalymov
tyalymov requested a review from Tapanito July 28, 2026 11:53
@tyalymov
tyalymov force-pushed the FN-32-vault-invariant-check-loss-unrealized-non-negative branch from 0ccf141 to e729fc5 Compare July 28, 2026 11:57
Comment thread src/libxrpl/tx/invariants/VaultInvariant.cpp

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

Left a small nit, other than that lgtm.

@tyalymov
tyalymov requested a review from Tapanito July 28, 2026 12:26

@Tapanito Tapanito 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 solid, well-tested.

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

lgtm

@tyalymov tyalymov added the Ready to merge *PR author* thinks it's ready to merge. Has passed code review. Perf sign-off may still be required. label Jul 28, 2026
@tyalymov tyalymov changed the title fix: Add VaultInvariant check that lossUnrealized is non-negative fix: Add VaultInvariant check that lossUnrealized is non-negative (FN-32) Jul 29, 2026
@bthomee
bthomee requested a review from Copilot July 30, 2026 11:55

Copilot AI 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.

Pull request overview

This PR strengthens the ValidVault::finalize invariant checks by adding a lower-bound validation for sfLossUnrealized (ensuring it cannot be negative when fixCleanup3_4_0 is enabled), and adds a unit test to assert both post- and pre-amendment behavior.

Changes:

  • Add an invariant failure when lossUnrealized < 0 under fixCleanup3_4_0.
  • Add a unit test that forces lossUnrealized = -1 and verifies invariant behavior with and without the amendment.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
src/libxrpl/tx/invariants/VaultInvariant.cpp Adds an amendment-gated lower-bound check rejecting negative lossUnrealized.
src/test/app/Invariants_test.cpp Adds coverage to ensure negative lossUnrealized trips the invariant only when the amendment is enabled.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/libxrpl/tx/invariants/VaultInvariant.cpp

@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. Clean implementation with comprehensive test coverage.

…non-negative

The VaultInvariant validated the upper bound on the vault's sfLossUnrealized
field but never the lower bound. A negative lossUnrealized would pass the
invariant and cause the withdraw math (assetTotal -= lossUnrealized) to use an
effective asset total greater than assetsTotal, letting withdrawers extract more
assets than the vault tracks.

Add a defense-in-depth check that lossUnrealized must not be negative. Because
featureSingleAssetVault is already released, the check is gated behind
fixCleanup3_4_0 to avoid introducing a new transaction failure path that could
fork the network.

The test uses ttLOAN_MANAGE (permitted to change lossUnrealized, isolating the
check) plus a paired case with the amendment disabled asserting the transaction
still succeeds.

Ref: FN-32.
@tyalymov
tyalymov force-pushed the FN-32-vault-invariant-check-loss-unrealized-non-negative branch from 96174dc to fce9ff6 Compare July 30, 2026 14:49
@tyalymov
tyalymov requested a review from Tapanito July 30, 2026 14:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Ready to merge *PR author* thinks it's ready to merge. Has passed code review. Perf sign-off may still be required.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants