Merge branch 'xrplf/sponsor' into mvadari/sponsor/adjustment

This commit is contained in:
Mayukha Vadari
2026-06-30 17:32:18 -04:00
committed by GitHub
2 changed files with 291 additions and 9 deletions

View File

@@ -640,7 +640,28 @@ Transactor::payFee()
if (!sle)
return tefINTERNAL; // LCOV_EXCL_LINE
auto const feeAmountAfter = sle->getFieldAmount(feePayer.balanceField) - feePaid;
if (feePaid == beast::kZero)
return tesSUCCESS;
XRPAmount balance = beast::kZero;
if (sle->isFieldPresent(feePayer.balanceField))
{
balance = sle->getFieldAmount(feePayer.balanceField).xrp();
}
else if (feePayer.balanceField != sfFeeAmount)
{
return tefINTERNAL; // LCOV_EXCL_LINE
}
if (feePaid > balance)
{
if ((balance > beast::kZero) && !view().open())
return tecINSUFF_FEE;
return terINSUF_FEE_B;
}
auto const feeAmountAfter = balance - feePaid;
if (feeAmountAfter == beast::kZero && feePayer.balanceField == sfFeeAmount)
{
@@ -1302,7 +1323,15 @@ Transactor::reset(XRPAmount fee)
if (!payerSle)
return {tefINTERNAL, beast::kZero}; // LCOV_EXCL_LINE
auto const balance = payerSle->getFieldAmount(feePayer.balanceField).xrp();
XRPAmount balance = beast::kZero;
if (payerSle->isFieldPresent(feePayer.balanceField))
{
balance = payerSle->getFieldAmount(feePayer.balanceField).xrp();
}
else if (feePayer.balanceField != sfFeeAmount)
{
return {tefINTERNAL, beast::kZero}; // LCOV_EXCL_LINE
}
if (feePayer.type == FeePayerType::SponsorPreFunded && payerSle->isFieldPresent(sfMaxFee))
{

View File

@@ -11,6 +11,7 @@
#include <test/jtx/escrow.h>
#include <test/jtx/fee.h>
#include <test/jtx/flags.h>
#include <test/jtx/ledgerStateFix.h>
#include <test/jtx/mpt.h>
#include <test/jtx/multisign.h>
#include <test/jtx/noop.h>
@@ -29,6 +30,7 @@
#include <test/jtx/vault.h>
#include <xrpl/basics/Number.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/basics/strHex.h>
@@ -39,6 +41,7 @@
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/OpenView.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
@@ -51,12 +54,16 @@
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/tx/apply.h>
#include <xrpl/tx/applySteps.h>
#include <cstdint>
#include <functional>
#include <memory>
#include <optional>
#include <set>
#include <string>
#include <utility>
#include <vector>
namespace xrpl::test {
@@ -1836,6 +1843,66 @@ public:
BEAST_EXPECT(sle->getFieldAmount(sfFeeAmount) == drops(990)); // 1000 - MaxFee(10)
}
// LedgerStateFix charges an owner-reserve fee and can claim that fee
// while returning tecFAILED_PROCESSING. That path must be safe when the
// fee is pre-funded by a sponsorship object.
{
Env env{*this, testableAmendments()};
Account const alice("alice");
Account const sponsor("sponsor");
env.fund(XRP(1000), alice, sponsor);
env.close();
auto const fixFee = drops(env.current()->fees().increment);
env(sponsor::set_fee(sponsor, 0, fixFee), sponsor::SponseeAcc(alice));
env.close();
env(ledgerStateFix::nftPageLinks(alice, alice),
Fee(fixFee),
sponsor::As(sponsor, spfSponsorFee),
Ter(tecFAILED_PROCESSING));
if (auto const sle = env.le(keylet::sponsorship(sponsor, alice)); BEAST_EXPECT(sle))
BEAST_EXPECT(!sle->isFieldPresent(sfFeeAmount));
}
// If preclaim saw spendable sponsored FeeAmount but the apply view no
// longer has it, the fee path should fail cleanly instead of throwing.
{
Env env{*this, testableAmendments()};
Account const alice("alice");
Account const sponsor("sponsor");
env.fund(XRP(1000), alice, sponsor);
env.close();
auto const fixFee = drops(env.current()->fees().increment);
env(sponsor::set_fee(sponsor, 0, fixFee), sponsor::SponseeAcc(alice));
env.close();
OpenView overlay(&*env.closed());
auto jt = env.jt(
ledgerStateFix::nftPageLinks(alice, alice),
Fee(fixFee),
sponsor::As(sponsor, spfSponsorFee));
auto const pf = preflight(env.app(), overlay.rules(), *jt.stx, TapNone, env.journal);
BEAST_EXPECT(isTesSuccess(pf.ter));
auto const pc = preclaim(pf, env.app(), overlay);
BEAST_EXPECT(isTesSuccess(pc.ter));
auto const original = overlay.read(keylet::sponsorship(sponsor, alice));
if (BEAST_EXPECT(original))
{
auto sle = std::make_shared<SLE>(*original);
sle->makeFieldAbsent(sfFeeAmount);
overlay.rawReplace(sle);
}
auto const result = doApply(pc, env.app(), overlay);
BEAST_EXPECT(result.ter == terINSUF_FEE_B);
BEAST_EXPECT(!result.applied);
}
// test lsfSponsorshipRequireSignForFee
{
Env env{*this, testableAmendments()};
@@ -2170,10 +2237,11 @@ public:
std::optional<std::function<void()>> expected = std::nullopt)
{
using namespace test::jtx;
// auto const sponsorOwnerCountBefore = ownerCount(env, sponsor);
auto const sponseeOwnerCountBefore = ownerCount(env, sponsee);
auto const sponseeSponsoredOwnerCountBefore = sponsoredOwnerCount(env, sponsee);
auto const sponseeSponsoringOwnerCountBefore = sponsoringOwnerCount(env, sponsee);
auto const sponsorOwnerCountBefore = ownerCount(env, sponsor);
auto const sponsorSponsoredOwnerCountBefore = sponsoredOwnerCount(env, sponsor);
auto const sponsorSponsoringOwnerCountBefore = sponsoringOwnerCount(env, sponsor);
std::optional<Sig> sponsorSig =
@@ -2213,7 +2281,7 @@ public:
env.close();
}
if (sponsorReserveCount - 1 > 0)
if (sponsorReserveCount > 1)
{
env(sponsor::set(sponsor, 0, sponsorReserveCount - 1, XRP(1)),
sponsor::SponseeAcc(sponsee));
@@ -2226,8 +2294,53 @@ public:
}
env.close();
}
// A failed sponsored create must not consume prefunded reserve or mutate owner counts.
auto const sponseeOwnerCountBeforeAttempt = ownerCount(env, sponsee);
auto const sponseeSponsoredOwnerCountBeforeAttempt = sponsoredOwnerCount(env, sponsee);
auto const sponseeSponsoringOwnerCountBeforeAttempt =
sponsoringOwnerCount(env, sponsee);
auto const sponsorOwnerCountBeforeAttempt = ownerCount(env, sponsor);
auto const sponsorSponsoredOwnerCountBeforeAttempt = sponsoredOwnerCount(env, sponsor);
auto const sponsorSponsoringOwnerCountBeforeAttempt =
sponsoringOwnerCount(env, sponsor);
auto const sponsorshipSleBeforeAttempt = env.le(keylet::sponsorship(sponsor, sponsee));
bool const reserveCountPresentBeforeAttempt = sponsorshipSleBeforeAttempt &&
sponsorshipSleBeforeAttempt->isFieldPresent(sfRemainingOwnerCount);
std::uint32_t const reserveCountBeforeAttempt = reserveCountPresentBeforeAttempt
? sponsorshipSleBeforeAttempt->getFieldU32(sfRemainingOwnerCount)
: 0;
callback(env, submit(insufficientReserveResult));
env.close();
BEAST_EXPECT(ownerCount(env, sponsee) == sponseeOwnerCountBeforeAttempt);
BEAST_EXPECT(
sponsoredOwnerCount(env, sponsee) == sponseeSponsoredOwnerCountBeforeAttempt);
BEAST_EXPECT(
sponsoringOwnerCount(env, sponsee) == sponseeSponsoringOwnerCountBeforeAttempt);
BEAST_EXPECT(ownerCount(env, sponsor) == sponsorOwnerCountBeforeAttempt);
BEAST_EXPECT(
sponsoredOwnerCount(env, sponsor) == sponsorSponsoredOwnerCountBeforeAttempt);
BEAST_EXPECT(
sponsoringOwnerCount(env, sponsor) == sponsorSponsoringOwnerCountBeforeAttempt);
auto const sponsorshipSleAfterAttempt = env.le(keylet::sponsorship(sponsor, sponsee));
BEAST_EXPECT(
static_cast<bool>(sponsorshipSleAfterAttempt) ==
static_cast<bool>(sponsorshipSleBeforeAttempt));
if (sponsorshipSleAfterAttempt)
{
BEAST_EXPECT(
sponsorshipSleAfterAttempt->isFieldPresent(sfRemainingOwnerCount) ==
reserveCountPresentBeforeAttempt);
if (reserveCountPresentBeforeAttempt)
{
BEAST_EXPECT(
sponsorshipSleAfterAttempt->getFieldU32(sfRemainingOwnerCount) ==
reserveCountBeforeAttempt);
}
}
}
// Success
@@ -2250,6 +2363,13 @@ public:
if (!cosigning)
{
// Prefunded success consumes the reserved owner slot before cleanup.
auto const sponsorshipSle = env.le(keylet::sponsorship(sponsor, sponsee));
BEAST_EXPECT(sponsorshipSle);
BEAST_EXPECT(
!sponsorshipSle->isFieldPresent(sfRemainingOwnerCount) ||
sponsorshipSle->getFieldU32(sfRemainingOwnerCount) == 0);
// cleanup sponsorship
env(sponsor::del(sponsor), sponsor::SponseeAcc(sponsee));
env.close();
@@ -2268,6 +2388,8 @@ public:
sponsorReserveCount);
BEAST_EXPECT(
sponsoringOwnerCount(env, sponsee) - sponseeSponsoringOwnerCountBefore == 0);
BEAST_EXPECT(ownerCount(env, sponsor) == sponsorOwnerCountBefore);
BEAST_EXPECT(sponsoredOwnerCount(env, sponsor) == sponsorSponsoredOwnerCountBefore);
BEAST_EXPECT(
sponsoringOwnerCount(env, sponsor) - sponsorSponsoringOwnerCountBefore ==
sponsorReserveCount);
@@ -2510,6 +2632,7 @@ public:
Account const alice("alice");
Account const sponsor("sponsor");
Account const sponsor2("sponsor2");
auto const credType = std::string("credType");
{
Env env{*this, testableAmendments()};
@@ -2537,6 +2660,114 @@ public:
env.close();
}
else
{
env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice));
env.close();
// No sponsor signature here: this exercises the prefunded reassign path.
env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key),
sponsor::As(sponsor2, spfSponsorReserve));
env.close();
auto const sponsor2Sle = env.le(keylet::sponsorship(sponsor2, alice));
BEAST_EXPECT(sponsor2Sle);
if (sponsor2Sle)
{
BEAST_EXPECT(
!sponsor2Sle->isFieldPresent(sfRemainingOwnerCount) ||
sponsor2Sle->getFieldU32(sfRemainingOwnerCount) == 0);
}
}
BEAST_EXPECT(ownerCount(env, alice) == 1);
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1);
// DepositPreauthDelete
env(deposit::unauth(alice, sponsor));
env.close();
BEAST_EXPECT(ownerCount(env, alice) == 0);
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0);
}
{
Env env{*this, testableAmendments()};
env.fund(XRP(1000000), alice, sponsor);
env.close();
auto const authCreds = std::vector<deposit::AuthorizeCredentials>{
{.issuer = sponsor, .credType = credType}};
auto const preauthKeylet = keylet::depositPreauth(
alice.id(),
std::set<std::pair<AccountID, Slice>>{
{sponsor.id(), Slice(credType.data(), credType.size())}});
// Cover DepositPreauth's sfAuthorizeCredentials sponsor-reserve branch.
testEachSponsorship(
env,
cosigning,
sponsor,
alice,
1,
1,
tecINSUFFICIENT_RESERVE,
[&](Env&, auto const& submit) {
submit(deposit::authCredentials(alice, authCreds));
});
// Cover sfUnauthorizeCredentials cleanup for a sponsored preauth object.
BEAST_EXPECT(env.le(preauthKeylet));
BEAST_EXPECT(ownerCount(env, alice) == 1);
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1);
env(deposit::unauthCredentials(alice, authCreds));
env.close();
BEAST_EXPECT(!env.le(preauthKeylet));
BEAST_EXPECT(ownerCount(env, alice) == 0);
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
}
}
void
testDID(bool cosigning)
{
testcase("DID");
using namespace test::jtx;
Account const alice("alice");
Account const sponsor("sponsor");
Account const sponsor2("sponsor2");
{
Env env{*this, testableAmendments()};
env.fund(XRP(1000000), alice, sponsor, sponsor2);
env.close();
// DIDSet
testEachSponsorship(
env,
cosigning,
sponsor,
alice,
1,
1,
tecINSUFFICIENT_RESERVE,
[&](Env& env, auto const& submit) { submit(did::set(alice), did::Uri("uri")); });
// transfer sponsor
auto const keylet = keylet::did(alice);
if (cosigning)
{
env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key),
sponsor::As(sponsor2, spfSponsorReserve),
Sig(sfSponsorSignature, sponsor2));
env.close();
}
else
{
env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice));
env.close();
@@ -2554,11 +2785,15 @@ public:
auto const sponsorshipSle = env.le(keylet::sponsorship(sponsor2, alice));
BEAST_EXPECT(sponsorshipSle);
if (sponsorshipSle)
BEAST_EXPECT(sponsorshipSle->getFieldU32(sfRemainingOwnerCount) == 0);
{
BEAST_EXPECT(
!sponsorshipSle->isFieldPresent(sfRemainingOwnerCount) ||
sponsorshipSle->getFieldU32(sfRemainingOwnerCount) == 0);
}
}
// DepositPreauthDelete
env(deposit::unauth(alice, sponsor));
// DIDDelete
env(did::del(alice));
env.close();
BEAST_EXPECT(ownerCount(env, alice) == 0);
@@ -3429,6 +3664,12 @@ public:
auto const requiredFee = drops(env.current()->fees().increment);
env(acctdelete(alice, bob), Fee(requiredFee), Ter(tecNO_SPONSOR_PERMISSION));
// The failed delete must leave the account sponsored by the original sponsor.
auto const aliceSle = env.le(keylet::account(alice));
BEAST_EXPECT(aliceSle);
if (aliceSle)
BEAST_EXPECT(aliceSle->getAccountID(sfSponsor) == sponsor.id());
auto const sponsorSle = env.le(keylet::account(sponsor));
BEAST_EXPECT(sponsorSle->getFieldU32(sfSponsoringAccountCount) == 1);
@@ -3472,13 +3713,19 @@ public:
// Verify sfSponsoringOwnerCount is set on sponsor
auto const sponsorSle = env.le(keylet::account(sponsor));
BEAST_EXPECT(sponsorSle->isFieldPresent(sfSponsoringOwnerCount));
BEAST_EXPECT(sponsorSle->getFieldU32(sfSponsoringOwnerCount) >= 1);
auto const sponsoringOwnerCount = sponsorSle->getFieldU32(sfSponsoringOwnerCount);
BEAST_EXPECT(sponsoringOwnerCount >= 1);
incLgrSeqForAccDel(env, sponsor);
// AccountDelete should fail
auto const requiredFee = drops(env.current()->fees().increment);
env(acctdelete(sponsor, bob), Fee(requiredFee), Ter(tecHAS_OBLIGATIONS));
// The failed delete must not decrement the outstanding sponsored-object count.
auto const sponsorSleAfter = env.le(keylet::account(sponsor));
BEAST_EXPECT(sponsorSleAfter->isFieldPresent(sfSponsoringOwnerCount));
BEAST_EXPECT(
sponsorSleAfter->getFieldU32(sfSponsoringOwnerCount) == sponsoringOwnerCount);
}
{
@@ -3495,13 +3742,19 @@ public:
// Verify sfSponsoringAccountCount is set on sponsor
auto const sponsorSle = env.le(keylet::account(sponsor));
BEAST_EXPECT(sponsorSle->isFieldPresent(sfSponsoringAccountCount));
BEAST_EXPECT(sponsorSle->getFieldU32(sfSponsoringAccountCount) == 1);
auto const sponsoringAccountCount = sponsorSle->getFieldU32(sfSponsoringAccountCount);
BEAST_EXPECT(sponsoringAccountCount == 1);
incLgrSeqForAccDel(env, sponsor);
// AccountDelete should fail
auto const requiredFee = drops(env.current()->fees().increment);
env(acctdelete(sponsor, bob), Fee(requiredFee), Ter(tecHAS_OBLIGATIONS));
// The failed delete must not decrement the outstanding sponsored-account count.
auto const sponsorSleAfter = env.le(keylet::account(sponsor));
BEAST_EXPECT(sponsorSleAfter->isFieldPresent(sfSponsoringAccountCount));
BEAST_EXPECT(
sponsorSleAfter->getFieldU32(sfSponsoringAccountCount) == sponsoringAccountCount);
}
}