fix: AMMClawback exact LP token boundary (#7373)

This commit is contained in:
Peter Chen
2026-08-26 13:14:53 +00:00
committed by GitHub
parent 50527485d3
commit f7ea645bf4
2 changed files with 70 additions and 5 deletions

View File

@@ -324,11 +324,13 @@ AMMClawback::equalWithdrawMatchingOneAmount(
auto amount2Withdraw = amount2Balance * frac;
auto const lpTokensWithdraw = toSTAmount(lptAMMBalance.asset(), lptAMMBalance * frac);
if (lpTokensWithdraw > holdLPtokens)
auto const& rules = sb.rules();
// Pre-fixCleanup3_4_0 only a strictly greater computed LP amount takes
// the withdraw-all path. Equality left the last holder unable to be
// fully clawed. The amendment treats equality as withdraw-all.
if (rules.enabled(fixCleanup3_4_0) ? lpTokensWithdraw >= holdLPtokens
: lpTokensWithdraw > holdLPtokens)
{
// if lptoken balance less than what the issuer intended to clawback,
// clawback all the tokens. Because we are doing a two-asset withdrawal,
// tfee is actually not used, so pass tfee as 0.
return AMMWithdraw::equalWithdrawTokens(
sb,
ammSle,
@@ -348,7 +350,6 @@ AMMClawback::equalWithdrawMatchingOneAmount(
ctx_.journal);
}
auto const& rules = sb.rules();
if (rules.enabled(fixAMMClawbackRounding))
{
auto tokensAdj = getRoundedLPTokens(rules, lptAMMBalance, frac, IsDeposit::No);

View File

@@ -13,7 +13,9 @@
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/ledger/helpers/AMMHelpers.h>
#include <xrpl/protocol/AmountConversions.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/XRPAmount.h>
@@ -2713,6 +2715,67 @@ class AMMClawback_test : public beast::unit_test::Suite
}
}
void
testExactLPTokenEquality(FeatureBitset features)
{
using namespace jtx;
if (!features[fixAMMv1_3] || !features[fixAMMClawbackRounding])
return;
testcase("test exact LP token equality boundary");
Env env(*this, features);
Account const gw{"gateway"}, alice{"alice"}, bob{"bob"};
env.fund(XRP(100000), gw, alice, bob);
env.close();
env(fset(gw, asfAllowTrustLineClawback));
env.close();
auto const usd = gw["USD"];
env.trust(usd(100000), alice);
env(pay(gw, alice, usd(50000)));
env.trust(usd(100000), bob);
env(pay(gw, bob, usd(40000)));
env.close();
// bob keeps alice from being the sole LP, otherwise the clawback
// first rewrites the AMM's LP balance to alice's tokens and the
// boundary is no longer distinguishable.
AMM amm(env, alice, XRP(2), usd(1));
amm.deposit(alice, IOUAmount{1'876123487565916, -15});
amm.deposit(bob, IOUAmount{1'000'000});
auto const [amountBalance, amount2Balance, lptAMMBalance] = amm.balances(usd, XRP);
auto const aliceLP = amm.getLPTokensBalance(alice);
auto const holderLPTokens = STAmount{aliceLP, amm.lptIssue()};
BEAST_EXPECT(lptAMMBalance > holderLPTokens);
// Clawing alice's pro-rata share lands the transactor's computed LP
// amount exactly on her balance.
auto const amount = toSTAmount(usd, Number{amountBalance} * holderLPTokens / lptAMMBalance);
BEAST_EXPECT(
toSTAmount(lptAMMBalance.asset(), lptAMMBalance * (Number{amount} / amountBalance)) ==
holderLPTokens);
env(amm::ammClawback(gw, alice, usd, XRP, amount));
env.close();
auto const aliceLPAfter = amm.getLPTokensBalance(alice);
if (features[fixCleanup3_4_0])
{
// Equality takes the withdraw-all path, redeeming alice's tokens
// exactly.
BEAST_EXPECT(aliceLPAfter == IOUAmount(0));
}
else
{
// The fall-through re-rounds the LP amount against the much
// larger pool balance, leaving alice with dust.
BEAST_EXPECT(aliceLPAfter != IOUAmount(0) && aliceLPAfter < aliceLP);
}
}
void
run() override
{
@@ -2746,6 +2809,7 @@ class AMMClawback_test : public beast::unit_test::Suite
testAssetFrozen(features);
testSingleDepositAndClawback(features);
testLastHolderLPTokenBalance(features);
testExactLPTokenEquality(features);
}
}
};