Skip to content

fix: Unify freeze checks for pseudo-account deposit/withdraw - #7382

Merged
bthomee merged 50 commits into
developfrom
tapanito/vault-freeze-check
Jun 26, 2026
Merged

bthomee merged 50 commits into
developfrom
tapanito/vault-freeze-check

Conversation

@Tapanito

@Tapanito Tapanito commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

High Level Overview of Change

Introduces two centralized freeze-checking helpers — checkWithdrawFreeze and checkDepositFreeze — and refactors every pseudo-account-backed transactor (VaultDeposit, VaultWithdraw, AMMDeposit, AMMWithdraw, LoanBrokerCoverDeposit, LoanBrokerCoverWithdraw) to use them. All behavioral changes are gated behind fixCleanup3_3_0.

Context of Change

Pre-fixCleanup3_3_0, freeze checks across pseudo-account transactors were ad-hoc and inconsistent:

  • VaultWithdraw checked the destination and the submitter's shares, but never checked the vault pseudo-account (source) itself.
  • VaultDeposit checked the depositor and share freeze but not the vault pseudo-account's freeze directly (it was caught indirectly via the transitive share check, yielding tecLOCKED instead of tecFROZEN).
  • LoanBrokerCoverWithdraw checked the broker pseudo-account and destination, but skipped the submitter's individual freeze entirely (a bug — frozen accounts could withdraw to third parties).
  • LoanBrokerCoverDeposit used checkDeepFrozen for the pseudo-account, allowing deposits when the pseudo-account was merely regular-frozen.
  • AMMDeposit/AMMWithdraw had their own inline freeze logic duplicating the same patterns.
  • Destination accounts were blocked on regular freeze rather than deep-freeze, which is overly restrictive (a regularly-frozen account can still receive; only deep-freeze prevents receiving).

Pseudo-Account Deposit / Withdraw Freeze Semantics

Pseudo-accounts (Vaults, AMM pools, LoanBrokers) hold pooled assets on behalf of participants. Their freeze semantics differ from regular account-to-account transfers because:

  1. A frozen pseudo-account traps funds. If the pseudo-account's trustline is frozen, deposited funds can never be withdrawn. Therefore deposits into a frozen pseudo-account must be blocked at the regular freeze level — not just deep freeze.
  2. Self-withdrawal is recovery, not a transfer. A regularly-frozen depositor retrieving their own funds should not be blocked — they are recovering assets, not moving them to a new party. Only deep freeze blocks self-withdrawal.
  3. The issuer can always receive their own token. Withdrawals to the asset issuer bypass all freeze checks (issuer redemption). Callers that need to block even issuer withdrawals (e.g. because pool math cannot handle it) must check checkFrozen(sourceAcct, asset) separately before calling checkWithdrawFreeze.

checkWithdrawFreeze(view, sourceAcct, submitterAcct, dstAcct, asset)

Models the flow source (pseudo) → submitter → destination:

Step Check Rationale
0. Issuer bypass dstAcct == asset.getIssuer()tesSUCCESS Issuer can always receive their own token
1. Source freeze checkFrozen(sourceAcct, asset) Pseudo-account's trustline / global freeze must not block sending
2. Submitter freeze checkFrozen(submitterAcct, asset)skipped when submitter == dst Regular freeze should not prevent recovering one's own funds (self-withdrawal)
3. Destination freeze checkDeepFrozen(dstAcct, asset) Only deep-freeze prevents receiving; regular freeze does not

For MPTs, "locked" is equivalent to deep-frozen, so locked MPT holders are always blocked (including self-withdrawal).

checkDepositFreeze(view, srcAcct, dstAcct, asset)

Models the flow depositor → pseudo-account:

Step Check Rationale
1. Source freeze checkFrozen(srcAcct, asset) Frozen depositor cannot send
2. Destination freeze checkFrozen(dstAcct, asset) Regular freeze blocks deposits because those funds could never be withdrawn

Both functions assert that the pseudo-account argument (sourceAcct / dstAcct) is a pseudo-account via XRPL_ASSERT.


Before / After

All behavioral changes are gated behind fixCleanup3_3_0.

Withdrawals

Scenario Pre-fix Post-fix Why
Withdrawal to issuer (globally frozen/locked) tecFROZEN / tecLOCKED tesSUCCESS Issuer redemption bypass
Self-withdrawal by regularly frozen depositor (IOU) tecFROZEN tesSUCCESS Regular freeze should not block recovery of own funds
Withdrawal to regularly-frozen 3rd party (IOU) tecFROZEN tesSUCCESS Regular freeze on destination does not prevent receiving
Withdrawal to deep-frozen 3rd party (IOU) not checked tecFROZEN Deep freeze prevents all receiving
LoanBrokerCoverWithdraw with frozen submitter → 3rd party tesSUCCESS (bug) tecFROZEN Submitter freeze was not checked
AMM issuer withdrawal from frozen pool tecFROZEN tesSUCCESS Issuer can always receive their own token; ammHolds uses IgnoreFreeze

Deposits

Scenario Pre-fix Post-fix Why
Vault pseudo-account regularly frozen (IOU) tecLOCKED (transitive share check) tecFROZEN (direct checkFrozen) Correct error code; share-level check removed
Vault pseudo-account deep-frozen (IOU) tecLOCKED (transitive share check) tecFROZEN (direct checkFrozen) Same — now caught by checkDepositFreeze directly
LoanBrokerCoverDeposit to regularly-frozen pseudo (IOU) tesSUCCESS (only checkDeepFrozen) tecFROZEN Regular freeze now blocks deposits to pseudo-accounts
AMM deposit with either pool asset frozen per-amount check only both pool assets checked upfront Unified with checkDepositFreeze on both sfAsset / sfAsset2

Vault share-level freeze removal (post-fix)

VaultDeposit previously checked isFrozen(depositor, vaultShare). Post-fix this check is removed: vault shares are issued by the vault pseudo-account, which cannot submit MPTokenIssuanceSet to individually lock a holder's MPToken. The only way shares become locked is transitively via the underlying asset, which checkDepositFreeze / checkWithdrawFreeze already covers.

VaultWithdraw::doApplyIgnoreFreeze for balance check

Post-fix, accountHolds in doApply uses FreezeHandling::IgnoreFreeze because preclaim already validated all freeze constraints via checkWithdrawFreeze. This prevents spurious "insufficient shares" failures for the relaxed self-withdrawal case (where the submitter's regular freeze would otherwise zero out the balance).


Authorization for AMM pseudo-accounts holding vault shares

When a vault share is one of an AMM pool's two assets, the AMM pseudo-account holds that share. requireAuth descends from a vault share to its underlying asset to verify the holder is authorized for the underlying. The AMM holds the share but not the underlying, so the descent wrongly returned tecNO_AUTH (MPT underlying) / tecNO_LINE (IOU underlying).

(Only AMM pools hold vault shares: a vault issues its shares from the vault pseudo-account, and LoanBroker cover is denominated in the vault's underlying asset — neither holds shares.)

On released behavior (SAV enabled, fixCleanup3_3_0 disabled) this left AMM-held shares stuck: an AMM could be created holding vault shares, but accountHolds(ZeroIfUnauthorized) read the pool's share balance as zero, so withdrawals failed with tecAMM_FAILED.

  • requireAuth: pseudo-accounts are exempted before the recursive underlying-asset check, gated by fixCleanup3_3_0. Pre-amendment behavior is preserved (the exemption stays after the recursion).
  • ValidMPTTransfer invariant: pseudo-accounts are treated as authorized in isAuthorized. This is ungated but fork-safe — the invariant only affects consensus under featureMPTokensV2 (unreleased).

Result: AMM pools can be created with, hold, and (post-fixCleanup3_3_0) withdraw vault shares.


API Impact

  • libxrpl change (any change that may affect libxrpl or dependents of libxrpl)

Test Plan

All tests run with both fixCleanup3_3_0 enabled and disabled to verify pre/post-amendment behavior:

  • Vault_test::testVaultDepositFreeze: IOU global freeze, depositor regular/deep freeze, vault-account regular/deep freeze, clawback-while-frozen. MPT global lock, depositor individual lock, vault pseudo-account individual lock.

  • Vault_test::testVaultWithdrawFreeze: IOU global freeze, vault-account regular freeze, depositor regular/deep freeze (self + 3rd party), destination regular/deep freeze, clawback-while-frozen, issuer bypass under global lock. MPT equivalents including issuer bypass.

  • LoanBroker_test::testCoverDepositFreezes: IOU source/pseudo regular/deep freeze, MPT global/individual locks.

  • LoanBroker_test::testCoverWithdrawFreezes: IOU source/submitter/destination freeze at regular and deep levels, submitter-to-self, issuer bypass. MPT equivalents.

  • AMM_test: Global/individual freeze deposit and withdraw tests updated with amendmentCombinations({fixCleanup3_3_0}) to cover both amendment states. Issuer withdrawal from frozen pool.

  • AMMMPT_test::testAMMWithVaultShares: AMM with an IOU-backed and an MPT-backed vault share as a pool asset; deposit/withdraw under underlying freeze/lock, pre- and post-fixCleanup3_3_0 (pre-amendment the shares are inaccessible to the pseudo-account, so withdraw fails with tecAMM_FAILED).

  • Invariants_test: side-specific AMM_WITHDRAW vault-share freeze invariant cases (pseudo-account as sender vs. receiver, pre/post-fixCleanup3_3_0).

Under fixCleanup3_2_0, VaultWithdraw::preclaim now explicitly checks that
the vault pseudo-account (sender) is not frozen, and replaces the
destination checkFrozen with checkDeepFrozen — a frozen account can still
receive assets, only a deep-frozen one cannot.

Pre-amendment behaviour is preserved via the else branch. Tests are updated
to reflect the changed error codes and new pre/post amendment cases are
added for the vault-account and 3rd-party freeze scenarios.

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

No issues.

Review by Claude Opus 4.6 · Prompt: V15

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

No issues.

Review by Claude Opus 4.6 · Prompt: V15

@codecov

codecov Bot commented Jun 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.80240% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.0%. Comparing base (50fdb38) to head (09e6aa1).

Files with missing lines Patch % Lines
src/libxrpl/ledger/helpers/TokenHelpers.cpp 93.5% 2 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff            @@
##           develop   #7382    +/-   ##
========================================
  Coverage     82.0%   82.0%            
========================================
  Files         1007    1007            
  Lines        76888   76996   +108     
  Branches      8971    8969     -2     
========================================
+ Hits         63042   63147   +105     
- Misses       13837   13840     +3     
  Partials         9       9            
Files with missing lines Coverage Δ
include/xrpl/tx/transactors/dex/AMMWithdraw.h 100.0% <ø> (ø)
src/libxrpl/ledger/helpers/MPTokenHelpers.cpp 96.4% <100.0%> (+0.1%) ⬆️
src/libxrpl/tx/invariants/MPTInvariant.cpp 85.4% <100.0%> (+0.6%) ⬆️
src/libxrpl/tx/transactors/dex/AMMDeposit.cpp 95.2% <100.0%> (-<0.1%) ⬇️
src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp 93.7% <100.0%> (+0.3%) ⬆️
.../tx/transactors/lending/LoanBrokerCoverDeposit.cpp 90.6% <100.0%> (+1.7%) ⬆️
...tx/transactors/lending/LoanBrokerCoverWithdraw.cpp 93.0% <100.0%> (+0.6%) ⬆️
src/libxrpl/tx/transactors/vault/VaultDeposit.cpp 95.1% <100.0%> (+0.1%) ⬆️
src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp 89.3% <100.0%> (+0.6%) ⬆️
src/libxrpl/ledger/helpers/TokenHelpers.cpp 95.2% <93.5%> (-0.1%) ⬇️

... and 3 files with indirect coverage changes

Impacted file tree graph

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

bthomee
bthomee previously approved these changes Jun 2, 2026
@bthomee
bthomee requested review from a1q123456 and vvysokikh1 June 2, 2026 17:16
Comment thread src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp
Verify the dstAcct != vaultAsset.getIssuer() guard introduced under
fixCleanup3_2_0: frozen vault trust line and MPT global lock both block
non-issuer withdrawals, while the issuer-destination path bypasses
preclaim's freeze checks (redemption path). Preclaim passes for the
issuer destination, but doApply::accountHolds(ZeroIfFrozen) still returns
0 for locked shares — a matching doApply fix is tracked via TODO comments.

Also updates expected error codes in existing freeze tests affected by
the new vault-sender and deep-frozen-destination checks.

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

No issues.

Review by Claude Sonnet 4.6 · Prompt: V15

Hoist dstAcct into doApply and use FreezeHandling::IgnoreFreeze for
accountHolds when dstAcct == vaultAsset.getIssuer() under fixCleanup3_2_0.
Without this the locked-share balance read as zero and the redemption
failed with tecINSUFFICIENT_FUNDS despite preclaim passing.

Update tests: issuer-redemption now succeeds end-to-end for both IOU and
MPT vaults.

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

No issues.

Review by Claude Sonnet 4.6 · Prompt: V15

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

No issues.

Review by Claude Sonnet 4.6 · Prompt: V15

@Tapanito
Tapanito requested a review from vvysokikh1 June 3, 2026 15:46
Tapanito added 2 commits June 3, 2026 17:55
The previous edit dropped the assertion that clawback still works when
the MPT asset is globally locked. Restore it before the issuer-redemption
step, claw back 50 of the 100 deposited units, then withdraw the remaining
50 to the issuer so both behaviours are exercised in the same test.
Mirror the MPT and trust-line-freeze variants: under global freeze the
redemption path (dstAcct == vaultAsset.getIssuer()) should still succeed.

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

No issues.

Review by Claude Sonnet 4.6 · Prompt: V15

Comment thread src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp
@vvysokikh1
vvysokikh1 self-requested a review June 3, 2026 16:15
vvysokikh1
vvysokikh1 previously approved these changes Jun 3, 2026
Tapanito and others added 7 commits June 11, 2026 14:46
…ezes

Introduce checkWithdrawFreezes helper gated behind fixCleanup3_3_0 that
consolidates source, submitter, and destination freeze checks for
pseudo-account withdrawals. The issuer-redemption exemption is now
handled in one place rather than scattered across callers.

Add MPT individual-lock tests for depositor and 3rd-party destination
withdrawal scenarios, and extend LoanBrokerCoverWithdraw with the same
consolidated freeze logic.
Signed-off-by: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com>
…kDepositFreeze

Reflect the new deposit freeze semantics: only a deep-frozen
pseudo-account blocks deposits; a regular freeze on the vault account's
trust line no longer does. Also account for the checkWithdrawFreezes
change where submitter == destination skips the submitter freeze check.

Parameterize the pre/post-fix test variants via runTest lambdas instead
of withFix ternaries. Add a dedicated deep-freeze deposit test.
Add checkDepositFreeze that checks the depositor for regular freeze and
the destination pseudo-account for deep freeze only. A regular freeze on
the vault account's trust line no longer blocks deposits.

Update checkWithdrawFreezes to skip the submitter freeze check when
submitter == destination (withdrawing to self).

Gate VaultDeposit with fixCleanup3_3_0; pre-fix path preserves the old
isFrozen-based checks. Use checkDepositFreeze in LoanBrokerCoverDeposit.
@Tapanito
Tapanito requested a review from bthomee June 12, 2026 12:40

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

No issues.

Review by Claude Sonnet 4.6 · Prompt: V15

@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 to me


Review by ReviewBot 🤖

Review by Claude Sonnet 4.6 · Prompt: V15

@github-actions

Copy link
Copy Markdown

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

gregtatcam
gregtatcam previously approved these changes Jun 26, 2026
…eze-check

# Conflicts:
#	src/libxrpl/ledger/helpers/MPTokenHelpers.cpp
#	src/test/app/Vault_test.cpp
@github-actions

Copy link
Copy Markdown

All conflicts have been resolved. Assigned reviewers can now start or resume their review.

shawnxie999
shawnxie999 previously approved these changes Jun 26, 2026

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

No issues.

Review by Claude Sonnet 4.6 · Prompt: V15

gregtatcam
gregtatcam previously approved these changes Jun 26, 2026
@Tapanito Tapanito 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 Jun 26, 2026
@bthomee
bthomee requested a review from Copilot June 26, 2026 16:58

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

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

Comment thread src/test/jtx/utility.h
@Tapanito
Tapanito dismissed stale reviews from gregtatcam and shawnxie999 via 661e5a7 June 26, 2026 17:32

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

No issues.

Review by Claude Sonnet 4.6 · Prompt: V15

@bthomee
bthomee enabled auto-merge June 26, 2026 18:39
@bthomee
bthomee added this pull request to the merge queue Jun 26, 2026
Merged via the queue into develop with commit 3e9f1d0 Jun 26, 2026
49 checks passed
@bthomee
bthomee deleted the tapanito/vault-freeze-check branch June 26, 2026 22:45
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.

8 participants