diff --git a/src/libxrpl/tx/transactors/sponsor/SponsorshipSet.cpp b/src/libxrpl/tx/transactors/sponsor/SponsorshipSet.cpp index e9376a1852..354710da0a 100644 --- a/src/libxrpl/tx/transactors/sponsor/SponsorshipSet.cpp +++ b/src/libxrpl/tx/transactors/sponsor/SponsorshipSet.cpp @@ -19,9 +19,29 @@ #include #include +#include namespace xrpl { +static bool +hasSponsorshipBudget( + SLE::const_ref sponsorshipSle, + std::optional const& feeAmount, + std::optional const& remainingOwnerCount) +{ + // A field the transaction omits keeps whatever the existing object holds, + // so fall back to the current SLE value when the tx does not set it. + bool const hasFeeAmount = feeAmount + ? *feeAmount > beast::kZero + : sponsorshipSle && (*sponsorshipSle)[~sfFeeAmount].value_or(STAmount{0}) > beast::kZero; + + bool const hasRemainingOwnerCount = remainingOwnerCount + ? *remainingOwnerCount > 0 + : sponsorshipSle && (*sponsorshipSle)[~sfRemainingOwnerCount].value_or(0) > 0; + + return hasFeeAmount || hasRemainingOwnerCount; +} + TxConsequences SponsorshipSet::makeTxConsequences(PreflightContext const& ctx) { @@ -134,6 +154,13 @@ SponsorshipSet::preclaim(PreclaimContext const& ctx) if (ctx.tx.isFlag(tfDeleteObject) && !sponsorshipSle) return tecNO_ENTRY; + // Reject creating or updating a Sponsorship that would be left with no + // budget (neither a positive FeeAmount nor a positive RemainingOwnerCount). + // Such an object is unusable yet still consumes the sponsor's reserve. + if (!ctx.tx.isFlag(tfDeleteObject) && + !hasSponsorshipBudget(sponsorshipSle, ctx.tx[~sfFeeAmount], ctx.tx[~sfRemainingOwnerCount])) + return temMALFORMED; + return tesSUCCESS; } diff --git a/src/test/app/Sponsor_test.cpp b/src/test/app/Sponsor_test.cpp index 4ad935c460..3013f7470a 100644 --- a/src/test/app/Sponsor_test.cpp +++ b/src/test/app/Sponsor_test.cpp @@ -591,20 +591,28 @@ public: env.close(); BEAST_EXPECT(!env.le(keylet::sponsorship(sponsor, alice))); - // create sponsorship with zero value + // Cannot create sponsorship with no fee or reserve budget. MaxFee + // and flags do not make a sponsorship object useful by themselves. + env(sponsor::set(sponsor, 0), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(temMALFORMED)); + env.close(); + BEAST_EXPECT(!env.le(keylet::sponsorship(sponsor, alice))); + + env(sponsor::set_max_fee(sponsor, 0, XRP(1)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(temMALFORMED)); + env.close(); + BEAST_EXPECT(!env.le(keylet::sponsorship(sponsor, alice))); + env(sponsor::set(sponsor, 0, 0, XRP(0), XRP(0)), sponsor::SponseeAcc(alice), - Fee(XRP(1))); + Fee(XRP(1)), + Ter(temMALFORMED)); env.close(); - - sle = env.le(keylet::sponsorship(sponsor, alice)); - BEAST_EXPECT(sle); - BEAST_EXPECT(!sle->isFieldPresent(sfRemainingOwnerCount)); - BEAST_EXPECT(!sle->isFieldPresent(sfFeeAmount)); - BEAST_EXPECT(!sle->isFieldPresent(sfMaxFee)); - // verify flags from previous sponsorship are not carried over - BEAST_EXPECT(!sle->isFlag(lsfSponsorshipRequireSignForFee)); - BEAST_EXPECT(!sle->isFlag(lsfSponsorshipRequireSignForReserve)); + BEAST_EXPECT(!env.le(keylet::sponsorship(sponsor, alice))); // update sponsorship with non-zero value env(sponsor::set(sponsor, 0, 100, XRP(100), XRP(1)), @@ -644,17 +652,55 @@ public: tfSponsorshipClearRequireSignForReserve, lsfSponsorshipRequireSignForReserve); - // update sponsorship with zero value + // Cannot update sponsorship so both fee and reserve budgets are absent. env(sponsor::set(sponsor, 0, 0, XRP(0), XRP(0)), sponsor::SponseeAcc(alice), - Fee(XRP(1))); + Fee(XRP(1)), + Ter(temMALFORMED)); + env.close(); + + sle = env.le(keylet::sponsorship(sponsor, alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 100); + BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(100)); + BEAST_EXPECT(sle->at(sfMaxFee) == XRP(1)); + } + + { + // Removing one budget field while the other remains keeps the + // Sponsorship valid. Starting state (from above): + // RemainingOwnerCount = 100, FeeAmount = XRP(100). + + // Remove only FeeAmount (set to 0); RemainingOwnerCount remains. + env(sponsor::set_fee(sponsor, 0, XRP(0)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tesSUCCESS)); + env.close(); + + auto sle = env.le(keylet::sponsorship(sponsor, alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(!sle->isFieldPresent(sfFeeAmount)); + BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 100); + + // Re-add FeeAmount, then remove only RemainingOwnerCount; + // FeeAmount remains. + env(sponsor::set_fee(sponsor, 0, XRP(100)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tesSUCCESS)); + env.close(); + + env(sponsor::set_reserve(sponsor, 0, 0), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tesSUCCESS)); env.close(); sle = env.le(keylet::sponsorship(sponsor, alice)); BEAST_EXPECT(sle); BEAST_EXPECT(!sle->isFieldPresent(sfRemainingOwnerCount)); - BEAST_EXPECT(!sle->isFieldPresent(sfFeeAmount)); - BEAST_EXPECT(!sle->isFieldPresent(sfMaxFee)); + BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(100)); } { diff --git a/src/test/rpc/LedgerEntry_test.cpp b/src/test/rpc/LedgerEntry_test.cpp index 875712b060..d321ab39aa 100644 --- a/src/test/rpc/LedgerEntry_test.cpp +++ b/src/test/rpc/LedgerEntry_test.cpp @@ -1899,7 +1899,7 @@ class LedgerEntry_test : public beast::unit_test::Suite Account const bob{"bob"}; env.fund(XRP(10000), alice, bob); env.close(); - env(sponsor::set(alice, 0), sponsor::SponseeAcc(bob)); + env(sponsor::set(alice, 0, 100), sponsor::SponseeAcc(bob)); env.close(); std::string const ledgerHash{to_string(env.closed()->header().hash)}; auto const sponsorshipIndex = to_string(keylet::sponsorship(alice.id(), bob.id()).key);