From ca2f855afbbc7f81ffa8e7854009a207c74fbff8 Mon Sep 17 00:00:00 2001 From: tequ Date: Fri, 12 Jun 2026 21:11:23 +0900 Subject: [PATCH] Fix ammLPHolds logic to include escrowed cases --- src/test/app/Escrow_test.cpp | 82 ++++++++++++++++++++++++++ src/xrpld/app/misc/detail/AMMUtils.cpp | 23 ++++++++ 2 files changed, 105 insertions(+) diff --git a/src/test/app/Escrow_test.cpp b/src/test/app/Escrow_test.cpp index 778e0bed7..db5a6c205 100644 --- a/src/test/app/Escrow_test.cpp +++ b/src/test/app/Escrow_test.cpp @@ -18,6 +18,7 @@ //============================================================================== #include +#include #include #include #include @@ -4316,6 +4317,86 @@ struct Escrow_test : public beast::unit_test::suite env.close(); } + void + testIOUAMM(FeatureBitset features) + { + testcase("IOU AMM"); + using namespace test::jtx; + using namespace std::chrono; + + Account alice{"alice"}; + Account bob{"bob"}; + Account gw{"gw"}; + + auto const USD = gw["USD"]; + + // AMMCreate fails - insufficient balance + Env env(*this, features); + env.fund(XRP(10000), alice, bob, gw); + env.close(); + + env.trust(USD(100000), alice); + env.close(); + env(pay(gw, alice, USD(1000))); + env.close(); + + env(escrow(alice, bob, USD(1000)), finish_time(env.now() + 1s)); + env.close(); + + // AMMCreate fails - insufficient balance + AMM ammFail(env, alice, XRP(1000), USD(1000), ter(tecUNFUNDED_AMM)); + + env(pay(gw, alice, USD(1000))); + env.close(); + + AMM ammAlice(env, alice, XRP(1000), USD(1000)); + BEAST_EXPECT(ammAlice.ammExists()); + + // Single Asset Deposit fails - insufficient balance + ammAlice.deposit( + alice, + USD(1000), + std::nullopt, + std::nullopt, + std::nullopt, + ter(tecUNFUNDED_AMM)); + + // Double Asset Deposit fails - insufficient balance + ammAlice.deposit( + alice, + USD(1000), + XRP(1000), + std::nullopt, + std::nullopt, + ter(tecUNFUNDED_AMM)); + + auto const lptoken = ammAlice.getLPTokensBalance(alice); + // lock all LP tokens + env(escrow(alice, bob, STAmount{lptoken, ammAlice.lptIssue()}), + finish_time(env.now() + 1s)); + env.close(); + + // Withdraw + ammAlice.withdraw( + alice, USD(1000), std::nullopt, std::nullopt, ter(tecAMM_BALANCE)); + ammAlice.withdrawAll(alice, USD(1000), ter(tecAMM_BALANCE)); + + env(ammAlice.bid(BidArg{ + .account = alice, + .assets = {{USD, XRP}}, + .bidMax = 100, + }), + ter(tecAMM_INVALID_TOKENS)); + + ammAlice.vote( + alice, + 1'000, + std::nullopt, + std::nullopt, + {{USD, XRP}}, + ter(tecAMM_INVALID_TOKENS)); + } + static uint256 getEscrowIndex(AccountID const& account, std::uint32_t uSequence) { @@ -4729,6 +4810,7 @@ struct Escrow_test : public beast::unit_test::suite testIOUTLINSF(features); testIOUPrecisionLoss(features); testIOUClawback(features); + testIOUAMM(features); } public: diff --git a/src/xrpld/app/misc/detail/AMMUtils.cpp b/src/xrpld/app/misc/detail/AMMUtils.cpp index 8a6917b8c..854c2e1d9 100644 --- a/src/xrpld/app/misc/detail/AMMUtils.cpp +++ b/src/xrpld/app/misc/detail/AMMUtils.cpp @@ -148,6 +148,29 @@ ammLPHolds( // Put balance in account terms. amount.negate(); } + + // If tokens can be escrowed then they can be locked in the trustline + // which means we must never spend them until the escrow is released. + if (view.rules().enabled(featurePaychanAndEscrowForTokens) && + sle->isFieldPresent(sfLockedBalance)) + { + STAmount const lockedBalance = sle->getFieldAmount(sfLockedBalance); + STAmount const spendableBalance = amount - + (lpAccount > ammAccount ? -lockedBalance : lockedBalance); + + // RH NOTE: this is defensively programmed, it should never fire + // if something bad does happen the trustline acts as a frozen line. + if (spendableBalance < beast::zero || spendableBalance > amount) + { + JLOG(j.error()) + << "SpendableBalance has illegal value in accountHolds " + << spendableBalance; + amount.clear(Issue{currency, ammAccount}); + } + else + amount = spendableBalance; + } + amount.setIssuer(ammAccount); JLOG(j.trace()) << "ammLPHolds:"