@@ -5987,40 +5987,59 @@ class Sponsor_test : public beast::unit_test::Suite
59875987 BEAST_EXPECT (
59885988 env.le (keylet::sponsorship (sponsor, alice))->getFieldU32 (sfRemainingOwnerCount) == 1 );
59895989
5990- // Exhaust the budget (a zero update makes the field absent).
5991- env (sponsor::set_reserve (sponsor, 0 , 0 ), sponsor::SponseeAcc (alice));
5992- env.close ();
5993- BEAST_EXPECT (
5994- !env.le (keylet::sponsorship (sponsor, alice))->isFieldPresent (sfRemainingOwnerCount));
5995-
59965990 // A second unsponsored check.
59975991 auto const seq2 = env.seq (alice);
59985992 env (check::create (alice, bob, XRP (1 )));
59995993 env.close ();
60005994 auto const checkId2 = keylet::check (alice, seq2).key ;
60015995
5996+ // A second co-signed transfer exhausts the budget 1 -> 0 (a
5997+ // SponsorshipSet update to an all-zero-budget object is itself
5998+ // rejected by SponsorshipSet::preclaim's hasSponsorshipBudget check,
5999+ // so the only way to reach a zero RemainingOwnerCount here is by
6000+ // spending it down via SponsorshipTransfer).
6001+ env (sponsor::transfer (alice, tfSponsorshipCreate, checkId2),
6002+ sponsor::As (sponsor, spfSponsorReserve),
6003+ Sig (sfSponsorSignature, sponsor));
6004+ env.close ();
6005+
6006+ BEAST_EXPECT (sponsoredOwnerCount (env, alice) == 2 );
6007+ BEAST_EXPECT (sponsoringOwnerCount (env, sponsor) == 2 );
6008+ BEAST_EXPECT (
6009+ env.le (keylet::sponsorship (sponsor, alice))->getFieldU32 (sfRemainingOwnerCount) == 0 );
6010+
6011+ // A third unsponsored check.
6012+ auto const seq3 = env.seq (alice);
6013+ env (check::create (alice, bob, XRP (1 )));
6014+ env.close ();
6015+ auto const checkId3 = keylet::check (alice, seq3).key ;
6016+
60026017 // With the budget exhausted, the same co-signed transfer fails in
60036018 // checkReserve (RemainingOwnerCount 0 < ownerCountDelta 1) with
60046019 // tecINSUFFICIENT_RESERVE, even though the sponsor's own balance is
60056020 // ample: an existing Sponsorship object's budget always bounds
60066021 // reserve sponsorship for that sponsor/sponsee pair.
6007- env (sponsor::transfer (alice, tfSponsorshipCreate, checkId2 ),
6022+ env (sponsor::transfer (alice, tfSponsorshipCreate, checkId3 ),
60086023 sponsor::As (sponsor, spfSponsorReserve),
60096024 Sig (sfSponsorSignature, sponsor),
60106025 Ter (tecINSUFFICIENT_RESERVE));
60116026 env.close ();
60126027
6013- BEAST_EXPECT (!env.le (keylet::check (alice, seq2 ))->isFieldPresent (sfSponsor));
6014- BEAST_EXPECT (sponsoredOwnerCount (env, alice) == 1 );
6015- BEAST_EXPECT (sponsoringOwnerCount (env, sponsor) == 1 );
6028+ BEAST_EXPECT (!env.le (keylet::check (alice, seq3 ))->isFieldPresent (sfSponsor));
6029+ BEAST_EXPECT (sponsoredOwnerCount (env, alice) == 2 );
6030+ BEAST_EXPECT (sponsoringOwnerCount (env, sponsor) == 2 );
60166031 }
60176032
60186033 void
60196034 testZeroValueUpdateOnAbsentFields ()
60206035 {
6021- // Updating a Sponsorship with all-zero values when the optional fields
6022- // are already absent must succeed: STObject::makeFieldAbsent
6023- // early-returns on fields that are already not present.
6036+ // Updating a Sponsorship with all-zero FeeAmount/MaxFee when those
6037+ // optional fields are already absent must succeed: STObject::
6038+ // makeFieldAbsent early-returns on fields that are already not
6039+ // present. A positive RemainingOwnerCount keeps the object clear of
6040+ // SponsorshipSet::preclaim's hasSponsorshipBudget invariant, which
6041+ // rejects any create/update leaving both FeeAmount and
6042+ // RemainingOwnerCount non-positive.
60246043 testcase (" Zero-value SponsorshipSet update on absent fields" );
60256044 using namespace test ::jtx;
60266045
@@ -6032,32 +6051,36 @@ class Sponsor_test : public beast::unit_test::Suite
60326051
60336052 auto const baseFee = env.current ()->fees ().base ;
60346053
6035- // Create a minimal sponsorship: all-zero values leave FeeAmount,
6036- // MaxFee, and RemainingOwnerCount absent.
6037- env (sponsor::set (sponsor, 0 , 0 , XRP (0 ), XRP (0 )), sponsor::SponseeAcc (alice));
6054+ // Create a minimal sponsorship: a positive reserve budget, with
6055+ // all-zero FeeAmount/ MaxFee left absent.
6056+ env (sponsor::set (sponsor, 0 , 5 , XRP (0 ), XRP (0 )), sponsor::SponseeAcc (alice));
60386057 env.close ();
60396058
60406059 auto sle = env.le (keylet::sponsorship (sponsor, alice));
60416060 BEAST_EXPECT (sle);
6061+ if (!sle)
6062+ return ;
60426063 BEAST_EXPECT (!sle->isFieldPresent (sfFeeAmount));
60436064 BEAST_EXPECT (!sle->isFieldPresent (sfMaxFee));
6044- BEAST_EXPECT (! sle->isFieldPresent (sfRemainingOwnerCount));
6065+ BEAST_EXPECT (sle->getFieldU32 (sfRemainingOwnerCount) == 5 );
60456066
6046- // Submit the identical all-zero update again: fields are already
6047- // absent, so each makeFieldAbsent call is a no-op and the update
6048- // succeeds.
6067+ // Submit the identical all-zero FeeAmount/MaxFee update again (with
6068+ // the same reserve budget): those fields are already absent, so each
6069+ // makeFieldAbsent call is a no-op and the update succeeds.
60496070 auto const sponsorBalance = env.balance (sponsor);
6050- env (sponsor::set (sponsor, 0 , 0 , XRP (0 ), XRP (0 )),
6071+ env (sponsor::set (sponsor, 0 , 5 , XRP (0 ), XRP (0 )),
60516072 sponsor::SponseeAcc (alice),
60526073 Fee (baseFee),
60536074 Ter (tesSUCCESS));
60546075 env.close ();
60556076
60566077 sle = env.le (keylet::sponsorship (sponsor, alice));
60576078 BEAST_EXPECT (sle);
6079+ if (!sle)
6080+ return ;
60586081 BEAST_EXPECT (!sle->isFieldPresent (sfFeeAmount));
60596082 BEAST_EXPECT (!sle->isFieldPresent (sfMaxFee));
6060- BEAST_EXPECT (! sle->isFieldPresent (sfRemainingOwnerCount));
6083+ BEAST_EXPECT (sle->getFieldU32 (sfRemainingOwnerCount) == 5 );
60616084 // Only the fee was charged.
60626085 BEAST_EXPECT (env.balance (sponsor) == sponsorBalance - drops (baseFee));
60636086 }
0 commit comments