From 44af7c2ae367085352a49bedcfcfe87386f1025b Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Tue, 23 Jun 2026 18:47:13 +0200 Subject: [PATCH] adds explicit pseudo-account freeze handling --- src/libxrpl/ledger/helpers/TokenHelpers.cpp | 15 +++ src/test/app/AMMMPT_test.cpp | 108 ++++++++++++++++++++ 2 files changed, 123 insertions(+) diff --git a/src/libxrpl/ledger/helpers/TokenHelpers.cpp b/src/libxrpl/ledger/helpers/TokenHelpers.cpp index 4d44757f78..a9f4c0459c 100644 --- a/src/libxrpl/ledger/helpers/TokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/TokenHelpers.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -192,6 +193,13 @@ checkWithdrawFreeze( if (auto const ret = checkGlobalFrozen(view, asset)) 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)) + { + 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)) @@ -227,6 +235,13 @@ checkDepositFreeze( if (auto const ret = checkGlobalFrozen(view, asset)) 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)) + { + return tecLOCKED; + } + if (srcAcct != asset.getIssuer()) { if (auto const ret = checkIndividualFrozen(view, srcAcct, asset)) diff --git a/src/test/app/AMMMPT_test.cpp b/src/test/app/AMMMPT_test.cpp index 117f2c104c..9d4c8851b2 100644 --- a/src/test/app/AMMMPT_test.cpp +++ b/src/test/app/AMMMPT_test.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -6903,6 +6904,112 @@ private: BEAST_EXPECT(!amm.ammExists()); } + void + testAMMWithVaultShares() + { + testcase("AMM with vault shares — underlying freeze blocks share withdrawal"); + using namespace jtx; + FeatureBitset const all{testableAmendments()}; + + // Withdrawing vault shares from an AMM is blocked when the vault's + // underlying asset is frozen for the withdrawer (post-fixCleanup3_3_0). + // Pre-fix, the old checkFrozen path only checks the AMM account's lock + // state on the share MPT, which is unset, so withdrawal succeeds. + + auto runIOU = [&](FeatureBitset const& features) { + bool const fix330 = features[fixCleanup3_3_0]; + Env env{*this, features}; + + env.fund(XRP(100'000), gw_, alice_); + env.close(); + + PrettyAsset const iou = gw_["IOU"]; + env.trust(iou(1'000'000), alice_); + env(pay(gw_, alice_, iou(10'000))); + env.close(); + + Vault vault{env}; + auto [createTx, vaultKeylet] = vault.create({.owner = alice_, .asset = iou}); + env(createTx); + env.close(); + + // 100 IOU → 100,000,000 vault shares (IOU vault scale = 6) + env(vault.deposit({.depositor = alice_, .id = vaultKeylet.key, .amount = iou(100)})); + env.close(); + + auto const shareMPTID = env.le(vaultKeylet)->at(sfShareMPTID); + STAmount const shareAmt{MPTIssue{shareMPTID}, 100'000'000}; + 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(); + + // post-fix330: isVaultPseudoAccountFrozen(alice, share) detects + // alice's frozen underlying → tecLOCKED + // pre-fix330: checkFrozen(ammAccount, share) → no lock → tesSUCCESS + amm.withdraw( + {.account = alice_, + .tokens = 1'000, + .err = Ter(fix330 ? TER(tecLOCKED) : TER(tesSUCCESS))}); + + env(trust(gw_, iou(0), alice_, tfClearFreeze)); + env.close(); + + amm.withdraw({.account = alice_, .tokens = 1'000}); + }; + + runIOU(all); + runIOU(all - fixCleanup3_3_0); + + auto runMPT = [&](FeatureBitset const& features) { + bool const fix330 = features[fixCleanup3_3_0]; + Env env{*this, features}; + + env.fund(XRP(100'000), gw_, alice_); + env.close(); + + MPTTester mptt{env, gw_, kMptInitNoFund}; + mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock}); + PrettyAsset const mpt = mptt.issuanceID(); + mptt.authorize({.account = alice_}); + env(pay(gw_, alice_, mpt(1'000))); + env.close(); + + Vault vault{env}; + auto [createTx, vaultKeylet] = vault.create({.owner = alice_, .asset = mpt}); + env(createTx); + env.close(); + + // 100 MPT → 100 vault shares (MPT vault scale = 0) + env(vault.deposit({.depositor = alice_, .id = vaultKeylet.key, .amount = mpt(100)})); + env.close(); + + auto const shareMPTID = env.le(vaultKeylet)->at(sfShareMPTID); + STAmount const shareAmt{MPTIssue{shareMPTID}, 100}; + AMM amm{env, alice_, XRP(100), shareAmt}; + env.close(); + + // Lock alice's underlying MPT + mptt.set({.holder = alice_, .flags = tfMPTLock}); + + // post-fix330: isVaultPseudoAccountFrozen finds alice locked → tecLOCKED + // pre-fix330: checkFrozen(ammAccount, share) → tesSUCCESS + amm.withdraw( + {.account = alice_, + .tokens = 10, + .err = Ter(fix330 ? TER(tecLOCKED) : TER(tesSUCCESS))}); + + mptt.set({.holder = alice_, .flags = tfMPTUnlock}); + + amm.withdraw({.account = alice_, .tokens = 10}); + }; + + runMPT(all); + runMPT(all - fixCleanup3_3_0); + } + void testAMMDepositWithFrozenAssets() { @@ -7153,6 +7260,7 @@ private: testLPTokenBalance(all); testLPTokenBalance(all - fixAMMv1_3); testAMMDepositWithFrozenAssets(); + testAMMWithVaultShares(); testAutoDelete(); } };