Merge branch 'xrplf/sponsor' into mvadari/sponsor/sherlock-1860

This commit is contained in:
Mayukha Vadari
2026-06-30 17:58:38 -04:00
committed by GitHub
14 changed files with 808 additions and 233 deletions

View File

@@ -0,0 +1,19 @@
#pragma once
#include <cstddef>
#include <cstdint>
namespace xrpl {
constexpr uint32_t kMinOracleReserveCount = 1;
constexpr uint32_t kMaxOracleReserveCount = 2;
constexpr std::size_t kOracleReserveCountThreshold = 5;
inline uint32_t
calculateOracleReserve(std::size_t priceDataSeriesCount)
{
return priceDataSeriesCount > kOracleReserveCountThreshold ? kMaxOracleReserveCount
: kMinOracleReserveCount;
}
} // namespace xrpl

View File

@@ -3,6 +3,7 @@
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/OracleHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STTx.h>
@@ -123,4 +124,99 @@ removeSponsorFromLedgerEntry(SLE::ref sle, SF_ACCOUNT const& field = sfSponsor)
sle->makeFieldAbsent(field);
}
template <typename T>
inline std::optional<AccountID>
getLedgerEntryOwner(ReadView const& view, T const& sle, AccountID const& account)
{
switch (sle->getType())
{
case ltCHECK:
case ltESCROW:
case ltPAYCHAN:
case ltMPTOKEN:
case ltDELEGATE:
case ltDEPOSIT_PREAUTH:
return sle->getAccountID(sfAccount);
case ltMPTOKEN_ISSUANCE:
return sle->getAccountID(sfIssuer);
case ltSIGNER_LIST: {
auto const signerList = view.read(keylet::signers(account));
if (!signerList)
return std::nullopt;
if (signerList->key() == sle->key())
return account;
return std::nullopt;
}
case ltCREDENTIAL: {
if (sle->isFlag(lsfAccepted))
return sle->getAccountID(sfSubject);
return sle->getAccountID(sfIssuer);
}
case ltRIPPLE_STATE: {
if (sle->isFlag(lsfHighReserve))
{
auto const highAccount = sle->getFieldAmount(sfHighLimit).getIssuer();
if (highAccount == account)
return highAccount;
}
if (sle->isFlag(lsfLowReserve))
{
auto const lowAccount = sle->getFieldAmount(sfLowLimit).getIssuer();
if (lowAccount == account)
return lowAccount;
}
return std::nullopt;
}
default:
UNREACHABLE("Object is not supported by sponsorship.");
return std::nullopt;
};
}
template <typename T>
inline std::uint32_t
getLedgerEntryOwnerCount(T const& sle)
{
switch (sle->getType())
{
case ltORACLE: {
return calculateOracleReserve(sle->getFieldArray(sfPriceDataSeries).size());
}
// Vaults require 2 owner counts (the vault and a pseudo-account)
case ltVAULT:
return 2;
default:
return 1;
}
};
template <typename T>
inline SF_ACCOUNT const&
getLedgerEntrySponsorField(T const& sle, AccountID const& owner)
{
switch (sle->getType())
{
case ltRIPPLE_STATE: {
if (sle->isFlag(lsfHighReserve))
{
auto const highAccount = sle->getFieldAmount(sfHighLimit).getIssuer();
if (highAccount == owner)
return sfHighSponsor;
}
if (sle->isFlag(lsfLowReserve))
{
auto const lowAccount = sle->getFieldAmount(sfLowLimit).getIssuer();
if (lowAccount == owner)
return sfLowSponsor;
}
// LCOV_EXCL_START
UNREACHABLE("Should not happen. Owner should be checked before calling this function.");
return sfSponsor;
// LCOV_EXCL_STOP
}
default:
return sfSponsor;
}
};
} // namespace xrpl

View File

@@ -22,12 +22,6 @@ public:
{
}
static uint32_t
calculateOracleReserve(std::size_t count)
{
return count > 5 ? 2 : 1;
}
static NotTEC
preflight(PreflightContext const& ctx);

View File

@@ -35,6 +35,7 @@
#include <memory>
#include <optional>
#include <stdexcept>
#include <utility>
namespace xrpl {
@@ -144,7 +145,7 @@ addEmptyHolding(
if (accountID == mptIssue.getIssuer())
return tesSUCCESS;
return authorizeMPToken(view, tx, priorBalance, mptID, accountID, journal);
return authorizeMPToken(view, tx, priorBalance, mptID, accountID, journal, 0, std::nullopt);
}
[[nodiscard]] TER
@@ -193,9 +194,14 @@ authorizeMPToken(
// - add the new mptokenKey to the owner directory
// - create the MPToken object for the holder
auto const sponsorSle = getTxReserveSponsor(view, tx);
if (!sponsorSle)
return sponsorSle.error(); // LCOV_EXCL_LINE
SLE::pointer sponsorSle;
if (account == tx[sfAccount])
{
auto sle = getTxReserveSponsor(view, tx);
if (!sle)
return sle.error(); // LCOV_EXCL_LINE
sponsorSle = std::move(*sle);
}
// The reserve that is required to create the MPToken. Note
// that although the reserve increases with every item
@@ -205,10 +211,10 @@ authorizeMPToken(
// 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(sleAcct, journal) >= 2)
if (sponsorSle || ownerCount(sleAcct, journal) >= 2)
{
if (auto const ret = checkInsufficientReserve(
view, tx, sleAcct, priorBalance, *sponsorSle, 1, 0, journal);
view, tx, sleAcct, priorBalance, sponsorSle, 1, 0, journal);
!isTesSuccess(ret))
return ret;
}
@@ -235,8 +241,8 @@ authorizeMPToken(
view.insert(mptoken);
// Update owner count.
adjustOwnerCount(view, sleAcct, *sponsorSle, 1, journal);
addSponsorToLedgerEntry(mptoken, *sponsorSle);
adjustOwnerCount(view, sleAcct, sponsorSle, 1, journal);
addSponsorToLedgerEntry(mptoken, sponsorSle);
return tesSUCCESS;
}

View File

@@ -665,7 +665,9 @@ addEmptyHolding(
return tecDUPLICATE;
SLE::pointer sponsorSle;
if (!isPseudoAccount(sleDst))
// A reserve sponsor only covers tx.Account's own objects.
if (!isPseudoAccount(sleDst) && accountID == tx[sfAccount])
{
auto sle = getTxReserveSponsor(view, tx);
if (!sle)

View File

@@ -35,6 +35,7 @@
#include <xrpl/protocol/SystemParameters.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/TxMeta.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/server/LoadFeeTrack.h>
@@ -202,6 +203,46 @@ preflight1Sponsor(PreflightContext const& ctx, AccountID const& id)
JLOG(ctx.j.debug()) << "preflight1: invalid sponsor flags";
return temINVALID_FLAG;
}
// Reserve sponsorship is only permitted for an explicit allow-list of
// transaction types, for v1. All other tx types reject spfSponsorReserve here.
if ((sponsorFlags & spfSponsorReserve) != 0u)
{
static std::unordered_set<TxType> const kReserveSponsorAllowed = {
// Explicitly allow-listed for v1.
ttDELEGATE_SET,
ttDEPOSIT_PREAUTH,
ttPAYMENT,
ttSIGNER_LIST_SET,
ttCHECK_CANCEL,
ttCHECK_CASH,
ttCHECK_CREATE,
ttESCROW_CANCEL,
ttESCROW_CREATE,
ttESCROW_FINISH,
ttPAYCHAN_CLAIM,
ttPAYCHAN_CREATE,
ttPAYCHAN_FUND,
ttCLAWBACK,
ttMPTOKEN_AUTHORIZE,
ttMPTOKEN_ISSUANCE_CREATE,
ttMPTOKEN_ISSUANCE_DESTROY,
ttMPTOKEN_ISSUANCE_SET,
ttTRUST_SET,
ttCREDENTIAL_ACCEPT,
ttCREDENTIAL_CREATE,
ttCREDENTIAL_DELETE,
ttACCOUNT_SET,
ttREGULAR_KEY_SET,
ttSPONSORSHIP_TRANSFER,
};
if (!kReserveSponsorAllowed.contains(ctx.tx.getTxnType()))
{
JLOG(ctx.j.debug())
<< "preflight1: spfSponsorReserve not allowed for this transaction type";
return temINVALID_FLAG;
}
}
}
else
{
@@ -599,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)
{
@@ -1261,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

@@ -4,13 +4,13 @@
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/OracleHelpers.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/transactors/oracle/OracleSet.h>
#include <cstdint>
#include <memory>
@@ -62,7 +62,7 @@ SponsorshipOwnerCountsMatch::visitEntry(
if (!sle->isFieldPresent(sfSponsor))
return 0;
auto const priceDataSeries = sle->getFieldArray(sfPriceDataSeries);
return OracleSet::calculateOracleReserve(priceDataSeries.size());
return calculateOracleReserve(priceDataSeries.size());
}
case ltVAULT: {
if (!sle->isFieldPresent(sfSponsor))

View File

@@ -16,7 +16,6 @@
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/Transactor.h>
#include <xrpl/tx/transactors/oracle/OracleSet.h>
#include <bit>
#include <cstdint>
@@ -54,6 +53,9 @@ SponsorshipTransfer::preflight(PreflightContext const& ctx)
if (ctx.tx.isFlag(tfSponsorshipCreate))
{
// Sponsor must be included
// SponsorFlags.spfSponsorReserve must be included
// Sponsee must be excluded
if (!isReserveSponsored(ctx.tx))
{
JLOG(ctx.j.debug())
@@ -69,6 +71,9 @@ SponsorshipTransfer::preflight(PreflightContext const& ctx)
}
if (ctx.tx.isFlag(tfSponsorshipReassign))
{
// Sponsor must be included
// SponsorFlags.spfSponsorReserve must be included
// Sponsee must be excluded
if (!isReserveSponsored(ctx.tx))
{
JLOG(ctx.j.debug())
@@ -84,6 +89,8 @@ SponsorshipTransfer::preflight(PreflightContext const& ctx)
}
if (ctx.tx.isFlag(tfSponsorshipEnd))
{
// Sponsor must be excluded
// SponsorFlags.spfSponsorReserve must be excluded
if (isReserveSponsored(ctx.tx))
{
JLOG(ctx.j.debug())
@@ -117,143 +124,18 @@ SponsorshipTransfer::preflight(PreflightContext const& ctx)
return tesSUCCESS;
}
template <typename T>
inline std::optional<AccountID>
getLedgerEntryOwner(ReadView const& view, T const& sle, AccountID const& account)
{
switch (sle->getType())
{
case ltNFTOKEN_OFFER:
case ltORACLE:
case ltPERMISSIONED_DOMAIN:
case ltVAULT:
case ltLOAN_BROKER:
return sle->getAccountID(sfOwner);
case ltCHECK:
case ltDID:
case ltTICKET:
case ltOFFER:
case ltXCHAIN_OWNED_CLAIM_ID:
case ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID:
case ltESCROW:
case ltPAYCHAN:
case ltMPTOKEN:
case ltDELEGATE:
case ltBRIDGE:
case ltDEPOSIT_PREAUTH:
return sle->getAccountID(sfAccount);
case ltMPTOKEN_ISSUANCE:
return sle->getAccountID(sfIssuer);
case ltLOAN:
return sle->getAccountID(sfBorrower);
case ltSIGNER_LIST: {
auto const signerList = view.read(keylet::signers(account));
if (!signerList)
return std::nullopt;
if (signerList->key() == sle->key())
return account;
return std::nullopt;
}
case ltCREDENTIAL: {
if (sle->isFlag(lsfAccepted))
return sle->getAccountID(sfSubject);
return sle->getAccountID(sfIssuer);
}
case ltNFTOKEN_PAGE: {
// the upper 20 bytes of the index of ltNFTokenPage are the Owner's
// AccountID
uint256 const& key = sle->key();
return AccountID::fromVoid(key.data());
}
case ltRIPPLE_STATE: {
if (sle->isFlag(lsfHighReserve))
{
auto const highAccount = sle->getFieldAmount(sfHighLimit).getIssuer();
if (highAccount == account)
return highAccount;
}
if (sle->isFlag(lsfLowReserve))
{
auto const lowAccount = sle->getFieldAmount(sfLowLimit).getIssuer();
if (lowAccount == account)
return lowAccount;
}
return std::nullopt;
}
case ltACCOUNT_ROOT: {
// AccountRoot is not supported for object sponsorship
return std::nullopt;
}
case ltNEGATIVE_UNL:
case ltDIR_NODE:
case ltAMENDMENTS:
case ltLEDGER_HASHES:
case ltFEE_SETTINGS:
case ltAMM:
return std::nullopt;
default:
return std::nullopt;
};
}
template <typename T>
inline std::uint32_t
getLedgerEntryOwnerCount(T const& sle)
{
switch (sle->getType())
{
case ltORACLE: {
return OracleSet::calculateOracleReserve(sle->getFieldArray(sfPriceDataSeries).size());
}
// Vaults require 2 owner counts (the vault and a pseudo-account)
case ltVAULT:
return 2;
default:
return 1;
}
};
template <typename T>
inline SF_ACCOUNT const&
getLedgerEntrySponsorField(T const& sle, AccountID const& owner)
{
switch (sle->getType())
{
case ltRIPPLE_STATE: {
if (sle->isFlag(lsfHighReserve))
{
auto const highAccount = sle->getFieldAmount(sfHighLimit).getIssuer();
if (highAccount == owner)
return sfHighSponsor;
}
if (sle->isFlag(lsfLowReserve))
{
auto const lowAccount = sle->getFieldAmount(sfLowLimit).getIssuer();
if (lowAccount == owner)
return sfLowSponsor;
}
// LCOV_EXCL_START
UNREACHABLE("Should not happen. Owner should be checked before calling this function.");
return sfSponsor;
// LCOV_EXCL_STOP
}
default:
return sfSponsor;
}
};
TER
SponsorshipTransfer::preclaim(PreclaimContext const& ctx)
{
auto const index = ctx.tx[~sfObjectID];
auto const newSponsorSle = getTxReserveSponsor(ctx.view, ctx.tx);
if (!newSponsorSle)
return newSponsorSle.error(); // LCOV_EXCL_LINE
auto const newSponsorSleExpected = getTxReserveSponsor(ctx.view, ctx.tx);
if (!newSponsorSleExpected)
return newSponsorSleExpected.error(); // LCOV_EXCL_LINE
auto const newSponsorSle = *newSponsorSleExpected;
bool const isObjectSponsor = index != std::nullopt;
bool const isObjectSponsor = !!index;
auto const account = ctx.tx[sfAccount];
auto const sponseeID = ctx.tx[~sfSponsee].value_or(account);
auto const sponseeSle = ctx.view.read(keylet::account(sponseeID));
if (!sponseeSle)
@@ -265,26 +147,49 @@ SponsorshipTransfer::preclaim(PreclaimContext const& ctx)
if (!sle)
return tecNO_ENTRY;
auto const ownerCountDelta = getLedgerEntryOwnerCount(sle);
// v1 scope: an object is only sponsorable via SponsorshipTransfer if
// its creating transaction type is itself permitted to set
// spfSponsorReserve (the allow-list in preflight1Sponsor). Otherwise
// an Oracle / Ticket / DID / etc. could be retroactively sponsored
// even though its creating tx cannot be, leaving downstream
// transactors with no path to maintain the sponsorship invariants.
switch (sle->getType())
{
case ltDELEGATE:
case ltDEPOSIT_PREAUTH:
case ltMPTOKEN:
case ltMPTOKEN_ISSUANCE:
case ltCREDENTIAL:
case ltRIPPLE_STATE:
case ltSIGNER_LIST:
case ltCHECK:
case ltESCROW:
case ltPAYCHAN:
break;
default:
return tecNO_PERMISSION;
}
std::uint32_t const ownerCountDelta = 1;
auto const owner = getLedgerEntryOwner(ctx.view, sle, sponseeID);
if (!owner || owner != sponseeID)
if (!owner.has_value() || owner.value() != sponseeID)
return tecNO_PERMISSION;
auto const& sponsorField = getLedgerEntrySponsorField(sle, *owner);
auto const& sponsorField = getLedgerEntrySponsorField(sle, owner.value());
if (ctx.tx.isFlag(tfSponsorshipCreate))
{
if (!*newSponsorSle)
if (!newSponsorSle)
return tecNO_PERMISSION;
// check object is not sponsored yet
// check that the object is not sponsored yet
if (sle->isFieldPresent(sponsorField))
return tecNO_PERMISSION;
}
else if (ctx.tx.isFlag(tfSponsorshipReassign))
{
if (!*newSponsorSle)
if (!newSponsorSle)
return tecNO_PERMISSION;
// check object is already ctx.sponsored
@@ -293,7 +198,7 @@ SponsorshipTransfer::preclaim(PreclaimContext const& ctx)
}
else if (ctx.tx.isFlag(tfSponsorshipEnd))
{
if (*newSponsorSle)
if (newSponsorSle)
return tecNO_PERMISSION;
// check object is sponsored
@@ -313,7 +218,7 @@ SponsorshipTransfer::preclaim(PreclaimContext const& ctx)
ctx.tx,
sponseeSle,
sponseeSle->getFieldAmount(sfBalance),
*newSponsorSle,
newSponsorSle,
ownerCountDelta,
0,
ctx.j);
@@ -324,7 +229,7 @@ SponsorshipTransfer::preclaim(PreclaimContext const& ctx)
{
if (ctx.tx.isFlag(tfSponsorshipCreate))
{
if (!*newSponsorSle)
if (!newSponsorSle)
return tecNO_PERMISSION;
// check account is not sponsored yet
@@ -333,7 +238,7 @@ SponsorshipTransfer::preclaim(PreclaimContext const& ctx)
}
else if (ctx.tx.isFlag(tfSponsorshipReassign))
{
if (!*newSponsorSle)
if (!newSponsorSle)
return tecNO_PERMISSION;
// check account is already sponsored
@@ -342,7 +247,7 @@ SponsorshipTransfer::preclaim(PreclaimContext const& ctx)
}
else if (ctx.tx.isFlag(tfSponsorshipEnd))
{
if (*newSponsorSle)
if (newSponsorSle)
return tecNO_PERMISSION;
// check account is sponsored
@@ -365,7 +270,7 @@ SponsorshipTransfer::preclaim(PreclaimContext const& ctx)
ctx.tx,
sponseeSle,
sponseeSle->getFieldAmount(sfBalance),
*newSponsorSle,
newSponsorSle,
0,
1,
ctx.j);
@@ -450,7 +355,7 @@ SponsorshipTransfer::doApply()
if (!ownerSle)
return tefINTERNAL; // LCOV_EXCL_LINE
std::int64_t const ownerCountDelta = getLedgerEntryOwnerCount(objSle);
std::int64_t const ownerCountDelta = 1;
auto const& sponsorField = getLedgerEntrySponsorField(objSle, *ownerID);

View File

@@ -8,6 +8,7 @@
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/LendingHelpers.h>
#include <xrpl/ledger/helpers/SponsorHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
@@ -56,6 +57,12 @@ LoanSet::preflight(PreflightContext const& ctx)
auto const& tx = ctx.tx;
if (tx.isFieldPresent(sfSponsorFlags) && isReserveSponsored(tx))
{
JLOG(ctx.j.debug()) << "LoanSet: reserve sponsorship is not allowed.";
return temINVALID_FLAG;
}
// Special case for Batch inner transactions
if (tx.isFlag(tfInnerBatchTxn) && ctx.rules.enabled(featureBatch) &&
!tx.isFieldPresent(sfCounterparty))

View File

@@ -18,6 +18,7 @@
#include <test/jtx/permissioned_domains.h>
#include <test/jtx/seq.h>
#include <test/jtx/sig.h>
#include <test/jtx/sponsor.h>
#include <test/jtx/tags.h>
#include <test/jtx/ter.h>
#include <test/jtx/trust.h>
@@ -4436,11 +4437,12 @@ protected:
Account const lender{"lender"};
Account const issuer{"issuer"};
Account const borrower{"borrower"};
Account const sponsor{"sponsor"};
auto const iou = issuer["IOU"];
auto testWrapper = [&](auto&& test) {
Env env(*this);
env.fund(XRP(1'000), lender, issuer, borrower);
env.fund(XRP(1'000), lender, issuer, borrower, sponsor);
env(trust(lender, iou(10'000'000)));
env(pay(issuer, lender, iou(5'000'000)));
BrokerInfo const brokerInfo{createVaultAndBroker(env, issuer["IOU"], lender)};
@@ -4455,6 +4457,15 @@ protected:
BrokerInfo const& brokerInfo,
jtx::Fee const& loanSetFee,
Number const& debtMaximumRequest) {
for (auto const sponsorFlags : {spfSponsorReserve, spfSponsorReserve | spfSponsorFee})
{
env(set(borrower, brokerInfo.brokerID, debtMaximumRequest),
sponsor::As(sponsor, sponsorFlags),
Sig(sfCounterpartySignature, lender),
loanSetFee,
Ter(temINVALID_FLAG));
}
// first temBAD_SIGNER: TODO
// invalid grace period
{

View File

@@ -11,9 +11,11 @@
#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>
#include <test/jtx/offer.h>
#include <test/jtx/paths.h>
#include <test/jtx/pay.h>
#include <test/jtx/sendmax.h>
@@ -22,10 +24,13 @@
#include <test/jtx/sponsor.h>
#include <test/jtx/ter.h>
#include <test/jtx/ticket.h>
#include <test/jtx/token.h>
#include <test/jtx/trust.h>
#include <test/jtx/txflags.h>
#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>
@@ -36,9 +41,11 @@
#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>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
@@ -47,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 {
@@ -677,6 +688,7 @@ public:
using namespace test::jtx;
Account const alice("alice");
Account const bob("bob");
Account const charlie("charlie");
Account const sponsor("sponsor");
{
@@ -715,10 +727,10 @@ public:
{
// if pre-funded value is not enough, error
Env env{*this, testableAmendments()};
env.fund(XRP(10000), alice, bob, sponsor);
env.fund(XRP(10000), alice, bob, charlie, sponsor);
env.close();
env(sponsor::set(sponsor, 0, 10, XRP(10), XRP(100)),
env(sponsor::set(sponsor, 0, 1, XRP(10), XRP(100)),
sponsor::SponseeAcc(alice),
Ter(tesSUCCESS));
env.close();
@@ -847,19 +859,21 @@ public:
Env env{*this, testableAmendments()};
Account const alice("alice");
Account const bob("bob");
Account const charlie("charlie");
Account const sponsor("sponsor");
env.fund(XRP(10000), alice, bob, sponsor);
env.close();
{
// sponsor object
env(did::set(alice),
did::Uri("uri"),
env.fund(XRP(1000), charlie);
env.close();
env(deposit::auth(alice, charlie),
sponsor::As(sponsor, spfSponsorReserve),
Sig(sfSponsorSignature, sponsor));
env.close();
auto const keylet = keylet::did(alice);
auto const keylet = keylet::depositPreauth(alice, charlie);
env(sponsor::transfer(bob, tfSponsorshipEnd, keylet.key),
sponsor::SponseeAcc(alice),
Ter(tecNO_PERMISSION));
@@ -1451,6 +1465,73 @@ public:
Ter(tecNO_PERMISSION));
}
}
{
// existing owner objects that are outside the v1 SponsorshipTransfer
// object allow-list
Env env{*this, testableAmendments()};
Account const alice("alice");
Account const sponsor("sponsor");
env.fund(XRP(10000), alice, sponsor);
env.close();
auto const checkBlocked = [&](Account const& account, uint256 const& objectID) {
env(sponsor::transfer(account, tfSponsorshipCreate, objectID),
sponsor::As(sponsor, spfSponsorReserve),
Sig(sfSponsorSignature, sponsor),
Ter(tecNO_PERMISSION));
env.close();
};
auto const ticketSeq = env.seq(alice);
env(ticket::create(alice, 1));
env.close();
auto const ticketID = keylet::TicketT()(alice, ticketSeq + 1).key;
BEAST_EXPECT(env.le(keylet::unchecked(ticketID)));
checkBlocked(alice, ticketID);
env(did::setValid(alice));
env.close();
auto const didKeylet = keylet::did(alice.id());
BEAST_EXPECT(env.le(didKeylet));
checkBlocked(alice, didKeylet.key);
env(token::mint(alice, 0u));
env.close();
auto const nftPageKeylet = keylet::nftpageMax(alice);
BEAST_EXPECT(env.le(nftPageKeylet));
checkBlocked(alice, nftPageKeylet.key);
Account const borrower("borrower");
env.fund(XRP(1000000), borrower);
env.close();
PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
Vault const vault{env};
auto [vaultTx, vaultKeylet] = vault.create({.owner = alice, .asset = xrpAsset});
env(vaultTx);
env.close();
env(vault.deposit(
{.depositor = alice, .id = vaultKeylet.key, .amount = xrpAsset(1000)}));
env.close();
auto const brokerKeylet = keylet::loanbroker(alice.id(), env.seq(alice));
env(loanBroker::set(alice, vaultKeylet.key),
loanBroker::kDebtMaximum(xrpAsset(1000).value()),
loanBroker::kManagementFeeRate(TenthBips16{0}),
loanBroker::kCoverRateMinimum(TenthBips32{0}),
loanBroker::kCoverRateLiquidation(TenthBips32{0}));
env.close();
auto const loanKeylet = keylet::loan(brokerKeylet.key, 1);
env(loan::set(borrower, brokerKeylet.key, xrpAsset(100).value()),
Sig(sfCounterpartySignature, alice),
Fee(env.current()->fees().base * 2));
env.close();
BEAST_EXPECT(env.le(loanKeylet));
checkBlocked(borrower, loanKeylet.key);
}
}
void
@@ -1761,6 +1842,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()};
@@ -2041,7 +2182,6 @@ public:
env.fund(XRP(10000), alice, bob, sponsor);
env.close();
// test Sufficient sponsor balance
if (cosigning)
{
adjustAccountXRPBalance(env, sponsor, reserve(env, 1) - drops(1));
@@ -2096,10 +2236,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 =
@@ -2139,7 +2280,7 @@ public:
env.close();
}
if (sponsorReserveCount - 1 > 0)
if (sponsorReserveCount > 1)
{
env(sponsor::set(sponsor, 0, sponsorReserveCount - 1, XRP(1)),
sponsor::SponseeAcc(sponsee));
@@ -2152,8 +2293,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
@@ -2176,6 +2362,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();
@@ -2194,6 +2387,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);
@@ -2436,6 +2631,7 @@ public:
Account const alice("alice");
Account const sponsor("sponsor");
Account const sponsor2("sponsor2");
auto const credType = std::string("credType");
{
Env env{*this, testableAmendments()};
@@ -2463,6 +2659,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();
@@ -2480,11 +2784,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);
@@ -3102,6 +3410,105 @@ public:
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0);
}
void
testSponsoredTrustLineNoFreeReserve()
{
// An account with ownerCount < 2 may create its first trust lines even
// without meeting the reserve. In any case, the sponsor pays the full
// reserve in all cases, even for the sponsee's very first trust line.
testcase("Sponsored trust line gets no free-reserve exception");
using namespace test::jtx;
Account const issuer("issuer");
Account const alice("alice");
Account const sponsor("sponsor");
Env env{*this, testableAmendments()};
env.fund(XRP(10000), issuer, alice, sponsor);
env.close();
auto const usd = issuer["usd"];
auto const lineKeylet = keylet::line(alice, issuer, usd.currency);
// Sponsor funded for exactly its base reserve
adjustAccountXRPBalance(env, sponsor, reserve(env, 0));
// alice's ownerCount is 0, so an unsponsored first trust line would be
// free; but because it is sponsored, the reserve check is enforced
// against the sponsor, which is one increment short.
env(trust(alice, usd(100)),
sponsor::As(sponsor, spfSponsorReserve),
Sig(sfSponsorSignature, sponsor),
Ter(tecNO_LINE_INSUF_RESERVE));
env.close();
BEAST_EXPECT(!env.le(lineKeylet));
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
// Give the sponsor has exactly one owner-reserve increment; the same
// sponsored first trust line now succeeds and the sponsor pays for it.
adjustAccountXRPBalance(env, sponsor, reserve(env, 1));
env(trust(alice, usd(100)),
sponsor::As(sponsor, spfSponsorReserve),
Sig(sfSponsorSignature, sponsor));
env.close();
BEAST_EXPECT(env.le(lineKeylet));
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1);
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
BEAST_EXPECT(ownerCount(env, alice) == 1);
}
void
testCoSignReserveBoundedBySponsorshipBudget()
{
// sponsor co-signs, so a fee-only object (ReserveCount == 0) makes a co-signed
// reserve sponsorship fail -- with no fallback to the sponsor's balance.
testcase("Co-signed reserve sponsorship is bounded by Sponsorship budget");
using namespace test::jtx;
Env env{*this, testableAmendments()};
Account const sponsor("sponsor");
Account const sponsee("sponsee");
env.fund(XRP(10000), sponsor, sponsee);
env.close();
// Prefund a FEE-only Sponsorship for the sponsee; ReserveCount
// defaults to 0.
env(sponsor::set_fee(sponsor, 0, XRP(100)), sponsor::SponseeAcc(sponsee));
env.close();
BEAST_EXPECT(env.le(keylet::sponsorship(sponsor, sponsee)));
// Sponsee creates a Check with the sponsor co-signing the reserve. The
// fee-only Sponsorship's has ReserveCount (0), so this fails
// with tecINSUFFICIENT_RESERVE
env(check::create(sponsee, sponsor, XRP(1)),
sponsor::As(sponsor, spfSponsorReserve),
Sig(sfSponsorSignature, sponsor),
Ter(tecINSUFFICIENT_RESERVE));
env.close();
BEAST_EXPECT(ownerCount(env, sponsee) == 0);
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
BEAST_EXPECT(sponsoredOwnerCount(env, sponsee) == 0);
// Bumping the Sponsorship's ReserveCount budget makes the same
// co-signed reserve sponsorship succeed, the budget is what gates it.
env(sponsor::set_reserve(sponsor, 0, 1), sponsor::SponseeAcc(sponsee));
env.close();
env(check::create(sponsee, sponsor, XRP(1)),
sponsor::As(sponsor, spfSponsorReserve),
Sig(sfSponsorSignature, sponsor),
Ter(tesSUCCESS));
env.close();
BEAST_EXPECT(ownerCount(env, sponsee) == 1);
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1);
BEAST_EXPECT(sponsoredOwnerCount(env, sponsee) == 1);
}
void
testTrustSet(bool cosigning)
{
@@ -3342,6 +3749,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);
@@ -3385,13 +3798,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);
}
{
@@ -3408,13 +3827,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);
}
}
@@ -3814,6 +4239,40 @@ public:
}
}
// Verify that the central allow-list in preflight1Sponsor rejects
// spfSponsorReserve for transaction types that v1 does not permit.
void
testReserveSponsorGate()
{
testcase("Reserve sponsor allow-list gate");
using namespace test::jtx;
Env env{*this, testableAmendments()};
Account const alice("alice");
Account const bob("bob");
Account const sponsor("sponsor");
env.fund(XRP(10000), alice, bob, sponsor);
env.close();
env(sponsor::set(sponsor, 0, 10, XRP(10)), sponsor::SponseeAcc(alice));
env.close();
auto checkBlocked = [&](json::Value const& jv) {
env(jv,
sponsor::As(sponsor, spfSponsorReserve),
Sig(sfSponsorSignature, sponsor),
Ter(temINVALID_FLAG));
};
checkBlocked(ticket::create(alice, 1));
checkBlocked(offer(alice, XRP(100), bob["USD"](100)));
checkBlocked(did::setValid(alice));
checkBlocked(token::mint(alice, 0u));
checkBlocked(sponsor::set(alice, 0, 10, XRP(10)));
checkBlocked(acctdelete(alice, bob));
checkBlocked(loan::set(alice, uint256(1), Number{1}));
}
void
testSponsorReserve(bool cosigning)
{
@@ -3855,6 +4314,10 @@ protected:
testDelegatePermission();
testBatch();
testSponsoredTrustLineNoFreeReserve();
testCoSignReserveBoundedBySponsorshipBudget();
testReserveSponsorGate();
}
void

View File

@@ -19,6 +19,8 @@
#include <test/jtx/require.h>
#include <test/jtx/sendmax.h>
#include <test/jtx/seq.h>
#include <test/jtx/sig.h>
#include <test/jtx/sponsor.h>
#include <test/jtx/tags.h>
#include <test/jtx/ter.h>
#include <test/jtx/ticket.h>
@@ -2335,6 +2337,43 @@ public:
BEAST_EXPECT(env.balance(alice) == drops(5));
}
void
testSponsorTxCannotQueue()
{
using namespace jtx;
testcase("disallow sponsored transaction from being queued");
Env env(*this, makeConfig({{Keys::kMinimumTxnInLedgerStandalone, "3"}}));
auto sponsor = Account("sponsor");
auto sponsee = Account("sponsee");
auto filler = Account("filler");
env.fund(XRP(50000), noripple(sponsor, sponsee));
env.close();
env.fund(XRP(50000), noripple(filler));
env.close();
fillQueue(env, filler);
checkMetrics(*this, env, 0, 6, 4, 3);
// Sponsored transactions are not allowed to be queued.
env(noop(sponsee),
sponsor::As(sponsor, spfSponsorFee),
Sig(sfSponsorSignature, sponsor),
Ter(telCAN_NOT_QUEUE));
checkMetrics(*this, env, 0, 6, 4, 3);
// Sponsored transactions may still apply directly if they pay the
// open ledger fee. They just cannot be held in the queue.
env(noop(sponsee),
sponsor::As(sponsor, spfSponsorFee),
Sig(sfSponsorSignature, sponsor),
Fee(openLedgerCost(env)),
Ter(tesSUCCESS));
checkMetrics(*this, env, 0, 6, 5, 3);
}
void
testConsequences()
{
@@ -4662,6 +4701,7 @@ public:
testBlockersSeq();
testBlockersTicket();
testInFlightBalance();
testSponsorTxCannotQueue();
testConsequences();
}

View File

@@ -1535,86 +1535,43 @@ public:
}
}
// A Sponsorship object is visible to both sides, but its reserve side
// belongs only to sfOwner.
// A Sponsorship object is visible to both sides.
{
Env env(*this, testableAmendments());
Account const owner("owner");
Account const sponsee("sponsee");
Account const sponsor("sponsor");
env.fund(XRP(10000), owner, sponsee, sponsor);
env.fund(XRP(10000), owner, sponsee);
env.close();
env(sponsor::set_reserve(sponsor, 0, 100), sponsor::SponseeAcc(owner));
env(sponsor::set(owner, 0, 100, XRP(100)), sponsor::SponseeAcc(sponsee));
env.close();
env(sponsor::set(owner, 0, 100, XRP(100)),
sponsor::SponseeAcc(sponsee),
sponsor::As(sponsor, spfSponsorReserve),
Sig(sfSponsorSignature, sponsor));
env.close();
auto const sponsorship = env.le(keylet::sponsorship(owner, sponsee));
if (!BEAST_EXPECT(sponsorship))
auto const sponsorshipKeylet = keylet::sponsorship(owner, sponsee);
if (!BEAST_EXPECT(env.le(sponsorshipKeylet)))
return;
BEAST_EXPECT(sponsorship->isFieldPresent(sfSponsor));
{
auto const resp = acctObjsSponsored(env, owner.id(), true, jss::sponsorship);
auto const resp = acctObjsSponsored(env, owner.id(), false, jss::sponsorship);
auto const& objs = resp[jss::result][jss::account_objects];
if (BEAST_EXPECT(objs.size() == 1))
BEAST_EXPECT(objs[0u][sfLedgerEntryType.jsonName] == jss::Sponsorship);
}
{
auto const resp = acctObjsSponsored(env, sponsee.id(), true, jss::sponsorship);
auto const& objs = resp[jss::result][jss::account_objects];
BEAST_EXPECT(objs.size() == 0);
}
{
auto const resp = acctObjsSponsored(env, sponsee.id(), false, jss::sponsorship);
auto const& objs = resp[jss::result][jss::account_objects];
if (BEAST_EXPECT(objs.size() == 1))
BEAST_EXPECT(objs[0u][sfLedgerEntryType.jsonName] == jss::Sponsorship);
}
}
// NFT page sponsored filter
{
// Mint an NFT for bob (creates NFT page)
env(token::mint(bob, 0));
env.close();
auto const nftPageKeylet = keylet::nftpageMax(bob);
if (!BEAST_EXPECT(env.le(nftPageKeylet)))
return;
// Sponsor the NFT page
env(sponsor::transfer(bob, tfSponsorshipCreate, nftPageKeylet.key),
sponsor::As(sponsor1, spfSponsorReserve),
Sig(sfSponsorSignature, sponsor1));
env.close();
// Verify NFT page has sponsor field
auto const nftPage = env.le(nftPageKeylet);
if (!BEAST_EXPECT(nftPage))
return;
BEAST_EXPECT(nftPage->isFieldPresent(sfSponsor));
// sponsored=true should include the sponsored NFT page
// sponsored=false should NOT include the sponsored NFT page
for (auto const sponsored : {true, false})
{
auto const resp = acctObjsSponsored(env, bob.id(), sponsored);
auto const resp = acctObjsSponsored(env, owner.id(), true, jss::sponsorship);
auto const& objs = resp[jss::result][jss::account_objects];
bool foundNFTPage = false;
for (auto const& obj : objs)
{
if (obj[sfLedgerEntryType.jsonName] == jss::NFTokenPage &&
obj.isMember(sfSponsor.jsonName))
foundNFTPage = true;
}
BEAST_EXPECT(foundNFTPage == sponsored);
BEAST_EXPECT(objs.size() == 0);
}
{
auto const resp = acctObjsSponsored(env, sponsee.id(), true, jss::sponsorship);
auto const& objs = resp[jss::result][jss::account_objects];
BEAST_EXPECT(objs.size() == 0);
}
}
}

View File

@@ -15,6 +15,7 @@
#include <xrpl/ledger/ApplyViewImpl.h>
#include <xrpl/ledger/OpenView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/SponsorHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Keylet.h>
@@ -398,6 +399,10 @@ TxQ::canBeHeld(
((flags & TapFailHard) != 0u))
return telCAN_NOT_QUEUE;
// Disallow sponsored transactions from being queued.
if (tx.isFieldPresent(sfSponsor) && isFeeSponsored(tx))
return telCAN_NOT_QUEUE;
{
// To be queued and relayed, the transaction needs to
// promise to stick around for long enough that it has