Merge remote-tracking branch 'origin/develop' into merge-develop-sponsor

This commit is contained in:
yinyiqian1
2026-06-22 20:00:11 -04:00
55 changed files with 2203 additions and 1934 deletions

View File

@@ -1599,7 +1599,7 @@ class Vault_test : public beast::unit_test::Suite
{.flags = tfMPTCanTransfer | tfMPTCanLock |
(args.enableClawback ? tfMPTCanClawback : kNone) |
(args.requireAuth ? tfMPTRequireAuth : kNone),
.mutableFlags = tmfMPTCanMutateCanTransfer});
.mutableFlags = tmfMPTCanEnableCanTransfer});
PrettyAsset const asset = mptt.issuanceID();
mptt.authorize({.account = owner});
mptt.authorize({.account = depositor});
@@ -2238,149 +2238,6 @@ class Vault_test : public beast::unit_test::Suite
env.close();
}
testCase([this](
Env& env,
Account const&,
Account const& owner,
Account const& depositor,
PrettyAsset const& asset,
Vault& vault,
MPTTester& mptt) {
testcase("MPT non-transferable: block deposit, allow withdraw");
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
env(tx);
env.close();
tx = vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)});
env(tx);
env.close();
// Issuer governance: clear CanTransfer. New exposure must be
// blocked, but recovery paths must remain open so existing
// depositors are not trapped.
mptt.set({.mutableFlags = tmfMPTClearCanTransfer});
env.close();
// New deposit is blocked.
env(tx, Ter{tecNO_AUTH});
env.close();
// Existing depositor can always withdraw, even though the asset
// is no longer freely transferable.
tx = vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(100)});
env(tx);
env.close();
// Delete vault with zero balance
env(vault.del({.owner = owner, .id = keylet.key}));
});
{
testcase("MPT non-transferable: pre-fixCleanup3_2_0 withdraw blocked");
// Regression: before fixCleanup3_2_0 a depositor was trapped if
// the issuer cleared lsfMPTCanTransfer. Verify that the legacy
// (broken) behavior is preserved when the amendment is disabled.
Env env{*this, testableAmendments() - fixCleanup3_2_0};
Account const issuer{"issuer"};
Account const owner{"owner"};
Account const depositor{"depositor"};
env.fund(XRP(10'000), issuer, owner, depositor);
env.close();
Vault const vault{env};
MPTTester mptt{env, issuer, kMptInitNoFund};
mptt.create(
{.flags = tfMPTCanTransfer | tfMPTCanLock,
.mutableFlags = tmfMPTCanMutateCanTransfer});
PrettyAsset const asset = mptt.issuanceID();
mptt.authorize({.account = owner});
mptt.authorize({.account = depositor});
env(pay(issuer, depositor, asset(1'000)));
env.close();
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
env(tx);
env.close();
env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)}));
env.close();
mptt.set({.mutableFlags = tmfMPTClearCanTransfer});
env.close();
// Pre-amendment: deposit blocked (matches new behavior).
env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)}),
Ter{tecNO_AUTH});
env.close();
// Pre-amendment: withdraw is also blocked - this is the bug
// that fixCleanup3_2_0 fixes.
env(vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(100)}),
Ter{tecNO_AUTH});
env.close();
}
{
testcase("MPT non-transferable: vault shares inherit restriction");
Env env{*this, testableAmendments()};
Account const issuer{"issuer"};
Account const owner{"owner"};
Account const alice{"alice"};
Account const bob{"bob"};
env.fund(XRP(10'000), issuer, owner, alice, bob);
env.close();
Vault const vault{env};
MPTTester mptt{env, issuer, kMptInitNoFund};
mptt.create(
{.flags = tfMPTCanTransfer | tfMPTCanLock,
.mutableFlags = tmfMPTCanMutateCanTransfer});
PrettyAsset const asset = mptt.issuanceID();
mptt.authorize({.account = owner});
mptt.authorize({.account = alice});
mptt.authorize({.account = bob});
env(pay(issuer, alice, asset(1'000)));
env(pay(issuer, bob, asset(1'000)));
env.close();
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
env(tx);
env.close();
env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = asset(500)}));
// Bob also deposits so he has a share MPToken to receive into.
env(vault.deposit({.depositor = bob, .id = keylet.key, .amount = asset(500)}));
env.close();
auto const shares = [&]() -> PrettyAsset {
auto const sle = env.le(keylet);
BEAST_EXPECT(sle != nullptr);
return MPTIssue(sle->at(sfShareMPTID));
}();
// Sanity: while CanTransfer is set on the underlying, peer-to-peer
// share transfers are allowed.
env(pay(alice, bob, shares(1)));
env.close();
// Issuer governance: clear CanTransfer on the underlying.
mptt.set({.mutableFlags = tmfMPTClearCanTransfer});
env.close();
// Vault shares inherit the restriction: third-party share-to-share
// payments are blocked.
env(pay(alice, bob, shares(1)), Ter{tecNO_AUTH});
env.close();
// Recovery path: existing share holders can still redeem shares
// for the underlying asset via VaultWithdraw.
env(vault.withdraw({.depositor = alice, .id = keylet.key, .amount = shares(1)}));
env.close();
}
{
testcase("MPT locked: vault shares inherit underlying lock");
@@ -2458,56 +2315,6 @@ class Vault_test : public beast::unit_test::Suite
BEAST_EXPECT(expectOffers(env, alice, 1));
}
{
testcase("MPT non-transferable: pre-fixCleanup3_2_0 share transfer succeeds");
// Regression: before fixCleanup3_2_0 a peer-to-peer share Payment
// succeeded even when the underlying asset's lsfMPTCanTransfer
// was cleared. Verify that the legacy (non-inheriting) behavior
// is preserved when the amendment is disabled.
Env env{*this, testableAmendments() - fixCleanup3_2_0};
Account const issuer{"issuer"};
Account const owner{"owner"};
Account const alice{"alice"};
Account const bob{"bob"};
env.fund(XRP(10'000), issuer, owner, alice, bob);
env.close();
Vault const vault{env};
MPTTester mptt{env, issuer, kMptInitNoFund};
mptt.create(
{.flags = tfMPTCanTransfer | tfMPTCanLock,
.mutableFlags = tmfMPTCanMutateCanTransfer});
PrettyAsset const asset = mptt.issuanceID();
mptt.authorize({.account = owner});
mptt.authorize({.account = alice});
mptt.authorize({.account = bob});
env(pay(issuer, alice, asset(1'000)));
env(pay(issuer, bob, asset(1'000)));
env.close();
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
env(tx);
env.close();
env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = asset(500)}));
env(vault.deposit({.depositor = bob, .id = keylet.key, .amount = asset(500)}));
env.close();
auto const shares = [&]() -> PrettyAsset {
auto const sle = env.le(keylet);
BEAST_EXPECT(sle != nullptr);
return MPTIssue(sle->at(sfShareMPTID));
}();
mptt.set({.mutableFlags = tmfMPTClearCanTransfer});
env.close();
// Pre-amendment: share transfer leaks past underlying restriction.
env(pay(alice, bob, shares(1)));
env.close();
}
{
testcase("MPT CanTrade governance: share inherits underlying on DEX and AMM");
@@ -2522,8 +2329,8 @@ class Vault_test : public beast::unit_test::Suite
MPTTester mptt{env, issuer, kMptInitNoFund};
mptt.create(
{.flags = tfMPTCanTransfer | tfMPTCanTrade | tfMPTCanLock,
.mutableFlags = tmfMPTCanMutateCanTrade});
{.flags = tfMPTCanTransfer | tfMPTCanLock,
.mutableFlags = tmfMPTCanEnableCanTrade});
PrettyAsset const asset = mptt.issuanceID();
mptt.authorize({.account = owner});
mptt.authorize({.account = alice});
@@ -2547,38 +2354,18 @@ class Vault_test : public beast::unit_test::Suite
return MPTIssue(sle->at(sfShareMPTID));
}();
// Sanity: while CanTrade is set on the underlying, both the asset
// and the vault share can be placed on the DEX.
env(offer(alice, XRP(1), asset(10)));
env(offer(alice, XRP(1), shares(1)));
env.close();
// Issuer governance: clear CanTrade on the underlying.
mptt.set({.mutableFlags = tmfMPTClearCanTrade});
env.close();
// Control: clearing CanTrade on the underlying is observable on
// the DEX path for that asset.
// CanTrade is not set on the underlying, both the asset and
// the vault share are blocked on the DEX.
env(offer(alice, XRP(1), asset(10)), Ter{tecNO_PERMISSION});
env(offer(alice, XRP(1), shares(1)), Ter{tecNO_PERMISSION});
env.close();
// Control: clearing CanTrade on the underlying is also observable
// on the AMM path for that asset.
AMM const ammUnderlyingFails(
// The inherited CanTrade restriction also blocks AMM creation.
AMM const ammUnderlyingFail(
env, alice, XRP(1'000), asset(1'000), Ter{tecNO_PERMISSION});
// Post-fixCleanup3_2_0: vault shares inherit the underlying's
// CanTrade restriction on the DEX path (canTrade reads the
// share's sfReferenceHolding and dispatches to the underlying).
env(offer(bob, XRP(1), shares(1)), Ter{tecNO_PERMISSION});
env.close();
// checkMPTAllowed mirrors the inheritance for AMM/Offer-
// crossing/Check paths, so a share AMM also cannot be created
// when the underlying CanTrade is cleared.
AMM const ammShares(env, alice, XRP(1'000), shares(100), Ter{tecNO_PERMISSION});
// Deposit still works (canAddHolding does not consult the field).
// Deposit still works before enabling CanTrade.
env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = asset(100)}));
env.close();
@@ -2587,9 +2374,19 @@ class Vault_test : public beast::unit_test::Suite
env(pay(alice, bob, shares(1)));
env.close();
// Withdraw still works.
// Withdraw still works before enabling CanTrade.
env(vault.withdraw({.depositor = alice, .id = keylet.key, .amount = asset(100)}));
env.close();
// Enable CanTrade on the underlying.
mptt.set({.mutableFlags = tmfMPTSetCanTrade});
env.close();
env(offer(alice, XRP(1), asset(10)));
env(offer(alice, XRP(1), shares(1)));
env.close();
AMM const ammUnderlying(env, alice, XRP(1'000), asset(1'000));
}
{