From 779b49cd9311f6d5d530815e24d2d52d50302c26 Mon Sep 17 00:00:00 2001 From: Zhiyuan Wang <96991820+Kassaking7@users.noreply.github.com> Date: Sat, 9 May 2026 10:43:56 -0400 Subject: [PATCH] fix: Prevent stale AuthAccounts from persisting after `tfTwoAssetIfEmpty` re-initialization (#6996) Co-authored-by: Bart --- src/libxrpl/ledger/helpers/AMMHelpers.cpp | 3 + src/test/app/AMM_test.cpp | 92 +++++++++++++++++++++++ 2 files changed, 95 insertions(+) diff --git a/src/libxrpl/ledger/helpers/AMMHelpers.cpp b/src/libxrpl/ledger/helpers/AMMHelpers.cpp index 4cef02e056..cd1796468c 100644 --- a/src/libxrpl/ledger/helpers/AMMHelpers.cpp +++ b/src/libxrpl/ledger/helpers/AMMHelpers.cpp @@ -817,6 +817,9 @@ initializeFeeAuctionVote( { auctionSlot.makeFieldAbsent(sfDiscountedFee); // LCOV_EXCL_LINE } + // Clear stale auth accounts from any previous auction slot holder. + if (rules.enabled(fixCleanup3_2_0) && auctionSlot.isFieldPresent(sfAuthAccounts)) + auctionSlot.makeFieldAbsent(sfAuthAccounts); } Expected diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index 8e2c0255ef..06e3eb6d01 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -7080,6 +7081,95 @@ private: {all}); } + void + testStaleAuthAccountsAfterReinit(FeatureBitset features) + { + testcase("Test AuthAccounts reset after empty pool reinitialization"); + using namespace jtx; + + Env env( + *this, + envconfig([](std::unique_ptr cfg) { + cfg->FEES.reference_fee = XRPAmount(1); + return cfg; + }), + features); + Account const dan("dan"); + Account const ed("ed"); + fund(env, gw_, {alice_, carol_, bob_, dan, ed}, XRP(50'000), {USD(50'000)}); + AMM amm(env, alice_, XRP(10'000), USD(10'000)); + // Create excess trustlines to prevent AMM auto-deletion on withdrawal. + for (auto i = 0; i < kMAX_DELETABLE_AMM_TRUST_LINES + 10; ++i) + { + Account const a{std::to_string(i)}; + env.fund(XRP(1'000), a); + env(trust(a, STAmount{amm.lptIssue(), 10'000})); + env.close(); + } + // Carol deposits so she has LP tokens to bid. + amm.deposit(carol_, 1'000'000); + // Carol wins the auction slot, authorizing bob and dan. + env(amm.bid({ + .account = carol_, + .bidMin = 100, + .authAccounts = {bob_, dan}, + })); + env.close(); + BEAST_EXPECT(amm.expectAuctionSlot({bob_.id(), dan.id()})); + // Withdraw all — AMM enters empty state but is not deleted because + // excess trustlines prevent auto-deletion. + amm.withdrawAll(alice_); + amm.withdrawAll(carol_); + BEAST_EXPECT(amm.ammExists()); + // Pre-conditions before re-init: AMM is empty and stale sfAuthAccounts + // from carol's bid are still present. + BEAST_EXPECT(amm.getLPTokensBalance() == IOUAmount{0}); + BEAST_EXPECT(amm.expectAuctionSlot({bob_.id(), dan.id()})); + // Ed re-initializes the AMM via tfTwoAssetIfEmpty with fee=500. + amm.deposit( + ed, + std::nullopt, + XRP(10'000), + USD(10'000), + std::nullopt, + tfTwoAssetIfEmpty, + std::nullopt, + std::nullopt, + 500); + + auto const ammSle = env.current()->read(keylet::amm(amm[0], amm[1])); + BEAST_EXPECT(ammSle && ammSle->isFieldPresent(sfAuctionSlot)); + auto const& slot = safeDowncast(ammSle->peekAtField(sfAuctionSlot)); + + // sfDiscountedFee = 500 / AUCTION_SLOT_DISCOUNTED_FEE_FRACTION = 50, + // sfPrice = 0 (reset on init), time interval = 0 (freshly issued slot). + BEAST_EXPECT(amm.expectAuctionSlot(50, 0, IOUAmount{0})); + // sfAccount must be the re-initializing depositor, not the previous + // slot holder (carol). + BEAST_EXPECT(slot[sfAccount] == ed.id()); + // sfTradingFee on the AMM SLE must reflect ed's deposit fee. + BEAST_EXPECT(ammSle->getFieldU16(sfTradingFee) == 500); + // sfVoteSlots must be reset to a single entry for ed. + auto const& votes = ammSle->getFieldArray(sfVoteSlots); + BEAST_EXPECT(votes.size() == 1); + if (!votes.empty()) + { + BEAST_EXPECT(votes[0].getAccountID(sfAccount) == ed.id()); + BEAST_EXPECT(votes[0].getFieldU16(sfTradingFee) == 500); + BEAST_EXPECT(votes[0].getFieldU32(sfVoteWeight) == kVOTE_WEIGHT_SCALE_FACTOR); + } + // sfAuthAccounts behaviour depends on the fix. + if (features[fixCleanup3_2_0]) + { + BEAST_EXPECT(!slot.isFieldPresent(sfAuthAccounts)); + } + else + { + BEAST_EXPECT( + slot.isFieldPresent(sfAuthAccounts) && !slot.getFieldArray(sfAuthAccounts).empty()); + } + } + void run() override { @@ -7150,6 +7240,8 @@ private: testWithdrawRounding(all); testWithdrawRounding(all - fixAMMv1_3); testFailedPseudoAccount(); + testStaleAuthAccountsAfterReinit(all); + testStaleAuthAccountsAfterReinit(all - fixCleanup3_2_0); } };