diff --git a/include/xrpl/ledger/helpers/AccountRootHelpers.h b/include/xrpl/ledger/helpers/AccountRootHelpers.h index 403626d06f..6c8233fa61 100644 --- a/include/xrpl/ledger/helpers/AccountRootHelpers.h +++ b/include/xrpl/ledger/helpers/AccountRootHelpers.h @@ -109,7 +109,7 @@ baseAccountReserve(ReadView const& view, std::int32_t ownerCount, std::int32_t a */ [[nodiscard]] TER checkInsufficientReserve( - ReadView const& view, + ApplyView const& view, STTx const& tx, SLE::const_ref accSle, STAmount const& accBalance, diff --git a/include/xrpl/ledger/helpers/OracleHelpers.h b/include/xrpl/ledger/helpers/OracleHelpers.h new file mode 100644 index 0000000000..635e47c564 --- /dev/null +++ b/include/xrpl/ledger/helpers/OracleHelpers.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +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 diff --git a/include/xrpl/ledger/helpers/SponsorHelpers.h b/include/xrpl/ledger/helpers/SponsorHelpers.h index be73b3252c..a1a5bb1db9 100644 --- a/include/xrpl/ledger/helpers/SponsorHelpers.h +++ b/include/xrpl/ledger/helpers/SponsorHelpers.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -123,4 +124,121 @@ removeSponsorFromLedgerEntry(SLE::ref sle, SF_ACCOUNT const& field = sfSponsor) sle->makeFieldAbsent(field); } +template +inline std::optional +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 +inline bool +isLedgerEntrySupportedBySponsorship(T const& sle) +{ + switch (sle->getType()) + { + case ltCHECK: + case ltESCROW: + case ltPAYCHAN: + case ltMPTOKEN: + case ltDELEGATE: + case ltDEPOSIT_PREAUTH: + case ltMPTOKEN_ISSUANCE: + case ltSIGNER_LIST: + case ltCREDENTIAL: + case ltRIPPLE_STATE: + return true; + default: + return false; + }; +} + +template +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 +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 diff --git a/include/xrpl/tx/transactors/oracle/OracleSet.h b/include/xrpl/tx/transactors/oracle/OracleSet.h index 51ed7be3dc..831c11b8c4 100644 --- a/include/xrpl/tx/transactors/oracle/OracleSet.h +++ b/include/xrpl/tx/transactors/oracle/OracleSet.h @@ -22,12 +22,6 @@ public: { } - static uint32_t - calculateOracleReserve(std::size_t count) - { - return count > 5 ? 2 : 1; - } - static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp b/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp index 3264fa1a45..26a3a0fabf 100644 --- a/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp +++ b/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp @@ -302,7 +302,7 @@ baseAccountReserve(ReadView const& view, std::int32_t ownerCount, std::int32_t a TER checkInsufficientReserve( - ReadView const& view, + ApplyView const& view, STTx const& tx, SLE::const_ref accSle, STAmount const& accBalance, diff --git a/src/libxrpl/tx/Transactor.cpp b/src/libxrpl/tx/Transactor.cpp index c38109bece..8ce60f214b 100644 --- a/src/libxrpl/tx/Transactor.cpp +++ b/src/libxrpl/tx/Transactor.cpp @@ -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)) { diff --git a/src/libxrpl/tx/invariants/SponsorshipInvariant.cpp b/src/libxrpl/tx/invariants/SponsorshipInvariant.cpp index f4db96a643..9ebc008473 100644 --- a/src/libxrpl/tx/invariants/SponsorshipInvariant.cpp +++ b/src/libxrpl/tx/invariants/SponsorshipInvariant.cpp @@ -4,13 +4,13 @@ #include #include #include +#include #include #include #include #include #include #include -#include #include #include @@ -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)) diff --git a/src/libxrpl/tx/transactors/Sponsor/SponsorshipTransfer.cpp b/src/libxrpl/tx/transactors/Sponsor/SponsorshipTransfer.cpp index 0c98511a5c..3840c390b1 100644 --- a/src/libxrpl/tx/transactors/Sponsor/SponsorshipTransfer.cpp +++ b/src/libxrpl/tx/transactors/Sponsor/SponsorshipTransfer.cpp @@ -10,7 +10,6 @@ #include #include #include -#include #include #include #include @@ -53,6 +52,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()) @@ -68,6 +70,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()) @@ -83,6 +88,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()) @@ -116,96 +123,18 @@ SponsorshipTransfer::preflight(PreflightContext const& ctx) return tesSUCCESS; } -template -inline std::optional -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 -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) @@ -217,49 +146,27 @@ SponsorshipTransfer::preclaim(PreclaimContext const& ctx) if (!sle) return tecNO_ENTRY; - // 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 (!isLedgerEntrySupportedBySponsorship(sle)) return tecNO_PERMISSION; - auto const& sponsorField = getLedgerEntrySponsorField(sle, *owner); + auto const owner = getLedgerEntryOwner(ctx.view, sle, sponseeID); + if (!owner.has_value() || owner.value() != sponseeID) + return tecNO_PERMISSION; + + 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 @@ -268,7 +175,7 @@ SponsorshipTransfer::preclaim(PreclaimContext const& ctx) } else if (ctx.tx.isFlag(tfSponsorshipEnd)) { - if (*newSponsorSle) + if (newSponsorSle) return tecNO_PERMISSION; // check object is sponsored @@ -280,26 +187,12 @@ SponsorshipTransfer::preclaim(PreclaimContext const& ctx) if (account != sponsor && account != sponseeID) return tecNO_PERMISSION; } - - // check new sponsor have sufficient balance - // NOLINTNEXTLINE(readability-suspicious-call-argument) - if (auto const ter = checkInsufficientReserve( - ctx.view, - ctx.tx, - sponseeSle, - sponseeSle->getFieldAmount(sfBalance), - *newSponsorSle, - ownerCountDelta, - 0, - ctx.j); - !isTesSuccess(ter)) - return ter; } else { if (ctx.tx.isFlag(tfSponsorshipCreate)) { - if (!*newSponsorSle) + if (!newSponsorSle) return tecNO_PERMISSION; // check account is not sponsored yet @@ -308,7 +201,7 @@ SponsorshipTransfer::preclaim(PreclaimContext const& ctx) } else if (ctx.tx.isFlag(tfSponsorshipReassign)) { - if (!*newSponsorSle) + if (!newSponsorSle) return tecNO_PERMISSION; // check account is already sponsored @@ -317,7 +210,7 @@ SponsorshipTransfer::preclaim(PreclaimContext const& ctx) } else if (ctx.tx.isFlag(tfSponsorshipEnd)) { - if (*newSponsorSle) + if (newSponsorSle) return tecNO_PERMISSION; // check account is sponsored @@ -329,23 +222,6 @@ SponsorshipTransfer::preclaim(PreclaimContext const& ctx) if (account != sponsor && account != sponseeID) return tecNO_PERMISSION; } - - // check account have sufficient balance - // In the case of removing an account sponsor, accSle should have no sfSponsor set - // (AccountReserve = 0). However, by setting accountCountDelta = 1 here, we are able to - // calculate the actual required Account Reserve. - // NOLINTNEXTLINE(readability-suspicious-call-argument) - if (auto const ter = checkInsufficientReserve( - ctx.view, - ctx.tx, - sponseeSle, - sponseeSle->getFieldAmount(sfBalance), - *newSponsorSle, - 0, - 1, - ctx.j); - !isTesSuccess(ter)) - return ter; } return tesSUCCESS; @@ -408,6 +284,12 @@ SponsorshipTransfer::doApply() return tesSUCCESS; }; + auto const balanceBeforeFee = [&](SLE::const_ref sle) -> STAmount { + if (sle->getAccountID(sfAccount) == accountID_) + return STAmount{preFeeBalance_}; + return sle->getFieldAmount(sfBalance); + }; + if (isObjectSponsor) { auto const hasSignature = tx.isFieldPresent(sfSponsorSignature); @@ -433,6 +315,23 @@ SponsorshipTransfer::doApply() { auto const newSponsorID = tx.getAccountID(sfSponsor); XRPL_ASSERT(!!newSponsorID, "New sponsor is required when creating sponsorship"); + auto const newSponsorSle = view().peek(keylet::account(newSponsorID)); + if (!newSponsorSle) + return tefINTERNAL; // LCOV_EXCL_LINE + + // check new sponsor have sufficient balance + // NOLINTNEXTLINE(readability-suspicious-call-argument) + if (auto const ter = checkInsufficientReserve( + ctx_.view(), + ctx_.tx, + sponseeSle, + sponseeSle->getFieldAmount(sfBalance), + newSponsorSle, + ownerCountDelta, + 0, + ctx_.journal); + !isTesSuccess(ter)) + return ter; // update owner's sponsored count if (auto const ter = @@ -442,9 +341,6 @@ SponsorshipTransfer::doApply() view().update(ownerSle); // increment new sponsor's sponsoring count - auto const newSponsorSle = view().peek(keylet::account(newSponsorID)); - if (!newSponsorSle) - return tefINTERNAL; // LCOV_EXCL_LINE if (auto const ter = setSponsorFieldU32(newSponsorSle, sfSponsoringOwnerCount, ownerCountDelta); !isTesSuccess(ter)) @@ -468,14 +364,31 @@ SponsorshipTransfer::doApply() { auto const newSponsorID = tx.getAccountID(sfSponsor); XRPL_ASSERT(!!newSponsorID, "New sponsor is required when reassigning sponsorship"); + 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"); - - // decrement old sponsor's sponsoring count auto const oldSponsorSle = view().peek(keylet::account(oldSponsorID)); if (!oldSponsorSle) return tefINTERNAL; // LCOV_EXCL_LINE + + // check new sponsor have sufficient balance + // NOLINTNEXTLINE(readability-suspicious-call-argument) + if (auto const ter = checkInsufficientReserve( + ctx_.view(), + ctx_.tx, + sponseeSle, + sponseeSle->getFieldAmount(sfBalance), + newSponsorSle, + ownerCountDelta, + 0, + ctx_.journal); + !isTesSuccess(ter)) + return ter; + + // decrement old sponsor's sponsoring count if (auto const ter = setSponsorFieldU32(oldSponsorSle, sfSponsoringOwnerCount, -ownerCountDelta); !isTesSuccess(ter)) @@ -483,9 +396,6 @@ SponsorshipTransfer::doApply() view().update(oldSponsorSle); // increment new sponsor's sponsoring count - auto const newSponsorSle = view().peek(keylet::account(newSponsorID)); - if (!newSponsorSle) - return tefINTERNAL; // LCOV_EXCL_LINE if (auto const ter = setSponsorFieldU32(newSponsorSle, sfSponsoringOwnerCount, ownerCountDelta); !isTesSuccess(ter)) @@ -514,6 +424,20 @@ SponsorshipTransfer::doApply() if (!oldSponsorSle) return tefINTERNAL; // LCOV_EXCL_LINE + // The owner takes the reserve burden back when the object is + // no longer sponsored. + if (auto const ter = checkInsufficientReserve( + ctx_.view(), + ctx_.tx, + ownerSle, + balanceBeforeFee(ownerSle), + SLE::pointer(), + ownerCountDelta, + 0, + ctx_.journal); + !isTesSuccess(ter)) + return ter; + // decrement sponsored count if (auto const ter = setSponsorFieldU32(sponseeSle, sfSponsoredOwnerCount, -ownerCountDelta); @@ -543,6 +467,19 @@ SponsorshipTransfer::doApply() auto const newSponsorSle = view().peek(keylet::account(newSponsorID)); if (!newSponsorSle) return tefINTERNAL; // LCOV_EXCL_LINE + + if (auto const ter = checkInsufficientReserve( + ctx_.view(), + ctx_.tx, + sponseeSle, + sponseeSle->getFieldAmount(sfBalance), + newSponsorSle, + 0, + 1, + ctx_.journal); + !isTesSuccess(ter)) + return ter; + if (auto const ter = setSponsorFieldU32(newSponsorSle, sfSponsoringAccountCount, 1); !isTesSuccess(ter)) return ter; @@ -560,6 +497,19 @@ SponsorshipTransfer::doApply() auto const newSponsorSle = view().peek(keylet::account(newSponsorID)); if (!newSponsorSle) return tefINTERNAL; // LCOV_EXCL_LINE + + if (auto const ter = checkInsufficientReserve( + ctx_.view(), + ctx_.tx, + sponseeSle, + sponseeSle->getFieldAmount(sfBalance), + newSponsorSle, + 0, + 1, + ctx_.journal); + !isTesSuccess(ter)) + return ter; + if (auto const ter = setSponsorFieldU32(newSponsorSle, sfSponsoringAccountCount, 1); !isTesSuccess(ter)) return ter; @@ -583,6 +533,21 @@ SponsorshipTransfer::doApply() { // dissolve account sponsor auto const oldSponsorID = sponseeSle->getAccountID(sfSponsor); + + // The sponsee must be able to hold its own account reserve after + // the sponsorship is removed. + if (auto const ter = checkInsufficientReserve( + ctx_.view(), + ctx_.tx, + sponseeSle, + balanceBeforeFee(sponseeSle), + SLE::pointer(), + 0, + 1, + ctx_.journal); + !isTesSuccess(ter)) + return ter; + sponseeSle->makeFieldAbsent(sfSponsor); view().update(sponseeSle); diff --git a/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp b/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp index 59f7ee2f12..da1a073afb 100644 --- a/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp +++ b/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp @@ -33,6 +33,7 @@ #include #include +#include #include #include #include @@ -440,16 +441,37 @@ EscrowCreate::doApply() auto const sponsorSle = getTxReserveSponsor(applyViewContext); if (!sponsorSle) return sponsorSle.error(); // LCOV_EXCL_LINE + // First check: whoever is on the hook for the new owner increment + // can cover it. When sponsored this hits the sponsor branch and + // validates the sponsor's reserve + remaining credit. When + // unsponsored this hits the source branch and validates the + // source's pre-lock balance against base + (currentOC+1)*increment. if (auto const ret = checkInsufficientReserve(ctx_.view(), ctx_.tx, sle, balance, *sponsorSle, 1, 0, j_); !isTesSuccess(ret)) return ret; - // Check reserve and funds availability if (isXRP(amount)) { + // Second check (XRP escrow only): after locking the escrowed + // amount, the source must still meet its own reserve floor. + // Always passes `{}` so the source branch runs (the sponsor's + // reserve was already validated above; here we're verifying the + // source can fund the lock without dipping below its own + // reserve). ownerCountAdj differs by case: + // - sponsored: adj=0 — sponsor covers the new owner increment, + // so the source only owes its base reserve. + // - unsponsored: adj=1 — source owes base + the new increment. + std::int32_t const ownerCountAdj = *sponsorSle ? 0 : 1; if (auto const ret = checkInsufficientReserve( - ctx_.view(), ctx_.tx, sle, balance - STAmount(amount).xrp(), {}, 1, 0, j_); + ctx_.view(), + ctx_.tx, + sle, + balance - STAmount(amount).xrp(), + {}, + ownerCountAdj, + 0, + j_); !isTesSuccess(ret)) return tecUNFUNDED; } diff --git a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp index 220bfde71c..6fcfbbaee1 100644 --- a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp +++ b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp @@ -22,6 +22,7 @@ #include #include +#include #include namespace xrpl { @@ -134,22 +135,38 @@ PaymentChannelCreate::doApply() return tecEXPIRED; } + auto const sponsorSle = getTxReserveSponsor(ctx_.getApplyViewContext()); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + if (ctx_.view().rules().enabled(featureSponsor)) { - auto const sponsorSle = getTxReserveSponsor(ctx_.view(), ctx_.tx); - if (!sponsorSle) - return sponsorSle.error(); + // First check: whoever is on the hook for the new owner increment + // can cover it. When sponsored this hits the sponsor branch and + // validates the sponsor's reserve + remaining credit. When + // unsponsored this hits the source branch and validates the + // source's pre-lock balance against base + (currentOC+1)*increment. if (auto const ret = checkInsufficientReserve( - ctx_.view(), ctx_.tx, sle, STAmount{preFeeBalance_}, *sponsorSle, 1, 0, j_); + ctx_.view(), ctx_.tx, sle, preFeeBalance_, *sponsorSle, 1, 0, j_); !isTesSuccess(ret)) return ret; + + // Second check: after locking sfAmount in the channel, the source + // must still meet its own reserve floor. Always passes `{}` so the + // source branch runs (the sponsor's reserve was already validated + // above; here we're verifying the source can fund the lock without + // dipping below its own reserve). ownerCountAdj differs by case: + // - sponsored: adj=0 — sponsor covers the new owner increment, + // so the source only owes its base reserve. + // - unsponsored: adj=1 — source owes base + the new increment. + std::int32_t const ownerCountAdj = *sponsorSle ? 0 : 1; if (auto const ret = checkInsufficientReserve( ctx_.view(), ctx_.tx, sle, - STAmount{preFeeBalance_ - ctx_.tx[sfAmount].xrp()}, + preFeeBalance_ - ctx_.tx[sfAmount].xrp(), {}, - 1, + ownerCountAdj, 0, j_); !isTesSuccess(ret)) @@ -203,10 +220,6 @@ PaymentChannelCreate::doApply() // Deduct owner's balance, increment owner count (*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount]; - auto applyViewContext = ctx_.getApplyViewContext(); - auto const sponsorSle = getTxReserveSponsor(applyViewContext); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE adjustOwnerCount(ctx_.view(), sle, *sponsorSle, 1, ctx_.journal); addSponsorToLedgerEntry(slep, *sponsorSle); ctx_.view().update(sle); diff --git a/src/test/app/Sponsor_test.cpp b/src/test/app/Sponsor_test.cpp index 6b974c28a1..74847ea40b 100644 --- a/src/test/app/Sponsor_test.cpp +++ b/src/test/app/Sponsor_test.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -29,6 +30,7 @@ #include #include +#include #include #include #include @@ -39,9 +41,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -50,12 +54,16 @@ #include #include #include +#include #include #include #include #include +#include #include +#include +#include namespace xrpl::test { @@ -1834,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(*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()}; @@ -2168,10 +2236,11 @@ public: std::optional> 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 sponsorSig = @@ -2211,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)); @@ -2224,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(sponsorshipSleAfterAttempt) == + static_cast(sponsorshipSleBeforeAttempt)); + if (sponsorshipSleAfterAttempt) + { + BEAST_EXPECT( + sponsorshipSleAfterAttempt->isFieldPresent(sfRemainingOwnerCount) == + reserveCountPresentBeforeAttempt); + if (reserveCountPresentBeforeAttempt) + { + BEAST_EXPECT( + sponsorshipSleAfterAttempt->getFieldU32(sfRemainingOwnerCount) == + reserveCountBeforeAttempt); + } + } } // Success @@ -2248,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(); @@ -2266,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); @@ -2508,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()}; @@ -2535,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{ + {.issuer = sponsor, .credType = credType}}; + auto const preauthKeylet = keylet::depositPreauth( + alice.id(), + std::set>{ + {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(); @@ -2552,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); @@ -2749,6 +2985,50 @@ public: BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 1); BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); } + + // A sponsored EscrowCreate must still verify that the source + // can fund the escrow amount and stay above its own base + // reserve. The sponsor covers the new object's owner + // increment, but cannot cover the source's base reserve. + { + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + // alice's balance is just above the base reserve. After + // locking escrowAmount she would dip below it. + adjustAccountXRPBalance(env, alice, accountReserve(env, 1) + XRP(1)); + + auto const escrowAmount = XRP(2); + auto const seq = env.seq(alice); + + if (cosigning) + { + env(escrow::create(alice, bob, escrowAmount), + escrow::kCondition(escrow::kCb1), + escrow::kCancelTime(env.now() + 100s), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tecUNFUNDED)); + } + else + { + env(sponsor::set(sponsor, 0, 1, XRP(1)), sponsor::SponseeAcc(alice)); + env.close(); + + env(escrow::create(alice, bob, escrowAmount), + escrow::kCondition(escrow::kCb1), + escrow::kCancelTime(env.now() + 100s), + sponsor::As(sponsor, spfSponsorReserve), + Ter(tecUNFUNDED)); + } + env.close(); + + BEAST_EXPECT(!env.le(keylet::escrow(alice, seq))); + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + } } void @@ -3015,6 +3295,48 @@ public: BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); } + + // A sponsored PaymentChannelCreate must still verify that the + // source can fund the channel amount and stay above its own + // base reserve. The sponsor covers the new object's owner + // increment, but cannot cover the source's base reserve. + { + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + // alice's balance is just above the base reserve. After + // locking channelAmount she would dip below it. + adjustAccountXRPBalance(env, alice, accountReserve(env, 1) + XRP(1)); + + auto const pk = alice.pk(); + auto const settleDelay = 10s; + auto const channelAmount = XRP(2); + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + + if (cosigning) + { + env(paychan::create(alice, bob, channelAmount, settleDelay, pk), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tecUNFUNDED)); + } + else + { + env(sponsor::set(sponsor, 0, 1, XRP(1)), sponsor::SponseeAcc(alice)); + env.close(); + + env(paychan::create(alice, bob, channelAmount, settleDelay, pk), + sponsor::As(sponsor, spfSponsorReserve), + Ter(tecUNFUNDED)); + } + env.close(); + + BEAST_EXPECT(!paychan::channelExists(*env.current(), chan)); + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + } } void @@ -3427,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); @@ -3470,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); } { @@ -3493,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); } } diff --git a/src/test/app/TxQ_test.cpp b/src/test/app/TxQ_test.cpp index 0ae6b4d80a..330706dca6 100644 --- a/src/test/app/TxQ_test.cpp +++ b/src/test/app/TxQ_test.cpp @@ -19,6 +19,8 @@ #include #include #include +#include +#include #include #include #include @@ -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(); } diff --git a/src/xrpld/app/misc/detail/TxQ.cpp b/src/xrpld/app/misc/detail/TxQ.cpp index 0326828a70..b4fb40f368 100644 --- a/src/xrpld/app/misc/detail/TxQ.cpp +++ b/src/xrpld/app/misc/detail/TxQ.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -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