Sponsorship should be non-obligated for sponsee (#7552)

This commit is contained in:
yinyiqian1
2026-06-17 14:08:06 -04:00
committed by GitHub
parent 4fc781ef93
commit 14986cc043
4 changed files with 145 additions and 37 deletions

View File

@@ -25,6 +25,9 @@ public:
static TER
preclaim(PreclaimContext const& ctx);
static TER
deleteSponsorship(ApplyView& view, SLE::ref sle, beast::Journal j);
TER
doApply() override;

View File

@@ -1,7 +1,10 @@
#include <xrpl/tx/transactors/sponsor/SponsorshipSet.h>
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/DelegateHelpers.h>
@@ -175,6 +178,47 @@ SponsorshipSet::preclaim(PreclaimContext const& ctx)
return tesSUCCESS;
}
TER
SponsorshipSet::deleteSponsorship(ApplyView& view, SLE::ref sle, beast::Journal j)
{
if (!sle)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const sponsorAccountID = (*sle)[sfOwner];
auto const sponseeAccountID = (*sle)[sfSponsee];
// The reserve for the Sponsorship object is held by the sponsor (Owner).
auto sponsorAccSle = view.peek(keylet::account(sponsorAccountID));
if (!sponsorAccSle)
return tecINTERNAL; // LCOV_EXCL_LINE
if (!view.dirRemove(keylet::ownerDir(sponsorAccountID), (*sle)[sfOwnerNode], sle->key(), false))
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Unable to delete Sponsorship from sponsor.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
if (!view.dirRemove(
keylet::ownerDir(sponseeAccountID), (*sle)[sfSponseeNode], sle->key(), false))
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Unable to delete Sponsorship from sponsee.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
adjustOwnerCountObj(view, sponsorAccSle, sle, -1, j);
// transfer feeAmount back to the sponsor
if (sle->isFieldPresent(sfFeeAmount))
(*sponsorAccSle)[sfBalance] += sle->getFieldAmount(sfFeeAmount);
view.erase(sle);
return tesSUCCESS;
}
TER
SponsorshipSet::doApply()
{
@@ -200,29 +244,7 @@ SponsorshipSet::doApply()
if (!sponsorObjSle)
return tecINTERNAL; // LCOV_EXCL_LINE
adjustOwnerCountObj(ctx_.view(), sponsorAccSle, sponsorObjSle, -1, ctx_.journal);
ctx_.view().dirRemove(
keylet::ownerDir(sponsorAccountID),
(*sponsorObjSle)[sfOwnerNode],
sponsorObjSle->key(),
false);
ctx_.view().dirRemove(
keylet::ownerDir(sponseeAccountID),
(*sponsorObjSle)[sfSponseeNode],
sponsorObjSle->key(),
false);
// transfer feeAmount from ledger entry
if (sponsorObjSle->isFieldPresent(sfFeeAmount))
{
auto const feeAmount = sponsorObjSle->getFieldAmount(sfFeeAmount);
(*sponsorAccSle)[sfBalance] += feeAmount;
}
ctx_.view().erase(sponsorObjSle);
return tesSUCCESS;
return deleteSponsorship(ctx_.view(), sponsorObjSle, ctx_.journal);
}
auto const feeAmount = ctx_.tx[~sfFeeAmount];

View File

@@ -30,6 +30,7 @@
#include <xrpl/tx/transactors/did/DIDDelete.h>
#include <xrpl/tx/transactors/oracle/OracleDelete.h>
#include <xrpl/tx/transactors/payment/DepositPreauth.h>
#include <xrpl/tx/transactors/sponsor/SponsorshipSet.h>
#include <cstdint>
#include <utility>
@@ -185,11 +186,23 @@ removeDelegateFromLedger(
return DelegateSet::deleteDelegate(view, sleDel, j);
}
// Return nullptr if the LedgerEntryType represents an obligation that can't
// be deleted. Otherwise return the pointer to the function that can delete
// the non-obligation
TER
removeSponsorshipFromLedger(
ServiceRegistry&,
ApplyView& view,
AccountID const&,
uint256 const&,
SLE::ref sleDel,
beast::Journal j)
{
return SponsorshipSet::deleteSponsorship(view, sleDel, j);
}
// Return nullptr if the object represents an obligation that can't be deleted
// during deletion of account. Otherwise return the pointer to the function
// that can delete the non-obligation.
DeleterFuncPtr
nonObligationDeleter(LedgerEntryType t)
nonObligationDeleter(LedgerEntryType t, SLE::const_ref sleItem, AccountID const& account)
{
switch (t)
{
@@ -211,6 +224,12 @@ nonObligationDeleter(LedgerEntryType t)
return removeCredentialFromLedger;
case ltDELEGATE:
return removeDelegateFromLedger;
case ltSPONSORSHIP:
// A Sponsorship lives in both the sponsor's (Owner) and the
// sponsee's owner directories, but it is an obligation only for the
// sponsor, who holds its reserve. The sponsee must remain free to
// delete its account.
return (*sleItem)[sfOwner] == account ? nullptr : removeSponsorshipFromLedger;
default:
return nullptr;
}
@@ -335,7 +354,7 @@ AccountDelete::preclaim(PreclaimContext const& ctx)
LedgerEntryType const nodeType{safeCast<LedgerEntryType>((*sleItem)[sfLedgerEntryType])};
if (nonObligationDeleter(nodeType) == nullptr)
if (nonObligationDeleter(nodeType, sleItem, account) == nullptr)
return tecHAS_OBLIGATIONS;
// We found a deletable directory entry. Count it. If we find too
@@ -376,7 +395,7 @@ AccountDelete::doApply()
[&](LedgerEntryType nodeType,
uint256 const& dirEntry,
SLE::pointer& sleItem) -> std::pair<TER, SkipEntry> {
if (auto deleter = nonObligationDeleter(nodeType))
if (auto deleter = nonObligationDeleter(nodeType, sleItem, accountID_))
{
TER const result{deleter(ctx_.registry, view(), accountID_, dirEntry, sleItem, j_)};

View File

@@ -5456,7 +5456,8 @@ public:
Account const sponsor("sponsor");
{
// Delete Sponsor/Sponsee Account with ltSponsorship (tecHAS_OBLIGATIONS)
// A Sponsorship object blocks deletion of the sponsor,
// but NOT of the sponsee.
Env env{*this, testableAmendments()};
env.fund(XRP(1000000), alice, bob, sponsor);
env.close();
@@ -5467,20 +5468,83 @@ public:
Ter(tesSUCCESS));
env.close();
incLgrSeqForAccDel(env, sponsor);
auto const keylet = keylet::sponsor(sponsor, alice);
auto const sponsorObj = env.le(keylet);
BEAST_EXPECT(sponsorObj);
BEAST_EXPECT(env.le(keylet));
// sponsor pays its own reserve here, so there is no SponsoredOwnerCount.
BEAST_EXPECT(ownerCount(env, sponsor) == 1);
BEAST_EXPECT(ownerCount(env, alice) == 0);
BEAST_EXPECT(sponsoredOwnerCount(env, sponsor) == 0);
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
// AccountDelete
// sponsor's sequence is the higher one, so a single call readies
// both accounts for deletion
incLgrSeqForAccDel(env, sponsor);
auto const requiredFee = drops(env.current()->fees().increment);
env(acctdelete(alice, bob), Fee(requiredFee), Ter(tecHAS_OBLIGATIONS));
// sponsor cannot be deleted while the Sponsorship exists
env(acctdelete(sponsor, bob), Fee(requiredFee), Ter(tecHAS_OBLIGATIONS));
env.close();
// sponsee can be deleted
auto const sponsorBalBefore = env.balance(sponsor);
env(acctdelete(alice, bob), Fee(requiredFee), Ter(tesSUCCESS));
env.close();
BEAST_EXPECT(!env.le(keylet));
BEAST_EXPECT(!env.le(keylet::account(alice)));
// deleting sponsee makes those counts zero.
BEAST_EXPECT(ownerCount(env, sponsor) == 0);
BEAST_EXPECT(sponsoredOwnerCount(env, sponsor) == 0);
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
// FeeAmount is returned to the sponsor
BEAST_EXPECT(env.balance(sponsor) == sponsorBalBefore + XRP(100));
}
{
// Delete SponsoredAccount
// The SponsorshipSet transaction itself
// is sponsored by a 3rd counterparty. Test deleting sponsee.
Env env{*this, testableAmendments()};
Account const counterparty("counterparty");
env.fund(XRP(1000000), alice, bob, sponsor, counterparty);
env.close();
// sponsor creates a Sponsorship for alice; the SponsorshipSet tx is
// itself reserve-sponsored by counterparty, so the object's reserve is counterparty's.
env(sponsor::set(sponsor, 0, 100, XRP(100)),
sponsor::SponseeAcc(alice),
sponsor::As(counterparty, spfSponsorReserve),
Sig(sfSponsorSignature, counterparty),
Ter(tesSUCCESS));
env.close();
auto const keylet = keylet::sponsor(sponsor, alice);
auto const obj = env.le(keylet);
BEAST_EXPECT(obj);
BEAST_EXPECT(
obj->isFieldPresent(sfSponsor) &&
obj->getAccountID(sfSponsor) == counterparty.id());
// sponsor owns the object; its reserve is sponsored by counterparty.
BEAST_EXPECT(ownerCount(env, sponsor) == 1);
BEAST_EXPECT(sponsoredOwnerCount(env, sponsor) == 1);
BEAST_EXPECT(sponsoringOwnerCount(env, counterparty) == 1);
auto const sponsorBalBefore = env.balance(sponsor);
incLgrSeqForAccDel(env, alice);
auto const requiredFee = drops(env.current()->fees().increment);
env(acctdelete(alice, bob), Fee(requiredFee), Ter(tesSUCCESS));
env.close();
BEAST_EXPECT(!env.le(keylet));
// deleting sponsee makes those counts zero.
BEAST_EXPECT(ownerCount(env, sponsor) == 0);
BEAST_EXPECT(sponsoredOwnerCount(env, sponsor) == 0);
BEAST_EXPECT(sponsoringOwnerCount(env, counterparty) == 0);
BEAST_EXPECT(env.balance(sponsor) == sponsorBalBefore + XRP(100));
}
{
// Deleting SponsoredAccount, whose account reserve is paid by a sponsor.
Env env{*this, testableAmendments()};
env.memoize(alice);
env.fund(XRP(1000000), bob, sponsor);