fix: Sponsor's MaxFee cap is bypassed in reset() path, allowing sponsee to drain entire pre-funded FeeAmount in a single tec-failing transaction

This commit is contained in:
tequ
2026-04-29 08:34:50 +09:00
committed by Oleksandr
parent c5941d6b14
commit 3ad93af34d
2 changed files with 44 additions and 0 deletions

View File

@@ -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),

View File

@@ -43,6 +43,7 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/json/json_value.h>
#include <xrpl/ledger/OpenView.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
@@ -57,6 +58,7 @@
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/tx/apply.h>
#include <chrono>
#include <cstdint>
@@ -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()};