Skip to content

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

Open
Tapanito wants to merge 22 commits into
developfrom
tapanito/vault-freeze-check
Open

fix: Unify freeze checks for pseudo-account deposit/withdraw#7382
Tapanito wants to merge 22 commits into
developfrom
tapanito/vault-freeze-check

Conversation

@Tapanito

@Tapanito Tapanito commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

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


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.

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

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 82.0%. Comparing base (772ea80) to head (34b4bb4).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff            @@
##           develop   #7382     +/-   ##
=========================================
- Coverage     82.0%   82.0%   -0.0%     
=========================================
  Files         1007    1007             
  Lines        76854   76885     +31     
  Branches      8984    8984             
=========================================
+ Hits         62984   63009     +25     
- Misses       13861   13867      +6     
  Partials         9       9             
Files with missing lines Coverage Δ
src/libxrpl/ledger/helpers/TokenHelpers.cpp 95.4% <100.0%> (+0.1%) ⬆️
.../tx/transactors/lending/LoanBrokerCoverDeposit.cpp 91.7% <100.0%> (+1.5%) ⬆️
...tx/transactors/lending/LoanBrokerCoverWithdraw.cpp 93.8% <100.0%> (+0.3%) ⬆️
src/libxrpl/tx/transactors/vault/VaultDeposit.cpp 95.1% <100.0%> (+0.1%) ⬆️
src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp 89.1% <100.0%> (+0.5%) ⬆️

... and 6 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 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
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
@Tapanito Tapanito requested a review from vvysokikh1 June 12, 2026 12:40
@Tapanito Tapanito changed the title fix: Check vault sender freeze and use checkDeepFrozen for destination fix: Consolidate and correct vault/broker freeze checks Jun 12, 2026
@Tapanito Tapanito requested review from gregtatcam and removed request for a1q123456 and bthomee June 12, 2026 12:42

@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

@Tapanito Tapanito requested a review from bthomee June 12, 2026 12:46
@Tapanito Tapanito changed the title fix: Consolidate and correct vault/broker freeze checks [W.I.P] fix: Consolidate and correct vault/broker freeze checks Jun 12, 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.

LGTM — clean consolidation of freeze logic.

Review by Claude Sonnet 4.6 · Prompt: V15

Extract all IOU freeze and MPT lock tests from testWithIOU and
testWithMPT into two focused test functions covering VaultDeposit
and VaultWithdraw respectively.

Each function tests both IOU (global, depositor regular/deep,
vault-account regular/deep) and MPT (global lock, depositor lock,
vault-account lock) for both pre- and post-fixCleanup3_3_0, plus
clawback-while-frozen assertions.

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

Went through the changes

One high-severity bug: checkDepositFreeze uses a shallow freeze check on the destination pseudo-account instead of checkDeepFrozen, allowing deposits into a deep-frozen vault that can never be withdrawn — see inline.


Review by ReviewBot 🤖

Review by Claude Sonnet 4.6 · Prompt: V15

Comment thread src/libxrpl/ledger/helpers/TokenHelpers.cpp
@Tapanito Tapanito changed the title [W.I.P] fix: Consolidate and correct vault/broker freeze checks fix: Unify freeze checks for pseudo-account deposit/withdraw Jun 18, 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.

Looked through this one

One high-severity bug: checkDepositFreeze uses regular freeze instead of deep freeze for the pseudo-account destination — see inline.


Review by ReviewBot 🤖

Review by Claude Sonnet 4.6 · Prompt: V15

Comment thread src/libxrpl/ledger/helpers/TokenHelpers.cpp

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 centralizes freeze/lock semantics for pseudo-account deposit/withdraw flows by introducing shared helpers and refactoring pseudo-account-backed transactors to use them under the fixCleanup3_3_0 amendment. It also restructures Vault/LoanBroker tests to concentrate freeze/lock coverage into dedicated test cases, and adjusts the Nix devshell to avoid flaky Conan test builds on Darwin.

Changes:

  • Add checkWithdrawFreezes and checkDepositFreeze helpers (public API) and use them in Vault and LoanBroker cover deposit/withdraw preclaim logic (gated by fixCleanup3_3_0).
  • Update VaultWithdraw share-balance checking to ignore freeze post-fix (since preclaim enforces the new freeze rules).
  • Refactor/move freeze-related assertions into new dedicated tests for Vault and LoanBroker; update the Nix devshell to skip Conan checks on Darwin.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/libxrpl/ledger/helpers/TokenHelpers.cpp Implements centralized pseudo-account deposit/withdraw freeze helpers.
include/xrpl/ledger/helpers/TokenHelpers.h Exposes the new helpers as public API (docs need to match semantics).
src/libxrpl/tx/transactors/vault/VaultDeposit.cpp Switches deposit preclaim freeze logic to checkDepositFreeze under fixCleanup3_3_0.
src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp Switches withdraw preclaim freeze logic to checkWithdrawFreezes under fixCleanup3_3_0 and uses IgnoreFreeze for share balance post-fix.
src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp Switches cover deposit preclaim freeze logic to checkDepositFreeze under fixCleanup3_3_0.
src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp Switches cover withdraw preclaim freeze logic to checkWithdrawFreezes under fixCleanup3_3_0.
src/test/app/Vault_test.cpp Removes inline freeze/lock tests and adds dedicated testVaultDepositFreeze / testVaultWithdrawFreeze.
src/test/app/LoanBroker_test.cpp Removes inline freeze/lock checks and adds dedicated testCoverDepositFreezes / testCoverWithdrawFreezes; reorders some run() calls.
nix/devshell.nix On Darwin, disables Conan’s test suite in the dev shell to avoid unreliable sandbox builds.

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

Comment thread include/xrpl/ledger/helpers/TokenHelpers.h
Comment thread include/xrpl/ledger/helpers/TokenHelpers.h
Comment thread src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp Outdated
Tapanito added 2 commits June 22, 2026 12:09
…/checkWithdrawFreeze

Migrate AMMDeposit and AMMWithdraw to use the unified freeze helpers
introduced for Vault and LoanBroker, gated on fixCleanup3_3_0.

AMMDeposit: checkDepositFreeze for both pool assets replaces the
two-layer checkAsset + checkAmount freeze logic. If either pool asset
is frozen (depositor or AMM account), all deposits are blocked.

AMMWithdraw: checkWithdrawFreeze per withdrawn amount replaces
checkFrozen(AMM) + checkIndividualFrozen(user). Regular IOU freeze
no longer blocks self-withdrawal; only deep freeze does. The issuer
exemption allows the token issuer to withdraw from a frozen pool;
issuerFreezeHandling() ensures doApply uses IgnoreFreeze so pool
math does not divide by zero.

Rename checkWithdrawFreezes -> checkWithdrawFreeze (singular) and
update all callers. Document both helpers with full freeze semantics.

@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 added 2 commits June 22, 2026 13:52
Restore unconditional IgnoreFreeze in VaultWithdraw::doApply when
fixCleanup3_3_0 is enabled. That accountHolds reads the submitter's
share balance, which recurses into the underlying asset, so freeze
gating must stay in preclaim; a dstAcct==issuer guard there wrongly
blocked self-withdrawal under a regular freeze.

Apply issuer-aware freeze handling to LoanBrokerCoverWithdraw::preclaim
so the issuer can redeem their own token from the broker pseudo-account
even under a global freeze, matching AMM and Vault. Here accountHolds
reads the pseudo-account's holdings, so the issuer-only IgnoreFreeze is
both necessary and sufficient.

Rename the checkWithdrawFreeze source parameter to srcAcct to match its
definition, drop a misleading comment, and hoist the fixCleanup feature
flags into named locals.
clang-tidy include-cleaner flagged amendmentCombinations using
std::initializer_list without a direct include.

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

Two freeze-check gaps flagged inline — deep-frozen depositor bypass in checkDepositFreeze and frozen-submitter bypass via issuer early-return in checkWithdrawFreeze.

Review by Claude Sonnet 4.6 · Prompt: V15

isPseudoAccount(view, dstAcct),
"xrpl::checkDepositFreeze : destination must be a pseudo-account");

if (auto const ret = checkFrozen(view, srcAcct, asset))

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.

checkDepositFreeze never calls checkDeepFrozen on srcAcct, so a deep-frozen depositor can still move funds into the pseudo-account. Add a deep-frozen check on the source before the regular freeze check on the destination:

    if (auto const ret = checkDeepFrozen(view, srcAcct, asset))
        return ret;

    if (auto const ret = checkFrozen(view, srcAcct, asset))
        return ret;

Comment thread src/libxrpl/ledger/helpers/TokenHelpers.cpp
@Tapanito Tapanito requested review from shawnxie999 and removed request for vvysokikh1 June 22, 2026 13:31
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.

6 participants