Merge branch 'xrplf/sponsor' of https://github.com/XRPLF/rippled into mvadari/sponsor/apply-view-context

This commit is contained in:
Mayukha Vadari
2026-06-29 11:47:05 -04:00
5 changed files with 58 additions and 25 deletions

View File

@@ -24,14 +24,6 @@ isReserveSponsored(STTx const& tx)
return (tx.getFieldU32(sfSponsorFlags) & spfSponsorReserve) != 0u;
}
inline bool
isSponsorReserveCoSigning(STTx const& tx)
{
if (!tx.isFieldPresent(sfSponsorSignature))
return false;
return isReserveSponsored(tx);
}
inline std::optional<AccountID>
getTxReserveSponsorAccountID(STTx const& tx)
{

View File

@@ -343,14 +343,14 @@ checkInsufficientReserve(
{
if (sponsorSle)
{
auto const isCoSigning = isSponsorReserveCoSigning(tx);
auto const sle = view.read(
keylet::sponsorship(
sponsorSle->getAccountID(sfAccount), accSle->getAccountID(sfAccount)));
// prefunded sponsor should have a sponsorship entry
if (!isCoSigning && !sle)
// A reserve-sponsored tx must carry a sponsor signature
// (cosigning path) and/or have a pre-existing sponsorship SLE
// (prefunded path). Absence of both is an internal invariant break.
if (isReserveSponsored(tx) && !sle && !tx.isFieldPresent(sfSponsorSignature))
return tecINTERNAL; // LCOV_EXCL_LINE
if (sle)

View File

@@ -196,17 +196,15 @@ authorizeMPToken(
if (!sponsorSle)
return sponsorSle.error(); // LCOV_EXCL_LINE
auto const isSponsoredAndPreFunded = *sponsorSle && !isSponsorReserveCoSigning(ctx.tx);
// The reserve that is required to create the MPToken. Note
// that although the reserve increases with every item
// an account owns, in the case of MPTokens we only
// *enforce* a reserve if the user owns more than two
// items. This is similar to the reserve requirements of trust lines.
// If PreFunded Sponsor, it must be checked whether sufficient
// ReserveCount exists.
if (ownerCount(view, *sponsorSle ? *sponsorSle : sleAcct, journal) >= 2 ||
isSponsoredAndPreFunded)
// The "free-tier" shortcut (ownerCount < 2) does not apply once a sponsor is on
// the tx — the sponsor must always cover the reserve (via balance or prefunded
// budget), so this check always runs for sponsored transactions.
if (*sponsorSle || ownerCount(view, sleAcct, journal) >= 2)
{
if (auto const ret = checkInsufficientReserve(
view, ctx.tx, sleAcct, priorBalance, *sponsorSle, 1, 0, journal);

View File

@@ -334,9 +334,9 @@ TrustSet::doApply()
std::uint32_t const uOwnerCount = ownerCount(view(), *sponsorSle ? *sponsorSle : sle, j_);
bool const isSponsoredAndPreFunded = *sponsorSle && !isSponsorReserveCoSigning(ctx_.tx);
// If PreFunded Sponsor, it must be checked whether sufficient
// ReserveCount exists.
// The "free-tier" shortcut (ownerCount < 2) only applies when there is no sponsor.
// With any sponsor on the tx, the sponsor must cover the reserve (via balance or
// prefunded budget), so the reserve check always runs.
bool const freeTrustLine = uOwnerCount < 2 && !*sponsorSle;
std::uint32_t const uQualityIn(bQualityIn ? ctx_.tx.getFieldU32(sfQualityIn) : 0);
@@ -540,7 +540,7 @@ TrustSet::doApply()
// calling adjustOwnerCount().
if (auto const ret = checkInsufficientReserve(
view(), ctx_.tx, sleLowAccount, preFeeBalance_, *sponsorSle, 1, 0, j_);
isSponsoredAndPreFunded && !isTesSuccess(ret))
*sponsorSle && !isTesSuccess(ret))
return tecINSUF_RESERVE_LINE;
// Set reserve for low account.
@@ -569,7 +569,7 @@ TrustSet::doApply()
// calling adjustOwnerCount().
if (auto const ret = checkInsufficientReserve(
view(), ctx_.tx, sleHighAccount, preFeeBalance_, *sponsorSle, 1, 0, j_);
isSponsoredAndPreFunded && !isTesSuccess(ret))
*sponsorSle && !isTesSuccess(ret))
return tecINSUF_RESERVE_LINE;
// Set reserve for high account.

View File

@@ -746,6 +746,47 @@ public:
}
}
void
testSponsoredFreeTierReserve()
{
testcase("Sponsored Free-Tier Reserve");
using namespace test::jtx;
Account const alice("alice");
Account const issuer("issuer");
Account const sponsor("sponsor");
// Trust lines and MPTokens normally skip the reserve check when the
// holder's ownerCount < 2 (the "free-tier" / first-two-items shortcut). When the
// tx is sponsored, that shortcut must not apply — the sponsor must
// still cover the reserve.
Env env{*this, testableAmendments()};
env.fund(XRP(10000), alice, issuer);
// Sponsor is funded just below the reserve required to cover a single
// sponsored item.
env.fund(reserve(env, 1) - drops(1), sponsor);
env.close();
BEAST_EXPECT(ownerCount(env, alice) == 0);
MPTTester mptt(env, issuer, {.fund = false});
mptt.create();
// Free-tier trust line cosigned by an undercapitalized sponsor must
// fail — the holder's free-first-two-items shortcut does not let the
// sponsor skip the reserve check.
env(trust(alice, issuer["USD"](100)),
sponsor::As(sponsor, spfSponsorReserve),
Sig(sfSponsorSignature, sponsor),
Ter(tecNO_LINE_INSUF_RESERVE));
env.close();
// Free-tier MPTokenAuthorize must also fail for the same reason.
env(MPTTester::authorizeJV({.account = alice, .id = mptt.issuanceID()}),
sponsor::As(sponsor, spfSponsorReserve),
Sig(sfSponsorSignature, sponsor),
Ter(tecINSUFFICIENT_RESERVE));
env.close();
}
void
testTransferSponsor()
{
@@ -2821,10 +2862,11 @@ public:
env(noop(sponsor), ticket::Use(ticketSeq));
env.close();
// pass (free mptoken)
// pass (free-tier mptoken for the holder, but the sponsor is still
// charged a reserve increment regardless of the ownerCount < 2 shortcut).
if (cosigning)
{
adjustAccountXRPBalance(env, sponsor, reserve(env, 2) - drops(1));
adjustAccountXRPBalance(env, sponsor, reserve(env, 2));
env(jv,
sponsor::As(sponsor, spfSponsorReserve),
Sig(sfSponsorSignature, sponsor),
@@ -3723,6 +3765,7 @@ protected:
testSimpleSponsorshipSet();
testPreFundAndCosign();
testSponsoredFreeTierReserve();
testTransferSponsor();
testSponsorFee();