From 6ee071c5b06825b781dee7b36624f2b52452528c Mon Sep 17 00:00:00 2001 From: tequ Date: Tue, 28 Apr 2026 15:57:49 +0900 Subject: [PATCH] fix: An incorrect available XRP balance check in AMM deposit allows reserve-locked funds to be used as adding liquidity --- src/libxrpl/tx/transactors/dex/AMMDeposit.cpp | 23 +++++++---- src/test/app/Sponsor_test.cpp | 41 +++++++++++++++++++ 2 files changed, 56 insertions(+), 8 deletions(-) diff --git a/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp b/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp index bfbb82675b..1f0fa57f0a 100644 --- a/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp @@ -239,6 +239,15 @@ AMMDeposit::preclaim(PreclaimContext const& ctx) auto const sponsorSle = getTxReserveSponsor(ctx.view, ctx.tx); auto const accountSle = ctx.view.read(keylet::account(accountID)); + auto const reserveAdj = (sponsorSle || sle) ? 0 : 1; + + if (xrpLiquid(ctx.view, accountID, reserveAdj, ctx.j) < deposit) + { + if (sle) + return tecUNFUNDED_AMM; + return tecINSUF_RESERVE_LINE; + } + if (auto const ret = checkInsufficientReserve( ctx.view, ctx.tx, @@ -247,11 +256,10 @@ AMMDeposit::preclaim(PreclaimContext const& ctx) sponsorSle, 1, !sle); - isTesSuccess(ret)) - return TER(tesSUCCESS); - if (sle) - return tecUNFUNDED_AMM; - return tecINSUF_RESERVE_LINE; + sponsorSle && !isTesSuccess(ret)) + return tecINSUF_RESERVE_LINE; + + return tesSUCCESS; } return accountFunds( ctx.view, @@ -556,9 +564,8 @@ AMMDeposit::deposit( // Adjust the reserve if LP doesn't have LPToken trustline auto const trustlineExists = view.exists(keylet::line(account_, lpIssue.account, lpIssue.currency)); - auto const ownerCountAdj = trustlineExists ? 0 : 1; - if (xrpLiquid(view, sponsor.value_or(account_), sponsor ? ownerCountAdj : 0, j_) >= - depositAmount) + auto const reserveAdj = (sponsor || trustlineExists) ? 0 : 1; + if (xrpLiquid(view, account_, reserveAdj, j_) >= depositAmount) return tesSUCCESS; } else if ( diff --git a/src/test/app/Sponsor_test.cpp b/src/test/app/Sponsor_test.cpp index d427b4f52b..fea76a7bac 100644 --- a/src/test/app/Sponsor_test.cpp +++ b/src/test/app/Sponsor_test.cpp @@ -2182,6 +2182,47 @@ public: submit(ammDeposit(env, bob, USD(100), EUR(100))); }); } + { + // AMMDeposit single-asset XRP: reserve sponsor covers LP trustline reserve + // but depositor's own liquid XRP is insufficient for the deposit → tecUNFUNDED_AMM + Env env{*this, testable_amendments()}; + env.fund(XRP(10000), alice, bob, gw, sponsor); + env.close(); + + env(trust(bob, USD(10000))); + env(trust(alice, USD(10000))); + env.close(); + env(pay(gw, bob, USD(1000))); + env.close(); + + AMM amm(env, bob, XRP(1000), USD(100)); + + // alice has 1 owner object (USD trust line); give her reserve + 5 XRP liquid + adjustAccountXRPBalance(env, alice, reserve(env, ownerCount(env, alice)) + XRP(5)); + + auto const jv = AMM::depositJv( + {.account = alice, + .asset1In = XRP(10), + .assets = std::make_pair(Asset{xrpIssue()}, Asset{USD.issue()})}); + + if (cosigning) + { + env(jv, + sponsor::as(sponsor, spfSponsorReserve), + sig(sfSponsorSignature, sponsor), + ter(tecINSUF_RESERVE_LINE)); + } + else + { + env(sponsor::set_reserve(sponsor, 0, 1), sponsor::sponseeAcc(alice)); + env.close(); + env(jv, sponsor::as(sponsor, spfSponsorReserve), ter(tecINSUF_RESERVE_LINE)); + env(sponsor::del(sponsor), sponsor::sponseeAcc(alice)); + } + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 1); // no LP token was created + } { // AMMWithdraw {