tests: invariant test for vault-share freeze gate

Add a test to testMPT() in Invariants_test.cpp that verifies the
ValidMPTTransfer invariant correctly gates isVaultPseudoAccountFrozen
on fixCleanup3_3_0.

The test creates an IOU vault whose shares are held by two accounts,
then freezes the underlying IOU for one account (a1). It simulates a
vault-share transfer by directly modifying the MPToken SLEs:

- post-fixCleanup3_3_0: isVaultPseudoAccountFrozen finds a1's underlying
  IOU frozen → invalidTransfer → "invalid MPToken transfer between
  holders" → tecINVARIANT_FAILED.

- pre-fixCleanup3_3_0: the vault-underlying check is not called;
  the transfer is allowed even though the underlying IOU is frozen
  → tesSUCCESS.
This commit is contained in:
Vito
2026-06-24 10:37:43 +02:00
parent 799f126bf4
commit 786a163517

View File

@@ -4592,6 +4592,78 @@ class Invariants_test : public beast::unit_test::Suite
}
}
}
// Vault-share transfer: ValidMPTTransfer gates isVaultPseudoAccountFrozen
// on fixCleanup3_3_0. Pre-amendment, vault-share transfers are allowed
// even when the underlying asset is individually frozen for the sender;
// post-amendment they are blocked.
{
Account const gw{"gw"};
MPTID shareID{};
auto const preclose = [&](Account const& a1, Account const& a2, Env& env) -> bool {
env.fund(XRP(1'000), gw);
env.trust(gw["IOU"](10'000), a1);
env.trust(gw["IOU"](10'000), a2);
env.close();
env(pay(gw, a1, gw["IOU"](500)));
env(pay(gw, a2, gw["IOU"](500)));
env.close();
PrettyAsset const iou = gw["IOU"];
Vault const vault{env};
auto [createTx, vaultKeylet] = vault.create({.owner = a1, .asset = iou});
env(createTx);
env.close();
// Both a1 and a2 deposit IOU, each receiving vault shares.
env(vault.deposit({.depositor = a1, .id = vaultKeylet.key, .amount = iou(100)}));
env(vault.deposit({.depositor = a2, .id = vaultKeylet.key, .amount = iou(100)}));
env.close();
shareID = env.le(vaultKeylet)->at(sfShareMPTID);
// Freeze a1's IOU trustline from the issuer side.
env(trust(gw, gw["IOU"](0), a1, tfSetFreeze));
env.close();
return true;
};
// Simulate a vault-share transfer: a1 sends 10 shares to a2.
auto const precheck =
[&](Account const& a1, Account const& a2, ApplyContext& ac) -> bool {
auto sle1 = ac.view().peek(keylet::mptoken(shareID, a1.id()));
auto sle2 = ac.view().peek(keylet::mptoken(shareID, a2.id()));
if (!sle1 || !sle2)
return false;
(*sle1)[sfMPTAmount] -= 10;
(*sle2)[sfMPTAmount] += 10;
ac.view().update(sle1);
ac.view().update(sle2);
return true;
};
// post-fixCleanup3_3_0: isVaultPseudoAccountFrozen finds a1's
// underlying IOU frozen → invalidTransfer → invariant fires.
doInvariantCheck(
Env{*this, defaultAmendments()},
{{"invalid MPToken transfer between holders"}},
precheck,
XRPAmount{},
STTx{ttAMM_WITHDRAW, [](STObject&) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
preclose);
// pre-fixCleanup3_3_0: isVaultPseudoAccountFrozen is not called;
// transfer is allowed even though the underlying IOU is frozen.
doInvariantCheck(
Env{*this, defaultAmendments() - fixCleanup3_3_0},
{},
precheck,
XRPAmount{},
STTx{ttAMM_WITHDRAW, [](STObject&) {}},
{tesSUCCESS, tesSUCCESS},
preclose);
}
}
void