This commit is contained in:
Mayukha Vadari
2026-07-15 12:14:26 -04:00
parent a39dadc5ac
commit 7d213f2238

View File

@@ -5987,40 +5987,59 @@ public:
BEAST_EXPECT(
env.le(keylet::sponsorship(sponsor, alice))->getFieldU32(sfRemainingOwnerCount) == 1);
// Exhaust the budget (a zero update makes the field absent).
env(sponsor::set_reserve(sponsor, 0, 0), sponsor::SponseeAcc(alice));
env.close();
BEAST_EXPECT(
!env.le(keylet::sponsorship(sponsor, alice))->isFieldPresent(sfRemainingOwnerCount));
// A second unsponsored check.
auto const seq2 = env.seq(alice);
env(check::create(alice, bob, XRP(1)));
env.close();
auto const checkId2 = keylet::check(alice, seq2).key;
// A second co-signed transfer exhausts the budget 1 -> 0 (a
// SponsorshipSet update to an all-zero-budget object is itself
// rejected by SponsorshipSet::preclaim's hasSponsorshipBudget check,
// so the only way to reach a zero RemainingOwnerCount here is by
// spending it down via SponsorshipTransfer).
env(sponsor::transfer(alice, tfSponsorshipCreate, checkId2),
sponsor::As(sponsor, spfSponsorReserve),
Sig(sfSponsorSignature, sponsor));
env.close();
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 2);
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 2);
BEAST_EXPECT(
env.le(keylet::sponsorship(sponsor, alice))->getFieldU32(sfRemainingOwnerCount) == 0);
// A third unsponsored check.
auto const seq3 = env.seq(alice);
env(check::create(alice, bob, XRP(1)));
env.close();
auto const checkId3 = keylet::check(alice, seq3).key;
// With the budget exhausted, the same co-signed transfer fails in
// checkReserve (RemainingOwnerCount 0 < ownerCountDelta 1) with
// tecINSUFFICIENT_RESERVE, even though the sponsor's own balance is
// ample: an existing Sponsorship object's budget always bounds
// reserve sponsorship for that sponsor/sponsee pair.
env(sponsor::transfer(alice, tfSponsorshipCreate, checkId2),
env(sponsor::transfer(alice, tfSponsorshipCreate, checkId3),
sponsor::As(sponsor, spfSponsorReserve),
Sig(sfSponsorSignature, sponsor),
Ter(tecINSUFFICIENT_RESERVE));
env.close();
BEAST_EXPECT(!env.le(keylet::check(alice, seq2))->isFieldPresent(sfSponsor));
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1);
BEAST_EXPECT(!env.le(keylet::check(alice, seq3))->isFieldPresent(sfSponsor));
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 2);
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 2);
}
void
testZeroValueUpdateOnAbsentFields()
{
// Updating a Sponsorship with all-zero values when the optional fields
// are already absent must succeed: STObject::makeFieldAbsent
// early-returns on fields that are already not present.
// Updating a Sponsorship with all-zero FeeAmount/MaxFee when those
// optional fields are already absent must succeed: STObject::
// makeFieldAbsent early-returns on fields that are already not
// present. A positive RemainingOwnerCount keeps the object clear of
// SponsorshipSet::preclaim's hasSponsorshipBudget invariant, which
// rejects any create/update leaving both FeeAmount and
// RemainingOwnerCount non-positive.
testcase("Zero-value SponsorshipSet update on absent fields");
using namespace test::jtx;
@@ -6032,22 +6051,24 @@ public:
auto const baseFee = env.current()->fees().base;
// Create a minimal sponsorship: all-zero values leave FeeAmount,
// MaxFee, and RemainingOwnerCount absent.
env(sponsor::set(sponsor, 0, 0, XRP(0), XRP(0)), sponsor::SponseeAcc(alice));
// Create a minimal sponsorship: a positive reserve budget, with
// all-zero FeeAmount/MaxFee left absent.
env(sponsor::set(sponsor, 0, 5, XRP(0), XRP(0)), sponsor::SponseeAcc(alice));
env.close();
auto sle = env.le(keylet::sponsorship(sponsor, alice));
BEAST_EXPECT(sle);
if (!sle)
return;
BEAST_EXPECT(!sle->isFieldPresent(sfFeeAmount));
BEAST_EXPECT(!sle->isFieldPresent(sfMaxFee));
BEAST_EXPECT(!sle->isFieldPresent(sfRemainingOwnerCount));
BEAST_EXPECT(sle->getFieldU32(sfRemainingOwnerCount) == 5);
// Submit the identical all-zero update again: fields are already
// absent, so each makeFieldAbsent call is a no-op and the update
// succeeds.
// Submit the identical all-zero FeeAmount/MaxFee update again (with
// the same reserve budget): those fields are already absent, so each
// makeFieldAbsent call is a no-op and the update succeeds.
auto const sponsorBalance = env.balance(sponsor);
env(sponsor::set(sponsor, 0, 0, XRP(0), XRP(0)),
env(sponsor::set(sponsor, 0, 5, XRP(0), XRP(0)),
sponsor::SponseeAcc(alice),
Fee(baseFee),
Ter(tesSUCCESS));
@@ -6055,9 +6076,11 @@ public:
sle = env.le(keylet::sponsorship(sponsor, alice));
BEAST_EXPECT(sle);
if (!sle)
return;
BEAST_EXPECT(!sle->isFieldPresent(sfFeeAmount));
BEAST_EXPECT(!sle->isFieldPresent(sfMaxFee));
BEAST_EXPECT(!sle->isFieldPresent(sfRemainingOwnerCount));
BEAST_EXPECT(sle->getFieldU32(sfRemainingOwnerCount) == 5);
// Only the fee was charged.
BEAST_EXPECT(env.balance(sponsor) == sponsorBalance - drops(baseFee));
}