diff --git a/src/libxrpl/tx/Transactor.cpp b/src/libxrpl/tx/Transactor.cpp index c38109bece..8ce60f214b 100644 --- a/src/libxrpl/tx/Transactor.cpp +++ b/src/libxrpl/tx/Transactor.cpp @@ -640,7 +640,28 @@ Transactor::payFee() if (!sle) return tefINTERNAL; // LCOV_EXCL_LINE - auto const feeAmountAfter = sle->getFieldAmount(feePayer.balanceField) - feePaid; + if (feePaid == beast::kZero) + return tesSUCCESS; + + XRPAmount balance = beast::kZero; + if (sle->isFieldPresent(feePayer.balanceField)) + { + balance = sle->getFieldAmount(feePayer.balanceField).xrp(); + } + else if (feePayer.balanceField != sfFeeAmount) + { + return tefINTERNAL; // LCOV_EXCL_LINE + } + + if (feePaid > balance) + { + if ((balance > beast::kZero) && !view().open()) + return tecINSUFF_FEE; + + return terINSUF_FEE_B; + } + + auto const feeAmountAfter = balance - feePaid; if (feeAmountAfter == beast::kZero && feePayer.balanceField == sfFeeAmount) { @@ -1302,7 +1323,15 @@ Transactor::reset(XRPAmount fee) if (!payerSle) return {tefINTERNAL, beast::kZero}; // LCOV_EXCL_LINE - auto const balance = payerSle->getFieldAmount(feePayer.balanceField).xrp(); + XRPAmount balance = beast::kZero; + if (payerSle->isFieldPresent(feePayer.balanceField)) + { + balance = payerSle->getFieldAmount(feePayer.balanceField).xrp(); + } + else if (feePayer.balanceField != sfFeeAmount) + { + return {tefINTERNAL, beast::kZero}; // LCOV_EXCL_LINE + } if (feePayer.type == FeePayerType::SponsorPreFunded && payerSle->isFieldPresent(sfMaxFee)) { diff --git a/src/test/app/Sponsor_test.cpp b/src/test/app/Sponsor_test.cpp index a615426463..a1d300dddc 100644 --- a/src/test/app/Sponsor_test.cpp +++ b/src/test/app/Sponsor_test.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -51,6 +52,7 @@ #include #include #include +#include #include #include @@ -1835,6 +1837,66 @@ public: BEAST_EXPECT(sle->getFieldAmount(sfFeeAmount) == drops(990)); // 1000 - MaxFee(10) } + // LedgerStateFix charges an owner-reserve fee and can claim that fee + // while returning tecFAILED_PROCESSING. That path must be safe when the + // fee is pre-funded by a sponsorship object. + { + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const sponsor("sponsor"); + env.fund(XRP(1000), alice, sponsor); + env.close(); + + auto const fixFee = drops(env.current()->fees().increment); + env(sponsor::set_fee(sponsor, 0, fixFee), sponsor::SponseeAcc(alice)); + env.close(); + + env(ledgerStateFix::nftPageLinks(alice, alice), + Fee(fixFee), + sponsor::As(sponsor, spfSponsorFee), + Ter(tecFAILED_PROCESSING)); + + if (auto const sle = env.le(keylet::sponsorship(sponsor, alice)); BEAST_EXPECT(sle)) + BEAST_EXPECT(!sle->isFieldPresent(sfFeeAmount)); + } + + // If preclaim saw spendable sponsored FeeAmount but the apply view no + // longer has it, the fee path should fail cleanly instead of throwing. + { + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const sponsor("sponsor"); + env.fund(XRP(1000), alice, sponsor); + env.close(); + + auto const fixFee = drops(env.current()->fees().increment); + env(sponsor::set_fee(sponsor, 0, fixFee), sponsor::SponseeAcc(alice)); + env.close(); + + OpenView overlay(&*env.closed()); + auto jt = env.jt( + ledgerStateFix::nftPageLinks(alice, alice), + Fee(fixFee), + sponsor::As(sponsor, spfSponsorFee)); + + auto const pf = preflight(env.app(), overlay.rules(), *jt.stx, TapNone, env.journal); + BEAST_EXPECT(isTesSuccess(pf.ter)); + auto const pc = preclaim(pf, env.app(), overlay); + BEAST_EXPECT(isTesSuccess(pc.ter)); + + auto const original = overlay.read(keylet::sponsorship(sponsor, alice)); + if (BEAST_EXPECT(original)) + { + auto sle = std::make_shared(*original); + sle->makeFieldAbsent(sfFeeAmount); + overlay.rawReplace(sle); + } + + auto const result = doApply(pc, env.app(), overlay); + BEAST_EXPECT(result.ter == terINSUF_FEE_B); + BEAST_EXPECT(!result.applied); + } + // test lsfSponsorshipRequireSignForFee { Env env{*this, testableAmendments()};