mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-05 01:37:00 +00:00
test: Add freeze-check tests for VaultWithdraw issuer guard
Verify the dstAcct != vaultAsset.getIssuer() guard introduced under fixCleanup3_2_0: frozen vault trust line and MPT global lock both block non-issuer withdrawals, while the issuer-destination path bypasses preclaim's freeze checks (redemption path). Preclaim passes for the issuer destination, but doApply::accountHolds(ZeroIfFrozen) still returns 0 for locked shares — a matching doApply fix is tracked via TODO comments. Also updates expected error codes in existing freeze tests affected by the new vault-sender and deep-frozen-destination checks.
This commit is contained in:
@@ -167,21 +167,30 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx)
|
||||
// - Only deep-frozen destinations (which cannot receive at all) are blocked.
|
||||
if (fix320Enabled)
|
||||
{
|
||||
if (auto const ret = checkFrozen(ctx.view, vaultAccount, vaultAsset))
|
||||
return ret;
|
||||
if (auto const ret = checkDeepFrozen(ctx.view, dstAcct, vaultAsset))
|
||||
return ret;
|
||||
if (dstAcct != vaultAsset.getIssuer())
|
||||
{
|
||||
if (auto const ret = checkFrozen(ctx.view, vaultAccount, vaultAsset))
|
||||
return ret;
|
||||
|
||||
if (auto const ret = checkDeepFrozen(ctx.view, dstAcct, vaultAsset))
|
||||
return ret;
|
||||
|
||||
// Cannot return shares to the vault, if the underlying asset was frozen for
|
||||
// the submitter
|
||||
if (auto const ret = checkFrozen(ctx.view, account, Asset{vaultShare}))
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (auto const ret = checkFrozen(ctx.view, dstAcct, vaultAsset))
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Cannot return shares to the vault, if the underlying asset was frozen for
|
||||
// the submitter
|
||||
if (auto const ret = checkFrozen(ctx.view, account, Asset{vaultShare}))
|
||||
return ret;
|
||||
// Cannot return shares to the vault, if the underlying asset was frozen for
|
||||
// the submitter
|
||||
if (auto const ret = checkFrozen(ctx.view, account, Asset{vaultShare}))
|
||||
return ret;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -1701,8 +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.
|
||||
tx[sfDestination] = issuer.human();
|
||||
env(tx, Ter(tecLOCKED));
|
||||
env(tx, Ter(tecINSUFFICIENT_FUNDS));
|
||||
|
||||
// Clawback is still permitted, even with global lock
|
||||
tx = vault.clawback(
|
||||
@@ -2894,6 +2900,85 @@ class Vault_test : public beast::unit_test::Suite
|
||||
env.close();
|
||||
}
|
||||
|
||||
testCase([&, this](
|
||||
Env& env,
|
||||
Account const& owner,
|
||||
Account const& issuer,
|
||||
Account const& charlie,
|
||||
auto vaultAccount,
|
||||
Vault& vault,
|
||||
PrettyAsset const& asset,
|
||||
auto issuanceId) {
|
||||
testcase("IOU frozen vault trust line, withdrawal to issuer is exempt");
|
||||
|
||||
// When the vault's trust line is frozen, withdrawals to any
|
||||
// non-issuer destination are blocked by checkFrozen(vaultAccount).
|
||||
// Withdrawals whose destination IS the IOU issuer bypass that
|
||||
// check entirely (redemption path).
|
||||
|
||||
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)}));
|
||||
env.close();
|
||||
|
||||
Asset const share = Asset(issuanceId(keylet));
|
||||
|
||||
// Freeze the trust line to the vault pseudo-account
|
||||
auto trustSet = [&, account = vaultAccount(keylet)]() {
|
||||
json::Value jv;
|
||||
jv[jss::Account] = issuer.human();
|
||||
{
|
||||
auto& ja = jv[jss::LimitAmount] =
|
||||
asset(0).value().getJson(JsonOptions::Values::None);
|
||||
ja[jss::issuer] = toBase58(account);
|
||||
}
|
||||
jv[jss::TransactionType] = jss::TrustSet;
|
||||
jv[jss::Flags] = tfSetFreeze;
|
||||
return jv;
|
||||
}();
|
||||
env(trustSet);
|
||||
env.close();
|
||||
|
||||
{
|
||||
// Non-issuer destinations are blocked
|
||||
auto t =
|
||||
vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(10)});
|
||||
env(t, Ter{tecFROZEN});
|
||||
|
||||
t[sfDestination] = charlie.human();
|
||||
env(t, Ter{tecFROZEN});
|
||||
env.close();
|
||||
}
|
||||
|
||||
{
|
||||
// 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.
|
||||
auto t =
|
||||
vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(50)});
|
||||
t[sfDestination] = issuer.human();
|
||||
env(t, Ter{tecINSUFFICIENT_FUNDS});
|
||||
env.close();
|
||||
}
|
||||
|
||||
// Vault unchanged (100 assets / 100'000'000 shares). Clear freeze
|
||||
// and drain before deletion.
|
||||
trustSet[jss::Flags] = tfClearFreeze;
|
||||
env(trustSet);
|
||||
env.close();
|
||||
|
||||
env(vault.withdraw(
|
||||
{.depositor = owner, .id = keylet.key, .amount = share(100'000'000)}));
|
||||
|
||||
env(vault.del({.owner = owner, .id = keylet.key}));
|
||||
env.close();
|
||||
});
|
||||
|
||||
testCase(
|
||||
[&, this](
|
||||
Env& env,
|
||||
|
||||
Reference in New Issue
Block a user