fix: Use IgnoreFreeze in doApply for issuer-redemption withdrawals

Hoist dstAcct into doApply and use FreezeHandling::IgnoreFreeze for
accountHolds when dstAcct == vaultAsset.getIssuer() under fixCleanup3_2_0.
Without this the locked-share balance read as zero and the redemption
failed with tecINSUFFICIENT_FUNDS despite preclaim passing.

Update tests: issuer-redemption now succeeds end-to-end for both IOU and
MPT vaults.
This commit is contained in:
Vito
2026-06-03 17:40:14 +02:00
parent 45fa34de4b
commit 43fc095b1a
2 changed files with 18 additions and 26 deletions

View File

@@ -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(

View File

@@ -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();