mirror of
https://github.com/XRPLF/rippled.git
synced 2026-10-10 21:58:03 +00:00
Compare commits
1 Commits
nb-baseuin
...
dangell7/s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d74e0af3b |
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
@@ -201,4 +202,16 @@ getLedgerEntryOwnerCount(SLE const& sle);
|
||||
SF_ACCOUNT const&
|
||||
getLedgerEntrySponsorField(SLE const& sle, AccountID const& owner);
|
||||
|
||||
/**
|
||||
* Delete a Sponsorship (ltSPONSORSHIP) ledger object cleanly.
|
||||
*
|
||||
* Unlinks the object from both the sponsor's and sponsee's owner directories,
|
||||
* releases the sponsor's owner reserve, refunds any prefunded sfFeeAmount to
|
||||
* the sponsor, and erases the object. Shared by SponsorshipSet (tfDeleteObject)
|
||||
* and AccountDelete's non-obligation deleter so a sponsored or sponsoring
|
||||
* account can be deleted without its Sponsorship object blocking it.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
deleteSponsorshipObject(ApplyView& view, SLE::ref sle, beast::Journal j);
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -230,7 +230,8 @@ inline constexpr FlagValue tfUniversalMask = ~tfUniversal;
|
||||
TRANSACTION(SponsorshipTransfer, \
|
||||
TF_FLAG(tfSponsorshipEnd, 0x00010000) \
|
||||
TF_FLAG(tfSponsorshipCreate, 0x00020000) \
|
||||
TF_FLAG(tfSponsorshipReassign, 0x00040000), \
|
||||
TF_FLAG(tfSponsorshipReassign, 0x00040000) \
|
||||
TF_FLAG(tfSponsorshipReap, 0x00080000), \
|
||||
MASK_ADJ(0))
|
||||
|
||||
// clang-format on
|
||||
|
||||
@@ -1172,7 +1172,7 @@ TRANSACTION(ttCONFIDENTIAL_MPT_CLAWBACK, 89, ConfidentialMPTClawback,
|
||||
TRANSACTION(ttSPONSORSHIP_TRANSFER, 90, SponsorshipTransfer,
|
||||
Delegation::NotDelegable,
|
||||
featureSponsor,
|
||||
NoPriv,
|
||||
MayDeleteAcct,
|
||||
({
|
||||
{sfObjectID, SoeOptional},
|
||||
{sfSponsee, SoeOptional},
|
||||
|
||||
@@ -21,7 +21,7 @@ class SponsorshipTransferBuilder;
|
||||
* Type: ttSPONSORSHIP_TRANSFER (90)
|
||||
* Delegable: Delegation::NotDelegable
|
||||
* Amendment: featureSponsor
|
||||
* Privileges: NoPriv
|
||||
* Privileges: MayDeleteAcct
|
||||
*
|
||||
* Immutable wrapper around STTx providing type-safe field access.
|
||||
* Use SponsorshipTransferBuilder to construct new transactions.
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
@@ -11,6 +13,37 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
* Check that an account satisfies the preconditions for deletion.
|
||||
*
|
||||
* Verifies there are no NFT obligations, the account is not sponsoring others,
|
||||
* the account's sequence is outside the 256-ledger replay window, and every
|
||||
* object it owns is a non-obligation deletable type. When the account carries a
|
||||
* reserve sponsor (sfSponsor), @p dst must be that sponsor.
|
||||
*
|
||||
* Shared by AccountDelete::preclaim and the SponsorshipTransfer reap path so
|
||||
* both enforce identical deletion safety.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
checkAccountDeletable(
|
||||
ReadView const& view,
|
||||
SLE::const_ref sleAccount,
|
||||
AccountID const& dst,
|
||||
beast::Journal j);
|
||||
|
||||
/**
|
||||
* Delete @p src, sweeping its residual XRP to @p dst.
|
||||
*
|
||||
* Tears down every non-obligation object @p src owns, transfers its remaining
|
||||
* balance to @p dst, unwinds any reserve sponsorship (decrementing the
|
||||
* sponsor's sfSponsoringAccountCount and clearing sfSponsor), removes the owner
|
||||
* directory, and erases the account root. Callers must have already confirmed
|
||||
* the account is deletable via checkAccountDeletable. Shared by
|
||||
* AccountDelete::doApply and the SponsorshipTransfer reap path.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
applyAccountDelete(ApplyContext& ctx, SLE::pointer src, SLE::pointer dst, beast::Journal j);
|
||||
|
||||
class AccountDelete : public Transactor
|
||||
{
|
||||
public:
|
||||
|
||||
@@ -25,6 +25,9 @@ public:
|
||||
static std::uint32_t
|
||||
getFlagsMask(PreflightContext const& ctx);
|
||||
|
||||
static XRPAmount
|
||||
calculateBaseFee(ReadView const& view, STTx const& tx);
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <xrpl/ledger/helpers/SponsorHelpers.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
@@ -342,4 +343,48 @@ getLedgerEntrySponsorField(SLE const& sle, AccountID const& owner)
|
||||
}
|
||||
}
|
||||
|
||||
TER
|
||||
deleteSponsorshipObject(ApplyView& view, SLE::ref sle, beast::Journal j)
|
||||
{
|
||||
if (!sle)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const sponsorID = (*sle)[sfOwner];
|
||||
auto const sponseeID = (*sle)[sfSponsee];
|
||||
|
||||
// The sponsor owns the Sponsorship object, so deletion releases the
|
||||
// sponsor's owner reserve.
|
||||
auto sponsorAccSle = view.peek(keylet::account(sponsorID));
|
||||
if (!sponsorAccSle)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
if (!view.dirRemove(keylet::ownerDir(sponsorID), (*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(sponseeID), (*sle)[sfSponseeNode], sle->key(), false))
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.fatal()) << "Unable to delete Sponsorship from sponsee.";
|
||||
return tefBAD_LEDGER;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
decreaseOwnerCountForObject(view, sponsorAccSle, sle, 1, j);
|
||||
|
||||
// Return any prefunded fee amount to the sponsor before erasing the object.
|
||||
if (sle->isFieldPresent(sfFeeAmount))
|
||||
{
|
||||
(*sponsorAccSle)[sfBalance] += sle->getFieldAmount(sfFeeAmount);
|
||||
view.update(sponsorAccSle);
|
||||
}
|
||||
|
||||
view.erase(sle);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
|
||||
#include <xrpl/ledger/helpers/NFTokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/OfferHelpers.h>
|
||||
#include <xrpl/ledger/helpers/SponsorHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
@@ -185,6 +186,18 @@ removeDelegateFromLedger(
|
||||
return DelegateSet::deleteDelegate(view, sleDel, j);
|
||||
}
|
||||
|
||||
TER
|
||||
removeSponsorshipFromLedger(
|
||||
ServiceRegistry&,
|
||||
ApplyView& view,
|
||||
AccountID const&,
|
||||
uint256 const&,
|
||||
SLE::ref sleDel,
|
||||
beast::Journal j)
|
||||
{
|
||||
return deleteSponsorshipObject(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
|
||||
@@ -211,6 +224,8 @@ nonObligationDeleter(LedgerEntryType t)
|
||||
return removeCredentialFromLedger;
|
||||
case ltDELEGATE:
|
||||
return removeDelegateFromLedger;
|
||||
case ltSPONSORSHIP:
|
||||
return removeSponsorshipFromLedger;
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
@@ -218,6 +233,198 @@ nonObligationDeleter(LedgerEntryType t)
|
||||
|
||||
} // namespace
|
||||
|
||||
TER
|
||||
checkAccountDeletable(
|
||||
ReadView const& view,
|
||||
SLE::const_ref sleAccount,
|
||||
AccountID const& dst,
|
||||
beast::Journal j)
|
||||
{
|
||||
AccountID const account = sleAccount->getAccountID(sfAccount);
|
||||
|
||||
// If an issuer has any issued NFTs resident in the ledger then it
|
||||
// cannot be deleted.
|
||||
if ((*sleAccount)[~sfMintedNFTokens] != (*sleAccount)[~sfBurnedNFTokens])
|
||||
return tecHAS_OBLIGATIONS;
|
||||
|
||||
// If the account owns any NFTs it cannot be deleted.
|
||||
Keylet const first = keylet::nftokenPageMin(account);
|
||||
Keylet const last = keylet::nftokenPageMax(account);
|
||||
|
||||
auto const cp =
|
||||
view.read(Keylet(ltNFTOKEN_PAGE, view.succ(first.key, last.key.next()).value_or(last.key)));
|
||||
if (cp)
|
||||
return tecHAS_OBLIGATIONS;
|
||||
|
||||
// A sponsored account may only pay its residual XRP to its own sponsor.
|
||||
if (sleAccount->isFieldPresent(sfSponsor))
|
||||
{
|
||||
if (dst != sleAccount->getAccountID(sfSponsor))
|
||||
return tecNO_SPONSOR_PERMISSION;
|
||||
}
|
||||
// An account that is sponsoring others cannot be deleted.
|
||||
if (sleAccount->isFieldPresent(sfSponsoringOwnerCount) ||
|
||||
sleAccount->isFieldPresent(sfSponsoringAccountCount))
|
||||
return tecHAS_OBLIGATIONS;
|
||||
|
||||
// We don't allow an account to be deleted if its sequence number
|
||||
// is within 256 of the current ledger. This prevents replay of old
|
||||
// transactions if this account is resurrected after it is deleted.
|
||||
//
|
||||
// We look at the account's Sequence rather than the transaction's
|
||||
// Sequence in preparation for Tickets.
|
||||
static constexpr std::uint32_t kSeqDelta{255};
|
||||
if ((*sleAccount)[sfSequence] + kSeqDelta > view.seq())
|
||||
return tecTOO_SOON;
|
||||
|
||||
// We don't allow an account to be deleted if
|
||||
// <FirstNFTokenSequence + MintedNFTokens> is within 256 of the
|
||||
// current ledger. This is to prevent having duplicate NFTokenIDs after
|
||||
// account re-creation.
|
||||
//
|
||||
// Without this restriction, duplicate NFTokenIDs can be reproduced when
|
||||
// authorized minting is involved. Because when the minter mints a NFToken,
|
||||
// the issuer's sequence does not change. So when the issuer re-creates
|
||||
// their account and mints a NFToken, it is possible that the
|
||||
// NFTokenSequence of this NFToken is the same as the one that the
|
||||
// authorized minter minted in a previous ledger.
|
||||
if ((*sleAccount)[~sfFirstNFTokenSequence].value_or(0) +
|
||||
(*sleAccount)[~sfMintedNFTokens].value_or(0) + kSeqDelta >
|
||||
view.seq())
|
||||
return tecTOO_SOON;
|
||||
|
||||
// Verify that the account does not own any objects that would prevent
|
||||
// the account from being deleted.
|
||||
Keylet const ownerDirKeylet{keylet::ownerDir(account)};
|
||||
if (dirIsEmpty(view, ownerDirKeylet))
|
||||
return tesSUCCESS;
|
||||
|
||||
SLE::const_pointer sleDirNode{};
|
||||
unsigned int uDirEntry{0};
|
||||
uint256 dirEntry{beast::kZero};
|
||||
|
||||
// Account has no directory at all. This _should_ have been caught
|
||||
// by the dirIsEmpty() check earlier, but it's okay to catch it here.
|
||||
if (!cdirFirst(view, ownerDirKeylet.key, sleDirNode, uDirEntry, dirEntry))
|
||||
return tesSUCCESS;
|
||||
|
||||
std::uint32_t deletableDirEntryCount{0};
|
||||
do
|
||||
{
|
||||
// Make sure any directory node types that we find are the kind
|
||||
// we can delete.
|
||||
auto sleItem = view.read(keylet::child(dirEntry));
|
||||
if (!sleItem)
|
||||
{
|
||||
// Directory node has an invalid index. Bail out.
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.fatal()) << "AccountDelete: directory node in ledger " << view.seq()
|
||||
<< " has index to object that is missing: " << to_string(dirEntry);
|
||||
return tefBAD_LEDGER;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
LedgerEntryType const nodeType{safeCast<LedgerEntryType>((*sleItem)[sfLedgerEntryType])};
|
||||
|
||||
if (nonObligationDeleter(nodeType) == nullptr)
|
||||
return tecHAS_OBLIGATIONS;
|
||||
|
||||
// We found a deletable directory entry. Count it. If we find too
|
||||
// many deletable directory entries then bail out.
|
||||
if (++deletableDirEntryCount > kMaxDeletableDirEntries)
|
||||
return tefTOO_BIG;
|
||||
|
||||
} while (cdirNext(view, ownerDirKeylet.key, sleDirNode, uDirEntry, dirEntry));
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
applyAccountDelete(ApplyContext& ctx, SLE::pointer src, SLE::pointer dst, beast::Journal j)
|
||||
{
|
||||
auto& view = ctx.view();
|
||||
AccountID const accountID = src->getAccountID(sfAccount);
|
||||
|
||||
Keylet const ownerDirKeylet{keylet::ownerDir(accountID)};
|
||||
auto const ter = cleanupOnAccountDelete(
|
||||
view,
|
||||
ownerDirKeylet,
|
||||
[&](LedgerEntryType nodeType,
|
||||
uint256 const& dirEntry,
|
||||
SLE::pointer& sleItem) -> std::pair<TER, SkipEntry> {
|
||||
if (auto deleter = nonObligationDeleter(nodeType))
|
||||
{
|
||||
TER const result{deleter(ctx.registry, view, accountID, dirEntry, sleItem, j)};
|
||||
|
||||
return {result, SkipEntry::No};
|
||||
}
|
||||
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::applyAccountDelete : undeletable item not found in preclaim");
|
||||
JLOG(j.error()) << "AccountDelete undeletable item not found in preclaim.";
|
||||
return {tecHAS_OBLIGATIONS, SkipEntry::No};
|
||||
// LCOV_EXCL_STOP
|
||||
},
|
||||
j);
|
||||
if (!isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// Transfer any XRP remaining after the fee is paid to the destination:
|
||||
auto const remainingBalance = src->getFieldAmount(sfBalance).xrp();
|
||||
(*dst)[sfBalance] = (*dst)[sfBalance] + remainingBalance;
|
||||
(*src)[sfBalance] = (*src)[sfBalance] - remainingBalance;
|
||||
ctx.deliver(remainingBalance);
|
||||
|
||||
if (src->isFieldPresent(sfSponsor))
|
||||
{
|
||||
auto const sponsorID = src->getAccountID(sfSponsor);
|
||||
auto sponsorSle = view.peek(keylet::account(sponsorID));
|
||||
|
||||
if (!sponsorSle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const sponsoringAccountCount = sponsorSle->getFieldU32(sfSponsoringAccountCount);
|
||||
|
||||
XRPL_ASSERT(
|
||||
sponsoringAccountCount != 0,
|
||||
"xrpl::applyAccountDelete : sponsoring account count is present");
|
||||
if (sponsoringAccountCount == 0)
|
||||
{
|
||||
// sanity check
|
||||
// Since sfSponsoringAccountCount is set to soeDEFAULT, the field will not be
|
||||
// present with a value of 0.
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
}
|
||||
sponsorSle->at(sfSponsoringAccountCount) = sponsoringAccountCount - 1;
|
||||
view.update(sponsorSle);
|
||||
|
||||
// Following line might look redundant, but without it, sfSponsor
|
||||
// would end up remaining in after-ltAccountRoot during the
|
||||
// InvariantCheck.
|
||||
src->makeFieldAbsent(sfSponsor);
|
||||
}
|
||||
|
||||
XRPL_ASSERT(
|
||||
(*src)[sfBalance] == XRPAmount(0), "xrpl::applyAccountDelete : source balance is zero");
|
||||
|
||||
// If there's still an owner directory associated with the source account
|
||||
// delete it.
|
||||
if (view.exists(ownerDirKeylet) && !view.emptyDirDelete(ownerDirKeylet))
|
||||
{
|
||||
JLOG(j.error()) << "AccountDelete cannot delete root dir node of " << toBase58(accountID);
|
||||
return tecHAS_OBLIGATIONS;
|
||||
}
|
||||
|
||||
// Re-arm the password change fee if we can and need to.
|
||||
if (remainingBalance > XRPAmount(0) && dst->isFlag(lsfPasswordSpent))
|
||||
dst->clearFlag(lsfPasswordSpent);
|
||||
|
||||
view.update(dst);
|
||||
view.erase(src);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
AccountDelete::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
@@ -253,99 +460,7 @@ AccountDelete::preclaim(PreclaimContext const& ctx)
|
||||
if (!sleAccount)
|
||||
return terNO_ACCOUNT;
|
||||
|
||||
// If an issuer has any issued NFTs resident in the ledger then it
|
||||
// cannot be deleted.
|
||||
if ((*sleAccount)[~sfMintedNFTokens] != (*sleAccount)[~sfBurnedNFTokens])
|
||||
return tecHAS_OBLIGATIONS;
|
||||
|
||||
// If the account owns any NFTs it cannot be deleted.
|
||||
Keylet const first = keylet::nftokenPageMin(account);
|
||||
Keylet const last = keylet::nftokenPageMax(account);
|
||||
|
||||
auto const cp = ctx.view.read(
|
||||
Keylet(ltNFTOKEN_PAGE, ctx.view.succ(first.key, last.key.next()).value_or(last.key)));
|
||||
if (cp)
|
||||
return tecHAS_OBLIGATIONS;
|
||||
|
||||
if (sleAccount->isFieldPresent(sfSponsor))
|
||||
{
|
||||
if (dst != sleAccount->getAccountID(sfSponsor))
|
||||
return tecNO_SPONSOR_PERMISSION;
|
||||
}
|
||||
if (sleAccount->isFieldPresent(sfSponsoringOwnerCount) ||
|
||||
sleAccount->isFieldPresent(sfSponsoringAccountCount))
|
||||
return tecHAS_OBLIGATIONS;
|
||||
|
||||
// We don't allow an account to be deleted if its sequence number
|
||||
// is within 256 of the current ledger. This prevents replay of old
|
||||
// transactions if this account is resurrected after it is deleted.
|
||||
//
|
||||
// We look at the account's Sequence rather than the transaction's
|
||||
// Sequence in preparation for Tickets.
|
||||
static constexpr std::uint32_t kSeqDelta{255};
|
||||
if ((*sleAccount)[sfSequence] + kSeqDelta > ctx.view.seq())
|
||||
return tecTOO_SOON;
|
||||
|
||||
// We don't allow an account to be deleted if
|
||||
// <FirstNFTokenSequence + MintedNFTokens> is within 256 of the
|
||||
// current ledger. This is to prevent having duplicate NFTokenIDs after
|
||||
// account re-creation.
|
||||
//
|
||||
// Without this restriction, duplicate NFTokenIDs can be reproduced when
|
||||
// authorized minting is involved. Because when the minter mints a NFToken,
|
||||
// the issuer's sequence does not change. So when the issuer re-creates
|
||||
// their account and mints a NFToken, it is possible that the
|
||||
// NFTokenSequence of this NFToken is the same as the one that the
|
||||
// authorized minter minted in a previous ledger.
|
||||
if ((*sleAccount)[~sfFirstNFTokenSequence].value_or(0) +
|
||||
(*sleAccount)[~sfMintedNFTokens].value_or(0) + kSeqDelta >
|
||||
ctx.view.seq())
|
||||
return tecTOO_SOON;
|
||||
|
||||
// Verify that the account does not own any objects that would prevent
|
||||
// the account from being deleted.
|
||||
Keylet const ownerDirKeylet{keylet::ownerDir(account)};
|
||||
if (dirIsEmpty(ctx.view, ownerDirKeylet))
|
||||
return tesSUCCESS;
|
||||
|
||||
SLE::const_pointer sleDirNode{};
|
||||
unsigned int uDirEntry{0};
|
||||
uint256 dirEntry{beast::kZero};
|
||||
|
||||
// Account has no directory at all. This _should_ have been caught
|
||||
// by the dirIsEmpty() check earlier, but it's okay to catch it here.
|
||||
if (!cdirFirst(ctx.view, ownerDirKeylet.key, sleDirNode, uDirEntry, dirEntry))
|
||||
return tesSUCCESS;
|
||||
|
||||
std::uint32_t deletableDirEntryCount{0};
|
||||
do
|
||||
{
|
||||
// Make sure any directory node types that we find are the kind
|
||||
// we can delete.
|
||||
auto sleItem = ctx.view.read(keylet::child(dirEntry));
|
||||
if (!sleItem)
|
||||
{
|
||||
// Directory node has an invalid index. Bail out.
|
||||
// LCOV_EXCL_START
|
||||
JLOG(ctx.j.fatal()) << "AccountDelete: directory node in ledger " << ctx.view.seq()
|
||||
<< " has index to object that is missing: " << to_string(dirEntry);
|
||||
return tefBAD_LEDGER;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
LedgerEntryType const nodeType{safeCast<LedgerEntryType>((*sleItem)[sfLedgerEntryType])};
|
||||
|
||||
if (nonObligationDeleter(nodeType) == nullptr)
|
||||
return tecHAS_OBLIGATIONS;
|
||||
|
||||
// We found a deletable directory entry. Count it. If we find too
|
||||
// many deletable directory entries then bail out.
|
||||
if (++deletableDirEntryCount > kMaxDeletableDirEntries)
|
||||
return tefTOO_BIG;
|
||||
|
||||
} while (cdirNext(ctx.view, ownerDirKeylet.key, sleDirNode, uDirEntry, dirEntry));
|
||||
|
||||
return tesSUCCESS;
|
||||
return checkAccountDeletable(ctx.view, sleAccount, dst, ctx.j);
|
||||
}
|
||||
|
||||
TER
|
||||
@@ -369,87 +484,7 @@ AccountDelete::doApply()
|
||||
return err;
|
||||
}
|
||||
|
||||
Keylet const ownerDirKeylet{keylet::ownerDir(accountID_)};
|
||||
auto const ter = cleanupOnAccountDelete(
|
||||
view(),
|
||||
ownerDirKeylet,
|
||||
[&](LedgerEntryType nodeType,
|
||||
uint256 const& dirEntry,
|
||||
SLE::pointer& sleItem) -> std::pair<TER, SkipEntry> {
|
||||
if (auto deleter = nonObligationDeleter(nodeType))
|
||||
{
|
||||
TER const result{deleter(ctx_.registry, view(), accountID_, dirEntry, sleItem, j_)};
|
||||
|
||||
return {result, SkipEntry::No};
|
||||
}
|
||||
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE(
|
||||
"xrpl::AccountDelete::doApply : undeletable item not found "
|
||||
"in preclaim");
|
||||
JLOG(j_.error()) << "AccountDelete undeletable item not "
|
||||
"found in preclaim.";
|
||||
return {tecHAS_OBLIGATIONS, SkipEntry::No};
|
||||
// LCOV_EXCL_STOP
|
||||
},
|
||||
ctx_.journal);
|
||||
if (!isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// Transfer any XRP remaining after the fee is paid to the destination:
|
||||
auto const remainingBalance = src->getFieldAmount(sfBalance).xrp();
|
||||
(*dst)[sfBalance] = (*dst)[sfBalance] + remainingBalance;
|
||||
(*src)[sfBalance] = (*src)[sfBalance] - remainingBalance;
|
||||
ctx_.deliver(remainingBalance);
|
||||
|
||||
if (src->isFieldPresent(sfSponsor))
|
||||
{
|
||||
auto const sponsorID = src->getAccountID(sfSponsor);
|
||||
auto sponsorSle = view().peek(keylet::account(sponsorID));
|
||||
|
||||
if (!sponsorSle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const sponsoringAccountCount = sponsorSle->getFieldU32(sfSponsoringAccountCount);
|
||||
|
||||
XRPL_ASSERT(
|
||||
sponsoringAccountCount != 0,
|
||||
"xrpl::AccountDelete::doApply : sponsoring account count is present");
|
||||
if (sponsoringAccountCount == 0)
|
||||
{
|
||||
// sanity check
|
||||
// Since sfSponsoringAccountCount is set to soeDEFAULT, the field will not be
|
||||
// present with a value of 0.
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
}
|
||||
sponsorSle->at(sfSponsoringAccountCount) = sponsoringAccountCount - 1;
|
||||
view().update(sponsorSle);
|
||||
|
||||
// Following line might look redundant, but without it, sfSponsor
|
||||
// would end up remaining in after-ltAccountRoot during the
|
||||
// InvariantCheck.
|
||||
src->makeFieldAbsent(sfSponsor);
|
||||
}
|
||||
|
||||
XRPL_ASSERT(
|
||||
(*src)[sfBalance] == XRPAmount(0), "xrpl::AccountDelete::doApply : source balance is zero");
|
||||
|
||||
// If there's still an owner directory associated with the source account
|
||||
// delete it.
|
||||
if (view().exists(ownerDirKeylet) && !view().emptyDirDelete(ownerDirKeylet))
|
||||
{
|
||||
JLOG(j_.error()) << "AccountDelete cannot delete root dir node of " << toBase58(accountID_);
|
||||
return tecHAS_OBLIGATIONS;
|
||||
}
|
||||
|
||||
// Re-arm the password change fee if we can and need to.
|
||||
if (remainingBalance > XRPAmount(0) && dst->isFlag(lsfPasswordSpent))
|
||||
dst->clearFlag(lsfPasswordSpent);
|
||||
|
||||
view().update(dst);
|
||||
view().erase(src);
|
||||
|
||||
return tesSUCCESS;
|
||||
return applyAccountDelete(ctx_, src, dst, j_);
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -164,50 +164,6 @@ SponsorshipSet::preclaim(PreclaimContext const& ctx)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
static TER
|
||||
deleteSponsorship(ApplyView& view, SLE::ref sle, beast::Journal j)
|
||||
{
|
||||
if (!sle)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const sponsorID = (*sle)[sfOwner];
|
||||
auto const sponseeID = (*sle)[sfSponsee];
|
||||
|
||||
// The sponsor owns the Sponsorship object, so deletion releases the
|
||||
// sponsor's owner reserve.
|
||||
auto sponsorAccSle = view.peek(keylet::account(sponsorID));
|
||||
if (!sponsorAccSle)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
if (!view.dirRemove(keylet::ownerDir(sponsorID), (*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(sponseeID), (*sle)[sfSponseeNode], sle->key(), false))
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.fatal()) << "Unable to delete Sponsorship from sponsee.";
|
||||
return tefBAD_LEDGER;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
decreaseOwnerCountForObject(view, sponsorAccSle, sle, 1, j);
|
||||
|
||||
// Return any prefunded fee amount to the sponsor before erasing the object.
|
||||
if (sle->isFieldPresent(sfFeeAmount))
|
||||
{
|
||||
(*sponsorAccSle)[sfBalance] += sle->getFieldAmount(sfFeeAmount);
|
||||
view.update(sponsorAccSle);
|
||||
}
|
||||
|
||||
view.erase(sle);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
SponsorshipSet::doApply()
|
||||
{
|
||||
@@ -232,7 +188,7 @@ SponsorshipSet::doApply()
|
||||
if (!sponsorshipSle)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
return deleteSponsorship(ctx_.view(), sponsorshipSle, ctx_.journal);
|
||||
return deleteSponsorshipObject(ctx_.view(), sponsorshipSle, ctx_.journal);
|
||||
}
|
||||
|
||||
auto const feeAmount = ctx_.tx[~sfFeeAmount];
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
#include <xrpl/tx/transactors/account/AccountDelete.h>
|
||||
|
||||
#include <bit>
|
||||
#include <cstdint>
|
||||
@@ -95,11 +96,22 @@ SponsorshipTransfer::getFlagsMask(PreflightContext const& ctx)
|
||||
return tfSponsorshipTransferMask;
|
||||
}
|
||||
|
||||
XRPAmount
|
||||
SponsorshipTransfer::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
// Reaping deletes an account, so it costs one owner reserve like
|
||||
// AccountDelete to keep account churn uneconomical. Other transfer flags
|
||||
// use the normal base fee.
|
||||
if (tx.isFlag(tfSponsorshipReap))
|
||||
return calculateOwnerReserveFee(view, tx);
|
||||
return Transactor::calculateBaseFee(view, tx);
|
||||
}
|
||||
|
||||
NotTEC
|
||||
SponsorshipTransfer::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
static constexpr auto transferFlags =
|
||||
tfSponsorshipCreate | tfSponsorshipReassign | tfSponsorshipEnd;
|
||||
tfSponsorshipCreate | tfSponsorshipReassign | tfSponsorshipEnd | tfSponsorshipReap;
|
||||
if (std::popcount(ctx.tx.getFlags() & transferFlags) != 1)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "preflight: Only one SponsorshipTransfer flag can be set per tx.";
|
||||
@@ -190,6 +202,37 @@ SponsorshipTransfer::preflight(PreflightContext const& ctx)
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.tx.isFlag(tfSponsorshipReap))
|
||||
{
|
||||
// Reaping deletes an abandoned, under-reserved sponsored account. The
|
||||
// sponsee is the target and the reaping sponsor is the tx sender, so no
|
||||
// sfSponsor is supplied, the target must be a named account other than
|
||||
// the sender, and no object may be specified (account-level only).
|
||||
if (ctx.tx.isFieldPresent(sfSponsor))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "preflight: sfSponsor must not be present when reaping";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
if (ctx.tx.isFieldPresent(sfObjectID))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "preflight: sfObjectID must not be present when reaping";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
if (!ctx.tx.isFieldPresent(sfSponsee))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "preflight: sfSponsee must be present when reaping";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
if (ctx.tx.getAccountID(sfSponsee) == ctx.tx.getAccountID(sfAccount))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "preflight: sfSponsee should not be the same as the account";
|
||||
return temMALFORMED;
|
||||
}
|
||||
}
|
||||
|
||||
// Account-level reserve sponsorship changes the reserve responsibility for
|
||||
// the account itself, so the new sponsor must explicitly co-sign. Object-level
|
||||
// sponsorship may use pre-funded reserve sponsorship instead.
|
||||
@@ -290,6 +333,37 @@ SponsorshipTransfer::preclaim(PreclaimContext const& ctx)
|
||||
if (account != sponsor && account != sponseeID)
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
else if (ctx.tx.isFlag(tfSponsorshipReap))
|
||||
{
|
||||
// Reaping introduces no new sponsor and is account-level only.
|
||||
if (newSponsorSle || objectID.has_value())
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// The target must be an account-level sponsored account.
|
||||
if (!isSponsored)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// Only the account's current sponsor may reap it.
|
||||
auto const sponsor = targetSle->getAccountID(*sponsorField);
|
||||
if (account != sponsor)
|
||||
return tecNO_SPONSOR_PERMISSION;
|
||||
|
||||
// Reaping is limited to a sponsee that cannot stand on its own reserve.
|
||||
// A solvent sponsee is never reapable — the sponsor should release the
|
||||
// sponsorship with tfSponsorshipEnd instead. Keeping its own reserve is
|
||||
// the sponsee's defense against being reaped.
|
||||
XRPAmount const balance = sponseeSle->getFieldAmount(sfBalance).xrp();
|
||||
XRPAmount const ownReserve =
|
||||
accountReserve(ctx.view, sponseeSle, ctx.j, {.accountCountDelta = 1});
|
||||
if (balance >= ownReserve)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// The sponsee account must itself satisfy the deletion preconditions
|
||||
// (no obligations, outside the sequence-replay window).
|
||||
if (auto const ter = checkAccountDeletable(ctx.view, sponseeSle, sponsor, ctx.j);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
@@ -518,6 +592,21 @@ SponsorshipTransfer::doApply()
|
||||
!isTesSuccess(ter))
|
||||
return ter; // LCOV_EXCL_LINE
|
||||
}
|
||||
else if (ctx_.tx.isFlag(tfSponsorshipReap))
|
||||
{
|
||||
// Reap the abandoned sponsee: delete its account, returning its
|
||||
// residual XRP (and any prefunded sponsorship) to the sponsor. The
|
||||
// account deletion unwinds the sfSponsor reserve sponsorship, so
|
||||
// the sponsor's SponsoringAccountCount is released here.
|
||||
auto const sponsorID = sponseeSle->getAccountID(sfSponsor);
|
||||
auto sponsorSle = view().peek(keylet::account(sponsorID));
|
||||
if (!sponsorSle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
if (auto const ter = applyAccountDelete(ctx_, sponseeSle, sponsorSle, ctx_.journal);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
}
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
|
||||
@@ -4368,27 +4368,60 @@ public:
|
||||
Account const sponsor("sponsor");
|
||||
|
||||
{
|
||||
// Delete Sponsor/Sponsee Account with ltSponsorship (tecHAS_OBLIGATIONS)
|
||||
// The sponsee may delete its account even while a Sponsorship
|
||||
// object exists: the object is torn down and its prefunded fee is
|
||||
// refunded to the sponsor.
|
||||
Env env{*this, testableAmendments()};
|
||||
env.fund(XRP(1000000), alice, bob, sponsor);
|
||||
env.close();
|
||||
|
||||
// set sponsor
|
||||
// prefunded Sponsorship object between sponsor and alice
|
||||
env(sponsor::set(sponsor, 0, 100, XRP(100)),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
incLgrSeqForAccDel(env, sponsor);
|
||||
auto const keylet = keylet::sponsorship(sponsor, alice);
|
||||
BEAST_EXPECT(env.le(keylet));
|
||||
|
||||
incLgrSeqForAccDel(env, alice);
|
||||
|
||||
auto const requiredFee = drops(env.current()->fees().increment);
|
||||
auto const sponsorBalBefore = env.balance(sponsor);
|
||||
env(acctdelete(alice, bob), Fee(requiredFee), Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(!env.le(keylet::account(alice)));
|
||||
BEAST_EXPECT(!env.le(keylet)); // Sponsorship object removed
|
||||
// Prefunded fee refunded to the sponsor.
|
||||
BEAST_EXPECT(env.balance(sponsor) == sponsorBalBefore + XRP(100));
|
||||
}
|
||||
|
||||
{
|
||||
// The sponsor that owns the Sponsorship object may also delete its
|
||||
// own account; the object is removed from both directories.
|
||||
Env env{*this, testableAmendments()};
|
||||
env.fund(XRP(1000000), alice, bob, sponsor);
|
||||
env.close();
|
||||
|
||||
env(sponsor::set(sponsor, 0, 100, XRP(100)),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
auto const keylet = keylet::sponsorship(sponsor, alice);
|
||||
auto const sponsorObj = env.le(keylet);
|
||||
BEAST_EXPECT(sponsorObj);
|
||||
BEAST_EXPECT(env.le(keylet));
|
||||
|
||||
incLgrSeqForAccDel(env, sponsor);
|
||||
|
||||
// AccountDelete
|
||||
auto const requiredFee = drops(env.current()->fees().increment);
|
||||
env(acctdelete(alice, bob), Fee(requiredFee), Ter(tecHAS_OBLIGATIONS));
|
||||
env(acctdelete(sponsor, bob), Fee(requiredFee), Ter(tecHAS_OBLIGATIONS));
|
||||
env(acctdelete(sponsor, bob), Fee(requiredFee), Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(!env.le(keylet::account(sponsor)));
|
||||
BEAST_EXPECT(!env.le(keylet)); // Sponsorship object removed
|
||||
// alice remains, no longer linked to the removed object.
|
||||
BEAST_EXPECT(env.le(keylet::account(alice)));
|
||||
}
|
||||
|
||||
{
|
||||
@@ -4527,6 +4560,231 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testReap()
|
||||
{
|
||||
testcase("Reap");
|
||||
using namespace test::jtx;
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
Account const sponsor("sponsor");
|
||||
|
||||
// preflight: malformed reap transactions
|
||||
{
|
||||
Env env{*this, testableAmendments()};
|
||||
env.fund(XRP(10000), alice, bob, sponsor);
|
||||
env.close();
|
||||
|
||||
// Missing sfSponsee.
|
||||
env(sponsor::transfer(sponsor, tfSponsorshipReap), Ter(temMALFORMED));
|
||||
|
||||
// sfSponsee equal to the account.
|
||||
env(sponsor::transfer(sponsor, tfSponsorshipReap),
|
||||
sponsor::SponseeAcc(sponsor),
|
||||
Ter(temMALFORMED));
|
||||
|
||||
// More than one transfer flag.
|
||||
env(sponsor::transfer(sponsor, tfSponsorshipReap | tfSponsorshipEnd),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Ter(temINVALID_FLAG));
|
||||
}
|
||||
|
||||
// Reap succeeds on an abandoned, under-reserved sponsored account.
|
||||
{
|
||||
Env env{*this, testableAmendments()};
|
||||
env.memoize(alice);
|
||||
env.fund(XRP(1000000), bob, sponsor);
|
||||
env.close();
|
||||
|
||||
// Create alice as a sponsored account holding a single drop.
|
||||
env(pay(sponsor, alice, drops(1)), Txflags(tfSponsorCreatedAccount), Fee(XRP(1)));
|
||||
env.close();
|
||||
|
||||
auto const aliceSle = env.le(keylet::account(alice));
|
||||
BEAST_EXPECT(aliceSle && aliceSle->getAccountID(sfSponsor) == sponsor.id());
|
||||
BEAST_EXPECT(sponsoringAccountCount(env, sponsor) == 1);
|
||||
|
||||
// Cannot reap until the sponsee clears the sequence-replay window.
|
||||
env(sponsor::transfer(sponsor, tfSponsorshipReap),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Fee(drops(env.current()->fees().increment)),
|
||||
Ter(tecTOO_SOON));
|
||||
|
||||
incLgrSeqForAccDel(env, alice);
|
||||
|
||||
auto const reapFee = drops(env.current()->fees().increment);
|
||||
auto const sponsorBalBefore = env.balance(sponsor);
|
||||
env(sponsor::transfer(sponsor, tfSponsorshipReap),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Fee(reapFee),
|
||||
Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
// The account is gone and the sponsor's reserve is released.
|
||||
BEAST_EXPECT(!env.le(keylet::account(alice)));
|
||||
auto const sponsorSle = env.le(keylet::account(sponsor));
|
||||
BEAST_EXPECT(!sponsorSle->isFieldPresent(sfSponsoringAccountCount));
|
||||
// The sponsor recovers the sponsee's residual drop, less the fee.
|
||||
BEAST_EXPECT(env.balance(sponsor) == sponsorBalBefore - reapFee + drops(1));
|
||||
}
|
||||
|
||||
// A solvent sponsee can never be reaped — keeping its own reserve is
|
||||
// the sponsee's defense.
|
||||
{
|
||||
Env env{*this, testableAmendments()};
|
||||
env.memoize(alice);
|
||||
env.fund(XRP(1000000), bob, sponsor);
|
||||
env.close();
|
||||
|
||||
env(pay(sponsor, alice, XRP(10000)), Txflags(tfSponsorCreatedAccount));
|
||||
env.close();
|
||||
|
||||
incLgrSeqForAccDel(env, alice);
|
||||
|
||||
env(sponsor::transfer(sponsor, tfSponsorshipReap),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Fee(drops(env.current()->fees().increment)),
|
||||
Ter(tecNO_PERMISSION));
|
||||
|
||||
// The account is untouched and still sponsored.
|
||||
auto const aliceSle = env.le(keylet::account(alice));
|
||||
BEAST_EXPECT(aliceSle && aliceSle->getAccountID(sfSponsor) == sponsor.id());
|
||||
BEAST_EXPECT(sponsoringAccountCount(env, sponsor) == 1);
|
||||
}
|
||||
|
||||
// Only the account's current sponsor may reap it.
|
||||
{
|
||||
Env env{*this, testableAmendments()};
|
||||
env.memoize(alice);
|
||||
env.fund(XRP(1000000), bob, sponsor);
|
||||
env.close();
|
||||
|
||||
env(pay(sponsor, alice, drops(1)), Txflags(tfSponsorCreatedAccount), Fee(XRP(1)));
|
||||
env.close();
|
||||
|
||||
incLgrSeqForAccDel(env, alice);
|
||||
|
||||
// bob is not alice's sponsor.
|
||||
env(sponsor::transfer(bob, tfSponsorshipReap),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Fee(drops(env.current()->fees().increment)),
|
||||
Ter(tecNO_SPONSOR_PERMISSION));
|
||||
|
||||
BEAST_EXPECT(env.le(keylet::account(alice)));
|
||||
}
|
||||
|
||||
// Reap also tears down a prefunded Sponsorship object for the sponsee,
|
||||
// refunding its fee to the sponsor.
|
||||
{
|
||||
Env env{*this, testableAmendments()};
|
||||
env.memoize(alice);
|
||||
env.fund(XRP(1000000), bob, sponsor);
|
||||
env.close();
|
||||
|
||||
env(pay(sponsor, alice, drops(1)), Txflags(tfSponsorCreatedAccount), Fee(XRP(1)));
|
||||
env.close();
|
||||
|
||||
// Prefund a Sponsorship object for the sponsee.
|
||||
env(sponsor::set(sponsor, 0, 100, XRP(100)),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
auto const sponsorship = keylet::sponsorship(sponsor, alice);
|
||||
BEAST_EXPECT(env.le(sponsorship));
|
||||
|
||||
incLgrSeqForAccDel(env, alice);
|
||||
|
||||
auto const reapFee = drops(env.current()->fees().increment);
|
||||
auto const sponsorBalBefore = env.balance(sponsor);
|
||||
env(sponsor::transfer(sponsor, tfSponsorshipReap),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Fee(reapFee),
|
||||
Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(!env.le(keylet::account(alice)));
|
||||
BEAST_EXPECT(!env.le(sponsorship)); // Sponsorship object removed
|
||||
auto const sponsorSle = env.le(keylet::account(sponsor));
|
||||
BEAST_EXPECT(!sponsorSle->isFieldPresent(sfSponsoringAccountCount));
|
||||
// Sponsor recovers the prefunded fee plus the residual drop.
|
||||
BEAST_EXPECT(env.balance(sponsor) == sponsorBalBefore - reapFee + XRP(100) + drops(1));
|
||||
}
|
||||
|
||||
// With featureSponsor disabled the transaction type is unavailable.
|
||||
{
|
||||
Env env{*this, testableAmendments() - featureSponsor};
|
||||
env.fund(XRP(10000), alice, bob, sponsor);
|
||||
env.close();
|
||||
|
||||
env(sponsor::transfer(sponsor, tfSponsorshipReap),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Ter(temDISABLED));
|
||||
}
|
||||
|
||||
// Reap is account-level only; supplying an object id is malformed.
|
||||
{
|
||||
Env env{*this, testableAmendments()};
|
||||
env.fund(XRP(1000000), alice, bob, sponsor);
|
||||
env.close();
|
||||
|
||||
env(sponsor::transfer(sponsor, tfSponsorshipReap, keylet::account(alice).key),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Ter(temMALFORMED));
|
||||
}
|
||||
|
||||
// A target that is not sponsored cannot be reaped.
|
||||
{
|
||||
Env env{*this, testableAmendments()};
|
||||
env.fund(XRP(1000000), alice, bob, sponsor);
|
||||
env.close();
|
||||
|
||||
incLgrSeqForAccDel(env, alice);
|
||||
|
||||
env(sponsor::transfer(sponsor, tfSponsorshipReap),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Fee(drops(env.current()->fees().increment)),
|
||||
Ter(tecNO_PERMISSION));
|
||||
|
||||
BEAST_EXPECT(env.le(keylet::account(alice)));
|
||||
}
|
||||
|
||||
// Reaping a sponsee that does not exist returns terNO_ACCOUNT.
|
||||
{
|
||||
Env env{*this, testableAmendments()};
|
||||
env.memoize(alice); // never funded
|
||||
env.fund(XRP(1000000), bob, sponsor);
|
||||
env.close();
|
||||
|
||||
env(sponsor::transfer(sponsor, tfSponsorshipReap),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Fee(drops(env.current()->fees().increment)),
|
||||
Ter(terNO_ACCOUNT));
|
||||
}
|
||||
|
||||
// A sponsor cannot pull back its reserve on an under-reserved sponsee
|
||||
// without deleting it: tfSponsorshipEnd is rejected, so the account is
|
||||
// never left as an unbacked, under-reserved zombie.
|
||||
{
|
||||
Env env{*this, testableAmendments()};
|
||||
env.memoize(alice);
|
||||
env.fund(XRP(1000000), bob, sponsor);
|
||||
env.close();
|
||||
|
||||
env(pay(sponsor, alice, drops(1)), Txflags(tfSponsorCreatedAccount), Fee(XRP(1)));
|
||||
env.close();
|
||||
|
||||
env(sponsor::transfer(sponsor, tfSponsorshipEnd),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Ter(tecINSUFFICIENT_RESERVE));
|
||||
|
||||
// Still sponsored — the reserve was not pulled back.
|
||||
auto const aliceSle = env.le(keylet::account(alice));
|
||||
BEAST_EXPECT(aliceSle && aliceSle->getAccountID(sfSponsor) == sponsor.id());
|
||||
BEAST_EXPECT(sponsoringAccountCount(env, sponsor) == 1);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testDelegatePermission()
|
||||
{
|
||||
@@ -5453,6 +5711,7 @@ protected:
|
||||
testSponsorAccount();
|
||||
|
||||
testAccountDelete();
|
||||
testReap();
|
||||
|
||||
testDelegatePermission();
|
||||
testDelegateBlockReserveSponsor();
|
||||
|
||||
Reference in New Issue
Block a user