diff --git a/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp b/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp index b6719bcff7..09f6328dc7 100644 --- a/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp @@ -277,8 +277,12 @@ VaultWithdraw::doApply() return tecPATH_DRY; } - if (accountHolds( - view(), accountID_, share, FreezeHandling::ZeroIfFrozen, AuthHandling::IgnoreAuth, j_) < + auto const dstAcct = ctx_.tx[~sfDestination].value_or(accountID_); + bool const isIssuerRedemption = + view().rules().enabled(fixCleanup3_2_0) && dstAcct == vaultAsset.getIssuer(); + auto const freezeHandling = + isIssuerRedemption ? FreezeHandling::IgnoreFreeze : FreezeHandling::ZeroIfFrozen; + if (accountHolds(view(), accountID_, share, freezeHandling, AuthHandling::IgnoreAuth, j_) < sharesRedeemed) { JLOG(j_.debug()) << "VaultWithdraw: account doesn't hold enough shares"; @@ -381,8 +385,6 @@ VaultWithdraw::doApply() // else quietly ignore, account balance is not zero } - auto const dstAcct = ctx_.tx[~sfDestination].value_or(accountID_); - associateAsset(*vault, vaultAsset); return doWithdraw( diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 233bedbb05..c7c75b26b2 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -1701,22 +1701,14 @@ class Vault_test : public beast::unit_test::Suite tx = vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(100)}); env(tx, Ter(tecLOCKED)); - // Redemption to the issuer: preclaim's issuer guard skips all - // freeze checks, but doApply::accountHolds(ZeroIfFrozen) still - // returns 0 for the locked shares (isVaultPseudoAccountFrozen is - // true while the asset is globally locked). - // TODO: doApply needs a matching fix to use Normal freeze handling - // for the issuer-destination path. + // Redemption to the issuer bypasses freeze checks end-to-end: + // preclaim's issuer guard skips all three checks, and doApply uses + // FreezeHandling::IgnoreFreeze for the accountHolds balance check. tx[sfDestination] = issuer.human(); - env(tx, Ter(tecINSUFFICIENT_FUNDS)); - - // Clawback is still permitted, even with global lock - tx = vault.clawback( - {.issuer = issuer, .id = keylet.key, .holder = depositor, .amount = asset(0)}); env(tx); env.close(); - // Clawback removed shares MPToken + // Withdrawal burned all depositor shares — MPToken is removed. auto const mptSle = env.le(keylet::mptoken(share, depositor.id())); BEAST_EXPECT(mptSle == nullptr); @@ -2953,27 +2945,25 @@ class Vault_test : public beast::unit_test::Suite } { - // Preclaim passes (issuer guard skips all three checks), but - // doApply::accountHolds(ZeroIfFrozen) still returns 0 for the - // owner's shares because isVaultPseudoAccountFrozen is true - // while the vault's trust line is frozen. - // TODO: doApply needs a matching fix to use Normal freeze - // handling for the issuer-destination path. + // Withdrawal to the IOU issuer succeeds end-to-end: the issuer + // guard skips all preclaim checks, and doApply uses + // FreezeHandling::IgnoreFreeze so accountHolds returns the + // actual balance rather than zero. auto t = vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(50)}); t[sfDestination] = issuer.human(); - env(t, Ter{tecINSUFFICIENT_FUNDS}); + env(t); env.close(); } - // Vault unchanged (100 assets / 100'000'000 shares). Clear freeze - // and drain before deletion. + // vault now has 50 assets / owner holds 50'000'000 shares + // Clear freeze and drain what remains. trustSet[jss::Flags] = tfClearFreeze; env(trustSet); env.close(); env(vault.withdraw( - {.depositor = owner, .id = keylet.key, .amount = share(100'000'000)})); + {.depositor = owner, .id = keylet.key, .amount = share(50'000'000)})); env(vault.del({.owner = owner, .id = keylet.key})); env.close();