From c9fcc629a510328f53f69c64ab101041d50795b7 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Mon, 29 Jun 2026 19:58:06 +0200 Subject: [PATCH 1/6] fix: disable Vault Share deposits to the AMM --- include/xrpl/ledger/helpers/MPTokenHelpers.h | 9 + include/xrpl/ledger/helpers/TokenHelpers.h | 32 +- src/libxrpl/ledger/helpers/TokenHelpers.cpp | 45 +- src/libxrpl/tx/transactors/dex/AMMCreate.cpp | 20 + src/test/app/AMMMPT_test.cpp | 176 +----- src/test/app/Vault_test.cpp | 607 ++++++++++--------- 6 files changed, 432 insertions(+), 457 deletions(-) diff --git a/include/xrpl/ledger/helpers/MPTokenHelpers.h b/include/xrpl/ledger/helpers/MPTokenHelpers.h index c709badab86..b7f87337cff 100644 --- a/include/xrpl/ledger/helpers/MPTokenHelpers.h +++ b/include/xrpl/ledger/helpers/MPTokenHelpers.h @@ -23,6 +23,15 @@ namespace xrpl { [[nodiscard]] bool isGlobalFrozen(ReadView const& view, MPTIssue const& mptIssue); +/** Returns true if @p account's MPToken for @p mptIssue carries the + * individual-lock flag (lsfMPTLocked). + * + * @warning This checks only the raw per-holder lock bit. It does **not** + * perform the transitive vault pseudo-account check: if @p mptIssue is a + * vault share whose underlying asset is frozen, this function returns false. + * Call @ref isFrozen instead when determining whether an account may send or + * receive tokens — it combines isIndividualFrozen, isGlobalFrozen, and + * isVaultPseudoAccountFrozen into a single complete check. */ [[nodiscard]] bool isIndividualFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mptIssue); diff --git a/include/xrpl/ledger/helpers/TokenHelpers.h b/include/xrpl/ledger/helpers/TokenHelpers.h index 0c9871cd764..c8a67f776a4 100644 --- a/include/xrpl/ledger/helpers/TokenHelpers.h +++ b/include/xrpl/ledger/helpers/TokenHelpers.h @@ -144,19 +144,18 @@ checkDeepFrozen(ReadView const& view, AccountID const& account, Asset const& ass * * Otherwise checks, in order: * 1. If the asset is globally frozen the remaining checks are redundant. - * 2. For MPT shares: The pseudo-account's vault share must not be transitively frozen via its - * underlying asset. - * 3. The pseudo-account's trustline / MPToken must not be frozen for sending. - * 4. Skipped when submitter == dst (self-withdrawal); a regular freeze should not prevent - * recovering one's own funds. - * 5. The destination must not be deep-frozen (cannot receive under any circumstance). + * 2. The pseudo-account's trustline / MPToken must not be individually frozen for sending. + * 3. The submitter's trustline / MPToken must not be individually frozen. Skipped when + * submitter == dst (self-withdrawal) so a regular freeze does not prevent recovering one's own + * funds. (Enforced as defensive code; no current caller exercises a frozen submitter ≠ dst.) + * 4. The destination must not be deep-frozen. * - * For IOUs a regular individual freeze on the withdrawer does NOT block self-withdrawal; only deep - * freeze does. For MPTs "locked" is equivalent to deep-frozen, so locked MPT holders are always + * For IOUs a regular individual freeze on the submitter does NOT block self-withdrawal; only deep + * freeze does. For MPTs "locked" is equivalent to deep-frozen, so locked MPT holders are always * blocked. * * @param view Ledger view to read freeze state from. - * @param srcAcct Pseudo-account the funds are withdrawn from (sender). + * @param pseudoAcct Pseudo-account the funds are withdrawn from (sender). * @param submitterAcct Account that submitted the withdrawal transaction. * @param dstAcct Account receiving the withdrawn funds. * @param asset Asset being withdrawn. @@ -166,7 +165,7 @@ checkDeepFrozen(ReadView const& view, AccountID const& account, Asset const& ass [[nodiscard]] TER checkWithdrawFreeze( ReadView const& view, - AccountID const& srcAcct, + AccountID const& pseudoAcct, AccountID const& submitterAcct, AccountID const& dstAcct, Asset const& asset); @@ -175,20 +174,17 @@ checkWithdrawFreeze( * Checks freeze compliance for depositing an asset into a pseudo-account (e.g. Vault, AMM, * LoanBroker). * - * * Checks, in order: * 1. If the asset is globally frozen the remaining checks are redundant. - * 2. For MPT shares: the pseudo-account's vault share must not be transitively frozen via its - * underlying asset (returns tecLOCKED). - * 3. The depositor must not be individually frozen. Skipped when srcAcct is the asset issuer, - * since the issuer can always send its own asset. - * 4. The pseudo-account must not be individually frozen for the asset. Unlike regular accounts, + * 2. The depositor must not be individually frozen for the asset. Skipped when srcAcct is the + * asset issuer, since the issuer can always send its own asset. + * 3. The pseudo-account must not be individually frozen for the asset. Unlike regular accounts, * pseudo-accounts cannot receive deposits under a regular freeze because the deposited funds * could not later be withdrawn. * * @param view Ledger view to read freeze state from. * @param srcAcct Depositor sending the funds. - * @param dstAcct Pseudo-account receiving the deposit. + * @param pseudoAcct Pseudo-account receiving the deposit. * @param asset Asset being deposited. * @return tesSUCCESS if the deposit is permitted, otherwise a freeze result * (tecFROZEN for IOUs, tecLOCKED for MPTs). @@ -197,7 +193,7 @@ checkWithdrawFreeze( checkDepositFreeze( ReadView const& view, AccountID const& srcAcct, - AccountID const& dstAcct, + AccountID const& pseudoAcct, Asset const& asset); //------------------------------------------------------------------------------ diff --git a/src/libxrpl/ledger/helpers/TokenHelpers.cpp b/src/libxrpl/ledger/helpers/TokenHelpers.cpp index 7988e24a56c..9756564e3ee 100644 --- a/src/libxrpl/ledger/helpers/TokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/TokenHelpers.cpp @@ -160,19 +160,24 @@ checkDeepFrozen(ReadView const& view, AccountID const& account, Asset const& ass [[nodiscard]] TER checkWithdrawFreeze( ReadView const& view, - AccountID const& srcAcct, + AccountID const& pseudoAcct, AccountID const& submitterAcct, AccountID const& dstAcct, Asset const& asset) { XRPL_ASSERT( - isPseudoAccount(view, srcAcct), "xrpl::checkWithdrawFreeze : source is a pseudo-account"); + isPseudoAccount(view, pseudoAcct), + "xrpl::checkWithdrawFreeze : source is a pseudo-account"); XRPL_ASSERT( !isPseudoAccount(view, submitterAcct), "xrpl::checkWithdrawFreeze : submitter is not a pseudo-account"); XRPL_ASSERT( !isPseudoAccount(view, dstAcct), "xrpl::checkWithdrawFreeze : destination is not a pseudo-account"); + // AMM,Vault,LoanBroker cannot be created using Vault Shares as an asset + XRPL_ASSERT( + !isPseudoAccount(view, asset.getIssuer()), + "xrpl::checkWithdrawFreeze : asset issuer cannot be a pseudo-account"); // Funds can always be sent to the issuer if (dstAcct == asset.getIssuer()) @@ -182,18 +187,20 @@ checkWithdrawFreeze( if (auto const ret = checkGlobalFrozen(view, asset); !isTesSuccess(ret)) return ret; - // Special case for shares - check if the shares (and the transitive asset) is not frozen - if (asset.holds() && - isVaultPseudoAccountFrozen(view, srcAcct, asset.get(), 0)) - { - return tecLOCKED; - } - // The transfer is from Submitter to Destination via Source (pseudo-account) // Both Source and Submitter must not be frozen to allow sending funds - if (auto const ret = checkIndividualFrozen(view, srcAcct, asset); !isTesSuccess(ret)) + if (auto const ret = checkIndividualFrozen(view, pseudoAcct, asset); !isTesSuccess(ret)) return ret; + if (asset.holds() && + isVaultPseudoAccountFrozen(view, pseudoAcct, asset.get(), 0)) + { + // LCOV_EXCL_START + UNREACHABLE("xrpl::checkWithdrawFreeze : pseudo-account backed object holds shares"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } + // Check submitter's individual freeze only when Submitter != Destination (a regular freeze // should not block self-withdrawal). if (submitterAcct != dstAcct) @@ -210,24 +217,30 @@ checkWithdrawFreeze( checkDepositFreeze( ReadView const& view, AccountID const& srcAcct, - AccountID const& dstAcct, + AccountID const& pseudoAcct, Asset const& asset) { XRPL_ASSERT( - isPseudoAccount(view, dstAcct), + isPseudoAccount(view, pseudoAcct), "xrpl::checkDepositFreeze : destination is a pseudo-account"); XRPL_ASSERT( !isPseudoAccount(view, srcAcct), "xrpl::checkDepositFreeze : source is not a pseudo-account"); + // AMM,Vault,LoanBroker cannot be created using Vault Shares as an asset + XRPL_ASSERT( + !isPseudoAccount(view, asset.getIssuer()), + "xrpl::checkDepositFreeze : asset issuer cannot be a pseudo-account"); if (auto const ret = checkGlobalFrozen(view, asset); !isTesSuccess(ret)) return ret; - // Special case for shares - check if the shares and the transitive asset is not frozen if (asset.holds() && - isVaultPseudoAccountFrozen(view, dstAcct, asset.get(), 0)) + isVaultPseudoAccountFrozen(view, pseudoAcct, asset.get(), 0)) { - return tecLOCKED; + // LCOV_EXCL_START + UNREACHABLE("xrpl::checkDepositFreeze : pseudo-account backed object holds shares"); + return tecINTERNAL; + // LCOV_EXCL_STOP } if (srcAcct != asset.getIssuer()) @@ -238,7 +251,7 @@ checkDepositFreeze( // Unlike regular accounts, pseudo-accounts cannot receive deposits under a regular freeze // because those funds cannot be later withdrawn - return checkIndividualFrozen(view, dstAcct, asset); + return checkIndividualFrozen(view, pseudoAcct, asset); } //------------------------------------------------------------------------------ diff --git a/src/libxrpl/tx/transactors/dex/AMMCreate.cpp b/src/libxrpl/tx/transactors/dex/AMMCreate.cpp index ef271ba362b..7e4d23f2cf5 100644 --- a/src/libxrpl/tx/transactors/dex/AMMCreate.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMCreate.cpp @@ -193,6 +193,26 @@ AMMCreate::preclaim(PreclaimContext const& ctx) pseudoAccountAddress(ctx.view, keylet::amm(amount.asset(), amount2.asset()).key); accountId == beast::kZero) return terADDRESS_COLLISION; + + if (ctx.view.rules().enabled(featureMPTokensV2)) + { + auto const isVaultShare = [&](Asset const& asset) { + if (asset.native()) + return false; + + if (asset.holds()) + return false; + + return isPseudoAccount(ctx.view, asset.getIssuer()); + }; + + if (isVaultShare(amount.asset()) || isVaultShare(amount2.asset())) + { + JLOG(ctx.j.debug()) + << "AMM Instance: can't create with vault shares " << amount << " " << amount2; + return tecWRONG_ASSET; + } + } } if (auto const ter = canMPTTradeAndTransfer(ctx.view, amount.asset(), accountID, accountID); diff --git a/src/test/app/AMMMPT_test.cpp b/src/test/app/AMMMPT_test.cpp index 9ff6c65c17f..c68270591a0 100644 --- a/src/test/app/AMMMPT_test.cpp +++ b/src/test/app/AMMMPT_test.cpp @@ -6892,168 +6892,38 @@ struct AMMMPT_test : public jtx::AMMTest void testAMMWithVaultShares() { - testcase("AMM with vault shares — underlying freeze blocks share withdrawal"); + testcase("AMM with vault shares not allowed"); using namespace jtx; + // AMMTestBase::testableAmendments() strips featureSingleAssetVault, // but vault shares require it. Use the global jtx set directly. - FeatureBitset const all{jtx::testableAmendments()}; - - // When alice's underlying asset is individually frozen: - // - // Deposit (post-fixCleanup3_3_0): checkDepositFreeze checks the AMM - // pseudo-account's underlying, not alice's — deposit is allowed. - // Pre-fix: featureAMMClawback calls isFrozen(alice, share) which - // descends via isVaultPseudoAccountFrozen(alice,...) and finds the - // frozen underlying — deposit is blocked. - // - // Withdrawal (post-fixCleanup3_3_0): checkWithdrawFreeze ends with - // checkDeepFrozen(alice, share) which calls isFrozen(alice, share) - // and finds the frozen underlying — withdrawal is blocked. - // Pre-fix: the old path only checks the AMM account's MPToken lock, - // which is unset — withdrawal succeeds. - - auto runIOU = [&](FeatureBitset const& features) { - bool const fix330 = features[fixCleanup3_3_0]; - Env env{*this, envconfig(), features, nullptr, beast::Severity::Disabled}; - - env.fund(XRP(100'000), gw_, alice_); - env(fset(gw_, asfDefaultRipple)); - env.close(); - - PrettyAsset const iou = gw_["IOU"]; - env.trust(iou(1'000'000), alice_); - env(pay(gw_, alice_, iou(10'000))); - env.close(); - - Vault const vault{env}; - auto [createTx, vaultKeylet] = vault.create({.owner = alice_, .asset = iou}); - env(createTx); - env.close(); - - // 200 IOU → 200,000,000 vault shares (IOU vault scale = 6) - env(vault.deposit({.depositor = alice_, .id = vaultKeylet.key, .amount = iou(200)})); - env.close(); - - auto const shareMPTID = env.le(vaultKeylet)->at(sfShareMPTID); - // Use half the shares for the AMM; alice keeps the other half. - STAmount const shareAmt{MPTIssue{shareMPTID}, 100'000'000}; - // Pool: XRP(100) = 1e8 drops, shares = 1e8 → LP ≈ 1e8 - AMM amm{env, alice_, XRP(100), shareAmt}; - env.close(); - - // Freeze alice's IOU trustline (individual freeze on underlying). - env(trust(gw_, iou(0), alice_, tfSetFreeze)); - env.close(); + Env env{*this, envconfig(), jtx::testableAmendments(), nullptr, beast::Severity::Disabled}; - // post-fix330: checkDepositFreeze checks AMM pseudo's underlying - // (not alice's) → deposit is allowed - // pre-fix330: featureAMMClawback path calls isFrozen(alice, share) - // which descends to alice's frozen IOU → tecLOCKED - amm.deposit( - {.account = alice_, - .asset1In = XRP(1), - .err = Ter(fix330 ? TER(tesSUCCESS) : TER(tecLOCKED))}); - - // post-fix330: checkWithdrawFreeze → checkDeepFrozen(alice, share) - // descends to alice's frozen IOU → tecLOCKED - // pre-fix330: the AMM pseudo-account is not authorized for the - // share's underlying (requireAuth recurses share→IOU - // and the AMM holds no IOU trustline), so - // accountHolds(ZeroIfUnauthorized) reports the pool's - // share balance as 0 and the withdrawal math fails → - // tecAMM_FAILED. Vault shares deposited into an AMM are - // only withdrawable once fixCleanup3_3_0 exempts the - // pseudo-account from the recursive auth check. - amm.withdraw( - {.account = alice_, - .tokens = 1'000, - .err = Ter(fix330 ? TER(tecLOCKED) : TER(tecAMM_FAILED))}); - - env(trust(gw_, iou(0), alice_, tfClearFreeze)); - env.close(); - - // Lifting the freeze lets the deposit through in both cases. The - // withdrawal only succeeds post-fix330; pre-fix330 the share balance - // remains inaccessible to the unauthorized pseudo-account, so the - // shares stay stuck → tecAMM_FAILED. - amm.deposit({.account = alice_, .asset1In = XRP(1)}); - amm.withdraw( - {.account = alice_, - .tokens = 1'000, - .err = Ter(fix330 ? TER(tesSUCCESS) : TER(tecAMM_FAILED))}); - }; - - runIOU(all); - runIOU(all - fixCleanup3_3_0); - - auto runMPT = [&](FeatureBitset const& features) { - bool const fix330 = features[fixCleanup3_3_0]; - // Expected freeze failures fire invariant checks that log at Error; - // silence them so the test output stays clean. - Env env{*this, envconfig(), features, nullptr, beast::Severity::Disabled}; - - env.fund(XRP(100'000), gw_, alice_); - env.close(); - - MPTTester mptt{env, gw_, kMptInitNoFund}; - mptt.create({.flags = kMptDexFlags | tfMPTCanLock}); - PrettyAsset const mpt = mptt.issuanceID(); - mptt.authorize({.account = alice_}); - env(pay(gw_, alice_, mpt(30'000))); - env.close(); - - Vault const vault{env}; - auto [createTx, vaultKeylet] = vault.create({.owner = alice_, .asset = mpt}); - env(createTx); - env.close(); - - // 20000 MPT → 20000 vault shares (MPT vault scale = 0) - env(vault.deposit({.depositor = alice_, .id = vaultKeylet.key, .amount = mpt(20'000)})); - env.close(); - - auto const shareMPTID = env.le(vaultKeylet)->at(sfShareMPTID); - // Use half the shares for the AMM; alice keeps the other half. - // Pool: XRP(100) = 1e8 drops, shares = 10000 → LP ≈ 1e6 - STAmount const shareAmt{MPTIssue{shareMPTID}, 10'000}; - AMM amm{env, alice_, XRP(100), shareAmt}; - env.close(); + env.fund(XRP(100'000), gw_, alice_); + env(fset(gw_, asfDefaultRipple)); + env.close(); - // Lock alice's underlying MPT (individual lock). - mptt.set({.holder = alice_, .flags = tfMPTLock}); + PrettyAsset const iou = gw_["IOU"]; + env.trust(iou(1'000'000), alice_); + env(pay(gw_, alice_, iou(10'000))); + env.close(); - // Same pre/post-fix330 semantics as the IOU case above. - amm.deposit( - {.account = alice_, - .asset1In = XRP(1), - .err = Ter(fix330 ? TER(tesSUCCESS) : TER(tecLOCKED))}); - - // {.tokens = 1'000} → frac = 1000/1e6 = 0.001 - // XRP out = 1e8 * 0.001 = 1e5 drops, shares out = 10000 * 0.001 = 10 - // post-fix330: checkWithdrawFreeze sees alice's locked underlying - // MPT via the share → tecLOCKED. - // pre-fix330: the AMM pseudo-account is unauthorized for the - // share's underlying MPT (it holds no underlying - // MPToken), so accountHolds(ZeroIfUnauthorized) zeros - // the pool's share balance and the math fails → - // tecAMM_FAILED. - amm.withdraw( - {.account = alice_, - .tokens = 1'000, - .err = Ter(fix330 ? TER(tecLOCKED) : TER(tecAMM_FAILED))}); + Vault const vault{env}; + auto [createTx, vaultKeylet] = vault.create({.owner = alice_, .asset = iou}); + env(createTx); + env.close(); - mptt.set({.holder = alice_, .flags = tfMPTUnlock}); + env(vault.deposit({.depositor = alice_, .id = vaultKeylet.key, .amount = iou(200)})); + env.close(); - // Unlocking lets the deposit through; the withdrawal only succeeds - // post-fix330 (pre-fix330 the shares remain stuck → tecAMM_FAILED). - amm.deposit({.account = alice_, .asset1In = XRP(1)}); - amm.withdraw( - {.account = alice_, - .tokens = 1'000, - .err = Ter(fix330 ? TER(tesSUCCESS) : TER(tecAMM_FAILED))}); - }; + auto const vaultSle = env.le(vaultKeylet); + if (!BEAST_EXPECT(vaultSle)) + return; - runMPT(all); - runMPT(all - fixCleanup3_3_0); + auto const shareMPTID = vaultSle->at(sfShareMPTID); + STAmount const shareAmt{MPTIssue{shareMPTID}, 100'000'000}; + AMM const amm{env, alice_, XRP(100), shareAmt, Ter(tecWRONG_ASSET)}; + env.close(); } void diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 9b294421973..190b47301f2 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -1634,8 +1634,6 @@ class Vault_test : public beast::unit_test::Suite env(tx, Ter(tecNO_ENTRY)); }); - // Freeze/lock tests are in testVaultDepositFreeze/testVaultWithdrawFreeze - testCase([this]( Env& env, Account const& issuer, @@ -7512,185 +7510,212 @@ class Vault_test : public beast::unit_test::Suite } void - testVaultDepositFreeze() + testVaultDepositFreezeIOU() { using namespace test::jtx; + testcase("VaultDeposit IOU freeze checks"); Account const issuer{"issuer"}; Account const owner{"owner"}; + Env env{*this}; + Vault vault{env}; - // === IOU === - { - testcase("VaultDeposit IOU freeze checks"); - Env env{*this}; - Vault vault{env}; - - env.fund(XRP(100'000), issuer, owner); - env(fset(issuer, asfAllowTrustLineClawback)); - env.close(); - PrettyAsset const asset = issuer["IOU"]; - env.trust(asset(1'000'000), owner); - env(pay(issuer, owner, asset(100'000))); - env.close(); + env.fund(XRP(100'000), issuer, owner); + env(fset(issuer, asfAllowTrustLineClawback)); + env.close(); + PrettyAsset const asset = issuer["IOU"]; + env.trust(asset(1'000'000), owner); + env(pay(issuer, owner, asset(100'000))); + env.close(); - auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); - env(tx); - env.close(); - auto const vaultAcct = Account("vault", env.le(keylet)->at(sfAccount)); + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + env(tx); + env.close(); + auto const vaultAcct = Account("vault", env.le(keylet)->at(sfAccount)); - // Initial deposit so the vault pseudo-account has a trustline - env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)})); - env.close(); + // Initial deposit so the vault pseudo-account has a trustline + env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)})); + env.close(); - auto runTests = [&]() { - auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0); + auto runTests = [&]() { + auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0); - // Global freeze + // Global freeze + { + testcase("VaultDeposit IOU global freeze"); env(fset(issuer, asfGlobalFreeze)); env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}), Ter(tecFROZEN)); env(fclear(issuer, asfGlobalFreeze)); + } - // Depositor regular freeze + // Depositor freeze + { + testcase("VaultDeposit IOU depositor freeze"); env(trust(issuer, asset(0), owner, tfSetFreeze)); env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}), Ter(tecFROZEN)); env(trust(issuer, asset(0), owner, tfClearFreeze)); + } - // Depositor deep freeze + // Depositor deep freeze + { + testcase("VaultDeposit IOU depositor deep freeze"); env(trust(issuer, asset(0), owner, tfSetFreeze | tfSetDeepFreeze)); env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}), Ter(tecFROZEN)); env(trust(issuer, asset(0), owner, tfClearFreeze | tfClearDeepFreeze)); + } - // Vault-account regular freeze - // Post-fix: checkDepositFreeze catches it → tecFROZEN - // Pre-fix: not checked directly, but the transitive share - // check triggers → tecLOCKED - { - auto trustSet = [&]() { - json::Value jv; - jv[jss::Account] = issuer.human(); - { - auto& ja = jv[jss::LimitAmount] = - asset(0).value().getJson(JsonOptions::Values::None); - ja[jss::issuer] = toBase58(vaultAcct.id()); - } - jv[jss::TransactionType] = jss::TrustSet; - return jv; - }(); + // Vault-account freeze + // Post-fix: checkDepositFreeze catches it → tecFROZEN + // Pre-fix: not checked directly, but the transitive share + // check triggers → tecLOCKED + { + testcase("VaultDeposit IOU pseudo-account freeze"); + auto trustSet = [&]() { + json::Value jv; + jv[jss::Account] = issuer.human(); + { + auto& ja = jv[jss::LimitAmount] = + asset(0).value().getJson(JsonOptions::Values::None); + ja[jss::issuer] = toBase58(vaultAcct.id()); + } + jv[jss::TransactionType] = jss::TrustSet; + return jv; + }(); - trustSet[jss::Flags] = tfSetFreeze; - env(trustSet); - env.close(); + trustSet[jss::Flags] = tfSetFreeze; + env(trustSet); + env.close(); - TER const expected = fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED); - env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}), - Ter(expected)); + TER const expected = fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED); + env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}), + Ter(expected)); - trustSet[jss::Flags] = tfClearFreeze; - env(trustSet); - env.close(); - } + trustSet[jss::Flags] = tfClearFreeze; + env(trustSet); + env.close(); + } - // Vault-account deep freeze - { - auto trustSet = [&]() { - json::Value jv; - jv[jss::Account] = issuer.human(); - { - auto& ja = jv[jss::LimitAmount] = - asset(0).value().getJson(JsonOptions::Values::None); - ja[jss::issuer] = toBase58(vaultAcct.id()); - } - jv[jss::TransactionType] = jss::TrustSet; - return jv; - }(); + // Vault-account deep freeze + { + testcase("VaultDeposit IOU pseudo-account deep freeze"); + auto trustSet = [&]() { + json::Value jv; + jv[jss::Account] = issuer.human(); + { + auto& ja = jv[jss::LimitAmount] = + asset(0).value().getJson(JsonOptions::Values::None); + ja[jss::issuer] = toBase58(vaultAcct.id()); + } + jv[jss::TransactionType] = jss::TrustSet; + return jv; + }(); - trustSet[jss::Flags] = tfSetFreeze | tfSetDeepFreeze; - env(trustSet); - env.close(); + trustSet[jss::Flags] = tfSetFreeze | tfSetDeepFreeze; + env(trustSet); + env.close(); - env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}), - Ter(fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED))); + env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}), + Ter(fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED))); - trustSet[jss::Flags] = tfClearFreeze | tfClearDeepFreeze; - env(trustSet); - env.close(); - } + trustSet[jss::Flags] = tfClearFreeze | tfClearDeepFreeze; + env(trustSet); + env.close(); + } - // Clawback works while frozen + // Clawback works while frozen + { + testcase("VaultDeposit IOU freeze clawback unaffected"); env(fset(issuer, asfGlobalFreeze)); env(vault.clawback( {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(1)})); env(fclear(issuer, asfGlobalFreeze)); env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)})); env.close(); - }; + } + }; - runTests(); - env.disableFeature(fixCleanup3_3_0); - runTests(); - env.enableFeature(fixCleanup3_3_0); - } + runTests(); + env.disableFeature(fixCleanup3_3_0); + runTests(); + env.enableFeature(fixCleanup3_3_0); + } - // === MPT === - { - testcase("VaultDeposit MPT lock checks"); - Env env{*this}; - Vault vault{env}; + void + testVaultDepositFreezeMPT() + { + using namespace test::jtx; + testcase("VaultDeposit MPT lock checks"); - env.fund(XRP(100'000), issuer, owner); - env.close(); + Account const issuer{"issuer"}; + Account const owner{"owner"}; + Env env{*this}; + Vault vault{env}; - MPTTester mptt{env, issuer, kMptInitNoFund}; - mptt.create( - {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock | tfMPTRequireAuth}); - PrettyAsset const mpt{mptt.issuanceID()}; + env.fund(XRP(100'000), issuer, owner); + env.close(); - mptt.authorize({.account = owner}); - mptt.authorize({.account = issuer, .holder = owner}); - env.close(); - env(pay(issuer, owner, mpt(100'000))); - env.close(); + MPTTester mptt{env, issuer, kMptInitNoFund}; + mptt.create( + {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock | tfMPTRequireAuth}); + PrettyAsset const mpt{mptt.issuanceID()}; - auto [tx, keylet] = vault.create({.owner = owner, .asset = mpt}); - env(tx); - env.close(); - auto const vaultAcctID = env.le(keylet)->at(sfAccount); - Account const vaultAcct("vault", vaultAcctID); + mptt.authorize({.account = owner}); + mptt.authorize({.account = issuer, .holder = owner}); + env.close(); + env(pay(issuer, owner, mpt(100'000))); + env.close(); - env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(100)})); - env.close(); + auto [tx, keylet] = vault.create({.owner = owner, .asset = mpt}); + env(tx); + env.close(); + auto const vaultAcctID = env.le(keylet)->at(sfAccount); + Account const vaultAcct("vault", vaultAcctID); - // For MPT isDeepFrozen == isFrozen, so all locks block in - // both pre- and post-fix. - auto runTests = [&]() { - // Global lock + env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(100)})); + env.close(); + + // For MPT isDeepFrozen == isFrozen, so all locks block in + // both pre- and post-fix. + auto runTests = [&]() { + // Global lock + { + testcase("VaultDeposit MPT global lock"); mptt.set({.flags = tfMPTLock}); env.close(); env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}), Ter(tecLOCKED)); mptt.set({.flags = tfMPTUnlock}); env.close(); + } - // Depositor individual lock + // Depositor individual lock + { + testcase("VaultDeposit MPT depositor lock"); mptt.set({.holder = owner, .flags = tfMPTLock}); env.close(); env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}), Ter(tecLOCKED)); mptt.set({.holder = owner, .flags = tfMPTUnlock}); env.close(); + } - // Vault pseudo-account individual lock + // Vault pseudo-account individual lock + { + testcase("VaultDeposit MPT pseudo-account lock"); mptt.set({.holder = vaultAcct, .flags = tfMPTLock}); env.close(); env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}), Ter(tecLOCKED)); mptt.set({.holder = vaultAcct, .flags = tfMPTUnlock}); env.close(); + } - // Clawback works while locked + // Clawback works while locked + { + testcase("VaultDeposit MPT lock clawback unaffected"); mptt.set({.flags = tfMPTLock}); env.close(); env(vault.clawback( @@ -7699,16 +7724,16 @@ class Vault_test : public beast::unit_test::Suite env.close(); env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)})); env.close(); - }; + } + }; - runTests(); - env.disableFeature(fixCleanup3_3_0); - runTests(); - env.enableFeature(fixCleanup3_3_0); - } + runTests(); + env.disableFeature(fixCleanup3_3_0); + runTests(); + env.enableFeature(fixCleanup3_3_0); } - // Focused demonstration: a depositor under a regular individual IOU freeze + // Focused demonstration: a depositor under a individual IOU freeze // can still withdraw to themselves (self-withdrawal), but is blocked from // withdrawing to a third party. // @@ -7750,7 +7775,7 @@ class Vault_test : public beast::unit_test::Suite auto runTests = [&]() { auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0); - // Set a regular individual freeze on the owner's IOU trustline. + // Set a individual freeze on the owner's IOU trustline. env(trust(issuer, asset(0), owner, tfSetFreeze)); env.close(); @@ -7782,110 +7807,111 @@ class Vault_test : public beast::unit_test::Suite } void - testVaultWithdrawFreeze() + testVaultWithdrawFreezeIOU() { using namespace test::jtx; + testcase("VaultWithdraw IOU freeze checks"); Account const issuer{"issuer"}; Account const owner{"owner"}; + Env env{*this}; + Vault const vault{env}; - // === IOU === - { - testcase("VaultWithdraw IOU freeze checks"); - Env env{*this}; - Vault vault{env}; - - env.fund(XRP(100'000), issuer, owner); - env(fset(issuer, asfAllowTrustLineClawback)); - env.close(); - PrettyAsset const asset = issuer["IOU"]; - env.trust(asset(1'000'000), owner); - env(pay(issuer, owner, asset(100'000))); - env.close(); - - auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); - env(tx); - env.close(); - auto const vaultAcct = Account("vault", env.le(keylet)->at(sfAccount)); + env.fund(XRP(100'000), issuer, owner); + env(fset(issuer, asfAllowTrustLineClawback)); + env.close(); + PrettyAsset const asset = issuer["IOU"]; + env.trust(asset(1'000'000), owner); + env(pay(issuer, owner, asset(100'000))); + env.close(); - env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)})); - env.close(); + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + env(tx); + env.close(); + auto const vaultAcct = Account("vault", env.le(keylet)->at(sfAccount)); - Account const charlie{"charlie"}; - env.fund(XRP(10'000), charlie); - env.trust(asset(1'000'000), charlie); - env.close(); + env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)})); + env.close(); - auto runTests = [&]() { - auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0); - // Post-fix: submitter freeze blocks withdraw to 3rd party - // Pre-fix: submitter's IOU freeze not checked, but - // checkFrozen(depositor, share) may trigger tecLOCKED - TER const submitterTo3rd = fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED); + Account const charlie{"charlie"}; + env.fund(XRP(10'000), charlie); + env.trust(asset(1'000'000), charlie); + env.close(); - // Global freeze → self-withdraw + auto runTests = [&]() { + auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0); + // Global freeze → self-withdraw + { + testcase("VaultWithdraw IOU global freeze"); env(fset(issuer, asfGlobalFreeze)); env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}), Ter(tecFROZEN)); // Global freeze → withdraw to 3rd party - { - auto withdrawToCharlie = - vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}); - withdrawToCharlie[sfDestination] = charlie.human(); - env(withdrawToCharlie, Ter(tecFROZEN)); - } - env(fclear(issuer, asfGlobalFreeze)); - - // Vault-account regular freeze - { - auto trustSet = [&]() { - json::Value jv; - jv[jss::Account] = issuer.human(); - { - auto& ja = jv[jss::LimitAmount] = - asset(0).value().getJson(JsonOptions::Values::None); - ja[jss::issuer] = toBase58(vaultAcct.id()); - } - jv[jss::TransactionType] = jss::TrustSet; - return jv; - }(); - trustSet[jss::Flags] = tfSetFreeze; - env(trustSet); - env.close(); + auto withdrawToCharlie = + vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}); + withdrawToCharlie[sfDestination] = charlie.human(); + env(withdrawToCharlie, Ter(tecFROZEN)); - TER const vaultAcctFreeze = fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED); + env(fclear(issuer, asfGlobalFreeze)); + } - // Self-withdraw - env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}), - Ter(vaultAcctFreeze)); - // Withdraw to 3rd party + // Vault-account freeze + { + testcase("VaultWithdraw IOU pseudo-account freeze"); + auto trustSet = [&]() { + json::Value jv; + jv[jss::Account] = issuer.human(); { - auto withdrawToCharlie = vault.withdraw( - {.depositor = owner, .id = keylet.key, .amount = asset(1)}); - withdrawToCharlie[sfDestination] = charlie.human(); - env(withdrawToCharlie, Ter(vaultAcctFreeze)); + auto& ja = jv[jss::LimitAmount] = + asset(0).value().getJson(JsonOptions::Values::None); + ja[jss::issuer] = toBase58(vaultAcct.id()); } + jv[jss::TransactionType] = jss::TrustSet; + return jv; + }(); - trustSet[jss::Flags] = tfClearFreeze; - env(trustSet); - env.close(); - } + trustSet[jss::Flags] = tfSetFreeze; + env(trustSet); + env.close(); + + TER const terExpected = fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED); - // Depositor regular freeze → self-withdraw + // Self-withdraw + env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}), + Ter(terExpected)); + // Withdraw to 3rd party + + auto withdrawToCharlie = + vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}); + withdrawToCharlie[sfDestination] = charlie.human(); + env(withdrawToCharlie, Ter(terExpected)); + + trustSet[jss::Flags] = tfClearFreeze; + env(trustSet); + env.close(); + } + + // Depositor freeze, self-withdraw + { + testcase("VaultWithdraw IOU self-withdraw freeze check"); env(trust(issuer, asset(0), owner, tfSetFreeze)); + // Post-fix: self-withdraw allowed (submitter==dst skip) // Pre-fix: isFrozen(depositor, iou) catches it env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}), Ter(fix330Enabled ? TER(tesSUCCESS) : TER(tecFROZEN))); - // Depositor regular freeze → withdraw to 3rd party - { - auto withdrawTo3rd = - vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}); - withdrawTo3rd[sfDestination] = charlie.human(); - env(withdrawTo3rd, Ter(submitterTo3rd)); - } + // Depositor freeze withdraw to 3rd party + auto withdrawTo3rd = + vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}); + withdrawTo3rd[sfDestination] = charlie.human(); + + // Post-fix: submitter freeze blocks withdraw to 3rd party + // Pre-fix: submitter's IOU freeze not checked, but checkFrozen(depositor, + // share) triggers tecLOCKED + env(withdrawTo3rd, Ter(fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED))); + env(trust(issuer, asset(0), owner, tfClearFreeze)); // Replenish what was withdrawn if (fix330Enabled) @@ -7893,104 +7919,131 @@ class Vault_test : public beast::unit_test::Suite env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)})); } env.close(); + } - // Depositor deep freeze → self-withdraw blocked + // Depositor deep freeze → self-withdraw blocked + { + testcase("VaultWithdraw IOU depositor deep freeze"); env(trust(issuer, asset(0), owner, tfSetFreeze | tfSetDeepFreeze)); - // TODO: branches are identical - confirm the intended pre/post-fix330 - // expectations and replace with the correct values (one branch may be wrong). + env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}), - // NOLINTNEXTLINE(bugprone-branch-clone) - Ter(fix330Enabled ? TER(tecFROZEN) : TER(tecFROZEN))); + Ter(tecFROZEN)); + env(trust(issuer, asset(0), owner, tfClearFreeze | tfClearDeepFreeze)); + } + + // Destination freeze → withdraw to 3rd party + { + testcase("VaultWithdraw IOU freeze withdraw to 3rd party"); - // Destination regular freeze → withdraw to 3rd party env(trust(issuer, asset(0), charlie, tfSetFreeze)); + // Self-withdraw unaffected by charlie's freeze env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)})); - { - auto withdrawToCharlie = - vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}); - withdrawToCharlie[sfDestination] = charlie.human(); - // Post-fix: regular freeze on dst allowed - // Pre-fix: checkFrozen(dst, iou) catches it - env(withdrawToCharlie, Ter(fix330Enabled ? TER(tesSUCCESS) : TER(tecFROZEN))); - } + + auto withdrawToCharlie = + vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}); + withdrawToCharlie[sfDestination] = charlie.human(); + + // Post-fix: freeze on dst allowed + // Pre-fix: checkFrozen(dst, iou) catches it + env(withdrawToCharlie, Ter(fix330Enabled ? TER(tesSUCCESS) : TER(tecFROZEN))); + env(trust(issuer, asset(0), charlie, tfClearFreeze)); + // Replenish: 1 for self-withdraw + 1 if charlie withdraw succeeded env(vault.deposit( {.depositor = owner, .id = keylet.key, .amount = asset(fix330Enabled ? 2 : 1)})); env.close(); + } + + // Destination deep freeze → withdraw to 3rd party blocked + { + testcase("VaultWithdraw IOU deep freeze withdraw to 3rd party"); - // Destination deep freeze → withdraw to 3rd party blocked env(trust(issuer, asset(0), charlie, tfSetFreeze | tfSetDeepFreeze)); - { - auto withdrawToCharlie = - vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}); - withdrawToCharlie[sfDestination] = charlie.human(); - env(withdrawToCharlie, Ter(tecFROZEN)); - } + + auto withdrawToCharlie = + vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}); + withdrawToCharlie[sfDestination] = charlie.human(); + env(withdrawToCharlie, Ter(tecFROZEN)); + // Destination deep freeze → self-withdraw unaffected env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)})); + env(trust(issuer, asset(0), charlie, tfClearFreeze | tfClearDeepFreeze)); env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)})); env.close(); + } - // Clawback works while frozen + // Clawback works while frozen + { + testcase("VaultWithdraw IOU freeze clawback unaffected"); env(fset(issuer, asfGlobalFreeze)); + env(vault.clawback( {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(1)})); + env(fclear(issuer, asfGlobalFreeze)); env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)})); env.close(); - }; + } + }; - runTests(); - env.disableFeature(fixCleanup3_3_0); - runTests(); - env.enableFeature(fixCleanup3_3_0); - } + runTests(); + env.disableFeature(fixCleanup3_3_0); + runTests(); + env.enableFeature(fixCleanup3_3_0); + } - // === MPT === - { - testcase("VaultWithdraw MPT lock checks"); - Env env{*this}; - Vault vault{env}; + void + testVaultWithdrawFreezeMPT() + { + using namespace test::jtx; + testcase("VaultWithdraw MPT lock checks"); - env.fund(XRP(100'000), issuer, owner); - env.close(); + Account const issuer{"issuer"}; + Account const owner{"owner"}; + Env env{*this}; + Vault vault{env}; - MPTTester mptt{env, issuer, kMptInitNoFund}; - mptt.create( - {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock | tfMPTRequireAuth}); - PrettyAsset const mpt{mptt.issuanceID()}; + env.fund(XRP(100'000), issuer, owner); + env.close(); - mptt.authorize({.account = owner}); - mptt.authorize({.account = issuer, .holder = owner}); - env.close(); - env(pay(issuer, owner, mpt(100'000))); - env.close(); + MPTTester mptt{env, issuer, kMptInitNoFund}; + mptt.create( + {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock | tfMPTRequireAuth}); + PrettyAsset const mpt{mptt.issuanceID()}; - auto [tx, keylet] = vault.create({.owner = owner, .asset = mpt}); - env(tx); - env.close(); - Account const vaultAcct("vault", env.le(keylet)->at(sfAccount)); + mptt.authorize({.account = owner}); + mptt.authorize({.account = issuer, .holder = owner}); + env.close(); + env(pay(issuer, owner, mpt(100'000))); + env.close(); - env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(100)})); - env.close(); + auto [tx, keylet] = vault.create({.owner = owner, .asset = mpt}); + env(tx); + env.close(); + Account const vaultAcct("vault", env.le(keylet)->at(sfAccount)); - Account const charlie{"charlie"}; - env.fund(XRP(10'000), charlie); - env.close(); - mptt.authorize({.account = charlie}); - mptt.authorize({.account = issuer, .holder = charlie}); - env.close(); + env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(100)})); + env.close(); + + Account const charlie{"charlie"}; + env.fund(XRP(10'000), charlie); + env.close(); + mptt.authorize({.account = charlie}); + mptt.authorize({.account = issuer, .holder = charlie}); + env.close(); - auto runTests = [&]() { - auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0); + auto runTests = [&]() { + auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0); - // Global lock + // Global lock + { + testcase("VaultWithdraw MPT global lock"); mptt.set({.flags = tfMPTLock}); env.close(); env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)}), @@ -8013,17 +8066,23 @@ class Vault_test : public beast::unit_test::Suite env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)})); } env.close(); + } - // Vault pseudo-account individual lock + // Vault pseudo-account individual lock + { + testcase("VaultWithdraw MPT pseudo-account lock"); mptt.set({.holder = vaultAcct, .flags = tfMPTLock}); env.close(); env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)}), Ter(tecLOCKED)); mptt.set({.holder = vaultAcct, .flags = tfMPTUnlock}); env.close(); + } - // Depositor individual lock → self-withdraw blocked - // (isDeepFrozen == isFrozen for MPT) + // Depositor individual lock → self-withdraw blocked + // (isDeepFrozen == isFrozen for MPT) + { + testcase("VaultWithdraw MPT depositor lock"); mptt.set({.holder = owner, .flags = tfMPTLock}); env.close(); env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)}), @@ -8052,8 +8111,11 @@ class Vault_test : public beast::unit_test::Suite env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)})); } env.close(); + } - // 3rd party destination lock → withdraw to 3rd party blocked + // 3rd party destination lock → withdraw to 3rd party blocked + { + testcase("VaultWithdraw MPT 3rd party destination lock"); mptt.set({.holder = charlie, .flags = tfMPTLock}); env.close(); { @@ -8068,8 +8130,11 @@ class Vault_test : public beast::unit_test::Suite env.close(); env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)})); env.close(); + } - // Clawback works while locked + // Clawback works while locked + { + testcase("VaultWithdraw MPT lock clawback unaffected"); mptt.set({.flags = tfMPTLock}); env.close(); env(vault.clawback( @@ -8078,13 +8143,13 @@ class Vault_test : public beast::unit_test::Suite env.close(); env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)})); env.close(); - }; + } + }; - runTests(); - env.disableFeature(fixCleanup3_3_0); - runTests(); - env.enableFeature(fixCleanup3_3_0); - } + runTests(); + env.disableFeature(fixCleanup3_3_0); + runTests(); + env.enableFeature(fixCleanup3_3_0); } public: @@ -8128,8 +8193,10 @@ class Vault_test : public beast::unit_test::Suite testWithdrawSoleShareholderPartialFixedSharesUsesFullPrice(); testWithdrawSoleShareholderLoanRepaymentExit(); - testVaultDepositFreeze(); - testVaultWithdrawFreeze(); + testVaultDepositFreezeIOU(); + testVaultDepositFreezeMPT(); + testVaultWithdrawFreezeIOU(); + testVaultWithdrawFreezeMPT(); testVaultSelfWithdrawWhileFrozen(); testReferenceHolding(); From eb6bc2c3cfc2592cff0f6493430a5a15634fb773 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Mon, 29 Jun 2026 21:04:04 +0200 Subject: [PATCH 2/6] fix: undo MPTInvariant changes pertaining vault shares --- src/libxrpl/tx/invariants/MPTInvariant.cpp | 25 +-- src/test/app/Invariants_test.cpp | 214 +++++---------------- src/test/app/Vault_test.cpp | 5 - 3 files changed, 51 insertions(+), 193 deletions(-) diff --git a/src/libxrpl/tx/invariants/MPTInvariant.cpp b/src/libxrpl/tx/invariants/MPTInvariant.cpp index 26bee4effbb..d8f7fcc27db 100644 --- a/src/libxrpl/tx/invariants/MPTInvariant.cpp +++ b/src/libxrpl/tx/invariants/MPTInvariant.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include #include @@ -837,8 +836,6 @@ ValidMPTTransfer::finalize( ReadView const& view, beast::Journal const& j) { - auto const fix330Enabled = view.rules().enabled(fixCleanup3_3_0); - if (hasPrivilege(tx, OverrideFreeze)) return true; @@ -901,27 +898,9 @@ ValidMPTTransfer::finalize( // Check once: if any involved account is frozen, the whole issuance transfer is // considered frozen. Only need to check for frozen if there is a transfer of funds. - // - // Post-fix330: full isFrozen() applies — vault-share transitive freeze is part of - // the freeze semantics for all changed holders. - // - // Pre-fix330: legacy AMM withdraw only checked individual freeze on the - // destination, not the transitive vault freeze. All other paths (and the AMM - // account itself as sender) did apply the full check. - MPTIssue const issue{mptID}; - auto const legacyAccountFrozen = [&] { - if (isGlobalFrozen(view, issue) || isIndividualFrozen(view, account, issue)) - return true; - bool const isReceiver = - !value.amtBefore.has_value() || *value.amtAfter > *value.amtBefore; - if (txnType == ttAMM_WITHDRAW && isReceiver) - return false; - return isVaultPseudoAccountFrozen(view, account, issue, 0); - }; - bool const accountFrozen = - fix330Enabled ? isFrozen(view, account, issue) : legacyAccountFrozen(); if (!invalidTransfer && - (accountFrozen || !isAuthorized(view, mptID, account, reqAuth))) + (isFrozen(view, account, MPTIssue{mptID}) || + !isAuthorized(view, mptID, account, reqAuth))) { invalidTransfer = true; } diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index 9fde52ecb20..afbc14960a4 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -4,7 +4,6 @@ #include #include #include -#include #include #include #include @@ -4600,15 +4599,16 @@ class Invariants_test : public beast::unit_test::Suite } } - // Vault-share transfer: ValidMPTTransfer gates isVaultPseudoAccountFrozen - // on fixCleanup3_3_0. Pre-amendment, vault-share transfers are allowed - // even when the underlying asset is individually frozen for the sender; - // post-amendment they are blocked. + // Vault-share freeze invariant: isVaultPseudoAccountFrozen descends + // through sfReferenceHolding to test the vault's underlying asset for + // each changed holder. { Account const gw{"gw"}; MPTID shareID{}; + AccountID vaultPseudoID{}; - auto const preclose = [&](Account const& a1, Account const& a2, Env& env) -> bool { + // Vault setup: a1 and a2 both deposit IOU and hold vault shares. + auto const setupVault = [&](Account const& a1, Account const& a2, Env& env) -> bool { env.fund(XRP(1'000), gw); env.trust(gw["IOU"](10'000), a1); env.trust(gw["IOU"](10'000), a2); @@ -4617,24 +4617,18 @@ class Invariants_test : public beast::unit_test::Suite env(pay(gw, a2, gw["IOU"](500))); env.close(); - PrettyAsset const iou = gw["IOU"]; Vault const vault{env}; - auto [createTx, vaultKeylet] = vault.create({.owner = a1, .asset = iou}); + auto [createTx, vaultKeylet] = vault.create({.owner = a1, .asset = gw["IOU"]}); env(createTx); env.close(); - // Both a1 and a2 deposit IOU, each receiving vault shares. - env(vault.deposit({.depositor = a1, .id = vaultKeylet.key, .amount = iou(100)})); - env(vault.deposit({.depositor = a2, .id = vaultKeylet.key, .amount = iou(100)})); + env(vault.deposit( + {.depositor = a1, .id = vaultKeylet.key, .amount = gw["IOU"](100)})); + env(vault.deposit( + {.depositor = a2, .id = vaultKeylet.key, .amount = gw["IOU"](100)})); env.close(); shareID = env.le(vaultKeylet)->at(sfShareMPTID); - - // Freeze a2's IOU trustline from the issuer side. - // a2 is the receiver in the simulated AMM withdraw; the - // distinction under test is that pre-fix330 the invariant - // does not apply the transitive vault freeze to receivers. - env(trust(gw, gw["IOU"](0), a2, tfSetFreeze)); - env.close(); + vaultPseudoID = env.le(vaultKeylet)->at(sfAccount); return true; }; @@ -4652,156 +4646,46 @@ class Invariants_test : public beast::unit_test::Suite return true; }; - // post-fixCleanup3_3_0: full isFrozen() applies to all holders; - // isVaultPseudoAccountFrozen finds a2's underlying IOU frozen → - // invalidTransfer → invariant fires. - doInvariantCheck( - Env{*this, defaultAmendments()}, - {{"invalid MPToken transfer between holders"}}, - precheck, - XRPAmount{}, - STTx{ttAMM_WITHDRAW, [](STObject&) {}}, - {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, - preclose); - - // pre-fixCleanup3_3_0: legacy AMM withdraw only checked - // checkIndividualFrozen on the destination, not the transitive - // vault freeze; a2 as receiver is exempt → invariant passes. - doInvariantCheck( - Env{*this, defaultAmendments() - fixCleanup3_3_0}, - {}, - precheck, - XRPAmount{}, - STTx{ttAMM_WITHDRAW, [](STObject&) {}}, - {tesSUCCESS, tesSUCCESS}, - preclose); - } - - // Side-specific vault-share AMM_WITHDRAW invariant tests. - // Both cases use a real vault (IOU underlying) and a real AMM whose - // pool includes vault shares. precheck simulates an AMM_WITHDRAW by - // transferring 10 vault shares from the AMM pseudo-account to a2. - { - MPTID shareID{}; - AccountID ammAcctID{}; - AccountID vaultPseudoID{}; - Account const gw{"gw"}; - - // Simulate AMM_WITHDRAW: AMM pseudo-account sends 10 vault shares - // to a2. The AMM pseudo is the sender (decreasing balance); - // a2 is the receiver (increasing balance). - auto const precheck2 = - [&](Account const& /*a1*/, Account const& a2, ApplyContext& ac) -> bool { - auto sleAMM = ac.view().peek(keylet::mptoken(shareID, ammAcctID)); - auto sle2 = ac.view().peek(keylet::mptoken(shareID, a2.id())); - if (!sleAMM || !sle2) - return false; - (*sleAMM)[sfMPTAmount] -= 10; - (*sle2)[sfMPTAmount] += 10; - ac.view().update(sleAMM); - ac.view().update(sle2); - return true; - }; - - // Shared vault + AMM setup: a1 deposits 500 IOU into a vault and - // creates an AMM with XRP + 100 vault shares, giving the AMM - // pseudo-account a vault-share MPToken balance. - auto const setupVaultAMM = [&](Account const& a1, Account const& a2, Env& env) -> bool { - env.fund(XRP(1'000), gw); - env(fset(gw, asfDefaultRipple)); - env.close(); - - env.trust(gw["IOU"](10'000), a1); - env.trust(gw["IOU"](10'000), a2); - env.close(); - env(pay(gw, a1, gw["IOU"](1'000))); - env(pay(gw, a2, gw["IOU"](500))); - env.close(); - - Vault const vault{env}; - auto [createTx, vaultKeylet] = vault.create({.owner = a1, .asset = gw["IOU"]}); - env(createTx); - env.close(); - - env(vault.deposit( - {.depositor = a1, .id = vaultKeylet.key, .amount = gw["IOU"](500)})); - env(vault.deposit( - {.depositor = a2, .id = vaultKeylet.key, .amount = gw["IOU"](200)})); - env.close(); - - shareID = env.le(vaultKeylet)->at(sfShareMPTID); - vaultPseudoID = env.le(vaultKeylet)->at(sfAccount); - - // a1 creates AMM with XRP + 100 vault shares; the AMM - // pseudo-account receives an MPToken record for shareID. - AMM const amm(env, a1, XRP(100), STAmount{MPTIssue{shareID}, 100}); - ammAcctID = amm.ammAccount(); - return true; - }; - - // Case 1: freeze the vault pseudo-account's IOU trustline. - // isVaultPseudoAccountFrozen(ammAcct) calls isAnyFrozen({vaultPseudo, - // ammAcct}, IOU); since vaultPseudo is frozen it returns true. The - // AMM sender has a decreasing balance (not a receiver) so it is - // never exempt from the check — invariant fires both pre- and - // post-fixCleanup3_3_0. - auto const preclose3 = [&](Account const& a1, Account const& a2, Env& env) -> bool { - if (!setupVaultAMM(a1, a2, env)) - return false; - env(trust(gw, gw["IOU"](0), Account{"vaultPseudo", vaultPseudoID}, tfSetFreeze)); - env.close(); - return true; - }; - - doInvariantCheck( - Env{*this, defaultAmendments()}, - {{"invalid MPToken transfer between holders"}}, - precheck2, - XRPAmount{}, - STTx{ttAMM_WITHDRAW, [](STObject&) {}}, - {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, - preclose3); + // Case: vault pseudo-account's IOU trustline is frozen. + { + auto const preclose = [&](Account const& a1, Account const& a2, Env& env) -> bool { + if (!setupVault(a1, a2, env)) + return false; + env(trust( + gw, gw["IOU"](0), Account{"vaultPseudo", vaultPseudoID}, tfSetFreeze)); + env.close(); + return true; + }; - doInvariantCheck( - Env{*this, defaultAmendments() - fixCleanup3_3_0}, - {{"invalid MPToken transfer between holders"}}, - precheck2, - XRPAmount{}, - STTx{ttAMM_WITHDRAW, [](STObject&) {}}, - {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, - preclose3); - - // Case 2: freeze a2's (receiver's) IOU trustline. - // isVaultPseudoAccountFrozen(a2) → isAnyFrozen({vaultPseudo, a2}, - // IOU) → true. The AMM sender's check passes (vaultPseudo and - // ammAcct are not frozen). Pre-fix330: receiver is exempt from - // isVaultPseudoAccountFrozen in ttAMM_WITHDRAW → passes. - // Post-fix330: full isFrozen() applied to a2 → fires. - auto const preclose4 = [&](Account const& a1, Account const& a2, Env& env) -> bool { - if (!setupVaultAMM(a1, a2, env)) - return false; - env(trust(gw, gw["IOU"](0), a2, tfSetFreeze)); - env.close(); - return true; - }; + doInvariantCheck( + Env{*this, defaultAmendments()}, + {{"invalid MPToken transfer between holders"}}, + precheck, + XRPAmount{}, + STTx{ttPAYMENT, [](STObject&) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + preclose); + } - doInvariantCheck( - Env{*this, defaultAmendments()}, - {{"invalid MPToken transfer between holders"}}, - precheck2, - XRPAmount{}, - STTx{ttAMM_WITHDRAW, [](STObject&) {}}, - {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, - preclose4); + // Case: receiver's (a2's) IOU trustline is frozen. + { + auto const preclose = [&](Account const& a1, Account const& a2, Env& env) -> bool { + if (!setupVault(a1, a2, env)) + return false; + env(trust(gw, gw["IOU"](0), a2, tfSetFreeze)); + env.close(); + return true; + }; - doInvariantCheck( - Env{*this, defaultAmendments() - fixCleanup3_3_0}, - {}, - precheck2, - XRPAmount{}, - STTx{ttAMM_WITHDRAW, [](STObject&) {}}, - {tesSUCCESS, tesSUCCESS}, - preclose4); + doInvariantCheck( + Env{*this, defaultAmendments()}, + {{"invalid MPToken transfer between holders"}}, + precheck, + XRPAmount{}, + STTx{ttPAYMENT, [](STObject&) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + preclose); + } } } diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 190b47301f2..4216c69a367 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -2236,11 +2236,6 @@ class Vault_test : public beast::unit_test::Suite env(offer(alice, XRP(1), shares(1)), Ter{tecNO_PERMISSION}); env.close(); - // The inherited CanTrade restriction also blocks AMM creation. - AMM const ammUnderlyingFail( - env, alice, XRP(1'000), asset(1'000), Ter{tecNO_PERMISSION}); - AMM const ammShares(env, alice, XRP(1'000), shares(100), Ter{tecNO_PERMISSION}); - // Deposit still works before enabling CanTrade. env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = asset(100)})); env.close(); From 3ef03714748512b949f75c813207131a95181f7c Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Tue, 30 Jun 2026 09:56:57 +0200 Subject: [PATCH 3/6] address reviewer feedback --- src/libxrpl/ledger/helpers/TokenHelpers.cpp | 44 +++++++++++--------- src/libxrpl/tx/transactors/dex/AMMCreate.cpp | 31 +++++++------- 2 files changed, 39 insertions(+), 36 deletions(-) diff --git a/src/libxrpl/ledger/helpers/TokenHelpers.cpp b/src/libxrpl/ledger/helpers/TokenHelpers.cpp index 9756564e3ee..a316289cdcd 100644 --- a/src/libxrpl/ledger/helpers/TokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/TokenHelpers.cpp @@ -192,15 +192,6 @@ checkWithdrawFreeze( if (auto const ret = checkIndividualFrozen(view, pseudoAcct, asset); !isTesSuccess(ret)) return ret; - if (asset.holds() && - isVaultPseudoAccountFrozen(view, pseudoAcct, asset.get(), 0)) - { - // LCOV_EXCL_START - UNREACHABLE("xrpl::checkWithdrawFreeze : pseudo-account backed object holds shares"); - return tecINTERNAL; - // LCOV_EXCL_STOP - } - // Check submitter's individual freeze only when Submitter != Destination (a regular freeze // should not block self-withdrawal). if (submitterAcct != dstAcct) @@ -210,7 +201,19 @@ checkWithdrawFreeze( } // The destination account must not be deep frozen to receive the funds - return checkDeepFrozen(view, dstAcct, asset); + if (auto const ret = checkDeepFrozen(view, dstAcct, asset); !isTesSuccess(ret)) + return ret; + + if (asset.holds() && + isVaultPseudoAccountFrozen(view, pseudoAcct, asset.get(), 0)) + { + // LCOV_EXCL_START + UNREACHABLE("xrpl::checkWithdrawFreeze : pseudo-account backed object holds shares"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } + + return tesSUCCESS; } [[nodiscard]] TER @@ -234,6 +237,17 @@ checkDepositFreeze( if (auto const ret = checkGlobalFrozen(view, asset); !isTesSuccess(ret)) return ret; + if (srcAcct != asset.getIssuer()) + { + if (auto const ret = checkIndividualFrozen(view, srcAcct, asset); !isTesSuccess(ret)) + return ret; + } + + // Unlike regular accounts, pseudo-accounts cannot receive deposits under a regular freeze + // because those funds cannot be later withdrawn + if (auto const ret = checkIndividualFrozen(view, pseudoAcct, asset); !isTesSuccess(ret)) + return ret; + if (asset.holds() && isVaultPseudoAccountFrozen(view, pseudoAcct, asset.get(), 0)) { @@ -243,15 +257,7 @@ checkDepositFreeze( // LCOV_EXCL_STOP } - if (srcAcct != asset.getIssuer()) - { - if (auto const ret = checkIndividualFrozen(view, srcAcct, asset); !isTesSuccess(ret)) - return ret; - } - - // Unlike regular accounts, pseudo-accounts cannot receive deposits under a regular freeze - // because those funds cannot be later withdrawn - return checkIndividualFrozen(view, pseudoAcct, asset); + return tesSUCCESS; } //------------------------------------------------------------------------------ diff --git a/src/libxrpl/tx/transactors/dex/AMMCreate.cpp b/src/libxrpl/tx/transactors/dex/AMMCreate.cpp index 7e4d23f2cf5..66d059c54d5 100644 --- a/src/libxrpl/tx/transactors/dex/AMMCreate.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMCreate.cpp @@ -194,24 +194,21 @@ AMMCreate::preclaim(PreclaimContext const& ctx) accountId == beast::kZero) return terADDRESS_COLLISION; - if (ctx.view.rules().enabled(featureMPTokensV2)) + auto const isMPTIssuerPseudo = [&](Asset const& asset) { + if (asset.native()) + return false; + + if (asset.holds()) + return false; + + return isPseudoAccount(ctx.view, asset.getIssuer()); + }; + + if (isMPTIssuerPseudo(amount.asset()) || isMPTIssuerPseudo(amount2.asset())) { - auto const isVaultShare = [&](Asset const& asset) { - if (asset.native()) - return false; - - if (asset.holds()) - return false; - - return isPseudoAccount(ctx.view, asset.getIssuer()); - }; - - if (isVaultShare(amount.asset()) || isVaultShare(amount2.asset())) - { - JLOG(ctx.j.debug()) - << "AMM Instance: can't create with vault shares " << amount << " " << amount2; - return tecWRONG_ASSET; - } + JLOG(ctx.j.debug()) << "AMM Instance: can't create with vault shares " << amount << " " + << amount2; + return tecWRONG_ASSET; } } From e9d8f6dee7b6321243d42c6db45dab7f69ae8c2e Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Tue, 30 Jun 2026 12:50:20 +0200 Subject: [PATCH 4/6] address AI feedback --- src/libxrpl/ledger/helpers/TokenHelpers.cpp | 4 ++-- src/test/app/Vault_test.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libxrpl/ledger/helpers/TokenHelpers.cpp b/src/libxrpl/ledger/helpers/TokenHelpers.cpp index a316289cdcd..0ab97a4b600 100644 --- a/src/libxrpl/ledger/helpers/TokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/TokenHelpers.cpp @@ -174,7 +174,7 @@ checkWithdrawFreeze( XRPL_ASSERT( !isPseudoAccount(view, dstAcct), "xrpl::checkWithdrawFreeze : destination is not a pseudo-account"); - // AMM,Vault,LoanBroker cannot be created using Vault Shares as an asset + // The asset being withdrawn must not be issued by a pseudo-account XRPL_ASSERT( !isPseudoAccount(view, asset.getIssuer()), "xrpl::checkWithdrawFreeze : asset issuer cannot be a pseudo-account"); @@ -229,7 +229,7 @@ checkDepositFreeze( XRPL_ASSERT( !isPseudoAccount(view, srcAcct), "xrpl::checkDepositFreeze : source is not a pseudo-account"); - // AMM,Vault,LoanBroker cannot be created using Vault Shares as an asset + // The asset being deposited must not be issued by a pseudo-account XRPL_ASSERT( !isPseudoAccount(view, asset.getIssuer()), "xrpl::checkDepositFreeze : asset issuer cannot be a pseudo-account"); diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 4216c69a367..a5c7338cca2 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -7728,7 +7728,7 @@ class Vault_test : public beast::unit_test::Suite env.enableFeature(fixCleanup3_3_0); } - // Focused demonstration: a depositor under a individual IOU freeze + // Focused demonstration: a depositor under an individual IOU freeze // can still withdraw to themselves (self-withdrawal), but is blocked from // withdrawing to a third party. // @@ -7770,7 +7770,7 @@ class Vault_test : public beast::unit_test::Suite auto runTests = [&]() { auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0); - // Set a individual freeze on the owner's IOU trustline. + // Set an individual freeze on the owner's IOU trustline. env(trust(issuer, asset(0), owner, tfSetFreeze)); env.close(); From d1cd673de2e89f553d62af80feba5801c93f9bfb Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Tue, 30 Jun 2026 19:32:05 +0200 Subject: [PATCH 5/6] address review comments --- src/test/app/Invariants_test.cpp | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index afbc14960a4..52ec8d903ea 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -4605,10 +4605,11 @@ class Invariants_test : public beast::unit_test::Suite { Account const gw{"gw"}; MPTID shareID{}; - AccountID vaultPseudoID{}; // Vault setup: a1 and a2 both deposit IOU and hold vault shares. - auto const setupVault = [&](Account const& a1, Account const& a2, Env& env) -> bool { + auto const setupVault = [&](Account const& a1, + Account const& a2, + Env& env) -> std::tuple { env.fund(XRP(1'000), gw); env.trust(gw["IOU"](10'000), a1); env.trust(gw["IOU"](10'000), a2); @@ -4627,9 +4628,7 @@ class Invariants_test : public beast::unit_test::Suite {.depositor = a2, .id = vaultKeylet.key, .amount = gw["IOU"](100)})); env.close(); - shareID = env.le(vaultKeylet)->at(sfShareMPTID); - vaultPseudoID = env.le(vaultKeylet)->at(sfAccount); - return true; + return {env.le(vaultKeylet)->at(sfShareMPTID), env.le(vaultKeylet)->at(sfAccount)}; }; // Simulate a vault-share transfer: a1 sends 10 shares to a2. @@ -4649,10 +4648,9 @@ class Invariants_test : public beast::unit_test::Suite // Case: vault pseudo-account's IOU trustline is frozen. { auto const preclose = [&](Account const& a1, Account const& a2, Env& env) -> bool { - if (!setupVault(a1, a2, env)) - return false; - env(trust( - gw, gw["IOU"](0), Account{"vaultPseudo", vaultPseudoID}, tfSetFreeze)); + auto [sid, vid] = setupVault(a1, a2, env); + shareID = sid; + env(trust(gw, gw["IOU"](0), Account{"vaultPseudo", vid}, tfSetFreeze)); env.close(); return true; }; @@ -4670,8 +4668,8 @@ class Invariants_test : public beast::unit_test::Suite // Case: receiver's (a2's) IOU trustline is frozen. { auto const preclose = [&](Account const& a1, Account const& a2, Env& env) -> bool { - if (!setupVault(a1, a2, env)) - return false; + auto [sid, vid] = setupVault(a1, a2, env); + shareID = sid; env(trust(gw, gw["IOU"](0), a2, tfSetFreeze)); env.close(); return true; From 047c24e161da95fe79f34a28ac000073b3dd7474 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Wed, 1 Jul 2026 12:51:16 +0200 Subject: [PATCH 6/6] clang-tidy --- src/test/app/Invariants_test.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index 52ec8d903ea..efb04546d84 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -64,6 +64,7 @@ #include #include #include +#include #include #include