From 3ad93af34dfc376ff8b3cf6c777f1711ec39d44e Mon Sep 17 00:00:00 2001 From: tequ Date: Wed, 29 Apr 2026 08:34:50 +0900 Subject: [PATCH] fix: Sponsor's MaxFee cap is bypassed in reset() path, allowing sponsee to drain entire pre-funded FeeAmount in a single tec-failing transaction --- src/libxrpl/tx/Transactor.cpp | 6 ++++++ src/test/app/Sponsor_test.cpp | 38 +++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/libxrpl/tx/Transactor.cpp b/src/libxrpl/tx/Transactor.cpp index 77a96f42b6..d1e4d72c82 100644 --- a/src/libxrpl/tx/Transactor.cpp +++ b/src/libxrpl/tx/Transactor.cpp @@ -1243,6 +1243,12 @@ Transactor::reset(XRPAmount fee) auto const balance = payerSle->getFieldAmount(payer.balanceField).xrp(); + if (payer.type == FeePayerType::SponsorPreFunded && payerSle->isFieldPresent(sfMaxFee)) + { + auto const cap = payerSle->getFieldAmount(sfMaxFee).xrp(); + fee = std::min(fee, cap); + } + // balance should have already been checked in checkFee / preFlight. XRPL_ASSERT( balance != beast::zero && (!view().open() || balance >= fee), diff --git a/src/test/app/Sponsor_test.cpp b/src/test/app/Sponsor_test.cpp index edc1e7daf4..987fdbbb69 100644 --- a/src/test/app/Sponsor_test.cpp +++ b/src/test/app/Sponsor_test.cpp @@ -43,6 +43,7 @@ #include #include #include +#include #include #include #include @@ -57,6 +58,7 @@ #include #include #include +#include #include #include @@ -1695,6 +1697,42 @@ public: } } + // MaxFee cap is enforced in reset() for tec-failing transactions. + // On a closed ledger view (!view.open()), checkFee returns tecINSUFF_FEE when + // fee > MaxFee (not terINSUF_FEE_B), triggering reset() + { + Env env{*this, testable_amendments()}; + Account const alice("alice"); + Account const carol("sponsor"); + + env.fund(XRP(10000), alice, carol); + env.close(); + + // FeeAmount=1000 drops, MaxFee=10 drops + env(sponsor::set_fee(carol, 0, drops(1000), drops(10)), sponsor::sponseeAcc(alice)); + env.close(); + + // Apply directly against the closed ledger view (open_ = false) so that + // checkFee returns tecINSUFF_FEE and reset() is invoked. + OpenView overlay(&*env.closed()); + + auto jt = env.jt( + noop(alice), + fee(drops(1000)), + seq(env.seq(alice)), + sponsor::as(carol, spfSponsorFee)); + + auto const result = xrpl::apply(env.app(), overlay, *jt.stx, tapNONE, env.journal); + BEAST_EXPECT(result.ter == tecINSUFF_FEE); + BEAST_EXPECT(result.applied); + + // Only MaxFee (10 drops) must be deducted, not the full 1000 drops. + auto const sle = overlay.read(keylet::sponsor(carol.id(), alice.id())); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->isFieldPresent(sfFeeAmount)); + BEAST_EXPECT(sle->getFieldAmount(sfFeeAmount) == drops(990)); // 1000 - MaxFee(10) + } + // test lsfSponsorshipRequireSignForFee { Env env{*this, testable_amendments()};