mirror of
https://github.com/XRPLF/rippled.git
synced 2026-07-27 00:50:45 +00:00
Merge branch 'xrplf/sponsor' of https://github.com/XRPLF/rippled into mvadari/sponsor/fixes
This commit is contained in:
@@ -346,6 +346,16 @@ EscrowFinish::doApply()
|
||||
}
|
||||
}
|
||||
|
||||
// With the Sponsor amendment, release the escrow reserve before delivery.
|
||||
// Token delivery can auto-create a destination holding, and the same
|
||||
// sponsor (or the same account, for a self-escrow) may cover both the
|
||||
// escrow being removed and the holding being created. Without the
|
||||
// amendment, keep the legacy order: releasing early changes the reserve
|
||||
// arithmetic for self-escrows and would break consensus if not gated.
|
||||
bool const sponsorEnabled = ctx_.view().rules().enabled(featureSponsor);
|
||||
if (sponsorEnabled)
|
||||
decreaseOwnerCountForObject(ctx_.view(), account, slep, 1, ctx_.journal);
|
||||
|
||||
STAmount const amount = slep->getFieldAmount(sfAmount);
|
||||
// Transfer amount to destination
|
||||
if (isXRP(amount))
|
||||
@@ -395,8 +405,9 @@ EscrowFinish::doApply()
|
||||
|
||||
ctx_.view().update(sled);
|
||||
|
||||
// Adjust source owner count
|
||||
decreaseOwnerCountForObject(ctx_.view(), account, slep, 1, ctx_.journal);
|
||||
// Adjust source owner count (legacy position, pre-Sponsor)
|
||||
if (!sponsorEnabled)
|
||||
decreaseOwnerCountForObject(ctx_.view(), account, slep, 1, ctx_.journal);
|
||||
|
||||
// Remove escrow from ledger
|
||||
ctx_.view().erase(slep);
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/SponsorHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
@@ -20,17 +19,74 @@
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
static std::optional<std::uint32_t>
|
||||
applyCountDelta(std::uint32_t current, std::int64_t delta)
|
||||
// Increment an uint32 sponsor count field and update the SLE.
|
||||
static TER
|
||||
incrementSponsorCount(
|
||||
ApplyView& view,
|
||||
SLE::ref sle,
|
||||
SF_UINT32 const& field,
|
||||
std::uint32_t const delta)
|
||||
{
|
||||
std::int64_t const next = static_cast<std::int64_t>(current) + delta;
|
||||
if (next < 0 || next > std::numeric_limits<std::uint32_t>::max())
|
||||
return std::nullopt;
|
||||
return static_cast<std::uint32_t>(next);
|
||||
auto const currentValue = sle->getFieldU32(field);
|
||||
if (std::numeric_limits<std::uint32_t>::max() - currentValue < delta)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::incrementSponsorCount : sponsor field overflow");
|
||||
return tecINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
sle->at(field) = currentValue + delta;
|
||||
view.update(sle);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
// Decrement an uint32 sponsor count field and update the SLE.
|
||||
static TER
|
||||
decrementSponsorCount(
|
||||
ApplyView& view,
|
||||
SLE::ref sle,
|
||||
SF_UINT32 const& field,
|
||||
std::uint32_t const delta)
|
||||
{
|
||||
auto const currentValue = sle->getFieldU32(field);
|
||||
if (currentValue < delta)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::decrementSponsorCount : sponsor field underflow");
|
||||
return tecINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
sle->at(field) = currentValue - delta;
|
||||
view.update(sle);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
// Consume the sponsor's pre-funded reserve budget and lowers the Sponsorship
|
||||
// object's RemainingOwnerCount.
|
||||
static TER
|
||||
decrementPrefundedReserveCount(ApplyView& view, SLE::ref sponsorshipSle, std::uint32_t const delta)
|
||||
{
|
||||
if (delta == 0)
|
||||
return tesSUCCESS; // LCOV_EXCL_LINE
|
||||
|
||||
auto const currentReserveCount = sponsorshipSle->getFieldU32(sfRemainingOwnerCount);
|
||||
if (currentReserveCount < delta)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
// Already verified by checkReserve (sufficient RemainingOwnerCount)
|
||||
UNREACHABLE("xrpl::decrementPrefundedReserveCount : invalid reserve count");
|
||||
return tefINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
sponsorshipSle->at(sfRemainingOwnerCount) = currentReserveCount - delta;
|
||||
view.update(sponsorshipSle);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
std::uint32_t
|
||||
@@ -233,79 +289,30 @@ SponsorshipTransfer::preclaim(PreclaimContext const& ctx)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
static TER
|
||||
reduceReserveCount(
|
||||
ApplyView& view,
|
||||
AccountID const& account,
|
||||
AccountID const& sponsor,
|
||||
int64_t delta)
|
||||
{
|
||||
if (delta == 0)
|
||||
return tesSUCCESS;
|
||||
if (delta > 0)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const sponsorKeylet = keylet::sponsorship(sponsor, account);
|
||||
auto const sponsorSle = view.peek(sponsorKeylet);
|
||||
if (!sponsorSle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const afterReserveCount =
|
||||
applyCountDelta(sponsorSle->getFieldU32(sfRemainingOwnerCount), delta);
|
||||
if (!afterReserveCount)
|
||||
{
|
||||
// already checked in preclaim()
|
||||
UNREACHABLE("xrpl::reduceReserveCount : invalid reserve count");
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
sponsorSle->at(sfRemainingOwnerCount) = *afterReserveCount;
|
||||
view.update(sponsorSle);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
SponsorshipTransfer::doApply()
|
||||
{
|
||||
auto const& tx = ctx_.tx;
|
||||
auto const objectID = ctx_.tx[~sfObjectID];
|
||||
|
||||
auto const index = tx[~sfObjectID];
|
||||
bool const isObjectSponsor = index != std::nullopt;
|
||||
|
||||
auto const sponseeID = tx[~sfSponsee].value_or(accountID_);
|
||||
auto const sponseeID = ctx_.tx[~sfSponsee].value_or(accountID_);
|
||||
auto const sponseeSle = view().peek(keylet::account(sponseeID));
|
||||
if (!sponseeSle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const setSponsorFieldU32 =
|
||||
[] [[nodiscard]] (auto const& sle, auto const& field, auto const& delta) -> TER {
|
||||
auto const newValue = applyCountDelta(sle->getFieldU32(field), delta);
|
||||
if (!newValue)
|
||||
{
|
||||
UNREACHABLE("xrpl::SponsorshipTransfer::doApply : Invalid sponsor field value");
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
sle->at(field) = *newValue;
|
||||
return tesSUCCESS;
|
||||
};
|
||||
|
||||
auto const balanceBeforeFee = [&](SLE::const_ref sle) -> STAmount {
|
||||
if (sle->getAccountID(sfAccount) == accountID_)
|
||||
return STAmount{preFeeBalance_};
|
||||
return sle->getFieldAmount(sfBalance);
|
||||
};
|
||||
|
||||
if (isObjectSponsor)
|
||||
if (objectID.has_value())
|
||||
{
|
||||
auto const hasSignature = tx.isFieldPresent(sfSponsorSignature);
|
||||
|
||||
// transfer object sponsor
|
||||
auto const objSle = view().peek(keylet::unchecked(*index));
|
||||
if (!objSle)
|
||||
// Transfer object sponsor
|
||||
auto const objectSle = view().peek(keylet::unchecked(*objectID));
|
||||
if (!objectSle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const ownerID = getLedgerEntryOwner(view(), objSle, sponseeID);
|
||||
auto const ownerID = getLedgerEntryOwner(view(), objectSle, sponseeID);
|
||||
if (!ownerID)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
@@ -313,19 +320,24 @@ SponsorshipTransfer::doApply()
|
||||
if (!ownerSle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const ownerCountDelta = static_cast<std::int32_t>(getLedgerEntryOwnerCount(objSle));
|
||||
|
||||
auto const& sponsorField = getLedgerEntrySponsorField(objSle, *ownerID);
|
||||
auto const ownerCountDelta = static_cast<std::int32_t>(getLedgerEntryOwnerCount(objectSle));
|
||||
auto const& sponsorField = getLedgerEntrySponsorField(objectSle, *ownerID);
|
||||
|
||||
if (ctx_.tx.isFlag(tfSponsorshipCreate))
|
||||
{
|
||||
auto const newSponsorID = tx.getAccountID(sfSponsor);
|
||||
XRPL_ASSERT(!!newSponsorID, "New sponsor is required when creating sponsorship");
|
||||
// Create object sponsor
|
||||
auto const newSponsor = ctx_.tx[~sfSponsor];
|
||||
XRPL_ASSERT(
|
||||
newSponsor.has_value(),
|
||||
"xrpl::SponsorshipTransfer::doApply : sfSponsor present for object sponsor create");
|
||||
if (!newSponsor)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
auto const newSponsorID = *newSponsor;
|
||||
auto const newSponsorSle = view().peek(keylet::account(newSponsorID));
|
||||
if (!newSponsorSle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
// check new sponsor have sufficient balance
|
||||
// Check if new sponsor has sufficient balance
|
||||
// NOLINTNEXTLINE(readability-suspicious-call-argument)
|
||||
if (auto const ter = checkReserve(
|
||||
ctx_.getApplyViewContext(),
|
||||
@@ -337,48 +349,55 @@ SponsorshipTransfer::doApply()
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// update owner's sponsored count
|
||||
// Update owner's sponsored count
|
||||
if (auto const ter =
|
||||
setSponsorFieldU32(ownerSle, sfSponsoredOwnerCount, ownerCountDelta);
|
||||
incrementSponsorCount(view(), ownerSle, sfSponsoredOwnerCount, ownerCountDelta);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
view().update(ownerSle);
|
||||
|
||||
// increment new sponsor's sponsoring count
|
||||
if (auto const ter =
|
||||
setSponsorFieldU32(newSponsorSle, sfSponsoringOwnerCount, ownerCountDelta);
|
||||
// Increment new sponsor's sponsoring count
|
||||
if (auto const ter = incrementSponsorCount(
|
||||
view(), newSponsorSle, sfSponsoringOwnerCount, ownerCountDelta);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
view().update(newSponsorSle);
|
||||
|
||||
// set new sponsor to object
|
||||
objSle->setAccountID(sponsorField, newSponsorID);
|
||||
view().update(objSle);
|
||||
// Object is now sponsored by new sponsor
|
||||
objectSle->setAccountID(sponsorField, newSponsorID);
|
||||
view().update(objectSle);
|
||||
|
||||
if (!hasSignature)
|
||||
auto const sponsorshipSle = view().peek(keylet::sponsorship(newSponsorID, sponseeID));
|
||||
if (sponsorshipSle)
|
||||
{
|
||||
// use ReserveCount for pre-funded sponsoring
|
||||
// Update ReserveCount for sponsorship object if it exists
|
||||
if (auto const ter =
|
||||
reduceReserveCount(view(), sponseeID, newSponsorID, -ownerCountDelta);
|
||||
decrementPrefundedReserveCount(view(), sponsorshipSle, ownerCountDelta);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
}
|
||||
}
|
||||
else if (ctx_.tx.isFlag(tfSponsorshipReassign))
|
||||
{
|
||||
auto const newSponsorID = tx.getAccountID(sfSponsor);
|
||||
XRPL_ASSERT(!!newSponsorID, "New sponsor is required when reassigning sponsorship");
|
||||
// Reassign object sponsor
|
||||
auto const newSponsor = ctx_.tx[~sfSponsor];
|
||||
XRPL_ASSERT(
|
||||
newSponsor.has_value(),
|
||||
"xrpl::SponsorshipTransfer::doApply : sfSponsor present for object sponsor "
|
||||
"reassign");
|
||||
if (!newSponsor)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
auto const newSponsorID = *newSponsor;
|
||||
auto const newSponsorSle = view().peek(keylet::account(newSponsorID));
|
||||
if (!newSponsorSle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const oldSponsorID = objSle->getAccountID(sponsorField);
|
||||
XRPL_ASSERT(!!oldSponsorID, "Old sponsor is required when reassigning sponsorship");
|
||||
auto const oldSponsorID = objectSle->getAccountID(sponsorField);
|
||||
if (!oldSponsorID)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
auto const oldSponsorSle = view().peek(keylet::account(oldSponsorID));
|
||||
if (!oldSponsorSle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
// check new sponsor have sufficient balance
|
||||
// Check if new sponsor has sufficient balance
|
||||
// NOLINTNEXTLINE(readability-suspicious-call-argument)
|
||||
if (auto const ter = checkReserve(
|
||||
ctx_.getApplyViewContext(),
|
||||
@@ -390,38 +409,38 @@ SponsorshipTransfer::doApply()
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// decrement old sponsor's sponsoring count
|
||||
if (auto const ter =
|
||||
setSponsorFieldU32(oldSponsorSle, sfSponsoringOwnerCount, -ownerCountDelta);
|
||||
// Decrement old sponsor's sponsoring count
|
||||
if (auto const ter = decrementSponsorCount(
|
||||
view(), oldSponsorSle, sfSponsoringOwnerCount, ownerCountDelta);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
view().update(oldSponsorSle);
|
||||
|
||||
// increment new sponsor's sponsoring count
|
||||
if (auto const ter =
|
||||
setSponsorFieldU32(newSponsorSle, sfSponsoringOwnerCount, ownerCountDelta);
|
||||
// Increment new sponsor's sponsoring count
|
||||
if (auto const ter = incrementSponsorCount(
|
||||
view(), newSponsorSle, sfSponsoringOwnerCount, ownerCountDelta);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
view().update(newSponsorSle);
|
||||
|
||||
// set new sponsor to object
|
||||
objSle->setAccountID(sponsorField, newSponsorID);
|
||||
view().update(objSle);
|
||||
// Object is now sponsored by new sponsor
|
||||
objectSle->setAccountID(sponsorField, newSponsorID);
|
||||
view().update(objectSle);
|
||||
|
||||
if (!hasSignature)
|
||||
auto const sponsorshipSle = view().peek(keylet::sponsorship(newSponsorID, sponseeID));
|
||||
if (sponsorshipSle)
|
||||
{
|
||||
// use ReserveCount for pre-funded sponsoring
|
||||
// Update ReserveCount for sponsorship object if it exists
|
||||
if (auto const ter =
|
||||
reduceReserveCount(view(), sponseeID, newSponsorID, -ownerCountDelta);
|
||||
decrementPrefundedReserveCount(view(), sponsorshipSle, ownerCountDelta);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
}
|
||||
}
|
||||
else if (ctx_.tx.isFlag(tfSponsorshipEnd))
|
||||
{
|
||||
auto const oldSponsorID = objSle->getAccountID(sponsorField);
|
||||
XRPL_ASSERT(!!oldSponsorID, "Old sponsor is required when ending sponsorship");
|
||||
|
||||
// End object sponsor
|
||||
auto const oldSponsorID = objectSle->getAccountID(sponsorField);
|
||||
if (!oldSponsorID)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
auto const oldSponsorSle = view().peek(keylet::account(oldSponsorID));
|
||||
if (!oldSponsorSle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
@@ -438,32 +457,38 @@ SponsorshipTransfer::doApply()
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// decrement sponsored count
|
||||
if (auto const ter =
|
||||
setSponsorFieldU32(sponseeSle, sfSponsoredOwnerCount, -ownerCountDelta);
|
||||
// Decrement sponsored count
|
||||
if (auto const ter = decrementSponsorCount(
|
||||
view(), sponseeSle, sfSponsoredOwnerCount, ownerCountDelta);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
view().update(sponseeSle);
|
||||
|
||||
// decrement old sponsoring count
|
||||
if (auto const ter =
|
||||
setSponsorFieldU32(oldSponsorSle, sfSponsoringOwnerCount, -ownerCountDelta);
|
||||
// Decrement old sponsoring count
|
||||
if (auto const ter = decrementSponsorCount(
|
||||
view(), oldSponsorSle, sfSponsoringOwnerCount, ownerCountDelta);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
view().update(oldSponsorSle);
|
||||
|
||||
// remove sponsor from object
|
||||
objSle->makeFieldAbsent(sponsorField);
|
||||
view().update(objSle);
|
||||
// Remove sponsor from object
|
||||
objectSle->makeFieldAbsent(sponsorField);
|
||||
view().update(objectSle);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Account-level sponsorship is always co-signed (preflight requires
|
||||
// sfSponsorSignature), so there is no pre-funded budget to draw down here.
|
||||
if (ctx_.tx.isFlag(tfSponsorshipCreate))
|
||||
{
|
||||
// create account sponsor
|
||||
// increment new sponsoring count
|
||||
auto const newSponsorID = tx.getAccountID(sfSponsor);
|
||||
// Create account sponsor
|
||||
auto const newSponsor = ctx_.tx[~sfSponsor];
|
||||
XRPL_ASSERT(
|
||||
newSponsor.has_value(),
|
||||
"xrpl::SponsorshipTransfer::doApply : sfSponsor present for account sponsor "
|
||||
"create");
|
||||
if (!newSponsor)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
auto const newSponsorID = *newSponsor;
|
||||
auto const newSponsorSle = view().peek(keylet::account(newSponsorID));
|
||||
if (!newSponsorSle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
@@ -478,20 +503,27 @@ SponsorshipTransfer::doApply()
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
if (auto const ter = setSponsorFieldU32(newSponsorSle, sfSponsoringAccountCount, 1);
|
||||
// Increment new sponsoring count
|
||||
if (auto const ter =
|
||||
incrementSponsorCount(view(), newSponsorSle, sfSponsoringAccountCount, 1);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
view().update(newSponsorSle);
|
||||
|
||||
// set new sponsor to account
|
||||
// Account is now sponsored by new sponsor
|
||||
sponseeSle->setAccountID(sfSponsor, newSponsorID);
|
||||
view().update(sponseeSle);
|
||||
}
|
||||
else if (ctx_.tx.isFlag(tfSponsorshipReassign))
|
||||
{
|
||||
// reassign account sponsor
|
||||
// increment new sponsoring count
|
||||
auto const newSponsorID = tx.getAccountID(sfSponsor);
|
||||
// Reassign account sponsor
|
||||
auto const newSponsor = ctx_.tx[~sfSponsor];
|
||||
XRPL_ASSERT(
|
||||
newSponsor.has_value(),
|
||||
"xrpl::SponsorshipTransfer::doApply : sfSponsor present for account sponsor "
|
||||
"reassign");
|
||||
if (!newSponsor)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
auto const newSponsorID = *newSponsor;
|
||||
auto const newSponsorSle = view().peek(keylet::account(newSponsorID));
|
||||
if (!newSponsorSle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
@@ -506,29 +538,37 @@ SponsorshipTransfer::doApply()
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
if (auto const ter = setSponsorFieldU32(newSponsorSle, sfSponsoringAccountCount, 1);
|
||||
// Increment new sponsoring count
|
||||
if (auto const ter =
|
||||
incrementSponsorCount(view(), newSponsorSle, sfSponsoringAccountCount, 1);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
view().update(newSponsorSle);
|
||||
|
||||
// decrement old sponsoring count
|
||||
// Decrement old sponsoring count
|
||||
auto const oldSponsorID = sponseeSle->getAccountID(sfSponsor);
|
||||
if (!oldSponsorID)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
auto const oldSponsorSle = view().peek(keylet::account(oldSponsorID));
|
||||
if (!oldSponsorSle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
if (auto const ter = setSponsorFieldU32(oldSponsorSle, sfSponsoringAccountCount, -1);
|
||||
if (auto const ter =
|
||||
decrementSponsorCount(view(), oldSponsorSle, sfSponsoringAccountCount, 1);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
view().update(oldSponsorSle);
|
||||
|
||||
// set new sponsor to account
|
||||
// Account is now sponsored by new sponsor
|
||||
sponseeSle->setAccountID(sfSponsor, newSponsorID);
|
||||
view().update(sponseeSle);
|
||||
}
|
||||
else if (ctx_.tx.isFlag(tfSponsorshipEnd))
|
||||
{
|
||||
// dissolve account sponsor
|
||||
// End account sponsor
|
||||
auto const oldSponsorID = sponseeSle->getAccountID(sfSponsor);
|
||||
if (!oldSponsorID)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
auto const oldSponsorSle = view().peek(keylet::account(oldSponsorID));
|
||||
if (!oldSponsorSle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
// The sponsee must be able to hold its own account reserve after
|
||||
// the sponsorship is removed.
|
||||
@@ -545,14 +585,11 @@ SponsorshipTransfer::doApply()
|
||||
sponseeSle->makeFieldAbsent(sfSponsor);
|
||||
view().update(sponseeSle);
|
||||
|
||||
// decrement account sponsoring count
|
||||
auto const oldSponsorSle = view().peek(keylet::account(oldSponsorID));
|
||||
if (!oldSponsorSle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
if (auto const ter = setSponsorFieldU32(oldSponsorSle, sfSponsoringAccountCount, -1);
|
||||
// Decrement account sponsoring count
|
||||
if (auto const ter =
|
||||
decrementSponsorCount(view(), oldSponsorSle, sfSponsoringAccountCount, 1);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
view().update(oldSponsorSle);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3241,6 +3241,84 @@ public:
|
||||
env.le(keylet::trustLine(bob, gw, usd.currency))->getAccountID(sfHighSponsor) ==
|
||||
sponsor2.id());
|
||||
}
|
||||
{
|
||||
// IOU EscrowFinish recycles reserve when the same sponsor backs
|
||||
// the escrow being removed and the destination line being created.
|
||||
Env env{*this, testableAmendments()};
|
||||
auto const baseFee = env.current()->fees().base;
|
||||
|
||||
env.fund(XRP(1000000), alice, bob, gw, sponsor);
|
||||
env.close();
|
||||
|
||||
env(fset(gw, asfAllowTrustLineLocking));
|
||||
env.close();
|
||||
|
||||
env.trust(usd(1000000), alice);
|
||||
env.close();
|
||||
env(pay(gw, alice, usd(10000)));
|
||||
env.close();
|
||||
|
||||
uint32_t seq = 0;
|
||||
testEachSponsorship(
|
||||
env,
|
||||
cosigning,
|
||||
sponsor,
|
||||
alice,
|
||||
1,
|
||||
1,
|
||||
tecINSUFFICIENT_RESERVE,
|
||||
[&](Env& env, auto const& submit) {
|
||||
seq = env.seq(alice);
|
||||
submit(
|
||||
escrow::create(alice, bob, usd(100)),
|
||||
escrow::kCondition(escrow::kCb1),
|
||||
escrow::kCancelTime(env.now() + 100s));
|
||||
});
|
||||
|
||||
BEAST_EXPECT(
|
||||
env.le(keylet::escrow(alice, seq))->getAccountID(sfSponsor) == sponsor.id());
|
||||
|
||||
if (cosigning)
|
||||
{
|
||||
adjustAccountXRPBalance(env, sponsor, reserve(env, 1));
|
||||
}
|
||||
else
|
||||
{
|
||||
env(sponsor::set_reserve(sponsor, 0, 1), sponsor::SponseeAcc(bob));
|
||||
env.close();
|
||||
}
|
||||
|
||||
if (cosigning)
|
||||
{
|
||||
env(escrow::finish(bob, alice, seq),
|
||||
escrow::kCondition(escrow::kCb1),
|
||||
escrow::kFulfillment(escrow::kFb1),
|
||||
sponsor::As(sponsor, spfSponsorReserve),
|
||||
Sig(sfSponsorSignature, sponsor),
|
||||
Fee(baseFee * 150),
|
||||
Ter(tesSUCCESS));
|
||||
}
|
||||
else
|
||||
{
|
||||
env(escrow::finish(bob, alice, seq),
|
||||
escrow::kCondition(escrow::kCb1),
|
||||
escrow::kFulfillment(escrow::kFb1),
|
||||
sponsor::As(sponsor, spfSponsorReserve),
|
||||
Fee(baseFee * 150),
|
||||
Ter(tesSUCCESS));
|
||||
}
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
||||
BEAST_EXPECT(ownerCount(env, bob) == 1);
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 1);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1);
|
||||
|
||||
BEAST_EXPECT(
|
||||
env.le(keylet::trustLine(bob, gw, usd.currency))->getAccountID(sfHighSponsor) ==
|
||||
sponsor.id());
|
||||
}
|
||||
{
|
||||
// IOU Escrow cancel re-creates the owner's trust line, and the
|
||||
// cancel transaction's sponsor can cover that new line's reserve.
|
||||
@@ -3368,6 +3446,83 @@ public:
|
||||
env.le(keylet::mptoken(mptGw.issuanceID(), bob))->getAccountID(sfSponsor) ==
|
||||
sponsor.id());
|
||||
}
|
||||
{
|
||||
// MPT EscrowFinish has the same reserve recycling behavior as IOU
|
||||
// when it creates the destination MPToken.
|
||||
Env env{*this, testableAmendments()};
|
||||
auto const baseFee = env.current()->fees().base;
|
||||
env.fund(XRP(1000000), bob, sponsor);
|
||||
env.close();
|
||||
|
||||
MPTTester mptGw(env, gw, {.holders = {alice}});
|
||||
mptGw.create(
|
||||
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer});
|
||||
mptGw.authorize({.account = alice});
|
||||
auto const mpt = mptGw["MPT"];
|
||||
env(pay(gw, alice, mpt(10'000)));
|
||||
env.close();
|
||||
|
||||
uint32_t seq = 0;
|
||||
testEachSponsorship(
|
||||
env,
|
||||
cosigning,
|
||||
sponsor,
|
||||
alice,
|
||||
1,
|
||||
1,
|
||||
tecINSUFFICIENT_RESERVE,
|
||||
[&](Env& env, auto const& submit) {
|
||||
seq = env.seq(alice);
|
||||
submit(
|
||||
escrow::create(alice, bob, mpt(100)),
|
||||
escrow::kCondition(escrow::kCb1),
|
||||
escrow::kCancelTime(env.now() + 100s));
|
||||
});
|
||||
|
||||
BEAST_EXPECT(
|
||||
env.le(keylet::escrow(alice, seq))->getAccountID(sfSponsor) == sponsor.id());
|
||||
|
||||
if (cosigning)
|
||||
{
|
||||
adjustAccountXRPBalance(env, sponsor, reserve(env, 1));
|
||||
}
|
||||
else
|
||||
{
|
||||
env(sponsor::set_reserve(sponsor, 0, 1), sponsor::SponseeAcc(bob));
|
||||
env.close();
|
||||
}
|
||||
|
||||
if (cosigning)
|
||||
{
|
||||
env(escrow::finish(bob, alice, seq),
|
||||
escrow::kCondition(escrow::kCb1),
|
||||
escrow::kFulfillment(escrow::kFb1),
|
||||
sponsor::As(sponsor, spfSponsorReserve),
|
||||
Sig(sfSponsorSignature, sponsor),
|
||||
Fee(baseFee * 150),
|
||||
Ter(tesSUCCESS));
|
||||
}
|
||||
else
|
||||
{
|
||||
env(escrow::finish(bob, alice, seq),
|
||||
escrow::kCondition(escrow::kCb1),
|
||||
escrow::kFulfillment(escrow::kFb1),
|
||||
sponsor::As(sponsor, spfSponsorReserve),
|
||||
Fee(baseFee * 150),
|
||||
Ter(tesSUCCESS));
|
||||
}
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
||||
BEAST_EXPECT(ownerCount(env, bob) == 1);
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 1);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1);
|
||||
|
||||
BEAST_EXPECT(
|
||||
env.le(keylet::mptoken(mptGw.issuanceID(), bob))->getAccountID(sfSponsor) ==
|
||||
sponsor.id());
|
||||
}
|
||||
|
||||
// A sponsored EscrowCreate must still verify that the source
|
||||
// can fund the escrow amount and stay above its own base
|
||||
@@ -4932,6 +5087,92 @@ public:
|
||||
BEAST_EXPECT(issuerSponsoredAfter == issuerSponsoredBefore);
|
||||
}
|
||||
|
||||
void
|
||||
testSelfEscrowFinishReserveGate()
|
||||
{
|
||||
testcase("Self-escrow finish reserve order gated by amendment");
|
||||
using namespace test::jtx;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
// Finishing a self-escrow (source == destination) whose trust line
|
||||
// was deleted while the escrow was outstanding auto-creates the line,
|
||||
// and the outcome of that reserve check depends on whether the escrow
|
||||
// reserve is released before delivery (Sponsor) or after (legacy).
|
||||
// With the source's balance in the one-increment window
|
||||
// [reserve(1), reserve(2)), the legacy order requires reserve(2) and
|
||||
// fails, while the Sponsor order requires reserve(1) and succeeds.
|
||||
auto runTest = [&](FeatureBitset features, TER expected) {
|
||||
Account const alice("alice");
|
||||
Account const gw("gw");
|
||||
auto const usd = gw["usd"];
|
||||
|
||||
Env env{*this, features};
|
||||
auto const baseFee = env.current()->fees().base;
|
||||
|
||||
env.fund(XRP(10000), alice, gw);
|
||||
env.close();
|
||||
|
||||
env(fset(gw, asfAllowTrustLineLocking));
|
||||
env.close();
|
||||
|
||||
env.trust(usd(1000), alice);
|
||||
env.close();
|
||||
env(pay(gw, alice, usd(100)));
|
||||
env.close();
|
||||
|
||||
// Escrow alice's entire USD balance to herself. The escrowed
|
||||
// IOUs return to the issuer, zeroing the line balance.
|
||||
auto const seq = env.seq(alice);
|
||||
env(escrow::create(alice, alice, usd(100)),
|
||||
escrow::kCondition(escrow::kCb1),
|
||||
escrow::kCancelTime(env.now() + 100s));
|
||||
env.close();
|
||||
|
||||
// Delete the now-empty trust line. Both accounts have
|
||||
// DefaultRipple set (jtx fund does that), so a plain limit-0
|
||||
// TrustSet returns the line to its default state.
|
||||
env(trust(alice, usd(0)));
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(keylet::trustLine(alice, gw, usd.currency)));
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 1); // just the escrow
|
||||
|
||||
// Put alice's balance in the window. Pay the excess away
|
||||
// directly: adjustAccountXRPBalance needs the Sponsor amendment.
|
||||
STAmount const target = reserve(env, 1) + XRP(1);
|
||||
env(pay(alice, env.master, env.balance(alice) - target - baseFee), Fee(baseFee));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.balance(alice) == target);
|
||||
|
||||
env(escrow::finish(alice, alice, seq),
|
||||
escrow::kCondition(escrow::kCb1),
|
||||
escrow::kFulfillment(escrow::kFb1),
|
||||
Fee(baseFee * 150),
|
||||
Ter(expected));
|
||||
env.close();
|
||||
|
||||
if (expected == tesSUCCESS)
|
||||
{
|
||||
BEAST_EXPECT(!env.le(keylet::escrow(alice, seq)));
|
||||
BEAST_EXPECT(env.le(keylet::trustLine(alice, gw, usd.currency)));
|
||||
BEAST_EXPECT(env.balance(alice, usd) == usd(100));
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 1); // the new line
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(env.le(keylet::escrow(alice, seq)));
|
||||
BEAST_EXPECT(!env.le(keylet::trustLine(alice, gw, usd.currency)));
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 1); // still the escrow
|
||||
}
|
||||
};
|
||||
|
||||
// Pre-amendment: legacy order — the escrow still counts against the
|
||||
// reserve while the auto-created line is checked.
|
||||
runTest(testableAmendments() - featureSponsor, tecNO_LINE_INSUF_RESERVE);
|
||||
|
||||
// Post-amendment: the escrow reserve is recycled into the new line.
|
||||
runTest(testableAmendments(), tesSUCCESS);
|
||||
}
|
||||
|
||||
void
|
||||
testFeeSponsoredVaultInvariant()
|
||||
{
|
||||
@@ -5092,6 +5333,7 @@ protected:
|
||||
|
||||
testZeroBalanceSponsoredPaymentFeePayerCheck();
|
||||
testTrustSetCounterpartySponsorMisroute();
|
||||
testSelfEscrowFinishReserveGate();
|
||||
|
||||
testFeeSponsoredVaultInvariant();
|
||||
testSponsoredObjectDeletionRefund();
|
||||
|
||||
Reference in New Issue
Block a user