diff --git a/src/libxrpl/tx/transactors/sponsor/SponsorshipTransfer.cpp b/src/libxrpl/tx/transactors/sponsor/SponsorshipTransfer.cpp index 0e036649fd..c3131714f8 100644 --- a/src/libxrpl/tx/transactors/sponsor/SponsorshipTransfer.cpp +++ b/src/libxrpl/tx/transactors/sponsor/SponsorshipTransfer.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -412,9 +413,24 @@ SponsorshipTransfer::doApply() if (!oldSponsorSle) return tefINTERNAL; // LCOV_EXCL_LINE - // The owner reclaims the reserve burden when the object is no longer sponsored. - // We do not check the sponsee's reserve here (via `checkReserve`) so that a sponsor can - // always end a sponsorship, even if the sponsee lacks sufficient reserve. + // The owner reclaims the reserve burden when the object is no longer + // sponsored, so it must be able to hold that reserve on its own once the + // sponsorship is removed. This mirrors the account-level End check below, + // keeping the behavior consistent across accounts and objects: a + // sponsorship can only be ended if the sponsee self-funds, another sponsor + // steps in (Reassign), or the object/account is deleted. + if (view().rules().enabled(fixCleanup3_4_0)) + { + if (auto const ter = checkReserve( + ctx_.getApplyViewContext(), + sponseeSle, + balanceBeforeFee(sponseeSle), + SLE::pointer(), + {.ownerCountDelta = ownerCountDelta}, + ctx_.journal); + !isTesSuccess(ter)) + return ter; + } // Decrement sponsored count if (auto const ter = decrementSponsorCount( diff --git a/src/test/app/Sponsor_test.cpp b/src/test/app/Sponsor_test.cpp index bcd31bc6a0..a1a9f80a11 100644 --- a/src/test/app/Sponsor_test.cpp +++ b/src/test/app/Sponsor_test.cpp @@ -1073,14 +1073,17 @@ public: } void - testTransferSponsor() + testTransferSponsor(FeatureBitset features) { - testcase("Transfer Sponsor"); + testcase( + std::string("Transfer Sponsor ") + + (features[fixCleanup3_4_0] ? "(fixCleanup3_4_0 enabled)" + : "(fixCleanup3_4_0 disabled)")); using namespace test::jtx; // Verify preflight checks { - Env env{*this, testableAmendments()}; + Env env{*this, features}; Account const alice("alice"); Account const bob("bob"); Account const sponsor("sponsor"); @@ -1164,7 +1167,7 @@ public: { // Invalid SponsorshipEnd permission (sponsor object/sponsor account) - Env env{*this, testableAmendments()}; + Env env{*this, features}; Account const alice("alice"); Account const bob("bob"); Account const charlie("charlie"); @@ -1209,7 +1212,7 @@ public: { // sponsor account - Env env{*this, testableAmendments()}; + Env env{*this, features}; Account const alice("alice"); Account const bob("bob"); Account const sponsor1("sponsor1"); @@ -1340,7 +1343,7 @@ public: } { // dissolve account sponsorship from sponsor - Env env{*this, testableAmendments()}; + Env env{*this, features}; Account const alice("alice"); Account const bob("bob"); Account const sponsor("sponsor"); @@ -1364,7 +1367,7 @@ public: { // sponsor object (co-signing) - Env env{*this, testableAmendments()}; + Env env{*this, features}; Account const alice("alice"); Account const bob("bob"); Account const sponsor1("sponsor1"); @@ -1473,10 +1476,20 @@ public: BEAST_EXPECT(sle2->isFieldPresent(sfSponsor)); BEAST_EXPECT(sle2->getAccountID(sfSponsor) == sponsor2.id()); - // dissolve sponsor: ending an object sponsorship succeeds even - // when the sponsee lacks sufficient reserve to reclaim the object. + // dissolve sponsor: ending an object sponsorship now (fixCleanup3_4_0) requires the + // sponsee to be able to self-fund the object's reserve. adjustAccountXRPBalance(env, alice, reserve(env, 1) - drops(1)); + if (features[fixCleanup3_4_0]) + { + // Under-funded: End is rejected until alice can self-fund. + env(sponsor::transfer(alice, tfSponsorshipEnd, checkId), + Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + + adjustAccountXRPBalance(env, alice, reserve(env, 1)); + } + env(sponsor::transfer(alice, tfSponsorshipEnd, checkId)); env.close(); @@ -1509,7 +1522,7 @@ public: } { // sponsor object (pre-funded + no ltSponsorship entry) - Env env{*this, testableAmendments()}; + Env env{*this, features}; Account const alice("alice"); Account const bob("bob"); Account const sponsor1("sponsor1"); @@ -1543,7 +1556,7 @@ public: } { // sponsor object (pre-funded) - Env env{*this, testableAmendments()}; + Env env{*this, features}; Account const alice("alice"); Account const bob("bob"); Account const sponsor1("sponsor1"); @@ -1646,7 +1659,7 @@ public: { // Dissolve object sponsorship from sponsor(no-ltSponsorship) - Env env{*this, testableAmendments()}; + Env env{*this, features}; Account const alice("alice"); Account const bob("bob"); Account const sponsor("sponsor"); @@ -1686,7 +1699,7 @@ public: { // Dissolve object sponsorship from sponsor (with ltSponsorship) - Env env{*this, testableAmendments()}; + Env env{*this, features}; Account const alice("alice"); Account const bob("bob"); Account const sponsor("sponsor"); @@ -1744,7 +1757,7 @@ public: for (bool const isIssuerHigh : {false, true}) { - Env env{*this, testableAmendments()}; + Env env{*this, features}; env.fund(XRP(10000), alice, bob, sponsor); env.close(); @@ -1788,7 +1801,7 @@ public: { // invalid transfer - Env env{*this, testableAmendments()}; + Env env{*this, features}; Account const alice("alice"); Account const bob("bob"); Account const sponsor("sponsor"); @@ -1825,7 +1838,7 @@ public: { // existing owner objects that are outside the v1 SponsorshipTransfer // object allow-list - Env env{*this, testableAmendments()}; + Env env{*this, features}; Account const alice("alice"); Account const sponsor("sponsor"); env.fund(XRP(10000), alice, sponsor); @@ -5671,7 +5684,8 @@ protected: testPreFundAndCosign(); testSponsoredFreeTierReserve(); - testTransferSponsor(); + testTransferSponsor(jtx::testableAmendments()); + testTransferSponsor(jtx::testableAmendments() - fixCleanup3_4_0); testLegacySignerListReserve(); testSponsorFee(); testSponsorAccount();