From 59f5dfa9f17092f0a1bb6c35e3c850020a9be503 Mon Sep 17 00:00:00 2001 From: Valentin Balaschenko <13349202+vlntb@users.noreply.github.com> Date: Mon, 5 Jan 2026 13:50:59 +0000 Subject: [PATCH] fixing Freeze tests --- include/xrpl/ledger/View.h | 16 ++++++++++++ src/libxrpl/ledger/View.cpp | 18 +++++++++++-- src/test/app/Freeze_test.cpp | 25 +++++++++++++++++++ .../app/tx/detail/LoanBrokerCoverWithdraw.cpp | 23 ++++++++--------- src/xrpld/app/tx/detail/VaultWithdraw.cpp | 11 +++----- 5 files changed, 70 insertions(+), 23 deletions(-) diff --git a/include/xrpl/ledger/View.h b/include/xrpl/ledger/View.h index 08d87faa80..f847cf3fe7 100644 --- a/include/xrpl/ledger/View.h +++ b/include/xrpl/ledger/View.h @@ -299,6 +299,22 @@ checkDeepFrozen( asset.value()); } +[[nodiscard]] inline bool +isAnyDeepOrIndividuallyFrozen( + ReadView const& view, + std::initializer_list const& accounts, + Asset const& asset, + int depth = 0) +{ + for (auto const& account : accounts) + { + if (isDeepFrozen(view, account, asset, depth) || + isIndividualFrozen(view, account, asset)) + return true; + } + return false; +} + [[nodiscard]] bool isLPTokenFrozen( ReadView const& view, diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index 1a6062d5d3..14b78fb765 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -331,7 +331,8 @@ isVaultPseudoAccountFrozen( // LCOV_EXCL_STOP } - return isAnyFrozen(view, {issuer, account}, vault->at(sfAsset), depth + 1); + return isAnyDeepOrIndividuallyFrozen( + view, {issuer, account}, vault->at(sfAsset), depth + 1); } bool @@ -1435,6 +1436,16 @@ doWithdraw( // LCOV_EXCL_STOP } + if (dstAcct != senderAcct && dstAcct != amount.getIssuer() && + senderAcct != amount.getIssuer()) + { + if (isGlobalFrozen(view, amount.getIssuer())) + { + return view.rules().enabled(featureLendingProtocol) ? tecPATH_DRY + : tecFROZEN; + } + } + // Move the funds directly from the broker's pseudo-account to the // dstAcct return accountSend( @@ -1456,7 +1467,10 @@ addEmptyHolding( auto const& issuerId = issue.getIssuer(); auto const& currency = issue.currency; if (isGlobalFrozen(view, issuerId)) - return tecFROZEN; // LCOV_EXCL_LINE + { + return view.rules().enabled(featureLendingProtocol) ? tecPATH_DRY + : tecFROZEN; + } auto const& srcId = issuerId; auto const& dstId = accountID; diff --git a/src/test/app/Freeze_test.cpp b/src/test/app/Freeze_test.cpp index 881345a874..659a12c20c 100644 --- a/src/test/app/Freeze_test.cpp +++ b/src/test/app/Freeze_test.cpp @@ -2210,8 +2210,15 @@ class Freeze_test : public beast::unit_test::suite env_post(coverDeposit(issuer, brokerKeylet.key, USD(20))); env_post.close(); + + // Withdrawal to holder should fail due to global freeze tx = coverWithdraw(owner, brokerKeylet.key, USD(15)); tx[sfDestination] = holder.human(); + env_post(tx, ter(tecPATH_DRY)); + + // Withdraw to issuer instead (issuer is exempt from global freeze) + tx = coverWithdraw(owner, brokerKeylet.key, USD(15)); + tx[sfDestination] = issuer.human(); env_post(tx); env_post.close(); @@ -2223,6 +2230,24 @@ class Freeze_test : public beast::unit_test::suite env_post(del(owner, brokerKeylet.key)); env_post.close(); + + // Withdraw remaining vault balances before deleting + // Holder has 40 shares (deposited 50, withdrew 10) + tx = vault.withdraw( + {.depositor = holder, + .id = vaultKeylet.key, + .amount = USD(40)}); + tx[sfDestination] = issuer.human(); + env_post(tx); + env_post.close(); + + // Issuer has 20 shares (deposited 20) + env_post(vault.withdraw( + {.depositor = issuer, + .id = vaultKeylet.key, + .amount = USD(20)})); + env_post.close(); + env_post(vault.del({.owner = owner, .id = vaultKeylet.key})); } } diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp index f40a1751b1..a12da1d597 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp @@ -96,12 +96,12 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx) if (auto const ter = requireAuth(ctx.view, vaultAsset, dstAcct, authType)) return ter; - // Check for freezes, unless sending directly to the issuer + // Skip source freeze check here - let doWithdraw handle it. + // This allows global freeze cases to properly return tecPATH_DRY instead of + // tecFROZEN. Check for destination deep freeze, unless sending directly to + // the issuer if (dstAcct != vaultAsset.getIssuer()) { - // Cannot send a frozen Asset - if (auto const ret = checkFrozen(ctx.view, pseudoAccountID, vaultAsset)) - return ret; // Destination account cannot receive if asset is deep frozen if (auto const ret = checkDeepFrozen(ctx.view, dstAcct, vaultAsset)) return ret; @@ -126,19 +126,16 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx) if ((coverAvail - amount) < minimumCover) return tecINSUFFICIENT_FUNDS; - // Allow returning frozen/locked assets to issuer for both IOUs and MPTs. - // When withdrawing to the issuer, ignore freeze since frozen IOUs and - // locked MPTs can both be sent back to their issuer. - FreezeHandling const freezeHandling = - (dstAcct == vaultAsset.getIssuer() || account == vaultAsset.getIssuer()) - ? FreezeHandling::fhIGNORE_FREEZE - : FreezeHandling::fhZERO_IF_FROZEN; - + // Check if the pseudo-account has sufficient funds. + // Use fhIGNORE_FREEZE because we want to allow the transaction to proceed + // to doApply where doWithdraw will perform the proper global freeze check + // and return tecPATH_DRY if appropriate. If we used fhZERO_IF_FROZEN here, + // we would return tecINSUFFICIENT_FUNDS prematurely. if (accountHolds( ctx.view, pseudoAccountID, vaultAsset, - freezeHandling, + FreezeHandling::fhIGNORE_FREEZE, AuthHandling::ahZERO_IF_UNAUTHORIZED, ctx.j) < amount) return tecINSUFFICIENT_FUNDS; diff --git a/src/xrpld/app/tx/detail/VaultWithdraw.cpp b/src/xrpld/app/tx/detail/VaultWithdraw.cpp index 8ac62b2c7b..668c41592b 100644 --- a/src/xrpld/app/tx/detail/VaultWithdraw.cpp +++ b/src/xrpld/app/tx/detail/VaultWithdraw.cpp @@ -79,14 +79,9 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx) !isTesSuccess(ter)) return ter; - // Cannot withdraw from a Vault an Asset frozen for the destination account. - // Skip freeze check if either party is the issuer (applies to both IOUs and - // MPTs). - if (dstAcct != vaultAsset.getIssuer() && account != vaultAsset.getIssuer()) - { - if (auto const ret = checkFrozen(ctx.view, dstAcct, vaultAsset)) - return ret; - } + // Skip destination asset freeze check here - let doWithdraw handle it. + // This allows global freeze cases to properly return tecPATH_DRY instead + // of tecFROZEN. // Cannot return shares to the vault, if the underlying asset was frozen for // the submitter. Skip freeze check if either party is the issuer (applies