Merge branch 'xrplf/sponsor' of https://github.com/XRPLF/rippled into mvadari/sponsor/fixes

This commit is contained in:
Mayukha Vadari
2026-07-08 11:18:41 -04:00
3 changed files with 430 additions and 140 deletions

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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();