diff --git a/include/xrpl/ledger/helpers/SponsorHelpers.h b/include/xrpl/ledger/helpers/SponsorHelpers.h index 13b7b0d188..be73b3252c 100644 --- a/include/xrpl/ledger/helpers/SponsorHelpers.h +++ b/include/xrpl/ledger/helpers/SponsorHelpers.h @@ -24,14 +24,6 @@ isReserveSponsored(STTx const& tx) return (tx.getFieldU32(sfSponsorFlags) & spfSponsorReserve) != 0u; } -inline bool -isSponsorReserveCoSigning(STTx const& tx) -{ - if (!tx.isFieldPresent(sfSponsorSignature)) - return false; - return isReserveSponsored(tx); -} - inline std::optional getTxReserveSponsorAccountID(STTx const& tx) { diff --git a/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp b/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp index 6d28dfc217..f3ea3da896 100644 --- a/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp +++ b/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp @@ -343,14 +343,14 @@ checkInsufficientReserve( { if (sponsorSle) { - auto const isCoSigning = isSponsorReserveCoSigning(tx); - auto const sle = view.read( keylet::sponsorship( sponsorSle->getAccountID(sfAccount), accSle->getAccountID(sfAccount))); - // prefunded sponsor should have a sponsorship entry - if (!isCoSigning && !sle) + // A reserve-sponsored tx must carry a sponsor signature + // (cosigning path) and/or have a pre-existing sponsorship SLE + // (prefunded path). Absence of both is an internal invariant break. + if (isReserveSponsored(tx) && !sle && !tx.isFieldPresent(sfSponsorSignature)) return tecINTERNAL; // LCOV_EXCL_LINE if (sle) diff --git a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp index c05b72d614..a32b6c74da 100644 --- a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp @@ -196,17 +196,15 @@ authorizeMPToken( if (!sponsorSle) return sponsorSle.error(); // LCOV_EXCL_LINE - auto const isSponsoredAndPreFunded = *sponsorSle && !isSponsorReserveCoSigning(ctx.tx); - // The reserve that is required to create the MPToken. Note // that although the reserve increases with every item // an account owns, in the case of MPTokens we only // *enforce* a reserve if the user owns more than two // items. This is similar to the reserve requirements of trust lines. - // If PreFunded Sponsor, it must be checked whether sufficient - // ReserveCount exists. - if (ownerCount(view, *sponsorSle ? *sponsorSle : sleAcct, journal) >= 2 || - isSponsoredAndPreFunded) + // The "free-tier" shortcut (ownerCount < 2) does not apply once a sponsor is on + // the tx — the sponsor must always cover the reserve (via balance or prefunded + // budget), so this check always runs for sponsored transactions. + if (*sponsorSle || ownerCount(view, sleAcct, journal) >= 2) { if (auto const ret = checkInsufficientReserve( view, ctx.tx, sleAcct, priorBalance, *sponsorSle, 1, 0, journal); diff --git a/src/libxrpl/tx/transactors/token/TrustSet.cpp b/src/libxrpl/tx/transactors/token/TrustSet.cpp index 1c0291e1f7..bce4beacc0 100644 --- a/src/libxrpl/tx/transactors/token/TrustSet.cpp +++ b/src/libxrpl/tx/transactors/token/TrustSet.cpp @@ -334,9 +334,9 @@ TrustSet::doApply() std::uint32_t const uOwnerCount = ownerCount(view(), *sponsorSle ? *sponsorSle : sle, j_); - bool const isSponsoredAndPreFunded = *sponsorSle && !isSponsorReserveCoSigning(ctx_.tx); - // If PreFunded Sponsor, it must be checked whether sufficient - // ReserveCount exists. + // The "free-tier" shortcut (ownerCount < 2) only applies when there is no sponsor. + // With any sponsor on the tx, the sponsor must cover the reserve (via balance or + // prefunded budget), so the reserve check always runs. bool const freeTrustLine = uOwnerCount < 2 && !*sponsorSle; std::uint32_t const uQualityIn(bQualityIn ? ctx_.tx.getFieldU32(sfQualityIn) : 0); @@ -540,7 +540,7 @@ TrustSet::doApply() // calling adjustOwnerCount(). if (auto const ret = checkInsufficientReserve( view(), ctx_.tx, sleLowAccount, preFeeBalance_, *sponsorSle, 1, 0, j_); - isSponsoredAndPreFunded && !isTesSuccess(ret)) + *sponsorSle && !isTesSuccess(ret)) return tecINSUF_RESERVE_LINE; // Set reserve for low account. @@ -569,7 +569,7 @@ TrustSet::doApply() // calling adjustOwnerCount(). if (auto const ret = checkInsufficientReserve( view(), ctx_.tx, sleHighAccount, preFeeBalance_, *sponsorSle, 1, 0, j_); - isSponsoredAndPreFunded && !isTesSuccess(ret)) + *sponsorSle && !isTesSuccess(ret)) return tecINSUF_RESERVE_LINE; // Set reserve for high account. diff --git a/src/test/app/Sponsor_test.cpp b/src/test/app/Sponsor_test.cpp index d8c6b46290..04d448e2bc 100644 --- a/src/test/app/Sponsor_test.cpp +++ b/src/test/app/Sponsor_test.cpp @@ -746,6 +746,47 @@ public: } } + void + testSponsoredFreeTierReserve() + { + testcase("Sponsored Free-Tier Reserve"); + using namespace test::jtx; + Account const alice("alice"); + Account const issuer("issuer"); + Account const sponsor("sponsor"); + + // Trust lines and MPTokens normally skip the reserve check when the + // holder's ownerCount < 2 (the "free-tier" / first-two-items shortcut). When the + // tx is sponsored, that shortcut must not apply — the sponsor must + // still cover the reserve. + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, issuer); + // Sponsor is funded just below the reserve required to cover a single + // sponsored item. + env.fund(reserve(env, 1) - drops(1), sponsor); + env.close(); + BEAST_EXPECT(ownerCount(env, alice) == 0); + + MPTTester mptt(env, issuer, {.fund = false}); + mptt.create(); + + // Free-tier trust line cosigned by an undercapitalized sponsor must + // fail — the holder's free-first-two-items shortcut does not let the + // sponsor skip the reserve check. + env(trust(alice, issuer["USD"](100)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tecNO_LINE_INSUF_RESERVE)); + env.close(); + + // Free-tier MPTokenAuthorize must also fail for the same reason. + env(MPTTester::authorizeJV({.account = alice, .id = mptt.issuanceID()}), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + } + void testTransferSponsor() { @@ -2821,10 +2862,11 @@ public: env(noop(sponsor), ticket::Use(ticketSeq)); env.close(); - // pass (free mptoken) + // pass (free-tier mptoken for the holder, but the sponsor is still + // charged a reserve increment regardless of the ownerCount < 2 shortcut). if (cosigning) { - adjustAccountXRPBalance(env, sponsor, reserve(env, 2) - drops(1)); + adjustAccountXRPBalance(env, sponsor, reserve(env, 2)); env(jv, sponsor::As(sponsor, spfSponsorReserve), Sig(sfSponsorSignature, sponsor), @@ -3723,6 +3765,7 @@ protected: testSimpleSponsorshipSet(); testPreFundAndCosign(); + testSponsoredFreeTierReserve(); testTransferSponsor(); testSponsorFee();