diff --git a/cspell.config.yaml b/cspell.config.yaml index b13e79779b..386e1367c3 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -319,6 +319,7 @@ words: - unserviced - unshareable - unshares + - unsponsored - unsquelch - unsquelched - unsquelching diff --git a/include/xrpl/ledger/View.h b/include/xrpl/ledger/View.h index 95581b7527..20de0634a9 100644 --- a/include/xrpl/ledger/View.h +++ b/include/xrpl/ledger/View.h @@ -201,7 +201,7 @@ canWithdraw(ReadView const& view, STTx const& tx); [[nodiscard]] TER doWithdraw( - ApplyViewContext const& ctx, + ApplyViewContext ctx, AccountID const& senderAcct, AccountID const& dstAcct, AccountID const& sourceAcct, diff --git a/include/xrpl/ledger/helpers/AccountRootHelpers.h b/include/xrpl/ledger/helpers/AccountRootHelpers.h index cd1caadfe2..ec4f9d9f2b 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/EscrowHelpers.h b/include/xrpl/ledger/helpers/EscrowHelpers.h index df99a8ac1e..c3da7ea621 100644 --- a/include/xrpl/ledger/helpers/EscrowHelpers.h +++ b/include/xrpl/ledger/helpers/EscrowHelpers.h @@ -19,7 +19,7 @@ namespace xrpl { template TER escrowUnlockApplyHelper( - ApplyViewContext const& ctx, + ApplyViewContext ctx, Rate lockedRate, SLE::ref sleDest, STAmount const& xrpBalance, @@ -33,7 +33,7 @@ escrowUnlockApplyHelper( template <> inline TER escrowUnlockApplyHelper( - ApplyViewContext const& ctx, + ApplyViewContext ctx, Rate lockedRate, SLE::ref sleDest, STAmount const& xrpBalance, @@ -168,7 +168,7 @@ escrowUnlockApplyHelper( template <> inline TER escrowUnlockApplyHelper( - ApplyViewContext const& ctx, + ApplyViewContext ctx, Rate lockedRate, SLE::ref sleDest, STAmount const& xrpBalance, diff --git a/include/xrpl/ledger/helpers/MPTokenHelpers.h b/include/xrpl/ledger/helpers/MPTokenHelpers.h index ba834392bf..24438ee044 100644 --- a/include/xrpl/ledger/helpers/MPTokenHelpers.h +++ b/include/xrpl/ledger/helpers/MPTokenHelpers.h @@ -71,7 +71,7 @@ canAddHolding(ReadView const& view, MPTIssue const& mptIssue); [[nodiscard]] TER authorizeMPToken( - ApplyViewContext const& ctx, + ApplyViewContext ctx, XRPAmount const& priorBalance, MPTID const& mptIssuanceID, AccountID const& account, @@ -102,7 +102,7 @@ requireAuth( */ [[nodiscard]] TER enforceMPTokenAuthorization( - ApplyViewContext const& ctx, + ApplyViewContext ctx, MPTID const& mptIssuanceID, AccountID const& account, XRPAmount const& priorBalance, @@ -188,7 +188,7 @@ canMPTTradeAndTransfer( [[nodiscard]] TER addEmptyHolding( - ApplyViewContext const& ctx, + ApplyViewContext ctx, AccountID const& accountID, XRPAmount priorBalance, MPTIssue const& mptIssue, @@ -196,7 +196,7 @@ addEmptyHolding( [[nodiscard]] TER removeEmptyHolding( - ApplyViewContext const& ctx, + ApplyViewContext ctx, AccountID const& accountID, MPTIssue const& mptIssue, beast::Journal journal); 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/RippleStateHelpers.h b/include/xrpl/ledger/helpers/RippleStateHelpers.h index 907455f522..53c843ec1d 100644 --- a/include/xrpl/ledger/helpers/RippleStateHelpers.h +++ b/include/xrpl/ledger/helpers/RippleStateHelpers.h @@ -229,7 +229,7 @@ canTransfer(ReadView const& view, Issue const& issue, AccountID const& from, Acc /// canAddHolding() in preflight with the same View and Asset [[nodiscard]] TER addEmptyHolding( - ApplyViewContext const& ctx, + ApplyViewContext ctx, AccountID const& accountID, XRPAmount priorBalance, Issue const& issue, @@ -237,7 +237,7 @@ addEmptyHolding( [[nodiscard]] TER removeEmptyHolding( - ApplyViewContext const& ctx, + ApplyViewContext ctx, AccountID const& accountID, Issue const& issue, beast::Journal journal); diff --git a/include/xrpl/ledger/helpers/SponsorHelpers.h b/include/xrpl/ledger/helpers/SponsorHelpers.h index 0433caaefd..42db1ba82c 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 @@ -107,4 +108,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/ledger/helpers/TokenHelpers.h b/include/xrpl/ledger/helpers/TokenHelpers.h index e6a3bc17f8..c8c42a8c80 100644 --- a/include/xrpl/ledger/helpers/TokenHelpers.h +++ b/include/xrpl/ledger/helpers/TokenHelpers.h @@ -230,7 +230,7 @@ canAddHolding(ReadView const& view, Asset const& asset); [[nodiscard]] TER addEmptyHolding( - ApplyViewContext const& ctx, + ApplyViewContext ctx, AccountID const& accountID, XRPAmount priorBalance, Asset const& asset, @@ -238,7 +238,7 @@ addEmptyHolding( [[nodiscard]] TER removeEmptyHolding( - ApplyViewContext const& ctx, + ApplyViewContext ctx, AccountID const& accountID, Asset const& asset, beast::Journal journal); 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/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h b/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h index 359e306c14..8d4cea1542 100644 --- a/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h +++ b/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h @@ -63,7 +63,7 @@ public: beast::Journal const& j) override; static std::expected - create(ApplyViewContext const& ctx, beast::Journal journal, MPTCreateArgs const& args); + create(ApplyViewContext ctx, beast::Journal journal, MPTCreateArgs const& args); }; } // namespace xrpl diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index 0ef2b3b854..affe2e028b 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -431,7 +431,7 @@ canWithdraw(ReadView const& view, STTx const& tx) TER doWithdraw( - ApplyViewContext const& ctx, + ApplyViewContext ctx, AccountID const& senderAcct, AccountID const& dstAcct, AccountID const& sourceAcct, @@ -442,12 +442,7 @@ doWithdraw( // Create trust line or MPToken for the receiving account if (dstAcct == senderAcct) { - if (auto const ter = addEmptyHolding( - ApplyViewContext::makeFromTx(ctx.view, ctx.tx), - senderAcct, - priorBalance, - amount.asset(), - j); + if (auto const ter = addEmptyHolding(ctx, senderAcct, priorBalance, amount.asset(), j); !isTesSuccess(ter) && ter != tecDUPLICATE) return ter; } diff --git a/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp b/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp index 7c08d5027a..177e05df14 100644 --- a/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp +++ b/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp @@ -317,7 +317,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/ledger/helpers/MPTokenHelpers.cpp b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp index 278efdcdad..2b7971757b 100644 --- a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp @@ -126,7 +126,7 @@ canAddHolding(ReadView const& view, MPTIssue const& mptIssue) [[nodiscard]] TER addEmptyHolding( - ApplyViewContext const& ctx, + ApplyViewContext ctx, AccountID const& accountID, XRPAmount priorBalance, MPTIssue const& mptIssue, @@ -149,7 +149,7 @@ addEmptyHolding( [[nodiscard]] TER authorizeMPToken( - ApplyViewContext const& ctx, + ApplyViewContext ctx, XRPAmount const& priorBalance, MPTID const& mptIssuanceID, AccountID const& account, @@ -158,6 +158,7 @@ authorizeMPToken( std::optional holderID) { auto& view = ctx.view; + auto const tx = ctx.tx; auto const sleAcct = view.peek(keylet::account(account)); if (!sleAcct) return tecINTERNAL; // LCOV_EXCL_LINE @@ -282,7 +283,7 @@ authorizeMPToken( [[nodiscard]] TER removeEmptyHolding( - ApplyViewContext const& ctx, + ApplyViewContext ctx, AccountID const& accountID, MPTIssue const& mptIssue, beast::Journal journal) @@ -414,7 +415,7 @@ requireAuth( [[nodiscard]] TER enforceMPTokenAuthorization( - ApplyViewContext const& ctx, + ApplyViewContext ctx, MPTID const& mptIssuanceID, AccountID const& account, XRPAmount const& priorBalance, // for MPToken authorization diff --git a/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp b/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp index 8f0f703eec..372192bbd5 100644 --- a/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp +++ b/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp @@ -632,13 +632,14 @@ canTransfer(ReadView const& view, Issue const& issue, AccountID const& from, Acc TER addEmptyHolding( - ApplyViewContext const& ctx, + ApplyViewContext ctx, AccountID const& accountID, XRPAmount priorBalance, Issue const& issue, beast::Journal journal) { auto& view = ctx.view; + auto const tx = ctx.tx; // Every account can hold XRP. An issuer can issue directly. if (issue.native() || accountID == issue.getIssuer()) return tesSUCCESS; @@ -669,7 +670,7 @@ addEmptyHolding( // Can the account cover the trust line reserve ? if (auto const ret = - checkInsufficientReserve(view, ctx.tx, sleDst, priorBalance, sponsorSle, 1, 0, journal); + checkInsufficientReserve(view, tx, sleDst, priorBalance, sponsorSle, 1, 0, journal); !isTesSuccess(ret)) return tecNO_LINE_INSUF_RESERVE; @@ -694,7 +695,7 @@ addEmptyHolding( TER removeEmptyHolding( - ApplyViewContext const& ctx, + ApplyViewContext ctx, AccountID const& accountID, Issue const& issue, beast::Journal journal) diff --git a/src/libxrpl/ledger/helpers/TokenHelpers.cpp b/src/libxrpl/ledger/helpers/TokenHelpers.cpp index 9839e0ecbb..df701c6d90 100644 --- a/src/libxrpl/ledger/helpers/TokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/TokenHelpers.cpp @@ -474,7 +474,7 @@ canAddHolding(ReadView const& view, Asset const& asset) TER addEmptyHolding( - ApplyViewContext const& ctx, + ApplyViewContext ctx, AccountID const& accountID, XRPAmount priorBalance, Asset const& asset, @@ -489,7 +489,7 @@ addEmptyHolding( TER removeEmptyHolding( - ApplyViewContext const& ctx, + ApplyViewContext ctx, AccountID const& accountID, Asset const& asset, beast::Journal journal) diff --git a/src/libxrpl/tx/Transactor.cpp b/src/libxrpl/tx/Transactor.cpp index cd92bc4eae..8ce60f214b 100644 --- a/src/libxrpl/tx/Transactor.cpp +++ b/src/libxrpl/tx/Transactor.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -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 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)) { 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 c4e9ac817f..3840c390b1 100644 --- a/src/libxrpl/tx/transactors/Sponsor/SponsorshipTransfer.cpp +++ b/src/libxrpl/tx/transactors/Sponsor/SponsorshipTransfer.cpp @@ -10,13 +10,11 @@ #include #include #include -#include #include #include #include #include #include -#include #include #include @@ -54,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()) @@ -69,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()) @@ -84,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()) @@ -117,143 +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 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 -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 -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 +146,27 @@ SponsorshipTransfer::preclaim(PreclaimContext const& ctx) if (!sle) return tecNO_ENTRY; - auto const ownerCountDelta = getLedgerEntryOwnerCount(sle); - - 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 @@ -293,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 @@ -305,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 @@ -333,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 @@ -342,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 @@ -354,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; @@ -433,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); @@ -450,7 +307,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); @@ -458,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 = @@ -467,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)) @@ -493,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)) @@ -508,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)) @@ -539,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); @@ -568,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; @@ -585,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; @@ -608,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 7d66ecfbba..51038238c1 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 @@ -438,16 +439,37 @@ EscrowCreate::doApply() auto const balance = sle->getFieldAmount(sfBalance).xrp(); auto const applyViewContext = ctx_.getApplyViewContext(); auto const sponsorSle = applyViewContext.reserveContext.sponsorSle; + // 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/lending/LoanBrokerCoverWithdraw.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp index 674e6d1994..24c762d14f 100644 --- a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp @@ -197,6 +197,7 @@ LoanBrokerCoverWithdraw::doApply() view().update(broker); associateAsset(*broker, vaultAsset); + return doWithdraw( ctx_.getApplyViewContext(), accountID_, diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp index 4eb518dee7..dc592f25a6 100644 --- a/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp @@ -159,6 +159,7 @@ LoanBrokerDelete::doApply() view(), brokerPseudoID, accountID_, coverAvailable, j_, {}, WaiveTransferFee::Yes)) return ter; } + if (auto ter = removeEmptyHolding(ctx_.getApplyViewContext(), brokerPseudoID, vaultAsset, j_)) return ter; diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp index f019b60254..81a6263dad 100644 --- a/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp @@ -248,6 +248,7 @@ LoanBrokerSet::doApply() return maybePseudo.error(); // LCOV_EXCL_LINE auto& pseudo = *maybePseudo; auto pseudoId = pseudo->at(sfAccount); + if (auto ter = addEmptyHolding( ctx_.getApplyViewContext(), pseudoId, preFeeBalance_, sleVault->at(sfAsset), j_)) return ter; diff --git a/src/libxrpl/tx/transactors/lending/LoanSet.cpp b/src/libxrpl/tx/transactors/lending/LoanSet.cpp index ab47b4eae8..0992b5d620 100644 --- a/src/libxrpl/tx/transactors/lending/LoanSet.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanSet.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -57,6 +58,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)) @@ -532,12 +539,9 @@ LoanSet::doApply() borrower == accountID_ || borrower == counterparty, "xrpl::LoanSet::doApply", "borrower signed transaction"); + auto applyViewContext = ctx_.getApplyViewContext(); if (auto const ter = addEmptyHolding( - ctx_.getApplyViewContext(), - borrower, - borrowerSle->at(sfBalance).value().xrp(), - vaultAsset, - j_); + applyViewContext, borrower, borrowerSle->at(sfBalance).value().xrp(), vaultAsset, j_); ter && ter != tecDUPLICATE) { // ignore tecDUPLICATE. That means the holding already exists, and @@ -560,7 +564,7 @@ LoanSet::doApply() "broker owner signed transaction"); if (auto const ter = addEmptyHolding( - ctx_.getApplyViewContext(), + applyViewContext, brokerOwner, brokerOwnerSle->at(sfBalance).value().xrp(), vaultAsset, diff --git a/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp b/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp index 6366b38359..73145ce809 100644 --- a/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp +++ b/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp @@ -152,6 +152,7 @@ DepositPreauth::preclaim(PreclaimContext const& ctx) TER DepositPreauth::doApply() { + auto applyViewContext = ctx_.getApplyViewContext(); if (ctx_.tx.isFieldPresent(sfAuthorize)) { auto const sleOwner = view().peek(keylet::account(accountID_)); diff --git a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp index c7dadced5b..edee96b58e 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,21 +135,40 @@ 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 applyViewContext = ctx_.getApplyViewContext(); auto const sponsorSle = applyViewContext.reserveContext.sponsorSle; + // 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)) diff --git a/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp b/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp index 5ed7c1d382..039d9bc8fa 100644 --- a/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp +++ b/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp @@ -103,7 +103,7 @@ MPTokenIssuanceCreate::preflight(PreflightContext const& ctx) std::expected MPTokenIssuanceCreate::create( - ApplyViewContext const& ctx, + ApplyViewContext ctx, beast::Journal journal, MPTCreateArgs const& args) { @@ -112,11 +112,7 @@ MPTokenIssuanceCreate::create( if (!acct) return std::unexpected(tecINTERNAL); // LCOV_EXCL_LINE - SLE::pointer sponsorSle; - if (!isPseudoAccount(acct)) - { - sponsorSle = ctx.reserveContext.sponsorSle; - } + SLE::pointer sponsorSle = isPseudoAccount(acct) ? nullptr : ctx.reserveContext.sponsorSle; if (args.priorBalance) { diff --git a/src/libxrpl/tx/transactors/vault/VaultCreate.cpp b/src/libxrpl/tx/transactors/vault/VaultCreate.cpp index c65719afad..7cb02242f7 100644 --- a/src/libxrpl/tx/transactors/vault/VaultCreate.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultCreate.cpp @@ -147,6 +147,7 @@ VaultCreate::doApply() // we can consider downgrading them to `tef` or `tem`. auto const& tx = ctx_.tx; + auto applyViewContext = ctx_.getApplyViewContext(); auto const sequence = tx.getSeqValue(); auto const owner = view().peek(keylet::account(accountID_)); if (owner == nullptr) @@ -168,7 +169,7 @@ VaultCreate::doApply() AccountID const pseudoId = pseudo->at(sfAccount); auto const asset = tx[sfAsset]; - if (auto ter = addEmptyHolding(ctx_.getApplyViewContext(), pseudoId, preFeeBalance_, asset, j_); + if (auto ter = addEmptyHolding(applyViewContext, pseudoId, preFeeBalance_, asset, j_); !isTesSuccess(ter)) return ter; @@ -198,7 +199,7 @@ VaultCreate::doApply() : keylet::line(pseudoId, asset.get()).key; }(); auto const maybeShare = MPTokenIssuanceCreate::create( - ctx_.getApplyViewContext(), + applyViewContext, j_, { .priorBalance = std::nullopt, @@ -245,7 +246,7 @@ VaultCreate::doApply() // Explicitly create MPToken for the vault owner if (auto const err = authorizeMPToken( - ctx_.getApplyViewContext(), preFeeBalance_, mptIssuanceID, accountID_, ctx_.journal); + applyViewContext, preFeeBalance_, mptIssuanceID, accountID_, ctx_.journal); !isTesSuccess(err)) return err; @@ -253,7 +254,7 @@ VaultCreate::doApply() if (tx.isFlag(tfVaultPrivate)) { if (auto const err = authorizeMPToken( - ctx_.getApplyViewContext(), + applyViewContext, preFeeBalance_, mptIssuanceID, pseudoId, diff --git a/src/libxrpl/tx/transactors/vault/VaultDelete.cpp b/src/libxrpl/tx/transactors/vault/VaultDelete.cpp index 31babdcabe..d2b640324e 100644 --- a/src/libxrpl/tx/transactors/vault/VaultDelete.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultDelete.cpp @@ -89,13 +89,14 @@ TER VaultDelete::doApply() { auto const vault = view().peek(keylet::vault(ctx_.tx[sfVaultID])); + auto applyViewContext = ctx_.getApplyViewContext(); if (!vault) return tefINTERNAL; // LCOV_EXCL_LINE // Destroy the asset holding. auto asset = vault->at(sfAsset); - if (auto ter = removeEmptyHolding(ctx_.getApplyViewContext(), vault->at(sfAccount), asset, j_); + if (auto ter = removeEmptyHolding(applyViewContext, vault->at(sfAccount), asset, j_); !isTesSuccess(ter)) return ter; @@ -124,8 +125,8 @@ VaultDelete::doApply() // Try to remove MPToken for vault shares for the vault owner if it exists. if (auto const mptoken = view().peek(keylet::mptoken(shareMPTID, accountID_))) { - if (auto const ter = removeEmptyHolding( - ctx_.getApplyViewContext(), accountID_, MPTIssue(shareMPTID), j_); + if (auto const ter = + removeEmptyHolding(applyViewContext, accountID_, MPTIssue(shareMPTID), j_); !isTesSuccess(ter)) { // LCOV_EXCL_START diff --git a/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp b/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp index 78a1c11df8..569e8d6983 100644 --- a/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp @@ -187,6 +187,7 @@ VaultDeposit::doApply() { bool const fix320Enabled = view().rules().enabled(fixCleanup3_2_0); auto const vault = view().peek(keylet::vault(ctx_.tx[sfVaultID])); + auto applyViewContext = ctx_.getApplyViewContext(); if (!vault) return tefINTERNAL; // LCOV_EXCL_LINE auto const vaultAsset = vault->at(sfAsset); @@ -221,7 +222,7 @@ VaultDeposit::doApply() if (vault->isFlag(lsfVaultPrivate) && accountID_ != vault->at(sfOwner)) { if (auto const err = enforceMPTokenAuthorization( - ctx_.getApplyViewContext(), mptIssuanceID, accountID_, preFeeBalance_, j_); + applyViewContext, mptIssuanceID, accountID_, preFeeBalance_, j_); !isTesSuccess(err)) return err; } @@ -231,7 +232,7 @@ VaultDeposit::doApply() if (!view().exists(keylet::mptoken(mptIssuanceID, accountID_))) { if (auto const err = authorizeMPToken( - ctx_.getApplyViewContext(), + applyViewContext, preFeeBalance_, mptIssuanceID->value(), accountID_, @@ -247,7 +248,7 @@ VaultDeposit::doApply() XRPL_ASSERT( accountID_ == vault->at(sfOwner), "xrpl::VaultDeposit::doApply : account is owner"); if (auto const err = authorizeMPToken( - ctx_.getApplyViewContext(), + applyViewContext, preFeeBalance_, // priorBalance mptIssuanceID->value(), // mptIssuanceID sleIssuance->at(sfIssuer), // account diff --git a/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp b/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp index 9955fbb02d..209b5d4273 100644 --- a/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp @@ -176,6 +176,7 @@ TER VaultWithdraw::doApply() { auto const vault = view().peek(keylet::vault(ctx_.tx[sfVaultID])); + auto applyViewContext = ctx_.getApplyViewContext(); if (!vault) return tefINTERNAL; // LCOV_EXCL_LINE @@ -337,8 +338,8 @@ VaultWithdraw::doApply() // Keep MPToken if holder is the vault owner. if (accountID_ != vault->at(sfOwner)) { - if (auto const ter = removeEmptyHolding( - ctx_.getApplyViewContext(), accountID_, sharesRedeemed.asset(), j_); + if (auto const ter = + removeEmptyHolding(applyViewContext, accountID_, sharesRedeemed.asset(), j_); isTesSuccess(ter)) { JLOG(j_.debug()) // @@ -365,13 +366,7 @@ VaultWithdraw::doApply() associateAsset(*vault, vaultAsset); return doWithdraw( - ctx_.getApplyViewContext(), - accountID_, - dstAcct, - vaultAccount, - preFeeBalance_, - assetsWithdrawn, - j_); + applyViewContext, accountID_, dstAcct, vaultAccount, preFeeBalance_, assetsWithdrawn, j_); } void diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 47ef458518..423fe7b6cc 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -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 { diff --git a/src/test/app/Sponsor_test.cpp b/src/test/app/Sponsor_test.cpp index 04d448e2bc..74847ea40b 100644 --- a/src/test/app/Sponsor_test.cpp +++ b/src/test/app/Sponsor_test.cpp @@ -11,9 +11,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -22,10 +24,13 @@ #include #include #include +#include #include #include #include +#include +#include #include #include #include @@ -36,9 +41,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -47,12 +54,16 @@ #include #include #include +#include #include #include #include #include +#include #include +#include +#include namespace xrpl::test { @@ -404,8 +415,6 @@ public: Account const alice("alice"); Account const bob("bob"); Account const sponsor("sponsor"); - Account const invalid("invalid"); - Account const signer1("signer1"); Account const signer2("signer2"); @@ -679,10 +688,11 @@ public: using namespace test::jtx; Account const alice("alice"); Account const bob("bob"); + Account const charlie("charlie"); Account const sponsor("sponsor"); { - // both pre-funded and co-signed,pre-funded value is used + // Both pre-funded and co-signed; the pre-funded value is used. Env env{*this, testableAmendments()}; env.fund(XRP(10000), alice, bob, sponsor); env.close(); @@ -710,17 +720,17 @@ public: sle = env.le(keylet::sponsorship(sponsor, alice)); BEAST_EXPECT(sle); - BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 99); // not paybacked + BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 99); // not restored BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(99)); } { // 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(); @@ -799,8 +809,7 @@ public: Account const alice("alice"); Account const bob("bob"); Account const sponsor1("sponsor1"); - Account const sponsor2("sponsor2"); - env.fund(XRP(10000), alice, bob, sponsor1, sponsor2); + env.fund(XRP(10000), alice, bob, sponsor1); env.close(); env(sponsor::transfer( @@ -850,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)); @@ -1454,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 @@ -1473,10 +1551,8 @@ public: env.close(); { - // Fee should be checked before permission check, - // otherwise tecNO_SPONSOR_PERMISSION returned when permission - // check fails could cause context reset to pay Fee because it - // is tec error + // Fee should be checked before sponsor permission, otherwise a tec + // result from a later check could cause context reset to pay Fee. auto aliceBalance = env.balance(alice); auto bobBalance = env.balance(bob); auto sponsorBalance = env.balance(sponsor); @@ -1553,8 +1629,6 @@ public: { // below reserve adjustAccountXRPBalance(env, sponsor, env.current()->fees().reserve); - env.close(); - auto const feeAmt = XRP(4); env(noop(alice), Fee(env.current()->fees().base), @@ -1588,10 +1662,8 @@ public: }; { - // Fee should be checked before permission check, - // otherwise tecNO_SPONSOR_PERMISSION returned when permission - // check fails could cause context reset to pay Fee because it - // is tec error + // Fee should be checked before sponsor permission, otherwise a tec + // result from a later check could cause context reset to pay Fee. auto aliceBalance = env.balance(alice); auto bobBalance = env.balance(bob); auto sponsorBalance = env.balance(sponsor); @@ -1770,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()}; @@ -2050,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)); @@ -2105,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 = @@ -2148,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)); @@ -2161,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 @@ -2185,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(); @@ -2203,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); @@ -2227,7 +2413,7 @@ public: env.fund(XRP(10000), alice, bob, sponsor, sponsor2); env.close(); - // CheckCreate -> Check = 0Cancel + // CheckCreate -> Check -> CheckCancel uint32_t seq = 0; testEachSponsorship( @@ -2293,7 +2479,7 @@ public: env.fund(XRP(10000), alice, bob, sponsor); env.close(); - // CheckCreate -> = 0 CheckCash + // CheckCreate -> CheckCash uint32_t seq2 = 0; testEachSponsorship( env, @@ -2334,7 +2520,7 @@ public: env(pay(gw, alice, usd(100))); env.close(); - // CheckCreat = 0e -> CheckCash + // CheckCreate -> CheckCash uint32_t seq2 = 0; testEachSponsorship( env, @@ -2445,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()}; @@ -2475,10 +2662,19 @@ public: { 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), - Sig(sfSponsorSignature, sponsor2)); + 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); @@ -2495,6 +2691,115 @@ public: 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(); + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + } + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + if (!cosigning) + { + auto const sponsorshipSle = env.le(keylet::sponsorship(sponsor2, alice)); + BEAST_EXPECT(sponsorshipSle); + if (sponsorshipSle) + { + BEAST_EXPECT( + !sponsorshipSle->isFieldPresent(sfRemainingOwnerCount) || + sponsorshipSle->getFieldU32(sfRemainingOwnerCount) == 0); + } + } + + // DIDDelete + env(did::del(alice)); + 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); + } } void @@ -2680,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 @@ -2828,14 +3177,11 @@ public: env(jv); env.close(); - // for free mptoken checks - // adjustAccountXRPBalance(env, sponsor, reserve(env, 2)); + // Create tickets so the sponsor is past free-tier reserve behavior. std::uint32_t const ticketSeq{env.seq(sponsor) + 1}; env(ticket::create(sponsor, 2)); env.close(); - // adjustAccountXRPBalance(env, sponsor, reserve(env, 3) - - // drops(1)); jv = {}; jv[sfTransactionType] = jss::MPTokenAuthorize; jv[sfAccount] = bob.human(); @@ -2949,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 @@ -3022,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) { @@ -3262,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); @@ -3305,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); } { @@ -3328,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); } } @@ -3649,7 +4154,7 @@ public: jt.jv[sfSponsorSignature.jsonName][sfSigningPubKey.jsonName] = ""; auto const seq = env.seq(alice); - // should fail BatchSigners does have signer for SponsorSignature + // should fail because BatchSigners does not have signer for SponsorSignature env(batch::outer(alice, seq, XRP(1), tfAllOrNothing), batch::Inner(jt.jv, seq + 1), batch::Inner(ticket::create(alice, 1), seq + 2), @@ -3734,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) { @@ -3775,6 +4314,10 @@ protected: testDelegatePermission(); testBatch(); + + testSponsoredTrustLineNoFreeReserve(); + testCoSignReserveBoundedBySponsorshipBudget(); + testReserveSponsorGate(); } void 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/test/rpc/AccountObjects_test.cpp b/src/test/rpc/AccountObjects_test.cpp index d319b92950..c040c0a5f5 100644 --- a/src/test/rpc/AccountObjects_test.cpp +++ b/src/test/rpc/AccountObjects_test.cpp @@ -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); } } } 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