fix: SponsorshipSet cannot create empty ltSPONSORSHIP objects (#7788)

This commit is contained in:
Kassaking7
2026-07-10 16:35:12 -04:00
committed by GitHub
parent a5ac1d4944
commit 710af21ce9
3 changed files with 89 additions and 16 deletions

View File

@@ -19,9 +19,29 @@
#include <cstdint>
#include <memory>
#include <optional>
namespace xrpl {
static bool
hasSponsorshipBudget(
SLE::const_ref sponsorshipSle,
std::optional<STAmount> const& feeAmount,
std::optional<std::uint32_t> 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;
}

View File

@@ -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));
}
{

View File

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