diff --git a/include/xrpl/ledger/helpers/AccountRootHelpers.h b/include/xrpl/ledger/helpers/AccountRootHelpers.h index 2e1c908095..4cc591c772 100644 --- a/include/xrpl/ledger/helpers/AccountRootHelpers.h +++ b/include/xrpl/ledger/helpers/AccountRootHelpers.h @@ -83,7 +83,7 @@ accountReserve(ReadView const& view, AccountID const& id, beast::Journal j, Adju return accountReserve(view, view.read(keylet::account(id)), j, adj); } -/** Check if an account has insufficient reserve. +/** Check if an account has sufficient reserve. * * The transaction-level reserve sponsor (if any) is taken from * `ctx.txReserveContext` and, per XLS-68, is only applied when `accSle` is the @@ -98,7 +98,7 @@ accountReserve(ReadView const& view, AccountID const& id, beast::Journal j, Adju * @return Transaction result code */ [[nodiscard]] TER -checkInsufficientReserve( +checkReserve( ApplyViewContext ctx, SLE::const_ref accSle, STAmount const& accBalance, diff --git a/include/xrpl/ledger/helpers/EscrowHelpers.h b/include/xrpl/ledger/helpers/EscrowHelpers.h index af58af613f..bd4b6c4e48 100644 --- a/include/xrpl/ledger/helpers/EscrowHelpers.h +++ b/include/xrpl/ledger/helpers/EscrowHelpers.h @@ -70,7 +70,7 @@ escrowUnlockApplyHelper( { // Can the account cover the trust line's reserve? if (auto const ret = - checkInsufficientReserve(ctx, sleDest, xrpBalance, {.ownerCountDelta = 1}, journal); + checkReserve(ctx, sleDest, xrpBalance, {.ownerCountDelta = 1}, journal); !isTesSuccess(ret)) { JLOG(journal.trace()) << "Trust line does not exist. " @@ -194,7 +194,7 @@ escrowUnlockApplyHelper( if (!ctx.view.exists(mptKeylet) && createAsset && !receiverIssuer) { if (auto const ret = - checkInsufficientReserve(ctx, sleDest, xrpBalance, {.ownerCountDelta = 1}, journal); + checkReserve(ctx, sleDest, xrpBalance, {.ownerCountDelta = 1}, journal); !isTesSuccess(ret)) return ret; diff --git a/include/xrpl/ledger/helpers/SponsorHelpers.h b/include/xrpl/ledger/helpers/SponsorHelpers.h index fdc8ba69de..c77af50015 100644 --- a/include/xrpl/ledger/helpers/SponsorHelpers.h +++ b/include/xrpl/ledger/helpers/SponsorHelpers.h @@ -31,88 +31,35 @@ isReserveSponsored(STTx const& tx) return (tx.getFieldU32(sfSponsorFlags) & spfSponsorReserve) != 0u; } -inline std::optional -getTxReserveSponsorAccountID(STTx const& tx) -{ - if (tx.isFieldPresent(sfSponsor) && isReserveSponsored(tx)) - { - return tx.getAccountID(sfSponsor); - } - return {}; -} +std::optional +getTxReserveSponsorAccountID(STTx const& tx); -inline std::expected -getTxReserveSponsor(ReadView const& view, STTx const& tx) -{ - auto const sponsorID = getTxReserveSponsorAccountID(tx); - if (sponsorID) - { - auto sle = view.read(keylet::account(*sponsorID)); +std::expected +getTxReserveSponsor(ReadView const& view, STTx const& tx); - // already checked in Transactor::checkSponsor - if (!sle) - return std::unexpected(tecINTERNAL); - return sle; - } - return SLE::pointer(); -} +std::optional +getLedgerEntryReserveSponsorAccountID(SLE::const_ref sle, SF_ACCOUNT const& field = sfSponsor); -inline std::optional -getLedgerEntryReserveSponsorAccountID(SLE::const_ref sle, SF_ACCOUNT const& field = sfSponsor) -{ - if (sle->isFieldPresent(field)) - return sle->getAccountID(field); - return {}; -} - -inline SLE::pointer +SLE::pointer getLedgerEntryReserveSponsor( ApplyView& view, SLE::const_ref sle, - SF_ACCOUNT const& field = sfSponsor) -{ - auto const sponsorID = getLedgerEntryReserveSponsorAccountID(sle, field); - if (sponsorID) - return view.peek(keylet::account(*sponsorID)); - return {}; -} + SF_ACCOUNT const& field = sfSponsor); -inline SLE::const_pointer +SLE::const_pointer getLedgerEntryReserveSponsor( ReadView const& view, SLE::const_ref sle, - SF_ACCOUNT const& field = sfSponsor) -{ - auto const sponsorID = getLedgerEntryReserveSponsorAccountID(sle, field); - if (sponsorID) - return view.read(keylet::account(*sponsorID)); - return {}; -} + SF_ACCOUNT const& field = sfSponsor); -inline void +void addSponsorToLedgerEntry( SLE::ref sle, SLE::const_ref sponsorSle, - SF_ACCOUNT const& field = sfSponsor) -{ - XRPL_ASSERT( - (sle->getType() == ltRIPPLE_STATE && (field == sfHighSponsor || field == sfLowSponsor)) || - (sle->getType() != ltRIPPLE_STATE && field == sfSponsor), - "addSponsorToLedgerEntry : Invalid field to the LedgerEntry"); - if (sponsorSle) - sle->setAccountID(field, sponsorSle->getAccountID(sfAccount)); -} + SF_ACCOUNT const& field = sfSponsor); -inline void -removeSponsorFromLedgerEntry(SLE::ref sle, SF_ACCOUNT const& field = sfSponsor) -{ - XRPL_ASSERT( - (sle->getType() == ltRIPPLE_STATE && (field == sfHighSponsor || field == sfLowSponsor)) || - (sle->getType() != ltRIPPLE_STATE && field == sfSponsor), - "removeSponsorFromLedgerEntry : Invalid field to the LedgerEntry"); - if (sle->isFieldPresent(field)) - sle->makeFieldAbsent(field); -} +void +removeSponsorFromLedgerEntry(SLE::ref sle, SF_ACCOUNT const& field = sfSponsor); template inline std::optional diff --git a/include/xrpl/protocol/STTx.h b/include/xrpl/protocol/STTx.h index b36207bf61..5d5424e623 100644 --- a/include/xrpl/protocol/STTx.h +++ b/include/xrpl/protocol/STTx.h @@ -141,6 +141,9 @@ public: [[nodiscard]] std::vector const& getBatchTransactionIDs() const; + [[nodiscard]] AccountID + getFeePayerID() const; + private: /** Check the signature. @param rules The current ledger rules. diff --git a/include/xrpl/tx/Transactor.h b/include/xrpl/tx/Transactor.h index d8f97d5a5b..bdb51b06ec 100644 --- a/include/xrpl/tx/Transactor.h +++ b/include/xrpl/tx/Transactor.h @@ -137,7 +137,8 @@ enum class FeePayerType { struct FeePayer { - Keylet entry; + AccountID id; + Keylet keylet; SF_AMOUNT const& balanceField; FeePayerType type{FeePayerType::Account}; }; @@ -317,6 +318,7 @@ public: static NotTEC checkSponsor(ReadView const& view, STTx const& tx); + ///////////////////////////////////////////////////// // Interface used by AccountDelete diff --git a/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp b/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp index a8b688602e..814f6e7570 100644 --- a/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp +++ b/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp @@ -347,7 +347,7 @@ accountReserve(ReadView const& view, SLE::const_ref sle, beast::Journal j, Adjus } TER -checkInsufficientReserve( +checkReserve( ApplyViewContext ctx, // NOTE: we still need the accSle to be passed in as the ctx.txReserveContext.accountSle may be // outdated diff --git a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp index b104a9c016..40da0363a0 100644 --- a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp @@ -206,8 +206,8 @@ authorizeMPToken( // budget), so this check always runs for sponsored transactions. if (isSponsored || ownerCount(sleAcct, journal) >= 2) { - if (auto const ret = checkInsufficientReserve( - ctx, sleAcct, priorBalance, {.ownerCountDelta = 1}, journal); + if (auto const ret = + checkReserve(ctx, sleAcct, priorBalance, {.ownerCountDelta = 1}, journal); !isTesSuccess(ret)) return ret; } diff --git a/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp b/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp index 91ee8dbea6..dbdf12411d 100644 --- a/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp +++ b/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp @@ -662,8 +662,7 @@ addEmptyHolding( return tecDUPLICATE; // Can the account cover the trust line reserve ? - if (auto const ret = - checkInsufficientReserve(ctx, sleDst, priorBalance, {.ownerCountDelta = 1}, journal); + if (auto const ret = checkReserve(ctx, sleDst, priorBalance, {.ownerCountDelta = 1}, journal); !isTesSuccess(ret)) return tecNO_LINE_INSUF_RESERVE; diff --git a/src/libxrpl/ledger/helpers/SponsorHelpers.cpp b/src/libxrpl/ledger/helpers/SponsorHelpers.cpp new file mode 100644 index 0000000000..83923bfbe0 --- /dev/null +++ b/src/libxrpl/ledger/helpers/SponsorHelpers.cpp @@ -0,0 +1,93 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl { + +std::optional +getTxReserveSponsorAccountID(STTx const& tx) +{ + if (tx.isFieldPresent(sfSponsor) && isReserveSponsored(tx)) + { + return tx.getAccountID(sfSponsor); + } + return {}; +} + +std::expected +getTxReserveSponsor(ReadView const& view, STTx const& tx) +{ + auto const sponsorID = getTxReserveSponsorAccountID(tx); + if (sponsorID) + { + auto sle = view.read(keylet::account(*sponsorID)); + + // already checked in Transactor::checkSponsor + if (!sle) + return std::unexpected(tecINTERNAL); + return sle; + } + return SLE::pointer(); +} + +std::optional +getLedgerEntryReserveSponsorAccountID(SLE::const_ref sle, SF_ACCOUNT const& field) +{ + if (sle->isFieldPresent(field)) + return sle->getAccountID(field); + return {}; +} + +SLE::pointer +getLedgerEntryReserveSponsor(ApplyView& view, SLE::const_ref sle, SF_ACCOUNT const& field) +{ + auto const sponsorID = getLedgerEntryReserveSponsorAccountID(sle, field); + if (sponsorID) + return view.peek(keylet::account(*sponsorID)); + return {}; +} + +SLE::const_pointer +getLedgerEntryReserveSponsor(ReadView const& view, SLE::const_ref sle, SF_ACCOUNT const& field) +{ + auto const sponsorID = getLedgerEntryReserveSponsorAccountID(sle, field); + if (sponsorID) + return view.read(keylet::account(*sponsorID)); + return {}; +} + +void +addSponsorToLedgerEntry(SLE::ref sle, SLE::const_ref sponsorSle, SF_ACCOUNT const& field) +{ + XRPL_ASSERT( + (sle->getType() == ltRIPPLE_STATE && (field == sfHighSponsor || field == sfLowSponsor)) || + (sle->getType() != ltRIPPLE_STATE && field == sfSponsor), + "addSponsorToLedgerEntry : Invalid field to the LedgerEntry"); + if (sponsorSle) + sle->setAccountID(field, sponsorSle->getAccountID(sfAccount)); +} + +void +removeSponsorFromLedgerEntry(SLE::ref sle, SF_ACCOUNT const& field) +{ + XRPL_ASSERT( + (sle->getType() == ltRIPPLE_STATE && (field == sfHighSponsor || field == sfLowSponsor)) || + (sle->getType() != ltRIPPLE_STATE && field == sfSponsor), + "removeSponsorFromLedgerEntry : Invalid field to the LedgerEntry"); + if (sle->isFieldPresent(field)) + sle->makeFieldAbsent(field); +} + +} // namespace xrpl diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp index 93c90c9d95..dd2ec92c90 100644 --- a/src/libxrpl/protocol/STTx.cpp +++ b/src/libxrpl/protocol/STTx.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include @@ -606,6 +607,15 @@ STTx::getBatchTransactionIDs() const return *batchTxnIds_; } +AccountID +STTx::getFeePayerID() const +{ + if (isFieldPresent(sfSponsor) && ((getFieldU32(sfSponsorFlags) & spfSponsorFee) != 0u)) + return at(sfSponsor); + + return getInitiator(); +} + //------------------------------------------------------------------------------ static bool diff --git a/src/libxrpl/tx/Transactor.cpp b/src/libxrpl/tx/Transactor.cpp index af1491acb0..1447554812 100644 --- a/src/libxrpl/tx/Transactor.cpp +++ b/src/libxrpl/tx/Transactor.cpp @@ -578,7 +578,7 @@ Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee) return tesSUCCESS; auto const feePayer = getFeePayer(ctx.view, ctx.tx); - auto const payerSle = ctx.view.read(feePayer.entry); + auto const payerSle = ctx.view.read(feePayer.keylet); if (!payerSle) { @@ -653,9 +653,9 @@ Transactor::payFee() auto const feePaid = ctx_.tx[sfFee].xrp(); auto const feePayer = getFeePayer(view(), ctx_.tx); - auto const sle = view().peek(feePayer.entry); + auto const sle = view().peek(feePayer.keylet); - JLOG(j_.trace()) << "Fee payer: " + to_string(feePayer.entry.key); + JLOG(j_.trace()) << "Fee payer: " + to_string(feePayer.id); if (!sle) return tefINTERNAL; // LCOV_EXCL_LINE @@ -1306,7 +1306,7 @@ Transactor::reset(XRPAmount fee) return {tefINTERNAL, beast::kZero}; auto const feePayer = getFeePayer(view(), ctx_.tx); - auto const payerSle = view().peek(feePayer.entry); + auto const payerSle = view().peek(feePayer.keylet); if (!payerSle) return {tefINTERNAL, beast::kZero}; // LCOV_EXCL_LINE @@ -1375,31 +1375,39 @@ Transactor::getFeePayer(ReadView const& view, STTx const& tx) { auto const sponsorID = tx.getAccountID(sfSponsor); auto const sponseeID = tx.getInitiator(); - auto const hasSponsorSignature = tx.isFieldPresent(sfSponsorSignature); auto const sponsorshipKeylet = keylet::sponsorship(sponsorID, sponseeID); // if pre-funded sponsorship exists, prefer it - if (hasSponsorSignature && !view.exists(sponsorshipKeylet)) + if (view.exists(sponsorshipKeylet)) { - // co-signed + // pre funded return FeePayer{ - .entry = keylet::account(sponsorID), - .balanceField = sfBalance, - .type = FeePayerType::SponsorCoSigned}; + .id = sponsorID, + .keylet = sponsorshipKeylet, + .balanceField = sfFeeAmount, + .type = FeePayerType::SponsorPreFunded}; } - // pre funded + // Checked in Transactor::checkSponsor + XRPL_ASSERT( + tx.isFieldPresent(sfSponsorSignature), + "xrpl::getFeePayer has sponsor signature without a sponsorship object"); + + // co-signed return FeePayer{ - .entry = sponsorshipKeylet, - .balanceField = sfFeeAmount, - .type = FeePayerType::SponsorPreFunded}; + .id = sponsorID, + .keylet = keylet::account(sponsorID), + .balanceField = sfBalance, + .type = FeePayerType::SponsorCoSigned}; } - auto const payerAccountKeylet = keylet::account(tx.getInitiator()); + AccountID const payerID = tx.getInitiator(); + auto const payerAccountKeylet = keylet::account(payerID); auto const payerType = tx.isFieldPresent(sfDelegate) ? FeePayerType::Delegate : FeePayerType::Account; - return FeePayer{.entry = payerAccountKeylet, .balanceField = sfBalance, .type = payerType}; + return FeePayer{ + .id = payerID, .keylet = payerAccountKeylet, .balanceField = sfBalance, .type = payerType}; } // The sole purpose of this function is to provide a convenient, named diff --git a/src/libxrpl/tx/transactors/Sponsor/SponsorshipSet.cpp b/src/libxrpl/tx/transactors/Sponsor/SponsorshipSet.cpp index 7098038b2b..0f745dc148 100644 --- a/src/libxrpl/tx/transactors/Sponsor/SponsorshipSet.cpp +++ b/src/libxrpl/tx/transactors/Sponsor/SponsorshipSet.cpp @@ -224,7 +224,7 @@ SponsorshipSet::doApply() if (hasPositiveFeeAmount) sponsorBalanceAfterFee -= *feeAmount; - if (auto const ret = checkInsufficientReserve( + if (auto const ret = checkReserve( applyViewContext, sponsorAccSle, sponsorBalanceAfterFee.xrp(), @@ -294,7 +294,7 @@ SponsorshipSet::doApply() STAmount sponsorBalanceAfterFee = (*sponsorAccSle)[sfBalance]; sponsorBalanceAfterFee -= feeAmountDelta; - if (auto const ret = checkInsufficientReserve( + if (auto const ret = checkReserve( ctx_.getApplyViewContext(), sponsorAccSle, sponsorBalanceAfterFee.xrp(), diff --git a/src/libxrpl/tx/transactors/Sponsor/SponsorshipTransfer.cpp b/src/libxrpl/tx/transactors/Sponsor/SponsorshipTransfer.cpp index c87ca1c9b6..39496c66f3 100644 --- a/src/libxrpl/tx/transactors/Sponsor/SponsorshipTransfer.cpp +++ b/src/libxrpl/tx/transactors/Sponsor/SponsorshipTransfer.cpp @@ -112,12 +112,18 @@ SponsorshipTransfer::preflight(PreflightContext const& ctx) return temMALFORMED; } - // sfSponsorFlags should not be present if it is ending sponsorship + // sfSponsorFlags should not be present if it is ending sponsorship. if (ctx.tx.isFieldPresent(sfSponsorFlags)) { - JLOG(ctx.j.debug()) - << "preflight: sfSponsorFlags should not be present when ending sponsorship"; + // Unreachable: reaching here means sfSponsor is absent, which is already checked above, + // and preflight1Sponsor already rejects sfSponsorFlags present without sfSponsor with + // temINVALID_FLAG. Keep this as a defensive check. + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::SponsorshipTransfer::preflight : sfSponsorFlags present without sfSponsor " + "when ending sponsorship"); return temINVALID_FLAG; + // LCOV_EXCL_STOP } if (ctx.tx.isFieldPresent(sfSponsee) && @@ -149,102 +155,79 @@ SponsorshipTransfer::preflight(PreflightContext const& ctx) TER SponsorshipTransfer::preclaim(PreclaimContext const& ctx) { - auto const index = ctx.tx[~sfObjectID]; + auto const objectID = ctx.tx[~sfObjectID]; auto const newSponsorSleExpected = getTxReserveSponsor(ctx.view, ctx.tx); if (!newSponsorSleExpected) return newSponsorSleExpected.error(); // LCOV_EXCL_LINE auto const newSponsorSle = *newSponsorSleExpected; - 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) - return tecINTERNAL; // LCOV_EXCL_LINE - - if (isObjectSponsor) { - auto const sle = ctx.view.read(keylet::unchecked(*index)); - if (!sle) + // If it is ending sponsorship, sfSponsee is user input, return terNO_ACCOUNT if it does not + // exist. + if (ctx.tx.isFieldPresent(sfSponsee)) + return terNO_ACCOUNT; + + // If it is creating or reassigning sponsorship, sfSponsee is the account itself, which is + // always present by the time preclaim runs. Return tecINTERNAL if it does not exist. + return tecINTERNAL; // LCOV_EXCL_LINE + } + + // Default setup with an account sponsorship transfer. If it is an object transfer, they will be + // overridden to the object SLE and its type-specific sponsor field: + // sfHighSponsor/sfLowSponsor for a RippleState, sfSponsor for other object types. + SLE::const_pointer targetSle = sponseeSle; + auto const* sponsorField = &sfSponsor; + + if (objectID.has_value()) + { + auto const objectSle = ctx.view.read(keylet::unchecked(*objectID)); + if (!objectSle) return tecNO_ENTRY; - if (!isLedgerEntrySupportedBySponsorship(sle)) + if (!isLedgerEntrySupportedBySponsorship(objectSle)) return tecNO_PERMISSION; - auto const owner = getLedgerEntryOwner(ctx.view, sle, sponseeID); + auto const owner = getLedgerEntryOwner(ctx.view, objectSle, 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) - return tecNO_PERMISSION; - - // check that the object is not sponsored yet - if (sle->isFieldPresent(sponsorField)) - return tecNO_PERMISSION; - } - else if (ctx.tx.isFlag(tfSponsorshipReassign)) - { - if (!newSponsorSle) - return tecNO_PERMISSION; - - // check object is already ctx.sponsored - if (!sle->isFieldPresent(sponsorField)) - return tecNO_PERMISSION; - } - else if (ctx.tx.isFlag(tfSponsorshipEnd)) - { - if (newSponsorSle) - return tecNO_PERMISSION; - - // check object is sponsored - if (!sle->isFieldPresent(sponsorField)) - return tecNO_PERMISSION; - - // only the sponsor or sponsee can end sponsorship - auto const sponsor = sle->getAccountID(sponsorField); - if (account != sponsor && account != sponseeID) - return tecNO_PERMISSION; - } + // Object transfer: the target is the object, and its sponsor field + // depends on the object type, a RippleState stores the sponsor in + // sfHighSponsor/sfLowSponsor, while other object type uses sfSponsor. + targetSle = objectSle; + sponsorField = &getLedgerEntrySponsorField(objectSle, owner.value()); } - else + + bool const isSponsored = targetSle->isFieldPresent(*sponsorField); + + if (ctx.tx.isFlag(tfSponsorshipCreate)) { - if (ctx.tx.isFlag(tfSponsorshipCreate)) - { - if (!newSponsorSle) - return tecNO_PERMISSION; + // Creating a new sponsorship: needs a new reserve sponsor, and the + // target must not already be sponsored + if (!newSponsorSle || isSponsored) + return tecNO_PERMISSION; + } + else if (ctx.tx.isFlag(tfSponsorshipReassign)) + { + // Reassigning sponsorship: needs a new reserve sponsor, and the target must already + // be sponsored + if (!newSponsorSle || !isSponsored) + return tecNO_PERMISSION; + } + else if (ctx.tx.isFlag(tfSponsorshipEnd)) + { + // Ending sponsorship: no new reserve sponsor, the target must be sponsored. + if (newSponsorSle || !isSponsored) + return tecNO_PERMISSION; - // check account is not sponsored yet - if (sponseeSle->isFieldPresent(sfSponsor)) - return tecNO_PERMISSION; - } - else if (ctx.tx.isFlag(tfSponsorshipReassign)) - { - if (!newSponsorSle) - return tecNO_PERMISSION; - - // check account is already sponsored - if (!sponseeSle->isFieldPresent(sfSponsor)) - return tecNO_PERMISSION; - } - else if (ctx.tx.isFlag(tfSponsorshipEnd)) - { - if (newSponsorSle) - return tecNO_PERMISSION; - - // check account is sponsored - if (!sponseeSle->isFieldPresent(sfSponsor)) - return tecNO_PERMISSION; - - // only the sponsor or sponsee can end sponsorship - auto const sponsor = sponseeSle->getAccountID(sfSponsor); - if (account != sponsor && account != sponseeID) - return tecNO_PERMISSION; - } + // Only the sponsor or sponsee can end sponsorship. + auto const sponsor = targetSle->getAccountID(*sponsorField); + if (account != sponsor && account != sponseeID) + return tecNO_PERMISSION; } return tesSUCCESS; @@ -344,7 +327,7 @@ SponsorshipTransfer::doApply() // check new sponsor have sufficient balance // NOLINTNEXTLINE(readability-suspicious-call-argument) - if (auto const ter = checkInsufficientReserve( + if (auto const ter = checkReserve( ctx_.getApplyViewContext(), sponseeSle, sponseeSle->getFieldAmount(sfBalance), @@ -396,7 +379,7 @@ SponsorshipTransfer::doApply() // check new sponsor have sufficient balance // NOLINTNEXTLINE(readability-suspicious-call-argument) - if (auto const ter = checkInsufficientReserve( + if (auto const ter = checkReserve( ctx_.getApplyViewContext(), sponseeSle, sponseeSle->getFieldAmount(sfBalance), @@ -443,7 +426,7 @@ SponsorshipTransfer::doApply() // The owner takes the reserve burden back when the object is // no longer sponsored. - if (auto const ter = checkInsufficientReserve( + if (auto const ter = checkReserve( ctx_.getApplyViewContext(), ownerSle, balanceBeforeFee(ownerSle), @@ -482,7 +465,7 @@ SponsorshipTransfer::doApply() if (!newSponsorSle) return tefINTERNAL; // LCOV_EXCL_LINE - if (auto const ter = checkInsufficientReserve( + if (auto const ter = checkReserve( ctx_.getApplyViewContext(), sponseeSle, sponseeSle->getFieldAmount(sfBalance), @@ -509,7 +492,7 @@ SponsorshipTransfer::doApply() if (!newSponsorSle) return tefINTERNAL; // LCOV_EXCL_LINE - if (auto const ter = checkInsufficientReserve( + if (auto const ter = checkReserve( ctx_.getApplyViewContext(), sponseeSle, sponseeSle->getFieldAmount(sfBalance), @@ -544,7 +527,7 @@ SponsorshipTransfer::doApply() // The sponsee must be able to hold its own account reserve after // the sponsorship is removed. - if (auto const ter = checkInsufficientReserve( + if (auto const ter = checkReserve( ctx_.getApplyViewContext(), sponseeSle, balanceBeforeFee(sponseeSle), diff --git a/src/libxrpl/tx/transactors/account/SignerListSet.cpp b/src/libxrpl/tx/transactors/account/SignerListSet.cpp index 748c594730..4963807191 100644 --- a/src/libxrpl/tx/transactors/account/SignerListSet.cpp +++ b/src/libxrpl/tx/transactors/account/SignerListSet.cpp @@ -321,7 +321,7 @@ SignerListSet::replaceSignerList() // with TicketCreate. auto const applyViewContext = ctx_.getApplyViewContext(); auto const sponsorSle = applyViewContext.txReserveContext.sponsorSle; - if (auto const ret = checkInsufficientReserve( + if (auto const ret = checkReserve( ctx_.getApplyViewContext(), sle, preFeeBalance_, diff --git a/src/libxrpl/tx/transactors/check/CheckCash.cpp b/src/libxrpl/tx/transactors/check/CheckCash.cpp index ba183e5d42..88fb80d67d 100644 --- a/src/libxrpl/tx/transactors/check/CheckCash.cpp +++ b/src/libxrpl/tx/transactors/check/CheckCash.cpp @@ -393,11 +393,11 @@ CheckCash::doApply() // Check reserve. Return destination account SLE if enough reserve, // otherwise return nullptr. - auto checkReserve = [&]() -> SLE::pointer { + auto checkDstReserve = [&]() -> SLE::pointer { auto sleDst = psb.peek(keylet::account(accountID_)); // Can the account cover the trust line's or MPT reserve? - if (auto const ret = checkInsufficientReserve( + if (auto const ret = checkReserve( applyViewContext, sleDst, preFeeBalance_, {.ownerCountDelta = 1}, j_); !isTesSuccess(ret)) { @@ -433,7 +433,7 @@ CheckCash::doApply() // a. this (destination) account and // b. issuing account (not sending account). - auto const sleDst = checkReserve(); + auto const sleDst = checkDstReserve(); if (sleDst == nullptr) return tecNO_LINE_INSUF_RESERVE; @@ -500,7 +500,7 @@ CheckCash::doApply() auto const mptokenKey = keylet::mptoken(mptID, accountID_); if (!psb.exists(mptokenKey)) { - auto sleDst = checkReserve(); + auto sleDst = checkDstReserve(); if (sleDst == nullptr) return tecINSUFFICIENT_RESERVE; diff --git a/src/libxrpl/tx/transactors/check/CheckCreate.cpp b/src/libxrpl/tx/transactors/check/CheckCreate.cpp index 08e6b2d9a5..8bb0c8b9b4 100644 --- a/src/libxrpl/tx/transactors/check/CheckCreate.cpp +++ b/src/libxrpl/tx/transactors/check/CheckCreate.cpp @@ -197,7 +197,7 @@ CheckCreate::doApply() // reserve to pay fees. auto const applyViewContext = ctx_.getApplyViewContext(); auto const sponsorSle = applyViewContext.txReserveContext.sponsorSle; - if (auto const ret = checkInsufficientReserve( + if (auto const ret = checkReserve( ctx_.getApplyViewContext(), sle, preFeeBalance_, {.ownerCountDelta = 1}, ctx_.journal); !isTesSuccess(ret)) return ret; diff --git a/src/libxrpl/tx/transactors/delegate/DelegateSet.cpp b/src/libxrpl/tx/transactors/delegate/DelegateSet.cpp index ea27e8a3b6..c24874708c 100644 --- a/src/libxrpl/tx/transactors/delegate/DelegateSet.cpp +++ b/src/libxrpl/tx/transactors/delegate/DelegateSet.cpp @@ -100,7 +100,7 @@ DelegateSet::doApply() return tecINTERNAL; // LCOV_EXCL_LINE auto const applyViewContext = ctx_.getApplyViewContext(); auto const sponsorSle = applyViewContext.txReserveContext.sponsorSle; - if (auto const ret = checkInsufficientReserve( + if (auto const ret = checkReserve( ctx_.getApplyViewContext(), sleOwner, preFeeBalance_, diff --git a/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp b/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp index d783abd7c7..52997095c8 100644 --- a/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp +++ b/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp @@ -443,8 +443,8 @@ EscrowCreate::doApply() // 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_.getApplyViewContext(), sle, balance, {.ownerCountDelta = 1}, j_); + if (auto const ret = + checkReserve(ctx_.getApplyViewContext(), sle, balance, {.ownerCountDelta = 1}, j_); !isTesSuccess(ret)) return ret; @@ -455,7 +455,7 @@ EscrowCreate::doApply() // is always the source's own balance against the source's own // reserve — the sponsor's reserve was already validated above, and // a sponsor never covers the locked funds. We compare directly - // (rather than via checkInsufficientReserve) because that helper + // (rather than via checkReserve) because that helper // diverts to the sponsor's balance when a sponsor is present and // would ignore the source's post-lock balance entirely. // ownerCountDelta differs by case: diff --git a/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp b/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp index 3443a85b7d..3f6eaab062 100644 --- a/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp +++ b/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp @@ -163,7 +163,7 @@ DepositPreauth::doApply() // A preauth counts against the reserve of the issuing account, but we // check the starting balance because we want to allow dipping into the // reserve to pay fees. - if (auto const ret = checkInsufficientReserve( + if (auto const ret = checkReserve( applyViewContext, sleOwner, preFeeBalance_, {.ownerCountDelta = 1}, j_); !isTesSuccess(ret)) return ret; @@ -209,7 +209,7 @@ DepositPreauth::doApply() // A preauth counts against the reserve of the issuing account, but we // check the starting balance because we want to allow dipping into the // reserve to pay fees. - if (auto const ret = checkInsufficientReserve( + if (auto const ret = checkReserve( applyViewContext, sleOwner, preFeeBalance_, {.ownerCountDelta = 1}, j_); !isTesSuccess(ret)) return ret; diff --git a/src/libxrpl/tx/transactors/payment/Payment.cpp b/src/libxrpl/tx/transactors/payment/Payment.cpp index f445e26178..41614b1943 100644 --- a/src/libxrpl/tx/transactors/payment/Payment.cpp +++ b/src/libxrpl/tx/transactors/payment/Payment.cpp @@ -691,9 +691,8 @@ Payment::doApply() // reserve. auto const reserve = accountReserve(view(), sleSrc, j_); - // In a delegated payment, the fee payer is the delegated account, - // not the source account (accountID_). - bool const accountIsPayer = (ctx_.tx.getInitiator() == accountID_); + // In a delegated / fee sponsored payment, the fee payer is not the source account (accountID_). + bool const accountIsPayer = ctx_.tx.getFeePayerID() == accountID_; // preFeeBalance_ is the balance on the source account (accountID_) BEFORE the fees // were charged. If source account is the fee payer, it must also cover the fee. diff --git a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp index 99ff39b449..4895a311b7 100644 --- a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp +++ b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp @@ -144,8 +144,8 @@ PaymentChannelCreate::doApply() // 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( - applyViewContext, sle, preFeeBalance_, {.ownerCountDelta = 1}, j_); + if (auto const ret = + checkReserve(applyViewContext, sle, preFeeBalance_, {.ownerCountDelta = 1}, j_); !isTesSuccess(ret)) return ret; @@ -154,7 +154,7 @@ PaymentChannelCreate::doApply() // source's own balance against the source's own reserve — the // sponsor's reserve was already validated above, and a sponsor // never covers the locked funds. We compare directly (rather than - // via checkInsufficientReserve) because that helper diverts to the + // via checkReserve) because that helper diverts to the // sponsor's balance when a sponsor is present and would ignore the // source's post-lock balance entirely. ownerCountDelta differs by // case: diff --git a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp index 853c387a91..a521575b06 100644 --- a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp +++ b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp @@ -90,12 +90,12 @@ PaymentChannelFund::doApply() // Check reserve and funds availability auto const balance = (*sle)[sfBalance]; auto const applyViewContext = ctx_.getApplyViewContext(); - if (auto const ret = checkInsufficientReserve(applyViewContext, sle, balance, {}, j_); + if (auto const ret = checkReserve(applyViewContext, sle, balance, {}, j_); !isTesSuccess(ret)) return ret; - if (auto const ret = checkInsufficientReserve( - applyViewContext, sle, balance - ctx_.tx[sfAmount], {}, j_); + if (auto const ret = + checkReserve(applyViewContext, sle, balance - ctx_.tx[sfAmount], {}, j_); !isTesSuccess(ret)) return tecUNFUNDED; } diff --git a/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp b/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp index e084802933..fb2d856108 100644 --- a/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp +++ b/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp @@ -128,8 +128,8 @@ MPTokenIssuanceCreate::create( if (args.priorBalance) { - if (auto const ret = checkInsufficientReserve( - ctx, acct, *(args.priorBalance), {.ownerCountDelta = 1}, journal); + if (auto const ret = + checkReserve(ctx, acct, *(args.priorBalance), {.ownerCountDelta = 1}, journal); !isTesSuccess(ret)) return std::unexpected(ret); // tecINSUFFICIENT_RESERVE } diff --git a/src/libxrpl/tx/transactors/token/TrustSet.cpp b/src/libxrpl/tx/transactors/token/TrustSet.cpp index a9f0655de3..564f30013f 100644 --- a/src/libxrpl/tx/transactors/token/TrustSet.cpp +++ b/src/libxrpl/tx/transactors/token/TrustSet.cpp @@ -330,12 +330,18 @@ TrustSet::doApply() auto const applyViewContext = ctx_.getApplyViewContext(); auto const sponsorSle = applyViewContext.txReserveContext.sponsorSle; - std::uint32_t const uOwnerCount = ownerCount(sponsorSle ? sponsorSle : sle, j_); + auto const sponsorSle = getTxReserveSponsor(ctx_.getApplyViewContext()); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + + auto getSponsor = [&sponsorSle, this](AccountID const& account) { + return (sponsorSle && account == accountID_) ? sponsorSle : SLE::pointer(); + }; // The "free-tier" shortcut (ownerCount < 2) only applies when there is no sponsor. // With any sponsor on the tx, the sponsor must cover the reserve (via balance or // prefunded budget), so the reserve check always runs. - bool const freeTrustLine = uOwnerCount < 2 && !sponsorSle; + bool const freeTrustLine = !sponsorSle && (ownerCount(sle, j_) < 2); std::uint32_t const uQualityIn(bQualityIn ? ctx_.tx.getFieldU32(sfQualityIn) : 0); std::uint32_t uQualityOut(bQualityOut ? ctx_.tx.getFieldU32(sfQualityOut) : 0); @@ -533,10 +539,12 @@ TrustSet::doApply() if (bLowReserveSet && !bLowReserved) { + SLE::pointer const lowSponsor = getSponsor(uLowAccountID); + // should be checked PreFunded Sponsor before increaseOwnerCount() // For PreFunded sponsors, we need to check if there are sufficient reserves before // calling increaseOwnerCount(). - if (auto const ret = checkInsufficientReserve( + if (auto const ret = checkReserve( ctx_.getApplyViewContext(), sleLowAccount, preFeeBalance_, @@ -548,12 +556,12 @@ TrustSet::doApply() // Set reserve for low account. increaseOwnerCount( view(), - ReserveContext::makeFromAccount(view(), sleLowAccount, sponsorSle), + ReserveContext::makeFromAccount(view(), sleLowAccount, lowSponsor), 1, viewJ); uFlagsOut |= lsfLowReserve; - addSponsorToLedgerEntry(sleRippleState, sponsorSle, sfLowSponsor); + addSponsorToLedgerEntry(sleRippleState, lowSponsor, sfLowSponsor); if (!bHigh) bReserveIncrease = true; @@ -574,27 +582,29 @@ TrustSet::doApply() if (bHighReserveSet && !bHighReserved) { + SLE::pointer const highSponsor = getSponsor(uHighAccountID); + // should be checked PreFunded Sponsor before increaseOwnerCount() // For PreFunded sponsors, we need to check if there are sufficient reserves before // calling increaseOwnerCount(). - if (auto const ret = checkInsufficientReserve( + if (auto const ret = checkReserve( ctx_.getApplyViewContext(), sleHighAccount, preFeeBalance_, {.ownerCountDelta = 1}, j_); - sponsorSle && !isTesSuccess(ret)) + highSponsor && !isTesSuccess(ret)) return tecINSUF_RESERVE_LINE; // Set reserve for high account. increaseOwnerCount( view(), - ReserveContext::makeFromAccount(view(), sleHighAccount, sponsorSle), + ReserveContext::makeFromAccount(view(), sleHighAccount, highSponsor), 1, viewJ); uFlagsOut |= lsfHighReserve; - addSponsorToLedgerEntry(sleRippleState, sponsorSle, sfHighSponsor); + addSponsorToLedgerEntry(sleRippleState, highSponsor, sfHighSponsor); if (bHigh) bReserveIncrease = true; @@ -624,8 +634,7 @@ TrustSet::doApply() } // Reserve is not scaled by load. else if ( - auto const ret = - checkInsufficientReserve(ctx_.getApplyViewContext(), sle, preFeeBalance_, {}, j_); + auto const ret = checkReserve(ctx_.getApplyViewContext(), sle, preFeeBalance_, {}, j_); !freeTrustLine && bReserveIncrease && !isTesSuccess(ret)) { JLOG(j_.trace()) << "Delay transaction: Insufficent reserve to " @@ -655,7 +664,7 @@ TrustSet::doApply() return tecNO_LINE_REDUNDANT; } else if ( - auto const ret = checkInsufficientReserve( + auto const ret = checkReserve( ctx_.getApplyViewContext(), sle, preFeeBalance_, diff --git a/src/test/app/Sponsor_test.cpp b/src/test/app/Sponsor_test.cpp index 2e7d5ab6f2..21320c034a 100644 --- a/src/test/app/Sponsor_test.cpp +++ b/src/test/app/Sponsor_test.cpp @@ -921,6 +921,14 @@ public: sponsor::SponseeAcc(alice), Ter(tecNO_PERMISSION)); } + { + // The provided sfSponsee account does not exist + // when ending sponsorship. + Account const ghost("ghost"); // never funded, absent from ledger + env(sponsor::transfer(sponsor, tfSponsorshipEnd), + sponsor::SponseeAcc(ghost), + Ter(terNO_ACCOUNT)); + } } { @@ -1112,6 +1120,13 @@ public: Ter(tecNO_PERMISSION)); env.close(); + // Reassign an object that is not sponsored yet + env(sponsor::transfer(alice, tfSponsorshipReassign, checkId), + sponsor::As(sponsor1, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor1), + Ter(tecNO_PERMISSION)); + env.close(); + // Valid Owner env(sponsor::transfer(alice, tfSponsorshipCreate, checkId), sponsor::As(sponsor1, spfSponsorReserve), @@ -1129,6 +1144,13 @@ public: BEAST_EXPECT(sle1->isFieldPresent(sfSponsor)); BEAST_EXPECT(sle1->getAccountID(sfSponsor) == sponsor1.id()); + // Create on an object that is already sponsored + env(sponsor::transfer(alice, tfSponsorshipCreate, checkId), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2), + Ter(tecNO_PERMISSION)); + env.close(); + // transfer sponsor env(sponsor::transfer(alice, tfSponsorshipReassign, checkId), sponsor::As(sponsor2, spfSponsorReserve), @@ -4398,6 +4420,151 @@ public: testTrustSet(cosigning); } + void + testZeroBalanceSponsoredPaymentFeePayerCheck() + { + // Zero-balance sponsored Payment: getFeePayer() consistency check + testcase("Sponsored Payment: minimal-balance account with sponsor-pays-fee"); + + using namespace jtx; + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const sponsor("sponsor"); + Account const dest("dest"); + + auto const baseFee = env.current()->fees().base; + auto const baseReserve = env.current()->fees().reserve; + + // Fund sponsor and dest generously, alice with base reserve + 1 XRP for payment + env.fund(XRP(10000), sponsor, dest); + env.fund(baseReserve + XRP(1), alice); + env.close(); + + // Precondition: alice has base reserve + 1 XRP (enough for payment but not fee) + BEAST_EXPECT(env.balance(alice) == baseReserve + XRP(1)); + + // Alice tries to send a Payment to dest where sponsor pays the fee via spfSponsorFee. + // Passed even alice balance doesn't have enough to pay fee. + auto const preDest = env.balance(dest); + auto const preSponsor = env.balance(sponsor); + + // Alice sends 1 XRP to dest, sponsor pays the fee + env(pay(alice, dest, XRP(1)), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Fee(baseFee)); + env.close(); + + // Payment succeeded + // Alice's balance decreased by 1 XRP (the payment amount, NOT the fee) + BEAST_EXPECT(env.balance(alice) == baseReserve); + + // Dest received 1 XRP + BEAST_EXPECT(env.balance(dest) == preDest + XRP(1)); + + // Sponsor paid the fee (NOT alice) + BEAST_EXPECT(env.balance(sponsor) == preSponsor - baseFee); + } + + void + testTrustSetCounterpartySponsorMisroute() + { + // TrustSet's modify path applies the tx-level reserve sponsor to whichever + // side has its reserve gate trip on this update, regardless of whether that + // side belongs to the tx submitter. trustCreate only sets the submitter's + // reserve flag and snapshots the counterparty's asfDefaultRipple state into + // the line's NoRipple bit; if the counterparty later toggles asfDefaultRipple + // (the canonical issuer flow), the line and account flags disagree and on + // the submitter's next TrustSet the counterparty-side gate fires. Sponsor still will be + // checked if it can be applied to that end of the trustLine. + + testcase("TrustSet modify with sponsor does not misroute onto counterparty side"); + + using namespace test::jtx; + + Env env(*this); + Account const alice{"alice_t2178"}; + Account const bob{"bob_t2178"}; + Account const carol{"carol_t2178"}; + + // Fund without auto-setting asfDefaultRipple + env.fund(XRP(100'000), alice, bob, carol); + env.close(); + + // Determine account ordering + bool const aliceIsHigh = alice.id() > bob.id(); + + // To trigger the bug, we need the COUNTERPARTY's reserve gate to trip + // We use issuer/holder terminology where: + // - holder creates the trust line (their reserve is set first) + // - issuer enables DefaultRipple after (creates flag mismatch) + // - holder's second TrustSet triggers issuer's reserve gate + + auto const issuer = aliceIsHigh ? bob : alice; + auto const holder = aliceIsHigh ? alice : bob; + auto const usd = issuer["USD"]; + + // Issuer must NOT have DefaultRipple set initially + // Clear it explicitly (env.fund may have set it) + env(fclear(issuer, asfDefaultRipple)); + env.close(); + + // Holder creates the trust line first (holder's reserve flag is set) + // At this point, issuer does NOT have DefaultRipple set, so + // the NoRipple bit on issuer's side is set according to issuer's current flag + env(trust(holder, usd(1'000))); + env.close(); + + // Issuer now enables asfDefaultRipple (canonical issuer flow) + // This creates a mismatch: issuer's account flag says DefaultRipple=true + // but the trust line's NoRipple bit on issuer's side is still set + env(fset(issuer, asfDefaultRipple)); + env.close(); + + SF_ACCOUNT const& issuerSponsorField = aliceIsHigh ? sfLowSponsor : sfHighSponsor; + SF_ACCOUNT const& holderSponsorField = aliceIsHigh ? sfHighSponsor : sfLowSponsor; + + auto const lineKey = keylet::trustLine(alice, bob, usd.currency); + auto const sleLineBefore = env.le(lineKey); + if (!BEAST_EXPECT(sleLineBefore)) + return; + BEAST_EXPECT(!sleLineBefore->isFieldPresent(sfLowSponsor)); + BEAST_EXPECT(!sleLineBefore->isFieldPresent(sfHighSponsor)); + + auto const carolBefore = sponsoringOwnerCount(env, carol); + BEAST_EXPECT(carolBefore == 0); + auto const issuerSponsoredBefore = sponsoredOwnerCount(env, issuer); + BEAST_EXPECT(issuerSponsoredBefore == 0); + + // Holder modifies the trust line with Carol as sponsor + // This should trigger the issuer's reserve gate because of the DefaultRipple mismatch + // Carol (sponsor) should NOT be applied to issuer's side (issuer != tx submitter) + env(trust(holder, usd(2'000)), + sponsor::As(carol, spfSponsorReserve), + Sig(sfSponsorSignature, carol), + Ter(tesSUCCESS)); + env.close(); + + auto const sleLineAfter = env.le(lineKey); + if (!BEAST_EXPECT(sleLineAfter)) + return; + + // Carol only agreed to back the holder, not the issuer + BEAST_EXPECT(!sleLineAfter->isFieldPresent(issuerSponsorField)); + + // Holder's side also has no sponsor because holder's reserve flag was + // already set on the FIRST TrustSet (no sponsor in scope then) + BEAST_EXPECT(!sleLineAfter->isFieldPresent(holderSponsorField)); + + // Carol's sponsoring count should remain unchanged (no misroute) + auto const carolAfter = sponsoringOwnerCount(env, carol); + BEAST_EXPECT(carolAfter == carolBefore); + + // Issuer's sponsored count should remain unchanged (no misroute) + auto const issuerSponsoredAfter = sponsoredOwnerCount(env, issuer); + BEAST_EXPECT(issuerSponsoredAfter == issuerSponsoredBefore); + } + protected: void testSponsor() @@ -4432,6 +4599,9 @@ protected: testSponsoredTrustLineNoFreeReserve(); testCoSignReserveBoundedBySponsorshipBudget(); testReserveSponsorGate(); + + testZeroBalanceSponsoredPaymentFeePayerCheck(); + testTrustSetCounterpartySponsorMisroute(); } void