diff --git a/src/libxrpl/tx/transactors/dex/AMMClawback.cpp b/src/libxrpl/tx/transactors/dex/AMMClawback.cpp index e690cd7693..b25c90069c 100644 --- a/src/libxrpl/tx/transactors/dex/AMMClawback.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMClawback.cpp @@ -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); diff --git a/src/test/app/AMMClawback_test.cpp b/src/test/app/AMMClawback_test.cpp index 90bface1fb..230d148ff9 100644 --- a/src/test/app/AMMClawback_test.cpp +++ b/src/test/app/AMMClawback_test.cpp @@ -13,7 +13,9 @@ #include #include +#include #include +#include #include #include #include @@ -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); } } };