Fix AMMClawback missing MPToken recreation for MPTs

This commit is contained in:
Gregory Tsipenyuk
2026-05-15 17:58:24 -04:00
parent 210b6e08ba
commit 21cfba66fd
4 changed files with 87 additions and 3 deletions

View File

@@ -171,6 +171,14 @@ createMPToken(
AccountID const& account,
std::uint32_t const flags);
TER
checkCreateMPT(
xrpl::ApplyView& view,
xrpl::MPTIssue const& mptIssue,
xrpl::AccountID const& holder,
std::uint32_t flags,
beast::Journal j);
TER
checkCreateMPT(
xrpl::ApplyView& view,

View File

@@ -812,6 +812,7 @@ checkCreateMPT(
xrpl::ApplyView& view,
xrpl::MPTIssue const& mptIssue,
xrpl::AccountID const& holder,
std::uint32_t flags,
beast::Journal j)
{
if (mptIssue.getIssuer() == holder)
@@ -821,7 +822,7 @@ checkCreateMPT(
auto const mptokenID = keylet::mptoken(mptIssuanceID.key, holder);
if (!view.exists(mptokenID))
{
if (auto const err = createMPToken(view, mptIssue.getMptID(), holder, 0);
if (auto const err = createMPToken(view, mptIssue.getMptID(), holder, flags);
!isTesSuccess(err))
{
return err;
@@ -836,6 +837,16 @@ checkCreateMPT(
return tesSUCCESS;
}
TER
checkCreateMPT(
xrpl::ApplyView& view,
xrpl::MPTIssue const& mptIssue,
xrpl::AccountID const& holder,
beast::Journal j)
{
return checkCreateMPT(view, mptIssue, holder, 0, j);
}
std::int64_t
maxMPTAmount(SLE const& sleIssuance)
{

View File

@@ -649,11 +649,21 @@ AMMWithdraw::withdraw(
if (mptokenKey && account != asset.getIssuer())
{
auto const& mptIssue = asset.get<MPTIssue>();
std::uint32_t createFlags = 0;
if (auto const err = requireAuth(view, mptIssue, account, AuthType::WeakAuth);
!isTesSuccess(err))
return err;
{
if (authHandling != AuthHandling::IgnoreAuth || err != tecNO_AUTH)
return err;
if (auto const err = checkCreateMPT(view, mptIssue, account, journal);
// AMMClawback ignores authorization so the issuer can recover
// MPT locked in the pool even if the holder deleted their
// MPToken. Recreate that MPToken as authorized for the
// clawback withdrawal path only.
createFlags = lsfMPTAuthorized;
}
if (auto const err = checkCreateMPT(view, mptIssue, account, createFlags, journal);
!isTesSuccess(err))
{
return err;

View File

@@ -1335,6 +1335,60 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
}
}
void
testClawbackCreatesMissingMPToken(FeatureBitset features)
{
testcase("test AMMClawback creates missing MPToken");
using namespace jtx;
auto test = [&](std::optional<std::uint64_t> const clawAmount) {
Env env{*this, features};
Account const gw{"gateway"};
Account const alice{"alice"};
env.fund(XRP(1'000'000), gw, alice);
env.close();
MPTTester token(
{.env = env,
.issuer = gw,
.holders = {alice},
.pay = 1'000,
.flags = tfMPTCanClawback | tfMPTRequireAuth | kMPT_DEX_FLAGS,
.authHolder = true});
AMM ammAlice(env, alice, token(1'000), XRP(1'000));
env.close();
BEAST_EXPECT(env.balance(alice, token) == token(0));
// The holder can delete the zero-balance MPToken while still
// holding LP tokens. A regular AMMWithdraw remains subject to
// RequireAuth and cannot recreate the missing token.
token.authorize({.account = alice, .flags = tfMPTUnauthorize});
env.close();
BEAST_EXPECT(!env.le(keylet::mptoken(token.issuanceID(), alice.id())));
ammAlice.withdrawAll(alice, std::nullopt, Ter(tecNO_AUTH));
env.close();
BEAST_EXPECT(!env.le(keylet::mptoken(token.issuanceID(), alice.id())));
// AMMClawback ignores authorization and must be able to recreate
// the holder MPToken so the issuer can recover MPT from the pool.
std::optional<STAmount> amount;
if (clawAmount)
amount = token(*clawAmount);
env(amm::ammClawback(gw, alice, token, XRP, amount));
env.close();
auto const sleMpt = env.le(keylet::mptoken(token.issuanceID(), alice.id()));
BEAST_EXPECT(sleMpt && sleMpt->isFlag(lsfMPTAuthorized));
env.require(Balance(alice, token(0)));
BEAST_EXPECT(clawAmount ? ammAlice.ammExists() : !ammAlice.ammExists());
};
test(std::nullopt);
test(400);
}
void
testSingleDepositAndClawback(FeatureBitset features)
{
@@ -1824,6 +1878,7 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
testAMMClawbackAllSameIssuer(all);
testAMMClawbackIssuesEachOther(all);
testAssetFrozenOrLocked(all);
testClawbackCreatesMissingMPToken(all);
testSingleDepositAndClawback(all);
testLastHolderLPTokenBalance(all);
testLastHolderLPTokenBalance(all - fixAMMv1_3 - fixAMMClawbackRounding);