diff --git a/include/xrpl/ledger/helpers/TokenHelpers.h b/include/xrpl/ledger/helpers/TokenHelpers.h index 9061f14a0f..661867c336 100644 --- a/include/xrpl/ledger/helpers/TokenHelpers.h +++ b/include/xrpl/ledger/helpers/TokenHelpers.h @@ -72,6 +72,9 @@ isIndividualFrozen(ReadView const& view, AccountID const& account, Asset const& [[nodiscard]] TER checkIndividualFrozen(ReadView const& view, AccountID const& account, Asset const& asset); +[[nodiscard]] TER +checkIndividualDeepFrozen(ReadView const& view, AccountID const& account, Asset const& asset); + /** * isFrozen check is recursive for MPT shares in a vault, descending to * assets in the vault, up to maxAssetCheckDepth recursion depth. This is diff --git a/src/libxrpl/ledger/helpers/TokenHelpers.cpp b/src/libxrpl/ledger/helpers/TokenHelpers.cpp index ef2d831ef4..4d44757f78 100644 --- a/src/libxrpl/ledger/helpers/TokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/TokenHelpers.cpp @@ -79,6 +79,14 @@ checkIndividualFrozen(ReadView const& view, AccountID const& account, Asset cons return tesSUCCESS; } +TER +checkIndividualDeepFrozen(ReadView const& view, AccountID const& account, Asset const& asset) +{ + if (isDeepFrozen(view, account, asset)) + return asset.holds() ? tecLOCKED : tecFROZEN; + return tesSUCCESS; +} + bool isFrozen(ReadView const& view, AccountID const& account, Asset const& asset, std::uint8_t depth) { @@ -176,25 +184,29 @@ checkWithdrawFreeze( isPseudoAccount(view, srcAcct), "xrpl::checkWithdrawFreeze : source must be a pseudo-account"); - // No matter what, funds can be sent to the issuer + // Funds can always be sent to the issuer if (dstAcct == asset.getIssuer()) return tesSUCCESS; + // If the asset is globally frozen, other checks are redundant + if (auto const ret = checkGlobalFrozen(view, asset)) + return ret; + // 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 = checkFrozen(view, srcAcct, asset)) + if (auto const ret = checkIndividualFrozen(view, srcAcct, asset)) return ret; // Check submitter's individual freeze only when Submitter != Destination (a regular freeze // should not block self-withdrawal). if (submitterAcct != dstAcct) { - if (auto const ret = checkFrozen(view, submitterAcct, asset)) + if (auto const ret = checkIndividualFrozen(view, submitterAcct, asset)) return ret; } // The destination account must not be deep frozen to receive the funds - return checkDeepFrozen(view, dstAcct, asset); + return checkIndividualDeepFrozen(view, dstAcct, asset); } [[nodiscard]] TER @@ -208,12 +220,22 @@ checkDepositFreeze( isPseudoAccount(view, dstAcct), "xrpl::checkDepositFreeze : destination must be a pseudo-account"); - if (auto const ret = checkFrozen(view, srcAcct, asset)) + // An Issuer cannot deposit when: + // 1. Asset is globally frozen + // 2. The trustline of the pseudo-account is frozen + + if (auto const ret = checkGlobalFrozen(view, asset)) return ret; + if (srcAcct != asset.getIssuer()) + { + if (auto const ret = checkIndividualFrozen(view, srcAcct, asset)) + return ret; + } + // Unlike regular accounts, pseudo-accounts cannot receive deposits under a regular freeze // because those funds cannot be later withdrawn - return checkFrozen(view, dstAcct, asset); + return checkIndividualFrozen(view, dstAcct, asset); } //------------------------------------------------------------------------------ diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index 15366730d4..0b2092f50b 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -7462,24 +7462,41 @@ class MPToken_test : public beast::unit_test::Suite // MPTLock is set usd.set({.flags = tfMPTLock}); auto const fix330 = env.current()->rules().enabled(fixCleanup3_3_0); - // carol can't withdraw the locked token; post-fixCleanup3_3_0 the - // issuer can still redeem its own locked token. - for (auto const& account : {carol, gw}) - { - auto const lockErr = - (fix330 && account == gw) ? Ter(tesSUCCESS) : Ter(tecLOCKED); - amm.withdraw( - {.account = account, - .asset1Out = usd(1), - .asset2Out = eur(1), - .err = lockErr}); - amm.withdraw({.account = account, .tokens = 1'000, .err = lockErr}); - // can single withdraw another asset - amm.withdraw( - {.account = account, - .asset1Out = eur(1), - .assets = std::make_pair(eur, usd)}); - } + + // carol can't withdraw the locked token (any withdrawal type) + amm.withdraw( + {.account = carol, + .asset1Out = usd(1), + .asset2Out = eur(1), + .err = Ter(tecLOCKED)}); + amm.withdraw({.account = carol, .tokens = 1'000, .err = Ter(tecLOCKED)}); + // can single withdraw the non-locked asset + amm.withdraw( + {.account = carol, .asset1Out = eur(1), .assets = std::make_pair(eur, usd)}); + + // post-fixCleanup3_3_0 the issuer can redeem even when locked. + // Each successful withdrawal burns LP tokens, so replenish between + // each type to keep gw's LP balance stable. + auto const gwLockErr = fix330 ? Ter(tesSUCCESS) : Ter(tecLOCKED); + auto const replenish = [&](IOUAmount tokens) { + usd.set({.flags = tfMPTUnlock}); + amm.deposit({.account = gw, .tokens = tokens}); + usd.set({.flags = tfMPTLock}); + }; + + amm.withdraw( + {.account = gw, .asset1Out = usd(1), .asset2Out = eur(1), .err = gwLockErr}); + if (fix330) + replenish(IOUAmount{1}); + + amm.withdraw({.account = gw, .tokens = 1'000, .err = gwLockErr}); + if (fix330) + replenish(IOUAmount{1'000}); + + // can single withdraw the non-locked asset + amm.withdraw( + {.account = gw, .asset1Out = eur(1), .assets = std::make_pair(eur, usd)}); + usd.set({.flags = tfMPTUnlock}); // MPTRequireAuth is set