diff --git a/include/xrpl/ledger/ApplyView.h b/include/xrpl/ledger/ApplyView.h index 362eae0f79b..05bc3aefd3c 100644 --- a/include/xrpl/ledger/ApplyView.h +++ b/include/xrpl/ledger/ApplyView.h @@ -275,7 +275,7 @@ class ApplyView : public ReadView // Called when the owner count changes // This is required to support PaymentSandbox virtual void - adjustOwnerCountHook(AccountID const& account, std::uint32_t cur, std::uint32_t next) + adjustOwnerCountHook(AccountID const& account, OwnerCounts const& cur, OwnerCounts const& next) { } diff --git a/include/xrpl/ledger/PaymentSandbox.h b/include/xrpl/ledger/PaymentSandbox.h index 1cd89d93886..5915a7b988f 100644 --- a/include/xrpl/ledger/PaymentSandbox.h +++ b/include/xrpl/ledger/PaymentSandbox.h @@ -99,12 +99,12 @@ class DeferredCredits issuerSelfDebitMPT(MPTIssue const& issue, std::uint64_t amount, std::int64_t origBalance); void - ownerCount(AccountID const& id, std::uint32_t cur, std::uint32_t next); + ownerCount(AccountID const& id, OwnerCounts const& cur, OwnerCounts const& next); // Get the adjusted owner count. Since DeferredCredits is meant to be used // in payments, and payments only decrease owner counts, return the max // remembered owner count. - [[nodiscard]] std::optional + [[nodiscard]] std::optional ownerCount(AccountID const& id) const; void @@ -116,7 +116,7 @@ class DeferredCredits std::map creditsIOU_; std::map creditsMPT_; - std::map ownerCounts_; + std::map ownerCounts_; }; } // namespace detail @@ -210,10 +210,11 @@ class PaymentSandbox final : public detail::ApplyViewBase override; void - adjustOwnerCountHook(AccountID const& account, std::uint32_t cur, std::uint32_t next) override; + adjustOwnerCountHook(AccountID const& account, OwnerCounts const& cur, OwnerCounts const& next) + override; - [[nodiscard]] std::uint32_t - ownerCountHook(AccountID const& account, std::uint32_t count) const override; + [[nodiscard]] OwnerCounts + ownerCountHook(AccountID const& account, OwnerCounts const& count) const override; /** Apply changes to base view. diff --git a/include/xrpl/ledger/ReadView.h b/include/xrpl/ledger/ReadView.h index f4ee7e6fd25..60e29418ea2 100644 --- a/include/xrpl/ledger/ReadView.h +++ b/include/xrpl/ledger/ReadView.h @@ -19,6 +19,53 @@ namespace xrpl { +struct OwnerCounts +{ + std::uint32_t owner = 0; + std::uint32_t sponsored = 0; + std::uint32_t sponsoring = 0; + + OwnerCounts() = default; + OwnerCounts(SLE const& sle) + : owner(sle[sfOwnerCount]) + , sponsored(sle[sfSponsoredOwnerCount]) + , sponsoring(sle[sfSponsoringOwnerCount]) + { + } + + [[nodiscard]] std::uint32_t + count() const + { + int64_t const x = static_cast(owner) - sponsored + sponsoring; + if (x < 0) + return 0; + if (x > std::numeric_limits::max()) + return std::numeric_limits::max(); + return static_cast(x); + } + + auto + operator<=>(OwnerCounts const& o) const + { + return count() <=> o.count(); + } + + bool + operator==(OwnerCounts const& o) const + { + return this == &o || *this == o; + } + + [[nodiscard]] bool + valid() const + { + int64_t const x = static_cast(owner) - sponsored + sponsoring; + if (x < 0 || x > std::numeric_limits::max()) + return false; + return owner >= sponsored; + } +}; + //------------------------------------------------------------------------------ /** A view into a ledger. @@ -182,8 +229,8 @@ class ReadView // changes that accounts make during a payment. `ownerCountHook` adjusts the // ownerCount so it returns the max value of the ownerCount so far. // This is required to support PaymentSandbox. - [[nodiscard]] virtual std::uint32_t - ownerCountHook(AccountID const& account, std::uint32_t count) const + [[nodiscard]] virtual OwnerCounts + ownerCountHook(AccountID const& account, OwnerCounts const& count) const { return count; } diff --git a/include/xrpl/ledger/helpers/AccountRootHelpers.h b/include/xrpl/ledger/helpers/AccountRootHelpers.h index e19fe23393b..9611bfe0f24 100644 --- a/include/xrpl/ledger/helpers/AccountRootHelpers.h +++ b/include/xrpl/ledger/helpers/AccountRootHelpers.h @@ -10,6 +10,7 @@ #include #include +#include #include namespace xrpl { @@ -33,12 +34,26 @@ isGlobalFrozen(ReadView const& view, AccountID const& issuer); [[nodiscard]] XRPAmount xrpLiquid(ReadView const& view, AccountID const& id, std::int32_t ownerCountAdj, beast::Journal j); -/** Returns the account reserve, in drops. - Actual owner count can be adjusted by delta in ownerCountAdj - The reserve is calculated as - (ownerCount + "sponsoring object count" - "sponsored object count" + additionalOwnerCount) * - increment + (1 if not sponsored account + sponsoringAccountCount) * "reserve base" -*/ +[[nodiscard]] XRPAmount +xrpLiquid( + ReadView const& view, + SLE::const_ref accSle, + std::int32_t ownerCountAdj, + beast::Journal j); + +[[nodiscard]] XRPAmount +xrpLiquid( + ApplyView const& view, + STTx const& tx, + SLE::const_ref accSle, + std::int32_t ownerCountAdj, + beast::Journal j); + +// Returns the account reserve. +// Actual owner count and reserve count can be adjusted +// The reserve is calculated as +// (ownerCount + "sponsoring count" - "sponsored count" + "owner adjustment") * increment +// + ("1 if not sponsored account else 0" + "sponsoring account count") * "reserve base" [[nodiscard]] XRPAmount accountReserve( ReadView const& view, @@ -58,34 +73,222 @@ accountReserve( return accountReserve(view, view.read(keylet::account(id)), j, ownerCountAdj, reserveCountAdj); } +// Returns basic reserve for abstract account for given objects count +// Mostly used by tests XRPAmount baseAccountReserve(ReadView const& view, std::int32_t ownerCount); -[[nodiscard]] TER -checkInsufficientReserve( +enum class FeePayerType { + Account, + Delegate, + SponsorCoSigned, + SponsorPreFunded, +}; +struct FeePayer +{ + AccountID id; + Keylet keylet; + SF_AMOUNT const& balanceField; + FeePayerType type{FeePayerType::Account}; +}; +FeePayer +getFeePayer(ReadView const& view, STTx const& tx); + +// checkXrpBalance - checks whether there are enough funds in the account for cover reserve and +// additional expenses (balanceAdj). Works through xrpLiquid() + +TER +checkXrpBalanceHlp( ReadView const& view, + bool apply, + STTx const& tx, + std::optional const& accID, + std::optional> const& accOpt, + XRPAmount balanceAcc, // if set disable automatic balance calculation + std::optional> const& sponsorOpt, + std::int32_t ownerCountAdj, + std::int32_t reserveCountAdj, + XRPAmount balanceAdj, + bool moreThan2, // special case, reserve doesn't check if current ownerCount < 2 + beast::Journal j, + bool checkApplicability = true // SponsorTransfer can break relations tx[sfAccount] === accSle +); + +// simple case, only ownerAdjustment +template +[[nodiscard]] TER +checkXrpBalance( + V const& view, + STTx const& tx, + AccountID const& accID, + std::int32_t ownerCountAdj, + beast::Journal j) +{ + static XRPAmount const kA; + bool apply = false; // NOLINT + if constexpr (std::is_base_of_v>) + apply = true; + return checkXrpBalanceHlp(view, apply, tx, accID, {}, kA, {}, ownerCountAdj, 0, kA, false, j); +} + +// simple + balance adjustment +template +[[nodiscard]] TER +checkXrpBalance( + V const& view, + STTx const& tx, + AccountID const& accID, + std::int32_t ownerCountAdj, + XRPAmount balanceAdj, + beast::Journal j) +{ + static XRPAmount const kA; + bool apply = false; // NOLINT + if constexpr (std::is_base_of_v>) + apply = true; + return checkXrpBalanceHlp( + view, apply, tx, accID, {}, kA, {}, ownerCountAdj, 0, balanceAdj, false, j); +} + +// simple/SLE + balance adjustment +template +[[nodiscard]] TER +checkXrpBalance( + V const& view, + STTx const& tx, + SLE::const_ref accSle, + std::int32_t ownerCountAdj, + XRPAmount balanceAdj, + beast::Journal j) +{ + static XRPAmount const kA; + bool apply = false; // NOLINT + if constexpr (std::is_base_of_v>) + apply = true; + return checkXrpBalanceHlp( + view, apply, tx, {}, accSle, kA, {}, ownerCountAdj, 0, balanceAdj, false, j); +} + +// simple/Sle + sponsor(re-usage, checks on caller) + balance adjustment +template +[[nodiscard]] TER +checkXrpBalance( + V const& view, STTx const& tx, SLE::const_ref accSle, - STAmount const& accBalance, SLE::const_ref sponsorSle, - std::int32_t ownerCountDelta, - std::int32_t reserveCountDelta = 0, - beast::Journal j = beast::Journal{beast::Journal::getNullSink()}); + std::int32_t ownerCountAdj, + XRPAmount balanceAdj, + beast::Journal j) +{ + static XRPAmount const kA; + bool apply = false; // NOLINT + if constexpr (std::is_base_of_v>) + apply = true; + return checkXrpBalanceHlp( + view, apply, tx, {}, accSle, kA, sponsorSle, ownerCountAdj, 0, balanceAdj, false, j); +} + +// simple + sponsor(re-usage, checks on caller) + balance adjustment +template +[[nodiscard]] TER +checkXrpBalance( + V const& view, + STTx const& tx, + AccountID const& accID, + SLE::const_ref sponsorSle, + std::int32_t ownerCountAdj, + XRPAmount balanceAdj, + beast::Journal j) +{ + static XRPAmount const kA; + bool apply = false; + if constexpr (std::is_base_of_v>) + apply = true; + return checkXrpBalanceHlp( + view, apply, tx, accID, {}, kA, sponsorSle, ownerCountAdj, 0, balanceAdj, false, j); +} + +// simple/accountSle + balance(passed manually) + sponsor(re-usage, checks on caller) + moreThan2 +// check +template +[[nodiscard]] TER +checkXrpBalance( + V const& view, + STTx const& tx, + SLE::const_ref accSle, + SLE::const_ref sponsorSle, + std::int32_t ownerCountAdj, + bool moreThan2, + beast::Journal j) +{ + static XRPAmount const kA; + + bool apply = false; // NOLINT + if constexpr (std::is_base_of_v>) + apply = true; + return checkXrpBalanceHlp( + view, apply, tx, {}, accSle, kA, sponsorSle, ownerCountAdj, 0, kA, moreThan2, j); +} + +// simple/accountSle + sponsor(re-usage, checks on caller) +template +[[nodiscard]] inline TER +checkXrpBalance( + V const& view, + STTx const& tx, + SLE::const_ref accSle, + SLE::const_ref sponsorSle, + std::int32_t ownerCountAdj, + beast::Journal j = beast::Journal{beast::Journal::getNullSink()}) +{ + return checkXrpBalance(view, tx, accSle, sponsorSle, ownerCountAdj, false, j); +} +// simple/accountSle + balance(passed manually) + sponsor(re-usage, checks on caller) +template +[[nodiscard]] inline TER +checkXrpBalance( + V const& view, + STTx const& tx, + SLE::const_ref accSle, + XRPAmount balanceAcc, + SLE::const_ref sponsorSle, + std::int32_t ownerCountAdj, + beast::Journal j = beast::Journal{beast::Journal::getNullSink()}) +{ + static XRPAmount const kA; + + bool apply = false; + if constexpr (std::is_base_of_v>) + apply = true; + return checkXrpBalanceHlp( + view, apply, tx, {}, accSle, balanceAcc, sponsorSle, ownerCountAdj, 0, kA, false, j); +} + +// owner count, true - overflow false - underflow +std::expected +baseOwnerCount( + std::uint32_t ownerCount, + std::uint32_t sponsoredCount, + std::uint32_t sponsoringCount); + +// Returns the number of objects owned by the account std::uint32_t ownerCount( ReadView const& view, - SLE::const_ref sle, + SLE::const_ref accSle, beast::Journal j, std::int32_t ownerCountAdj = 0); -/** Adjust the owner count up or down. */ +// Adjust the owner count up or down for Account, Sponsor Account(if set) and Sponsorship object(if +// exists) void adjustOwnerCount( ApplyView& view, SLE::ref accountSle, SLE::ref sponsorSle, - std::int32_t amount, + std::int32_t ownerCountAdj, beast::Journal j = beast::Journal{beast::Journal::getNullSink()}); inline void @@ -93,23 +296,26 @@ adjustOwnerCount( ApplyView& view, AccountID const& account, std::optional const& sponsor, - std::int32_t amount, + std::int32_t ownerCountAdj, beast::Journal j = beast::Journal{beast::Journal::getNullSink()}) { adjustOwnerCount( view, view.peek(keylet::account(account)), sponsor ? view.peek(keylet::account(*sponsor)) : SLE::pointer(), - amount, + ownerCountAdj, j); } +// Wrappers for adjustOwnerCount, retrive Sponsor(if exists) from the object. +// If |adjustment| > 0 then object is complex (like SignerList). +// ownerCountAdj < 0 void adjustOwnerCountObj( ApplyView& view, SLE::ref accountSle, SLE::ref objectSle, - std::int32_t amount, + std::int32_t ownerCountAdj, beast::Journal j = beast::Journal{beast::Journal::getNullSink()}); inline void @@ -117,11 +323,11 @@ adjustOwnerCountObj( ApplyView& view, AccountID const& account, SLE::ref objectSle, - std::int32_t amount, + std::int32_t ownerCountAdj, beast::Journal j = beast::Journal{beast::Journal::getNullSink()}) { SLE::ref accountSle = view.peek(keylet::account(account)); - adjustOwnerCountObj(view, accountSle, objectSle, amount, j); + adjustOwnerCountObj(view, accountSle, objectSle, ownerCountAdj, j); } /** Returns IOU issuer transfer fee as Rate. Rate specifies diff --git a/include/xrpl/ledger/helpers/EscrowHelpers.h b/include/xrpl/ledger/helpers/EscrowHelpers.h index bd27f172232..6bf0f68a664 100644 --- a/include/xrpl/ledger/helpers/EscrowHelpers.h +++ b/include/xrpl/ledger/helpers/EscrowHelpers.h @@ -59,17 +59,14 @@ escrowUnlockApplyHelper( if (!view.exists(trustLineKey) && createAsset) { // Can the account cover the trust line's reserve? - auto const sponsorSle = getTxReserveSponsor(view, tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - - if (auto const ret = - checkInsufficientReserve(view, tx, sleDest, xrpBalance, *sponsorSle, 1, 0, journal); + auto const sponsorSle = getTxReserveSponsor(view, tx, sleDest->at(sfAccount)); + if (auto const ret = checkXrpBalance(view, tx, sleDest, sponsorSle, 1, journal); !isTesSuccess(ret)) { JLOG(journal.trace()) << "Trust line does not exist. " "Insufficient reserve to create line."; - + if (ret == tecINTERNAL && !view.rules().enabled(fixCleanup3_2_0)) + return tefEXCEPTION; return tecNO_LINE_INSUF_RESERVE; } @@ -92,7 +89,7 @@ escrowUnlockApplyHelper( Issue(currency, receiver), // limit of zero 0, // quality in 0, // quality out - *sponsorSle, // sponsor + sponsorSle, // sponsor journal); // journal !isTesSuccess(ter)) { @@ -189,25 +186,21 @@ escrowUnlockApplyHelper( auto const mptKeylet = keylet::mptoken(issuanceKey.key, receiver); if (!view.exists(mptKeylet) && createAsset && !receiverIssuer) { - auto const sponsorSle = getTxReserveSponsor(view, tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - - if (auto const ret = - checkInsufficientReserve(view, tx, sleDest, xrpBalance, *sponsorSle, 1, 0, journal); + auto const sponsorSle = getTxReserveSponsor(view, tx, sleDest->at(sfAccount)); + if (auto const ret = checkXrpBalance(view, tx, sleDest, sponsorSle, 1, journal); !isTesSuccess(ret)) return ret; - if (auto const ter = createMPToken(view, mptID, receiver, *sponsorSle, 0); + if (auto const ter = createMPToken(view, mptID, receiver, sponsorSle, 0); !isTesSuccess(ter)) { return ter; // LCOV_EXCL_LINE } // update owner count. - adjustOwnerCount(view, sleDest, *sponsorSle, 1, journal); + adjustOwnerCount(view, sleDest, sponsorSle, 1, journal); auto mptSle = view.peek(mptKeylet); - addSponsorToLedgerEntry(mptSle, *sponsorSle); + addSponsorToLedgerEntry(mptSle, sponsorSle); } if (!view.exists(mptKeylet) && !receiverIssuer) diff --git a/include/xrpl/ledger/helpers/NFTokenHelpers.h b/include/xrpl/ledger/helpers/NFTokenHelpers.h index 86036fd023e..d33a408ac9f 100644 --- a/include/xrpl/ledger/helpers/NFTokenHelpers.h +++ b/include/xrpl/ledger/helpers/NFTokenHelpers.h @@ -39,7 +39,14 @@ findTokenAndPage(ApplyView& view, AccountID const& owner, uint256 const& nftoken /** Insert the token in the owner's token directory. */ TER -insertToken(ApplyView& view, STTx const& tx, AccountID owner, SLE::ref sponsorSle, STObject&& nft); +insertToken( + ApplyView& view, + STTx const& tx, + SLE::ref ownerSle, + XRPAmount priorBalance, + SLE::ref sponsorSle, + STObject&& nft, + bool checkBalance = true); /** Remove the token from the owner's token directory. */ TER @@ -108,7 +115,7 @@ TER tokenOfferCreateApply( ApplyView& view, STTx const& tx, - AccountID const& acctID, + SLE::ref accSle, STAmount const& amount, std::optional const& dest, std::optional const& expiration, diff --git a/include/xrpl/ledger/helpers/SponsorHelpers.h b/include/xrpl/ledger/helpers/SponsorHelpers.h index 525f109484b..a40918cde38 100644 --- a/include/xrpl/ledger/helpers/SponsorHelpers.h +++ b/include/xrpl/ledger/helpers/SponsorHelpers.h @@ -9,9 +9,17 @@ #include #include +#include +#include namespace xrpl { +inline bool +isFeeSponsored(STTx const& tx) +{ + return (tx.getFieldU32(sfSponsorFlags) & spfSponsorFee) != 0u; +} + inline bool isReserveSponsored(STTx const& tx) { @@ -21,58 +29,111 @@ isReserveSponsored(STTx const& tx) inline bool isSponsorReserveCoSigning(STTx const& tx) { - if (!tx.isFieldPresent(sfSponsorSignature)) - return false; - return isReserveSponsored(tx); + return isReserveSponsored(tx) && tx.isFieldPresent(sfSponsorSignature); } inline std::optional -getTxReserveSponsorAccountID(STTx const& tx) +getTxReserveSponsorAccountID(STTx const& tx, std::optional const& acc = {}) { - if (tx.isFieldPresent(sfSponsor) && isReserveSponsored(tx)) - { - return tx.getAccountID(sfSponsor); - } - return {}; + return (!acc || acc == tx[sfAccount]) && tx.isFieldPresent(sfSponsor) && isReserveSponsored(tx) + ? std::make_optional(tx[sfSponsor]) + : std::nullopt; } -inline std::expected -getTxReserveSponsor(ApplyView& view, STTx const& tx) +template +auto +getTxReserveSponsorBase(V&& view, STTx const& tx) { auto const sponsorID = getTxReserveSponsorAccountID(tx); if (sponsorID) { - auto sle = view.peek(keylet::account(*sponsorID)); + if constexpr (std::is_base_of_v>) + { + return view.peek(keylet::account(*sponsorID)); + } + else + { + return view.read(keylet::account(*sponsorID)); + } + } - // already checked in Transactor::checkSponsor - if (!sle) - return std::unexpected(tecINTERNAL); - return sle; + if constexpr (std::is_base_of_v>) + { + return SLE::pointer(); + } + else + { + return SLE::const_pointer(); } - return SLE::pointer(); } -inline std::expected -getTxReserveSponsor(ReadView const& view, STTx const& tx) +template +auto +getTxReserveSponsor(V&& view, STTx const& tx, std::optional const& acc = {}) { - auto const sponsorID = getTxReserveSponsorAccountID(tx); + auto const sponsorID = getTxReserveSponsorAccountID(tx, acc); if (sponsorID) { - auto sle = view.read(keylet::account(*sponsorID)); + if constexpr (std::is_base_of_v>) + { + auto sle = view.peek(keylet::account(*sponsorID)); + // already checked in Transactor::checkSponsor + if (!sle) + Throw("Empty sponsor"); // LCOV_EXCL_LINE + return sle; + } + else + { + auto sle = view.read(keylet::account(*sponsorID)); + // already checked in Transactor::checkSponsor + if (!sle) + Throw("Empty sponsor"); // LCOV_EXCL_LINE + return sle; + } + } + + if constexpr (std::is_base_of_v>) + { + return SLE::pointer(); + } + else + { + return SLE::const_pointer(); + } +} - // already checked in Transactor::checkSponsor - if (!sle) - return std::unexpected(tecINTERNAL); - return sle; +inline SF_ACCOUNT const& +getLedgerEntrySponsorField(SLE 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."); + // LCOV_EXCL_STOP + } + default: + return sfSponsor; } - return SLE::pointer(); } inline std::optional getLedgerEntryReserveSponsorAccountID(SLE::const_ref sle, SF_ACCOUNT const& field = sfSponsor) { if (sle->isFieldPresent(field)) - return sle->getAccountID(field); + return sle->at(field); return {}; } @@ -106,12 +167,42 @@ addSponsorToLedgerEntry( 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)); + { + XRPL_ASSERT( + (sle->getType() == ltRIPPLE_STATE && + (field == sfHighSponsor || field == sfLowSponsor)) || + (sle->getType() != ltRIPPLE_STATE && field == sfSponsor), + "addSponsorToLedgerEntry : field type"); + + XRPL_ASSERT( + sponsorSle->getType() == ltACCOUNT_ROOT, "addSponsorToLedgerEntry : sponsor type"); + + sle->at(field) = sponsorSle->at(sfAccount); + } +} + +inline void +addSponsorToLedgerEntry( + ApplyView& view, + SLE::ref sle, + SLE::const_ref sponsorSle, + SF_ACCOUNT const& field = sfSponsor) +{ + if (sponsorSle) + { + XRPL_ASSERT( + (sle->getType() == ltRIPPLE_STATE && + (field == sfHighSponsor || field == sfLowSponsor)) || + (sle->getType() != ltRIPPLE_STATE && field == sfSponsor), + "addSponsorToLedgerEntry : Invalid field to the LedgerEntry"); + + XRPL_ASSERT( + sponsorSle->getType() == ltACCOUNT_ROOT, "addSponsorToLedgerEntry : sponsor type"); + + sle->at(field) = sponsorSle->at(sfAccount); + view.update(sle); + } } inline void diff --git a/include/xrpl/protocol/STTx.h b/include/xrpl/protocol/STTx.h index 659fede31da..3aa448d2e79 100644 --- a/include/xrpl/protocol/STTx.h +++ b/include/xrpl/protocol/STTx.h @@ -84,7 +84,7 @@ class STTx final : public STObject, public CountedObject getSeqValue() const; AccountID - getFeePayer() const; + getInitiator() const; boost::container::flat_set getMentionedAccounts() const; diff --git a/include/xrpl/protocol/XRPAmount.h b/include/xrpl/protocol/XRPAmount.h index f09ddc337ae..74d37933022 100644 --- a/include/xrpl/protocol/XRPAmount.h +++ b/include/xrpl/protocol/XRPAmount.h @@ -26,7 +26,7 @@ class XRPAmount : private boost::totally_ordered, using value_type = std::int64_t; private: - value_type drops_; + value_type drops_ = 0; public: XRPAmount() = default; @@ -39,7 +39,7 @@ class XRPAmount : private boost::totally_ordered, { } - constexpr XRPAmount(beast::Zero) : drops_(0) + constexpr XRPAmount(beast::Zero) { } @@ -145,6 +145,12 @@ class XRPAmount : private boost::totally_ordered, return drops(); } + [[nodiscard]] bool + negative() const noexcept + { + return drops_ < 0; + } + /** Return the sign of the amount */ [[nodiscard]] constexpr int signum() const noexcept diff --git a/include/xrpl/tx/Transactor.h b/include/xrpl/tx/Transactor.h index d419e561283..2ca189d1b40 100644 --- a/include/xrpl/tx/Transactor.h +++ b/include/xrpl/tx/Transactor.h @@ -109,20 +109,6 @@ struct PreflightResult; // Needed for preflight specialization class Change; -enum class FeePayerType { - Account, - Delegate, - SponsorCoSigned, - SponsorPreFunded, -}; - -struct FeePayer -{ - Keylet entry; - SF_AMOUNT const& balanceField; - FeePayerType type{FeePayerType::Account}; -}; - class Transactor { protected: @@ -131,7 +117,7 @@ class Transactor beast::Journal const j_; AccountID const accountID_; - XRPAmount preFeeBalance_{}; // Balance before fees. + XRPAmount preFeeBalance_; // Balance before fees. public: virtual ~Transactor() = default; @@ -374,9 +360,6 @@ class Transactor std::pair reset(XRPAmount fee); - static FeePayer - getFeePayer(ReadView const& view, STTx const& tx); - TER consumeSeqProxy(SLE::pointer const& sleAccount); TER diff --git a/src/libxrpl/ledger/PaymentSandbox.cpp b/src/libxrpl/ledger/PaymentSandbox.cpp index 97e3e53cbfb..092d0ff111b 100644 --- a/src/libxrpl/ledger/PaymentSandbox.cpp +++ b/src/libxrpl/ledger/PaymentSandbox.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -170,7 +171,7 @@ DeferredCredits::issuerSelfDebitMPT( } void -DeferredCredits::ownerCount(AccountID const& id, std::uint32_t cur, std::uint32_t next) +DeferredCredits::ownerCount(AccountID const& id, OwnerCounts const& cur, OwnerCounts const& next) { auto const v = std::max(cur, next); auto r = ownerCounts_.emplace(id, v); @@ -181,7 +182,7 @@ DeferredCredits::ownerCount(AccountID const& id, std::uint32_t cur, std::uint32_ } } -std::optional +std::optional DeferredCredits::ownerCount(AccountID const& id) const { auto i = ownerCounts_.find(id); @@ -391,10 +392,10 @@ PaymentSandbox::balanceHookSelfIssueMPT(xrpl::MPTIssue const& issue, std::int64_ return STAmount{issue}; } -std::uint32_t -PaymentSandbox::ownerCountHook(AccountID const& account, std::uint32_t count) const +OwnerCounts +PaymentSandbox::ownerCountHook(AccountID const& account, OwnerCounts const& count) const { - std::uint32_t result = count; + OwnerCounts result = count; for (auto curSB = this; curSB != nullptr; curSB = curSB->ps_) { if (auto adj = curSB->tab_.ownerCount(account)) @@ -442,8 +443,8 @@ PaymentSandbox::issuerSelfDebitHookMPT( void PaymentSandbox::adjustOwnerCountHook( AccountID const& account, - std::uint32_t cur, - std::uint32_t next) + OwnerCounts const& cur, + OwnerCounts const& next) { tab_.ownerCount(account, cur, next); } diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index 7bc99edc02e..d0fe7d1bd64 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -471,12 +471,9 @@ doWithdraw( } auto const sponsorSle = getTxReserveSponsor(view, tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - // Move the funds directly from the broker's pseudo-account to the - // dstAcct - return accountSend(view, sourceAcct, dstAcct, amount, j, *sponsorSle, WaiveTransferFee::Yes); + // Move the funds directly from the broker's pseudo-account to the dstAcct + return accountSend(view, sourceAcct, dstAcct, amount, j, sponsorSle, WaiveTransferFee::Yes); } TER diff --git a/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp b/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp index 2d0573f36e8..5df1c1a073a 100644 --- a/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp +++ b/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp @@ -24,11 +24,15 @@ #include #include #include +#include #include #include #include #include #include +#include +#include +#include #include namespace xrpl { @@ -43,21 +47,21 @@ isGlobalFrozen(ReadView const& view, AccountID const& issuer) return false; } -// An owner count cannot be negative. If adjustment would cause a negative +// An owner count cannot be negative. If ownerCountAdj would cause a negative // owner count, clamp the owner count at 0. Similarly for overflow. This -// adjustment allows the ownerCount to be adjusted up or down in multiple steps. +// ownerCountAdj allows the ownerCount to be adjusted up or down in multiple steps. // If id != std::nullopt, then do error reporting. // // Returns adjusted owner count. static std::uint32_t confineOwnerCount( std::uint32_t current, - std::int32_t adjustment, + std::int32_t ownerCountAdj, std::optional const& id = std::nullopt, beast::Journal j = beast::Journal{beast::Journal::getNullSink()}) { - std::uint32_t adjusted{current + adjustment}; - if (adjustment > 0) + std::uint32_t adjusted{current + ownerCountAdj}; + if (ownerCountAdj > 0) { // Overflow is well defined on unsigned if (adjusted < current) @@ -85,79 +89,89 @@ confineOwnerCount( return adjusted; } +std::expected +baseOwnerCount( + std::uint32_t ownerCount, + std::uint32_t sponsoredCount, + std::uint32_t sponsoringCount) +{ + int64_t const x = static_cast(ownerCount) - sponsoredCount + sponsoringCount; + if (x < 0) + return std::unexpected(false); + if (x > std::numeric_limits::max()) + return std::unexpected(true); + return static_cast(x); +} + static std::uint32_t ownerCountHlp( ReadView const& view, SLE::const_ref sle, - std::int32_t adjustment, + std::int32_t ownerCountAdj, bool reportConfine, beast::Journal j) { - AccountID const id = sle->getAccountID(sfAccount); - std::uint32_t const savedCount = sle->at(sfOwnerCount); - std::uint32_t const hookedCount = view.ownerCountHook(id, savedCount); - - std::uint32_t const sponsoredCount = sle->at(sfSponsoredOwnerCount); - std::uint32_t const sponsoringCount = sle->at(sfSponsoringOwnerCount); + AccountID const id = sle->at(sfAccount); + OwnerCounts const currentCount(*sle); + auto const hookedCount = view.ownerCountHook(id, currentCount); - if (hookedCount < sponsoredCount) - { - Throw( - "xrpl::ownerCountHlp : OwnerCount must be greater than or equal to " - "SponsoredOwnerCount"); - } + if (!hookedCount.valid()) + Throw("xrpl::ownerCountHlp : Invalid OwnerCount "); // LCOV_EXCL_LINE std::int64_t deltaCount = - static_cast(adjustment) - sponsoredCount + sponsoringCount; + static_cast(ownerCountAdj) - hookedCount.sponsored + hookedCount.sponsoring; if (deltaCount > std::numeric_limits::max()) { deltaCount = std::numeric_limits::max(); - JLOG(j.fatal()) << "Account " << id << " delta count exceeds max, " - << "adjustment: " << adjustment << ", sponsoredCount: " << sponsoredCount - << ", sponsoringOwnerCount: " << sponsoringCount; + JLOG(j.error()) << "Account " << id << " adjustment exceeds max, " + << "Owner count: " << hookedCount.owner << ", adjustment: " << ownerCountAdj + << ", sponsoredCount: " << hookedCount.sponsored + << ", sponsoringCount: " << hookedCount.sponsoring; } else if (deltaCount < std::numeric_limits::min()) { deltaCount = std::numeric_limits::min(); - JLOG(j.fatal()) << "Account " << id << " delta count exceeds min, " - << "adjustment: " << adjustment << ", sponsoredCount: " << sponsoredCount - << ", sponsoringCount: " << sponsoringCount; + JLOG(j.error()) << "Account " << id << " adjustment exceeds min, " + << "Owner count: " << hookedCount.owner << ", adjustment: " << ownerCountAdj + << ", sponsoredCount: " << hookedCount.sponsored + << ", sponsoringCount: " << hookedCount.sponsoring; } std::uint32_t const confinedCount = reportConfine - ? confineOwnerCount(hookedCount, deltaCount, id, j) - : confineOwnerCount(hookedCount, deltaCount); + ? confineOwnerCount(hookedCount.owner, deltaCount, id, j) + : confineOwnerCount(hookedCount.owner, deltaCount); return confinedCount; } static std::uint32_t -reserveCountHlp(SLE::const_ref sle, std::int32_t adjustment, beast::Journal j) +reserveCountHlp(SLE::const_ref sle, std::int32_t reserveCountAdj, beast::Journal j) { + AccountID const id = sle->at(sfAccount); bool const isSponsored = sle->isFieldPresent(sfSponsor); - std::uint32_t const sponsoringCount = sle->getFieldU32(sfSponsoringAccountCount); - std::uint32_t const reserveCount = (isSponsored ? 0 : 1) + sponsoringCount; + std::int64_t const sponsoringCount = sle->at(sfSponsoringAccountCount); + std::int64_t const reserveCount = (isSponsored ? 0 : 1) + sponsoringCount; - std::uint32_t adjusted{reserveCount + adjustment}; - if (adjustment > 0) + // Fix like confineOwnerCount + std::int64_t adjusted = reserveCount + reserveCountAdj; + if (adjusted > std::numeric_limits::max()) { - // Overflow is well defined on unsigned - if (adjusted < reserveCount) - { - JLOG(j.fatal()) << "Reserve count exceeds max!"; - adjusted = std::numeric_limits::max(); - } + JLOG(j.error()) << "Account " << id << " reserve count exceeds max, " + << "adjustment: " << reserveCountAdj + << ", sponsoringCount: " << sponsoringCount + << ", reserveCount: " << reserveCount; + adjusted = std::numeric_limits::max(); } - else + else if (adjusted < 0) { - // Underflow is well defined on unsigned - if (adjusted > reserveCount) - { - JLOG(j.fatal()) << "Reserve count set below 0!"; - adjusted = 0; - } + JLOG(j.fatal()) << "Account " << id << " reserve count below 0, " + << "adjustment: " << reserveCountAdj + << ", sponsoringCount: " << sponsoringCount + << ", reserveCount: " << reserveCount; + adjusted = 0; } - return adjusted; + + return static_cast(adjusted); } static inline XRPAmount @@ -167,51 +181,190 @@ baseReserveHlp(ReadView const& view, std::uint32_t ownerCount, std::uint32_t res return (fees.reserve * reserveCount) + (fees.increment * ownerCount); } -static XRPAmount -reserveHlp( +// reserve, ownerCount, reserveCount +static std::tuple +accountReserveHlp( ReadView const& view, - SLE::const_ref sle, - std::uint32_t ownerCount, - std::uint32_t reserveCount) + SLE::const_ref accSle, + std::uint32_t ownerCountAdj, + std::uint32_t reserveCountAdj, + bool reportConfine, + beast::Journal j) { // Pseudo-accounts have no reserve requirement - if (isPseudoAccount(sle)) - return XRPAmount(0); + if (isPseudoAccount(accSle)) + return {XRPAmount(), 0, 0}; + std::uint32_t const ownerCount = ownerCountHlp(view, accSle, ownerCountAdj, reportConfine, j); + std::uint32_t const reserveCount = reserveCountHlp(accSle, reserveCountAdj, j); auto const reserve = baseReserveHlp(view, ownerCount, reserveCount); - return reserve; + return {reserve, ownerCount, reserveCount}; } std::uint32_t -ownerCount(ReadView const& view, SLE::const_ref sle, beast::Journal j, std::int32_t adjustment) +ownerCount( + ReadView const& view, + SLE::const_ref accSle, + beast::Journal j, + std::int32_t ownerCountAdj) { - return ownerCountHlp(view, sle, adjustment, true, j); + if (!accSle) + Throw("xrpl::ownerCount: empty sle type"); + + auto const sleType = accSle->getType(); + bool const validType = sleType == ltLOAN_BROKER || sleType == ltACCOUNT_ROOT; + if (!validType) + Throw("xrpl::ownerCount: valid sle type"); + + return ownerCountHlp(view, accSle, ownerCountAdj, true, j); } -XRPAmount -xrpLiquid(ReadView const& view, AccountID const& id, std::int32_t ownerCountAdj, beast::Journal j) +static FeePayer +getFeePayerHlp( + ReadView const& view, + STTx const& tx, + std::optional> const& sponsorshipSle) { - auto const sle = view.read(keylet::account(id)); - if (sle == nullptr) - return beast::kZero; + if (tx.isFieldPresent(sfDelegate)) + { + AccountID const payerID = tx[sfDelegate]; + return FeePayer{ + .id = payerID, + .keylet = keylet::account(payerID), + .balanceField = sfBalance, + .type = FeePayerType::Delegate}; + } + + if (tx.isFieldPresent(sfSponsor) && isFeeSponsored(tx)) + { + AccountID const sponsorAccountID = tx.getAccountID(sfSponsor); + AccountID const sponseeAccountID = tx.getAccountID(sfAccount); + auto const sponsorshipKeylet = keylet::sponsor(sponsorAccountID, sponseeAccountID); + + if (sponsorshipSle) + { + if (sponsorshipSle->get()) + { + // pre funded + if (!sponsorshipKeylet.check(*(sponsorshipSle->get()))) + { + Throw( + "getFeePayerHlp Invalid sponsorship"); // LCOV_EXCL_LINE + } + + return FeePayer{ + .id = sponsorAccountID, + .keylet = sponsorshipKeylet, + .balanceField = sfFeeAmount, + .type = FeePayerType::SponsorPreFunded}; + } + } + else if (view.exists(sponsorshipKeylet)) + { + // pre funded + return FeePayer{ + .id = sponsorAccountID, + .keylet = sponsorshipKeylet, + .balanceField = sfFeeAmount, + .type = FeePayerType::SponsorPreFunded}; + } + + if (!tx.isFieldPresent(sfSponsorSignature)) + { + Throw( + "Transactor::getFeePayer valid sponsor signature"); // LCOV_EXCL_LINE + } - std::uint32_t const ownerCount = ownerCountHlp(view, sle, ownerCountAdj, false, j); - std::uint32_t const reserveCount = reserveCountHlp(sle, 0, j); - auto const reserve = reserveHlp(view, sle, ownerCount, reserveCount); + // co-signed + return FeePayer{ + .id = sponsorAccountID, + .keylet = keylet::account(sponsorAccountID), + .balanceField = sfBalance, + .type = FeePayerType::SponsorCoSigned}; + } - auto const fullBalance = sle->getFieldAmount(sfBalance); + AccountID const payerID = tx[sfAccount]; + return FeePayer{ + .id = payerID, + .keylet = keylet::account(payerID), + .balanceField = sfBalance, + .type = FeePayerType::Account}; +} - auto const balance = view.balanceHookIOU(id, xrpAccount(), fullBalance); +FeePayer +getFeePayer(ReadView const& view, STTx const& tx) +{ + return getFeePayerHlp(view, tx, {}); +} - STAmount const amount = (balance < reserve) ? STAmount{0} : balance - reserve; +static XRPAmount +xrpLiquidHlp( + ReadView const& view, + SLE::const_ref accSle, + std::int32_t ownerCountAdj, + std::int32_t reserveCountAdj, + std::pair feeAdj, + beast::Journal j) +{ + AccountID const id = accSle->at(sfAccount); + auto [reserve, ownerCount, reserveCount] = + accountReserveHlp(view, accSle, ownerCountAdj, reserveCountAdj, false, j); + + STAmount const fullBalance = accSle->at(sfBalance); + XRPAmount const balance = view.balanceHookIOU(id, xrpAccount(), fullBalance).xrp(); + + XRPAmount const fee = feeAdj.first == id ? feeAdj.second : XRPAmount(); + XRPAmount const preFeeBalance = balance + fee; + XRPAmount const amount = preFeeBalance - std::max(reserve, fee); JLOG(j.trace()) << "accountHolds:" << " account=" << to_string(id) - << " amount=" << amount.getFullText() + << " amount=" << amount.decimalXRP() << " fullBalance=" << fullBalance.getFullText() - << " balance=" << balance.getFullText() << " reserve=" << reserve - << " ownerCount=" << ownerCount << " ownerCountAdj=" << ownerCountAdj; + << " balance=" << balance.decimalXRP() << " reserve=" << reserve.decimalXRP() + << " ownerCount=" << ownerCount << " ownerCountAdj=" << ownerCountAdj + << " reserveCount=" << reserveCount << " reserveCountAdj=" << reserveCountAdj + << " fee adj=" << fee.decimalXRP(); + return amount; +} + +XRPAmount +xrpLiquid(ReadView const& view, AccountID const& id, std::int32_t ownerCountAdj, beast::Journal j) +{ + auto const accSle = view.read(keylet::account(id)); + if (!accSle) + return beast::kZero; + + auto const x = + xrpLiquidHlp(view, accSle, ownerCountAdj, 0, std::pair(), j); + return x.negative() ? XRPAmount() : x; +} + +XRPAmount +xrpLiquid(ReadView const& view, SLE::const_ref accSle, std::int32_t ownerCountAdj, beast::Journal j) +{ + if (!accSle) + return beast::kZero; + + auto const x = + xrpLiquidHlp(view, accSle, ownerCountAdj, 0, std::pair(), j); + return x.negative() ? XRPAmount() : x; +} + +XRPAmount +xrpLiquid( + ApplyView const& view, + STTx const& tx, + SLE::const_ref accSle, + std::int32_t ownerCountAdj, + beast::Journal j) +{ + if (!accSle) + return beast::kZero; - return amount.xrp(); + XRPAmount const feePayed(tx[sfFee].xrp()); + AccountID const feePayer = getFeePayerHlp(view, tx, {}).id; + auto const x = xrpLiquidHlp(view, accSle, ownerCountAdj, 0, {feePayer, feePayed}, j); + return x.negative() ? XRPAmount() : x; } Rate @@ -225,22 +378,20 @@ transferRate(ReadView const& view, AccountID const& issuer) return kParityRate; } -static void +static std::uint32_t adjustOwnerCountHlp( ApplyView& view, SLE::ref sle, SF_UINT32 const& sfield, AccountID const& accID, - std::int32_t adjustment, - beast::Journal j, - bool callHook = true) + std::int32_t ownerCountAdj, + beast::Journal j) { std::uint32_t const current = sle->at(sfield); - std::uint32_t const adjusted = confineOwnerCount(current, adjustment, accID, j); - if (callHook) - view.adjustOwnerCountHook(accID, current, adjusted); + std::uint32_t const adjusted = confineOwnerCount(current, ownerCountAdj, accID, j); sle->at(sfield) = adjusted; view.update(sle); + return adjusted; } void @@ -248,7 +399,7 @@ adjustOwnerCount( ApplyView& view, SLE::ref accountSle, SLE::ref sponsorSle, - std::int32_t adjustment, + std::int32_t ownerCountAdj, beast::Journal j) { if (!accountSle) @@ -260,10 +411,13 @@ adjustOwnerCount( if (!validType) Throw("xrpl::adjustOwnerCount : valid account sle type"); - XRPL_ASSERT(adjustment, "xrpl::adjustOwnerCount : nonzero adjustment input"); - if (adjustment == 0) + XRPL_ASSERT(ownerCountAdj, "xrpl::adjustOwnerCount : nonzero adjustment input"); + if (ownerCountAdj == 0) return; + OwnerCounts const current(sleType == ltACCOUNT_ROOT ? OwnerCounts(*accountSle) : OwnerCounts()); + OwnerCounts adjusted(current); + auto const accountID = accountSle->getAccountID(sfAccount); if (sponsorSle) { @@ -271,19 +425,35 @@ adjustOwnerCount( Throw("xrpl::adjustOwnerCount : valid sponsor sle type"); auto const sponsorID = sponsorSle->getAccountID(sfAccount); - adjustOwnerCountHlp(view, accountSle, sfSponsoredOwnerCount, accountID, adjustment, j); - adjustOwnerCountHlp(view, sponsorSle, sfSponsoringOwnerCount, sponsorID, adjustment, j); + if (accountID == sponsorID) + Throw("adjustOwnerCount : account can't be sponsor for themself"); - auto sponsorObjSle = view.peek(keylet::sponsor(sponsorID, accountID)); - if (sponsorObjSle && adjustment > 0) + adjusted.sponsored = adjustOwnerCountHlp( + view, accountSle, sfSponsoredOwnerCount, accountID, ownerCountAdj, j); + + { + OwnerCounts const sponsorCurrent(*sponsorSle); + OwnerCounts sponsorAdjustment(sponsorCurrent); + sponsorAdjustment.sponsoring = adjustOwnerCountHlp( + view, sponsorSle, sfSponsoringOwnerCount, sponsorID, ownerCountAdj, j); + view.adjustOwnerCountHook(sponsorID, sponsorCurrent, sponsorAdjustment); + } + + auto sponsorshipSle = view.peek(keylet::sponsor(sponsorID, accountID)); + if (sponsorshipSle && ownerCountAdj > 0) { - // update the pre-funded ReserveCount on Sponsorship ledger object - // Reserve count moves opposite to adjustment: +adjustment => consume reserve (-), - adjustOwnerCountHlp( - view, sponsorObjSle, sfReserveCount, sponsorID, -adjustment, j, false); + // Only decrease the pre-funded ReserveCount on Sponsorship if we assign new objects. + // Removing/reassigning ownership of the object doesn't increase ReserveCount back. + // Don't call hook because this counter is not something that require reserve (like + // other sf...OwnerCounts do). + adjustOwnerCountHlp(view, sponsorshipSle, sfReserveCount, sponsorID, -ownerCountAdj, j); } } - adjustOwnerCountHlp(view, accountSle, sfOwnerCount, accountID, adjustment, j); + + adjusted.owner = + adjustOwnerCountHlp(view, accountSle, sfOwnerCount, accountID, ownerCountAdj, j); + if (sleType == ltACCOUNT_ROOT) + view.adjustOwnerCountHook(accountID, current, adjusted); } void @@ -291,16 +461,22 @@ adjustOwnerCountObj( ApplyView& view, SLE::ref accountSle, SLE::ref objectSle, - std::int32_t amount, + std::int32_t ownerCountAdj, beast::Journal j) { if (!objectSle) - Throw("xrpl::adjustOwnerCount : valid object sle"); + Throw("xrpl::adjustOwnerCountObj : valid object sle"); if (objectSle->getType() == ltACCOUNT_ROOT) - Throw("xrpl::adjustOwnerCount : valid object sle type"); + Throw("xrpl::adjustOwnerCountObj : valid object sle type"); + if (ownerCountAdj >= 0) + Throw("xrpl::adjustOwnerCountObj : adjustment >= 0"); + + XRPL_ASSERT(ownerCountAdj, "xrpl::adjustOwnerCount : nonzero adjustment input"); + if (ownerCountAdj == 0) + return; SLE::ref sponsorSle = getLedgerEntryReserveSponsor(view, objectSle); - adjustOwnerCount(view, accountSle, sponsorSle, amount, j); + adjustOwnerCount(view, accountSle, sponsorSle, ownerCountAdj, j); } XRPAmount @@ -316,10 +492,10 @@ accountReserve( if (sle->getType() != ltACCOUNT_ROOT) Throw("xrpl::accountReserve : valid sle type"); - std::uint32_t const ownerCount = ownerCountHlp(view, sle, ownerCountAdj, true, j); - std::uint32_t const reserveCount = reserveCountHlp(sle, reserveCountAdj, j); + [[maybe_unused]] auto [reserve, _1, _2] = + accountReserveHlp(view, sle, ownerCountAdj, reserveCountAdj, true, j); - return reserveHlp(view, sle, ownerCount, reserveCount); + return reserve; } XRPAmount @@ -329,52 +505,158 @@ baseAccountReserve(ReadView const& view, std::int32_t ownerCount) return reserve; } -TER -checkInsufficientReserve( +static TER +checkXrpBalanceGeneral( ReadView const& view, + bool apply, STTx const& tx, SLE::const_ref accSle, - STAmount const& accBalance, + XRPAmount balanceAcc, SLE::const_ref sponsorSle, - std::int32_t ownerCountDelta, - std::int32_t reserveCountDelta, - beast::Journal j) + std::int32_t ownerCountAdj, + std::int32_t reserveCountAdj, + XRPAmount balanceAdj, + bool moreThan2, + beast::Journal j, + bool checkApplicability) { - if (sponsorSle) - { - auto const isCoSigning = isSponsorReserveCoSigning(tx); + // Passed 'balance' means checks are on caller, needs for some non-standard checks + if (balanceAcc && balanceAdj) + return tecINTERNAL; // LCOV_EXCL_LINE + + if (balanceAcc.negative()) + return tecINSUFFICIENT_FUNDS; + + XRPL_ASSERT(!moreThan2 || (!balanceAcc && !balanceAdj), "small owner count with balance"); - auto const sle = view.read( - keylet::sponsor(sponsorSle->getAccountID(sfAccount), accSle->getAccountID(sfAccount))); + // With sponsored account reserve requirements can be 0. But some checks for liquidity assume we + // have reserve and we can borrow fee from it in edge cases. We can end up in potential negative + // balance. Here is 'apply' for - to distinguish if sfBalance contains preFee or postFee amount. + // Then fee participating in xrpLiquid calculation. + XRPAmount const feePayed(apply ? tx[sfFee].xrp() : XRPAmount()); + AccountID feePayer; - // prefunded sponsor should have a sponsorship entry - if (!isCoSigning && !sle) + bool const isDelegating = tx.isFieldPresent(sfDelegate); + bool const isCoSigning = isSponsorReserveCoSigning(tx); + + // !delegated || isCoSigning - delegate doesn't allow sponsorship, check accSle for reserve + if (sponsorSle && (!isDelegating || isCoSigning)) + { + auto const accID = accSle->at(sfAccount); + // Check if sponsor applicable (for manually passed sponsorSle) + if (checkApplicability && (accID != tx[sfAccount])) return tecINTERNAL; // LCOV_EXCL_LINE - if (sle) + AccountID const sponsorID = sponsorSle->at(sfAccount); + + bool const skipSponsorshipReserve = isDelegating || (ownerCountAdj <= 0); + auto const sponsorshipSle = + !skipSponsorshipReserve ? view.read(keylet::sponsor(sponsorID, accID)) : SLE::pointer(); + + // Sponsorship have priority before co-signing + if (!skipSponsorshipReserve) { - auto const ownerCountAllowed = sle->getFieldU32(sfReserveCount); - if (ownerCountAllowed < ownerCountDelta) - return tecINSUFFICIENT_RESERVE; + if (!isCoSigning && !sponsorshipSle) // checked in Transactor::checkSponsor + return tecINTERNAL; // LCOV_EXCL_LINE + + if (sponsorshipSle) + { + std::uint32_t const remainingOwnerCount = sponsorshipSle->at(sfReserveCount); + if (std::cmp_less(remainingOwnerCount, ownerCountAdj)) + return tecINSUFFICIENT_RESERVE; + } } - auto const sponsorBalance = sponsorSle->getFieldAmount(sfBalance); - STAmount const sponsorReserve = - accountReserve(view, sponsorSle, j, ownerCountDelta, reserveCountDelta); + feePayer = getFeePayerHlp(view, tx, sponsorshipSle).id; - if (sponsorBalance < sponsorReserve) + // co-signing or pre-fund still check sponsor capabilities + auto const sponsorLiquid = + xrpLiquidHlp(view, sponsorSle, ownerCountAdj, reserveCountAdj, {feePayer, feePayed}, j); + if (sponsorLiquid.negative()) return tecINSUFFICIENT_RESERVE; } else { - STAmount const reserve = - accountReserve(view, accSle, j, ownerCountDelta, reserveCountDelta); - if (accBalance < reserve) + // Special case for amm/trustlines/authorizeMPtoken - to not to demand reserve if + // ownerCount less than 2. Sponsor still check reserve for full count. + if (moreThan2 && (ownerCountHlp(view, accSle, 0, true, j) < 2)) + return tesSUCCESS; + + feePayer = getFeePayerHlp(view, tx, {}).id; + } + + auto const oca = sponsorSle ? 0 : ownerCountAdj; + auto const rca = sponsorSle ? 0 : reserveCountAdj; + + if (balanceAcc) + { + // balance passed, fee checks on caller, just check for reserve + [[maybe_unused]] auto [reserve, _1, _2] = + accountReserveHlp(view, accSle, oca, rca, true, j); + XRPAmount const accLiquid = balanceAcc - reserve; + if (accLiquid.negative()) return tecINSUFFICIENT_RESERVE; + return tesSUCCESS; } + + { + XRPAmount const accLiquid = xrpLiquidHlp(view, accSle, oca, rca, {feePayer, feePayed}, j); + auto const accAdjusted = accLiquid + balanceAdj; + // positive balance can improve liquidity + if (accLiquid.negative() && accAdjusted.negative()) + return tecINSUFFICIENT_RESERVE; + if (accAdjusted.negative()) + return tecINSUFFICIENT_FUNDS; + } + return tesSUCCESS; } +TER +checkXrpBalanceHlp( + ReadView const& view, + bool apply, + STTx const& tx, + std::optional const& accID, + std::optional> const& accOpt, + XRPAmount balanceAcc, + std::optional> const& sponsorOpt, + std::int32_t ownerCountAdj, + std::int32_t reserveCountAdj, + XRPAmount balanceAdj, + bool moreThan2, + beast::Journal j, + bool checkApplicability) +{ + if ((!accID && !accOpt) || (accID && accOpt)) + return tecINTERNAL; // LCOV_EXCL_LINE + + SLE::const_ref accSle = !accOpt ? view.read(keylet::account(*accID)) : accOpt->get(); + if (!accSle || (accSle->getType() != ltACCOUNT_ROOT)) + return tecINTERNAL; // LCOV_EXCL_LINE + + SLE::const_ref sponsorSle = !sponsorOpt + ? getTxReserveSponsor( + view, + tx, + checkApplicability ? std::make_optional(accSle->at(sfAccount)) : std::nullopt) + : sponsorOpt->get(); + + return checkXrpBalanceGeneral( + view, + apply, + tx, + accSle, + balanceAcc, + sponsorSle, + ownerCountAdj, + reserveCountAdj, + balanceAdj, + moreThan2, + j, + checkApplicability); +} + // ---------------------------------------------------- AccountID diff --git a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp index 23269f710a2..0a6116a2fdd 100644 --- a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp @@ -193,27 +193,18 @@ authorizeMPToken( // - add the new mptokenKey to the owner directory // - create the MPToken object for the holder - auto const sponsorSle = getTxReserveSponsor(view, tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - - auto const isSponsoredAndPreFunded = *sponsorSle && !isSponsorReserveCoSigning(tx); - // The reserve that is required to create the MPToken. Note // that although the reserve increases with every item // an account owns, in the case of MPTokens we only // *enforce* a reserve if the user owns more than two // items. This is similar to the reserve requirements of trust lines. // If PreFunded Sponsor, it must be checked whether sufficient - // ReserveCount exists. - if (ownerCount(view, *sponsorSle ? *sponsorSle : sleAcct, journal) >= 2 || - isSponsoredAndPreFunded) - { - if (auto const ret = checkInsufficientReserve( - view, tx, sleAcct, priorBalance, *sponsorSle, 1, 0, journal); - !isTesSuccess(ret)) - return ret; - } + // ReserveCount exists. See also TrustSet::doApply() and AMMWithdraw::withdraw() + + auto const sponsorSle = getTxReserveSponsor(view, tx, account); + if (auto const ter = checkXrpBalance(view, tx, sleAcct, sponsorSle, 1, true, journal); + !isTesSuccess(ter)) + return tecINSUFFICIENT_RESERVE; // Defensive check before we attempt to create MPToken for the issuer auto const mpt = view.read(keylet::mptIssuance(mptIssuanceID)); @@ -234,11 +225,11 @@ authorizeMPToken( (*mptoken)[sfAccount] = account; (*mptoken)[sfMPTokenIssuanceID] = mptIssuanceID; (*mptoken)[sfFlags] = 0; + addSponsorToLedgerEntry(mptoken, sponsorSle); view.insert(mptoken); // Update owner count. - adjustOwnerCount(view, sleAcct, *sponsorSle, 1, journal); - addSponsorToLedgerEntry(mptoken, *sponsorSle); + adjustOwnerCount(view, sleAcct, sponsorSle, 1, journal); return tesSUCCESS; } @@ -307,7 +298,7 @@ removeEmptyHolding( return authorizeMPToken( view, tx, - {}, // priorBalance + {}, // priorBalance, not used with tfMPTUnauthorize mptID, accountID, journal, @@ -929,10 +920,7 @@ createMPToken( (*mptoken)[sfMPTokenIssuanceID] = mptIssuanceID; (*mptoken)[sfFlags] = flags; (*mptoken)[sfOwnerNode] = *ownerNode; - - if (sponsorSle) - addSponsorToLedgerEntry(mptoken, sponsorSle); - + addSponsorToLedgerEntry(mptoken, sponsorSle); view.insert(mptoken); return tesSUCCESS; diff --git a/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp b/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp index c77f49c8649..6f61218fbd2 100644 --- a/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp @@ -74,12 +74,9 @@ locatePage(ApplyView& view, AccountID const& owner, uint256 const& id) static std::expected getPageForToken( ApplyView& view, - STTx const& tx, AccountID const& owner, - SLE::ref sponsorSle, uint256 const& id, - std::function const& - createCallback) + std::function const& createCallback) { auto const base = keylet::nftpageMin(owner); auto const first = keylet::nftpage(base, id); @@ -99,7 +96,7 @@ getPageForToken( cp->setFieldArray(sfNFTokens, arr); view.insert(cp); - if (auto const ret = createCallback(view, tx, cp, owner, sponsorSle); !isTesSuccess(ret)) + if (auto const ret = createCallback(cp); !isTesSuccess(ret)) return std::unexpected(ret); return cp; } @@ -213,7 +210,7 @@ getPageForToken( cp->setFieldH256(sfPreviousPageMin, np->key()); view.update(cp); - if (auto const ret = createCallback(view, tx, np, owner, sponsorSle); ret != tesSUCCESS) + if (auto const ret = createCallback(np); !isTesSuccess(ret)) return std::unexpected(ret); return (first.key < np->key()) ? np : cp; @@ -270,55 +267,56 @@ changeTokenURI( /** Insert the token in the owner's token directory. */ TER -insertToken(ApplyView& view, STTx const& tx, AccountID owner, SLE::ref sponsorSle, STObject&& nft) +insertToken( + ApplyView& view, + STTx const& tx, + SLE::ref ownerSle, + XRPAmount priorBalance, + SLE::ref sponsorSle, + STObject&& nft, + bool checkBalance) { XRPL_ASSERT(nft.isFieldPresent(sfNFTokenID), "xrpl::nft::insertToken : has NFT token"); // First, we need to locate the page the NFT belongs to, creating it // if necessary. This operation may fail if it is impossible to insert // the NFT. - auto createCallback = [](ApplyView& view, - STTx const& tx, - std::shared_ptr const& newPage, - AccountID const& owner, - SLE::ref sponsorSle) -> TER { - if (isReserveSponsored(tx)) + auto createCallback = [&](SLE::ref newPage) -> TER { + if (checkBalance) { - auto const ownerSle = view.read(keylet::account(owner)); - auto const ownerBalance = ownerSle->getFieldAmount(sfBalance); - if (auto const ret = - checkInsufficientReserve(view, tx, ownerSle, ownerBalance, sponsorSle, 1); + // using balance as NFTokens doesn't allow fee into reserve + if (auto const ret = checkXrpBalance(view, tx, ownerSle, priorBalance, sponsorSle, 1); !isTesSuccess(ret)) - return ret; + return view.rules().enabled(featureSponsor) ? ret : tecINSUFFICIENT_RESERVE; } - adjustOwnerCount(view, view.peek(keylet::account(owner)), sponsorSle, 1); - + adjustOwnerCount(view, ownerSle, sponsorSle, 1); addSponsorToLedgerEntry(newPage, sponsorSle); + return tesSUCCESS; }; - auto const page = - getPageForToken(view, tx, owner, sponsorSle, nft[sfNFTokenID], createCallback); + auto const ret = + getPageForToken(view, ownerSle->at(sfAccount), nft[sfNFTokenID], createCallback); + if (!ret) + return ret.error(); - if (!page.has_value()) - return page.error(); - - if (!(*page)) + SLE::ref page = *ret; + if (!page) return tecNO_SUITABLE_NFTOKEN_PAGE; { - auto arr = (*page)->getFieldArray(sfNFTokens); + auto arr = page->getFieldArray(sfNFTokens); arr.pushBack(std::move(nft)); arr.sort([](STObject const& o1, STObject const& o2) { return compareTokens(o1.getFieldH256(sfNFTokenID), o2.getFieldH256(sfNFTokenID)); }); - (*page)->setFieldArray(sfNFTokens, arr); + page->setFieldArray(sfNFTokens, arr); } - view.update((*page)); + view.update(page); return tesSUCCESS; } @@ -928,24 +926,22 @@ TER tokenOfferCreateApply( ApplyView& view, STTx const& tx, - AccountID const& acctID, + SLE::ref accSle, STAmount const& amount, std::optional const& dest, std::optional const& expiration, SeqProxy seqProxy, uint256 const& nftokenID, - XRPAmount const& priorBalance, + XRPAmount const&, beast::Journal j, std::uint32_t txFlags) { - Keylet const acctKeylet = keylet::account(acctID); - auto const acct = view.read(acctKeylet); + if (!accSle) + return tecINTERNAL; // LCOV_EXCL_LINE + + AccountID const acctID = accSle->at(sfAccount); auto const sponsorSle = getTxReserveSponsor(view, tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - if (auto const ret = - checkInsufficientReserve(view, tx, acct, priorBalance, *sponsorSle, 1, 0, j); - !isTesSuccess(ret)) + if (auto const ret = checkXrpBalance(view, tx, accSle, sponsorSle, 1, j); !isTesSuccess(ret)) return ret; auto const offerID = keylet::nftoffer(acctID, seqProxy.value()); @@ -993,13 +989,12 @@ tokenOfferCreateApply( if (dest) (*offer)[sfDestination] = *dest; - addSponsorToLedgerEntry(offer, *sponsorSle); - + addSponsorToLedgerEntry(offer, sponsorSle); view.insert(offer); } // Update owner count. - adjustOwnerCount(view, view.peek(acctKeylet), *sponsorSle, 1, j); + adjustOwnerCount(view, accSle, sponsorSle, 1, j); return tesSUCCESS; } diff --git a/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp b/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp index c715894145e..77dc46e4994 100644 --- a/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp +++ b/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp @@ -31,7 +31,6 @@ #include #include #include -#include namespace xrpl { @@ -231,7 +230,7 @@ trustCreate( return tecDIR_FULL; // LCOV_EXCL_LINE bool const bSetDst = saLimit.getIssuer() == uDstAccountID; - bool const bSetHigh = bSrcHigh ^ bSetDst; + bool const bSetHigh = bSrcHigh != bSetDst; XRPL_ASSERT(sleAccount, "xrpl::trustCreate : non-null SLE"); if (!sleAccount) @@ -285,8 +284,8 @@ trustCreate( } sleRippleState->setFieldU32(sfFlags, uFlags); - adjustOwnerCount(view, sleAccount, sponsorSle, 1, j); + adjustOwnerCount(view, sleAccount, sponsorSle, 1, j); addSponsorToLedgerEntry(sleRippleState, sponsorSle, bSetHigh ? sfHighSponsor : sfLowSponsor); // ONLY: Create ripple balance. @@ -664,18 +663,11 @@ addEmptyHolding( if (view.read(index)) return tecDUPLICATE; - SLE::pointer sponsorSle; - if (!isPseudoAccount(sleDst)) - { - auto sle = getTxReserveSponsor(view, tx); - if (!sle) - return sle.error(); // LCOV_EXCL_LINE - sponsorSle = std::move(*sle); - } + SLE::pointer const sponsorSle = + !isPseudoAccount(sleDst) ? getTxReserveSponsor(view, tx, dstId) : SLE::pointer(); // Can the account cover the trust line reserve ? - if (auto const ret = - checkInsufficientReserve(view, tx, sleDst, priorBalance, sponsorSle, 1, 0, journal); + if (auto const ret = checkXrpBalance(view, tx, sleDst, sponsorSle, 1, journal); !isTesSuccess(ret)) return tecNO_LINE_INSUF_RESERVE; diff --git a/src/libxrpl/ledger/helpers/TokenHelpers.cpp b/src/libxrpl/ledger/helpers/TokenHelpers.cpp index beb5413bc0e..d24acc975a9 100644 --- a/src/libxrpl/ledger/helpers/TokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/TokenHelpers.cpp @@ -748,9 +748,7 @@ directSendNoLimitIOU( TER terResult = directSendNoFeeIOU(view, issuer, uReceiverID, saAmount, true, sponsorSle, j); if (tesSUCCESS == terResult) - { terResult = directSendNoFeeIOU(view, uSenderID, issuer, saActual, true, sponsorSle, j); - } return terResult; } diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp index 4bcf579926d..d193820e4d0 100644 --- a/src/libxrpl/protocol/STTx.cpp +++ b/src/libxrpl/protocol/STTx.cpp @@ -213,7 +213,7 @@ STTx::getSeqValue() const } AccountID -STTx::getFeePayer() const +STTx::getInitiator() const { // If sfDelegate is present, the delegate account is the payer // note: if a delegate is specified, its authorization to act on behalf of the account is @@ -442,7 +442,7 @@ STTx::checkBatchSingleSign(STObject const& batchSigner) const return singleSignHelper(batchSigner, msg.slice()); } -std::expected +static std::expected multiSignHelper( STObject const& sigObject, std::optional txnAccountID, @@ -546,7 +546,7 @@ STTx::checkMultiSign(Rules const& rules, STObject const& sigObject) const // For delegated transactions sfDelegate is the account whose signer list is checked, // the delegate account itself can not be among the signers. auto const txnAccountID = - &sigObject != this ? std::nullopt : std::optional(getFeePayer()); + &sigObject != this ? std::nullopt : std::optional(getInitiator()); // We can ease the computational load inside the loop a bit by // pre-constructing part of the data that we hash. Fill a Serializer diff --git a/src/libxrpl/tx/Transactor.cpp b/src/libxrpl/tx/Transactor.cpp index dd643537ecc..8f4b0739598 100644 --- a/src/libxrpl/tx/Transactor.cpp +++ b/src/libxrpl/tx/Transactor.cpp @@ -18,7 +18,6 @@ #include #include #include -#include #include #include #include @@ -377,11 +376,10 @@ Transactor::checkSponsor(ReadView const& view, STTx const& tx) if (!tx.isFieldPresent(sfSponsor)) return tesSUCCESS; - if (auto const sponsorSle = getTxReserveSponsor(view, tx); !sponsorSle) + if (!view.exists(keylet::account(tx[sfSponsor]))) return terNO_ACCOUNT; auto const hasSponsorSignature = tx.isFieldPresent(sfSponsorSignature); - if (hasSponsorSignature) return tesSUCCESS; @@ -500,7 +498,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) { @@ -575,9 +573,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 @@ -1260,7 +1258,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 @@ -1314,40 +1312,6 @@ Transactor::reset(XRPAmount fee) return {ter, fee}; } -FeePayer -Transactor::getFeePayer(ReadView const& view, STTx const& tx) -{ - if (tx.isFieldPresent(sfSponsor) && ((tx.getFieldU32(sfSponsorFlags) & spfSponsorFee) != 0u)) - { - auto const sponsorAccountID = tx.getAccountID(sfSponsor); - auto const sponseeAccountID = tx.getAccountID(sfAccount); - auto const hasSponsorSignature = tx.isFieldPresent(sfSponsorSignature); - auto const sponsorshipKeylet = keylet::sponsor(sponsorAccountID, sponseeAccountID); - - // if pre-funded sponsorship exists, prefer it - if (hasSponsorSignature && !view.exists(sponsorshipKeylet)) - { - // co-signed - return FeePayer{ - .entry = keylet::account(sponsorAccountID), - .balanceField = sfBalance, - .type = FeePayerType::SponsorCoSigned}; - } - - // pre funded - return FeePayer{ - .entry = sponsorshipKeylet, - .balanceField = sfFeeAmount, - .type = FeePayerType::SponsorPreFunded}; - } - - auto const payerAccountKeylet = keylet::account(tx.getFeePayer()); - auto const payerType = - tx.isFieldPresent(sfDelegate) ? FeePayerType::Delegate : FeePayerType::Account; - - return FeePayer{.entry = payerAccountKeylet, .balanceField = sfBalance, .type = payerType}; -} - // The sole purpose of this function is to provide a convenient, named // location to set a breakpoint, to be used when replaying transactions. void diff --git a/src/libxrpl/tx/paths/XRPEndpointStep.cpp b/src/libxrpl/tx/paths/XRPEndpointStep.cpp index 314780c3a72..f857d38bd6f 100644 --- a/src/libxrpl/tx/paths/XRPEndpointStep.cpp +++ b/src/libxrpl/tx/paths/XRPEndpointStep.cpp @@ -176,7 +176,6 @@ class XRPEndpointPaymentStep : public XRPEndpointStep xrpLiquid(ReadView& sb) const { return xrpLiquidImpl(sb, 0); - ; } [[nodiscard]] std::string diff --git a/src/libxrpl/tx/transactors/Sponsor/SponsorshipSet.cpp b/src/libxrpl/tx/transactors/Sponsor/SponsorshipSet.cpp index 07dd8628f39..fff32b0a5cc 100644 --- a/src/libxrpl/tx/transactors/Sponsor/SponsorshipSet.cpp +++ b/src/libxrpl/tx/transactors/Sponsor/SponsorshipSet.cpp @@ -178,93 +178,82 @@ SponsorshipSet::preclaim(PreclaimContext const& ctx) TER SponsorshipSet::doApply() { - auto const sponsorAccountID = ctx_.tx[~sfCounterpartySponsor].value_or(accountID_); - auto const sponseeAccountID = ctx_.tx[~sfSponsee].value_or(accountID_); + auto const sponsorID = ctx_.tx[~sfCounterpartySponsor].value_or(accountID_); + auto const sponseeID = ctx_.tx[~sfSponsee].value_or(accountID_); - if (sponseeAccountID == sponsorAccountID) + if (sponseeID == sponsorID) return tecINTERNAL; // LCOV_EXCL_LINE - auto const sponsorAccSle = ctx_.view().peek(keylet::account(sponsorAccountID)); - if (!sponsorAccSle) + auto const sponsorSle = ctx_.view().peek(keylet::account(sponsorID)); + if (!sponsorSle) return tecINTERNAL; // LCOV_EXCL_LINE - if (!ctx_.view().exists(keylet::account(sponseeAccountID))) + if (!ctx_.view().exists(keylet::account(sponseeID))) return tecINTERNAL; // LCOV_EXCL_LINE - auto const sponsorKeylet = keylet::sponsor(sponsorAccountID, sponseeAccountID); - auto const sponsorObjSle = ctx_.view().peek(sponsorKeylet); + auto const sponsorKeylet = keylet::sponsor(sponsorID, sponseeID); + auto const sponsorshipSle = ctx_.view().peek(sponsorKeylet); if (ctx_.tx.isFlag(tfDeleteObject)) { // Delete - if (!sponsorObjSle) + if (!sponsorshipSle) return tecINTERNAL; // LCOV_EXCL_LINE - adjustOwnerCountObj(ctx_.view(), sponsorAccSle, sponsorObjSle, -1, ctx_.journal); + adjustOwnerCountObj(ctx_.view(), sponsorSle, sponsorshipSle, -1, ctx_.journal); ctx_.view().dirRemove( - keylet::ownerDir(sponsorAccountID), - (*sponsorObjSle)[sfOwnerNode], - sponsorObjSle->key(), + keylet::ownerDir(sponsorID), + (*sponsorshipSle)[sfOwnerNode], + sponsorshipSle->key(), false); ctx_.view().dirRemove( - keylet::ownerDir(sponseeAccountID), - (*sponsorObjSle)[sfSponseeNode], - sponsorObjSle->key(), + keylet::ownerDir(sponseeID), + (*sponsorshipSle)[sfSponseeNode], + sponsorshipSle->key(), false); // transfer feeAmount from ledger entry - if (sponsorObjSle->isFieldPresent(sfFeeAmount)) + if (sponsorshipSle->isFieldPresent(sfFeeAmount)) { - auto const feeAmount = sponsorObjSle->getFieldAmount(sfFeeAmount); - (*sponsorAccSle)[sfBalance] += feeAmount; + auto const feeAmount = sponsorshipSle->getFieldAmount(sfFeeAmount); + (*sponsorSle)[sfBalance] += feeAmount; } - ctx_.view().erase(sponsorObjSle); + ctx_.view().erase(sponsorshipSle); return tesSUCCESS; } auto const feeAmount = ctx_.tx[~sfFeeAmount]; auto const maxFee = ctx_.tx[~sfMaxFee]; - auto const reserveCount = ctx_.tx[~sfReserveCount]; - - auto reserveSponsorAccSle = getTxReserveSponsor(view(), ctx_.tx); - if (!reserveSponsorAccSle) - return reserveSponsorAccSle.error(); // LCOV_EXCL_LINE + auto const remainingOwnerCount = ctx_.tx[~sfReserveCount]; - if (!sponsorObjSle) + auto txSponsorSle = getTxReserveSponsor(view(), ctx_.tx, sponsorID); + if (!sponsorshipSle) { // Create auto newSle = std::make_shared(sponsorKeylet); - (*newSle)[sfOwner] = sponsorAccountID; - (*newSle)[sfSponsee] = sponseeAccountID; - if (feeAmount && (*feeAmount).xrp() > (*sponsorAccSle)[sfBalance]) + (*newSle)[sfOwner] = sponsorID; + (*newSle)[sfSponsee] = sponseeID; + + STAmount const balanceAdj = feeAmount ? *feeAmount : STAmount(); + if (auto const ret = checkXrpBalance( + ctx_.view(), ctx_.tx, sponsorSle, txSponsorSle, 1, -balanceAdj.xrp(), ctx_.journal); + !isTesSuccess(ret)) return tecUNFUNDED; if (feeAmount && *feeAmount > XRPAmount(0)) { - (*sponsorAccSle)[sfBalance] -= *feeAmount; + (*sponsorSle)[sfBalance] -= *feeAmount; (*newSle)[sfFeeAmount] = *feeAmount; } - if (auto const ret = checkInsufficientReserve( - ctx_.view(), - ctx_.tx, - sponsorAccSle, - STAmount{(*sponsorAccSle)[sfBalance]}.xrp(), - *reserveSponsorAccSle, - 1, - 0, - ctx_.journal); - !isTesSuccess(ret)) - return tecUNFUNDED; - if (maxFee && *maxFee > XRPAmount(0)) (*newSle)[sfMaxFee] = *maxFee; - if (reserveCount && *reserveCount > 0) - (*newSle)[sfReserveCount] = *reserveCount; + if (remainingOwnerCount && *remainingOwnerCount > 0) + (*newSle)[sfReserveCount] = *remainingOwnerCount; auto flags = 0; if (ctx_.tx.isFlag(tfSponsorshipSetRequireSignForFee)) @@ -276,20 +265,20 @@ SponsorshipSet::doApply() (*newSle)[sfFlags] = flags; auto const sponsorPage = view().dirInsert( - keylet::ownerDir(sponsorAccountID), sponsorKeylet, describeOwnerDir(sponsorAccountID)); + keylet::ownerDir(sponsorID), sponsorKeylet, describeOwnerDir(sponsorID)); if (!sponsorPage) return tecDIR_FULL; // LCOV_EXCL_LINE (*newSle)[sfOwnerNode] = *sponsorPage; auto const sponseePage = view().dirInsert( - keylet::ownerDir(sponseeAccountID), sponsorKeylet, describeOwnerDir(sponseeAccountID)); + keylet::ownerDir(sponseeID), sponsorKeylet, describeOwnerDir(sponseeID)); if (!sponseePage) return tecDIR_FULL; // LCOV_EXCL_LINE (*newSle)[sfSponseeNode] = *sponseePage; // NOLINTNEXTLINE(readability-suspicious-call-argument) - adjustOwnerCount(view(), sponsorAccSle, *reserveSponsorAccSle, 1, ctx_.journal); - addSponsorToLedgerEntry(newSle, *reserveSponsorAccSle); + adjustOwnerCount(view(), sponsorSle, txSponsorSle, 1, ctx_.journal); + addSponsorToLedgerEntry(newSle, txSponsorSle); ctx_.view().insert(newSle); return tesSUCCESS; @@ -298,37 +287,27 @@ SponsorshipSet::doApply() // Update if (feeAmount) { - auto const currentFeeAmount = (*sponsorObjSle)[~sfFeeAmount].valueOr(XRPAmount(0)); + auto const currentFeeAmount = (*sponsorshipSle)[~sfFeeAmount].valueOr(XRPAmount(0)); auto feeAmountDelta = XRPAmount(*feeAmount - currentFeeAmount); - if (feeAmountDelta > beast::kZero && feeAmountDelta > (*sponsorAccSle)[sfBalance]) + if (auto const ret = checkXrpBalance( + ctx_.view(), ctx_.tx, sponsorSle, txSponsorSle, 0, -feeAmountDelta, ctx_.journal); + !isTesSuccess(ret)) return tecUNFUNDED; // transfer feeAmount to ledger entry if (feeAmountDelta != beast::kZero) { - (*sponsorAccSle)[sfBalance] -= feeAmountDelta; + (*sponsorSle)[sfBalance] -= feeAmountDelta; if (*feeAmount == XRPAmount(0)) { - (*sponsorObjSle).makeFieldAbsent(sfFeeAmount); + (*sponsorshipSle).makeFieldAbsent(sfFeeAmount); } else { - (*sponsorObjSle).setFieldAmount(sfFeeAmount, *feeAmount); + (*sponsorshipSle).setFieldAmount(sfFeeAmount, *feeAmount); } - - if (auto const ret = checkInsufficientReserve( - ctx_.view(), - ctx_.tx, - sponsorAccSle, - STAmount{(*sponsorAccSle)[sfBalance]}.xrp(), - *reserveSponsorAccSle, - 0, - 0, - ctx_.journal); - !isTesSuccess(ret)) - return tecUNFUNDED; } } @@ -336,19 +315,19 @@ SponsorshipSet::doApply() { if (*maxFee == XRPAmount(0)) { - (*sponsorObjSle).makeFieldAbsent(sfMaxFee); + (*sponsorshipSle).makeFieldAbsent(sfMaxFee); } else { - (*sponsorObjSle)[sfMaxFee] = *maxFee; + (*sponsorshipSle)[sfMaxFee] = *maxFee; } } - if (reserveCount) - sponsorObjSle->at(sfReserveCount) = *reserveCount; + if (remainingOwnerCount) + sponsorshipSle->at(sfReserveCount) = *remainingOwnerCount; // update Flags - auto flags = sponsorObjSle->getFieldU32(sfFlags); + auto flags = sponsorshipSle->getFieldU32(sfFlags); if (ctx_.tx.isFlag(tfSponsorshipSetRequireSignForFee)) flags |= lsfSponsorshipRequireSignForFee; @@ -361,10 +340,10 @@ SponsorshipSet::doApply() if (ctx_.tx.isFlag(tfSponsorshipClearRequireSignForReserve)) flags &= ~lsfSponsorshipRequireSignForReserve; - if (flags != (*sponsorObjSle)[sfFlags]) - (*sponsorObjSle)[sfFlags] = flags; + if (flags != (*sponsorshipSle)[sfFlags]) + (*sponsorshipSle)[sfFlags] = flags; - view().update(sponsorObjSle); + view().update(sponsorshipSle); return tesSUCCESS; } diff --git a/src/libxrpl/tx/transactors/Sponsor/SponsorshipTransfer.cpp b/src/libxrpl/tx/transactors/Sponsor/SponsorshipTransfer.cpp index 79995c07a99..713b4bf4e88 100644 --- a/src/libxrpl/tx/transactors/Sponsor/SponsorshipTransfer.cpp +++ b/src/libxrpl/tx/transactors/Sponsor/SponsorshipTransfer.cpp @@ -2,6 +2,8 @@ #include #include +#include +#include #include #include #include @@ -12,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -20,13 +23,47 @@ #include #include +#include #include #include +#include +#include namespace xrpl { +// SponsorshipTransfer can break dependency tx[sfAccount] === accSle to use sponsor. +template +[[nodiscard]] inline TER +checkXrpBalanceTransfer( + V const& view, + STTx const& tx, + SLE::const_ref accSle, + SLE::const_ref sponsorSle, + std::int32_t ownerCountAdj, + std::int32_t reserveCountAdj = 0, + beast::Journal j = beast::Journal{beast::Journal::getNullSink()}) +{ + bool apply = false; // NOLINT + if constexpr (std::is_base_of_v>) + apply = true; + return checkXrpBalanceHlp( + view, + apply, + tx, + {}, + accSle, + XRPAmount(), + sponsorSle, + ownerCountAdj, + reserveCountAdj, + XRPAmount(), + false, + j, + false); +} + std::uint32_t -SponsorshipTransfer::getFlagsMask(PreflightContext const& ctx) +SponsorshipTransfer::getFlagsMask(PreflightContext const&) { return tfSponsorshipTransferMask; } @@ -107,18 +144,17 @@ SponsorshipTransfer::preflight(PreflightContext const& ctx) return tesSUCCESS; } -template -inline std::optional -getLedgerEntryOwner(ReadView const& view, T const& sle, AccountID const& account) +static std::optional +getLedgerEntryOwner(ReadView const& view, SLE const& sle, AccountID const& account) { - switch (sle->getType()) + switch (sle.getType()) { case ltNFTOKEN_OFFER: case ltORACLE: case ltPERMISSIONED_DOMAIN: case ltVAULT: case ltLOAN_BROKER: - return sle->getAccountID(sfOwner); + return sle.getAccountID(sfOwner); case ltCHECK: case ltDID: case ltTICKET: @@ -131,40 +167,40 @@ getLedgerEntryOwner(ReadView const& view, T const& sle, AccountID const& account case ltDELEGATE: case ltBRIDGE: case ltDEPOSIT_PREAUTH: - return sle->getAccountID(sfAccount); + return sle.getAccountID(sfAccount); case ltMPTOKEN_ISSUANCE: - return sle->getAccountID(sfIssuer); + return sle.getAccountID(sfIssuer); case ltLOAN: - return sle->getAccountID(sfBorrower); + 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()) + if (signerList->key() == sle.key()) return account; return std::nullopt; } case ltCREDENTIAL: { - if (sle->isFlag(lsfAccepted)) - return sle->getAccountID(sfSubject); - return sle->getAccountID(sfIssuer); + 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(); + uint256 const& key = sle.key(); return AccountID::fromVoid(key.data()); } case ltRIPPLE_STATE: { - if (sle->isFlag(lsfHighReserve)) + if (sle.isFlag(lsfHighReserve)) { - auto const highAccount = sle->getFieldAmount(sfHighLimit).getIssuer(); + auto const highAccount = sle.getFieldAmount(sfHighLimit).getIssuer(); if (highAccount == account) return highAccount; } - if (sle->isFlag(lsfLowReserve)) + if (sle.isFlag(lsfLowReserve)) { - auto const lowAccount = sle->getFieldAmount(sfLowLimit).getIssuer(); + auto const lowAccount = sle.getFieldAmount(sfLowLimit).getIssuer(); if (lowAccount == account) return lowAccount; } @@ -186,62 +222,29 @@ getLedgerEntryOwner(ReadView const& view, T const& sle, AccountID const& account }; } -template -inline std::uint32_t -getLedgerEntryOwnerCount(T const& sle) +static inline std::uint32_t +getLedgerEntryOwnerCount(SLE const& sle) { - switch (sle->getType()) + switch (sle.getType()) { case ltORACLE: { - return OracleSet::calculateOracleReserve(sle->getFieldArray(sfPriceDataSeries).size()); + return OracleSet::calculateOracleReserve(sle.getFieldArray(sfPriceDataSeries).size()); } 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."); - // 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 - bool const isObjectSponsor = index != std::nullopt; - + auto const newSponsorSle = getTxReserveSponsor(ctx.view, ctx.tx); auto const account = ctx.tx[sfAccount]; + auto const sponseeID = ctx.tx[~sfSponsee].value_or(account); + auto const sponseeSle = ctx.view.read(keylet::account(sponseeID)); - auto const sponseeAccountID = ctx.tx[~sfSponsee].value_or(account); - auto const sponseeSle = ctx.view.read(keylet::account(sponseeAccountID)); if (!sponseeSle) return tecINTERNAL; // LCOV_EXCL_LINE @@ -251,17 +254,16 @@ SponsorshipTransfer::preclaim(PreclaimContext const& ctx) if (!sle) return tecNO_ENTRY; - auto const ownerCountDelta = getLedgerEntryOwnerCount(sle); - - auto const owner = getLedgerEntryOwner(ctx.view, sle, sponseeAccountID); - if (!owner || owner != sponseeAccountID) + auto const ownerCountDelta = getLedgerEntryOwnerCount(*sle); + auto const owner = getLedgerEntryOwner(ctx.view, *sle, sponseeID); + if (!owner || owner != sponseeID) return tecNO_PERMISSION; - auto const& sponsorField = getLedgerEntrySponsorField(sle, *owner); + auto const& sponsorField = getLedgerEntrySponsorField(*sle, *owner); if (ctx.tx.isFlag(tfSponsorshipCreate)) { - if (!*newSponsorSle) + if (!newSponsorSle) return tecNO_PERMISSION; // check object is not sponsored yet @@ -270,7 +272,7 @@ SponsorshipTransfer::preclaim(PreclaimContext const& ctx) } else if (ctx.tx.isFlag(tfSponsorshipReassign)) { - if (!*newSponsorSle) + if (!newSponsorSle) return tecNO_PERMISSION; // check object is already ctx.sponsored @@ -279,7 +281,7 @@ SponsorshipTransfer::preclaim(PreclaimContext const& ctx) } else if (ctx.tx.isFlag(tfSponsorshipEnd)) { - if (*newSponsorSle) + if (newSponsorSle) return tecNO_PERMISSION; // check object is sponsored @@ -288,29 +290,21 @@ SponsorshipTransfer::preclaim(PreclaimContext const& ctx) // only the sponsor or sponsee can end sponsorship auto const sponsor = sle->getAccountID(sponsorField); - if (account != sponsor && account != sponseeAccountID) + 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; + if (auto const ret = checkXrpBalanceTransfer( + ctx.view, ctx.tx, sponseeSle, newSponsorSle, ownerCountDelta, 0, ctx.j); + !isTesSuccess(ret)) + return ret; } else { if (ctx.tx.isFlag(tfSponsorshipCreate)) { - if (!*newSponsorSle) + if (!newSponsorSle) return tecNO_PERMISSION; // check account is not sponsored yet @@ -319,7 +313,7 @@ SponsorshipTransfer::preclaim(PreclaimContext const& ctx) } else if (ctx.tx.isFlag(tfSponsorshipReassign)) { - if (!*newSponsorSle) + if (!newSponsorSle) return tecNO_PERMISSION; // check account is already sponsored @@ -328,7 +322,7 @@ SponsorshipTransfer::preclaim(PreclaimContext const& ctx) } else if (ctx.tx.isFlag(tfSponsorshipEnd)) { - if (*newSponsorSle) + if (newSponsorSle) return tecNO_PERMISSION; // check account is sponsored @@ -337,26 +331,18 @@ SponsorshipTransfer::preclaim(PreclaimContext const& ctx) // only the sponsor or sponsee can end sponsorship auto const sponsor = sponseeSle->getAccountID(sfSponsor); - if (account != sponsor && account != sponseeAccountID) + 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 + // (AccountReserve = 0). However, by setting reserveCountAdj = 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; + if (auto const ret = + checkXrpBalanceTransfer(ctx.view, ctx.tx, sponseeSle, newSponsorSle, 0, 1, ctx.j); + !isTesSuccess(ret)) + return ret; } return tesSUCCESS; @@ -367,7 +353,7 @@ reduceReserveCount( ApplyView& view, AccountID const& account, AccountID const& sponsor, - int32_t delta) + int64_t delta) { if (delta == 0) return tesSUCCESS; @@ -379,8 +365,8 @@ reduceReserveCount( if (!sponsorSle) return tefINTERNAL; // LCOV_EXCL_LINE - auto const reserveCount = sponsorSle->getFieldU32(sfReserveCount); - int32_t const afterReserveCount = reserveCount + delta; + uint32_t const reserveCount = sponsorSle->getFieldU32(sfReserveCount); + int64_t const afterReserveCount = reserveCount + delta; if (afterReserveCount < 0) { @@ -393,6 +379,15 @@ reduceReserveCount( return tesSUCCESS; } +void +setSponsorFieldU32(SLE& sle, SF_UINT32 const& field, std::int64_t delta) +{ + int64_t const newValue = sle[field] + delta; + if ((newValue < 0) || (newValue > std::numeric_limits::max())) + Throw("xrpl::SponsorshipTransfer::doApply : Invalid sponsor field value"); + sle[field] = static_cast(newValue); +} + TER SponsorshipTransfer::doApply() { @@ -406,18 +401,6 @@ SponsorshipTransfer::doApply() if (!sponseeSle) return tefINTERNAL; // LCOV_EXCL_LINE - auto const setSponsorFieldU32 = [](auto const& sle, auto const& field, auto const& delta) { - int32_t const newValue = static_cast(sle->getFieldU32(field)) + delta; - - if (newValue < 0) - { - UNREACHABLE("xrpl::SponsorshipTransfer::doApply : Invalid sponsor field value"); - return; - } - - sle->at(field) = static_cast(newValue); - }; - if (isObjectSponsor) { auto const hasSignature = tx.isFieldPresent(sfSponsorSignature); @@ -427,7 +410,7 @@ SponsorshipTransfer::doApply() if (!objSle) return tefINTERNAL; // LCOV_EXCL_LINE - auto const ownerAccountID = getLedgerEntryOwner(view(), objSle, sponseeAccountID); + auto const ownerAccountID = getLedgerEntryOwner(view(), *objSle, sponseeAccountID); if (!ownerAccountID) return tefINTERNAL; // LCOV_EXCL_LINE @@ -435,9 +418,9 @@ SponsorshipTransfer::doApply() if (!ownerSle) return tefINTERNAL; // LCOV_EXCL_LINE - auto const ownerCountDelta = getLedgerEntryOwnerCount(objSle); + std::int64_t const ownerCountDelta = getLedgerEntryOwnerCount(*objSle); - auto const& sponsorField = getLedgerEntrySponsorField(objSle, *ownerAccountID); + auto const& sponsorField = getLedgerEntrySponsorField(*objSle, *ownerAccountID); if (ctx_.tx.isFlag(tfSponsorshipCreate)) { @@ -445,14 +428,14 @@ SponsorshipTransfer::doApply() XRPL_ASSERT(!!newSponsorAccountID, "New sponsor is required when creating sponsorship"); // update owner's sponsored count - setSponsorFieldU32(ownerSle, sfSponsoredOwnerCount, ownerCountDelta); + setSponsorFieldU32(*ownerSle, sfSponsoredOwnerCount, ownerCountDelta); view().update(ownerSle); // increment new sponsor's sponsoring count auto const newSponsorSle = view().peek(keylet::account(newSponsorAccountID)); if (!newSponsorSle) return tefINTERNAL; // LCOV_EXCL_LINE - setSponsorFieldU32(newSponsorSle, sfSponsoringOwnerCount, ownerCountDelta); + setSponsorFieldU32(*newSponsorSle, sfSponsoringOwnerCount, ownerCountDelta); view().update(newSponsorSle); // set new sponsor to object @@ -482,14 +465,14 @@ SponsorshipTransfer::doApply() auto const oldSponsorSle = view().peek(keylet::account(oldSponsorAccountID)); if (!oldSponsorSle) return tefINTERNAL; // LCOV_EXCL_LINE - setSponsorFieldU32(oldSponsorSle, sfSponsoringOwnerCount, -ownerCountDelta); + setSponsorFieldU32(*oldSponsorSle, sfSponsoringOwnerCount, -ownerCountDelta); view().update(oldSponsorSle); // increment new sponsor's sponsoring count auto const newSponsorSle = view().peek(keylet::account(newSponsorAccountID)); if (!newSponsorSle) return tefINTERNAL; // LCOV_EXCL_LINE - setSponsorFieldU32(newSponsorSle, sfSponsoringOwnerCount, ownerCountDelta); + setSponsorFieldU32(*newSponsorSle, sfSponsoringOwnerCount, ownerCountDelta); view().update(newSponsorSle); // set new sponsor to object @@ -515,11 +498,11 @@ SponsorshipTransfer::doApply() return tefINTERNAL; // LCOV_EXCL_LINE // decrement sponsored count - setSponsorFieldU32(sponseeSle, sfSponsoredOwnerCount, -ownerCountDelta); + setSponsorFieldU32(*sponseeSle, sfSponsoredOwnerCount, -ownerCountDelta); view().update(sponseeSle); // decrement old sponsoring count - setSponsorFieldU32(oldSponsorSle, sfSponsoringOwnerCount, -ownerCountDelta); + setSponsorFieldU32(*oldSponsorSle, sfSponsoringOwnerCount, -ownerCountDelta); view().update(oldSponsorSle); // remove sponsor from object @@ -537,7 +520,7 @@ SponsorshipTransfer::doApply() auto const newSponsorSle = view().peek(keylet::account(newSponsorAccountID)); if (!newSponsorSle) return tefINTERNAL; // LCOV_EXCL_LINE - setSponsorFieldU32(newSponsorSle, sfSponsoringAccountCount, 1); + setSponsorFieldU32(*newSponsorSle, sfSponsoringAccountCount, 1); view().update(newSponsorSle); // set new sponsor to account @@ -552,7 +535,7 @@ SponsorshipTransfer::doApply() auto const newSponsorSle = view().peek(keylet::account(newSponsorAccountID)); if (!newSponsorSle) return tefINTERNAL; // LCOV_EXCL_LINE - setSponsorFieldU32(newSponsorSle, sfSponsoringAccountCount, 1); + setSponsorFieldU32(*newSponsorSle, sfSponsoringAccountCount, 1); view().update(newSponsorSle); // decrement old sponsoring count @@ -560,7 +543,7 @@ SponsorshipTransfer::doApply() auto const oldSponsorSle = view().peek(keylet::account(oldSponsor)); if (!oldSponsorSle) return tefINTERNAL; // LCOV_EXCL_LINE - setSponsorFieldU32(oldSponsorSle, sfSponsoringAccountCount, -1); + setSponsorFieldU32(*oldSponsorSle, sfSponsoringAccountCount, -1); view().update(oldSponsorSle); // set new sponsor to account @@ -578,7 +561,7 @@ SponsorshipTransfer::doApply() auto const oldSponsorSle = view().peek(keylet::account(oldSponsorAccountID)); if (!oldSponsorSle) return tefINTERNAL; // LCOV_EXCL_LINE - setSponsorFieldU32(oldSponsorSle, sfSponsoringAccountCount, -1); + setSponsorFieldU32(*oldSponsorSle, sfSponsoringAccountCount, -1); view().update(oldSponsorSle); } } diff --git a/src/libxrpl/tx/transactors/account/SignerListSet.cpp b/src/libxrpl/tx/transactors/account/SignerListSet.cpp index d92194900cd..8b0c0bc04fa 100644 --- a/src/libxrpl/tx/transactors/account/SignerListSet.cpp +++ b/src/libxrpl/tx/transactors/account/SignerListSet.cpp @@ -323,17 +323,8 @@ SignerListSet::replaceSignerList() // allow dipping into the reserve to pay fees. This behavior is consistent // with TicketCreate. auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - if (auto const ret = checkInsufficientReserve( - ctx_.view(), - ctx_.tx, - sle, - preFeeBalance_, - *sponsorSle, - kAddedOwnerCount, - 0, - ctx_.journal); + if (auto const ret = + checkXrpBalance(ctx_.view(), ctx_.tx, sle, sponsorSle, kAddedOwnerCount, ctx_.journal); !isTesSuccess(ret)) return ret; @@ -357,8 +348,8 @@ SignerListSet::replaceSignerList() // If we succeeded, the new entry counts against the // creator's reserve. - adjustOwnerCount(view(), sle, *sponsorSle, kAddedOwnerCount, viewJ); - addSponsorToLedgerEntry(signerList, *sponsorSle); + adjustOwnerCount(view(), sle, sponsorSle, kAddedOwnerCount, viewJ); + addSponsorToLedgerEntry(signerList, sponsorSle); return tesSUCCESS; } diff --git a/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp b/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp index 8229f9fd7b6..dce6a044996 100644 --- a/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp +++ b/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp @@ -437,7 +437,7 @@ transferHelper( return tecINTERNAL; // LCOV_EXCL_LINE { - auto const reserve = accountReserve(psb, sleSrc, j, 0, 0); + auto const reserve = accountReserve(psb, sleSrc, j); auto const availableBalance = [&]() -> STAmount { STAmount curBal = (*sleSrc)[sfBalance]; @@ -1029,12 +1029,10 @@ applyCreateAccountAttestations( return std::unexpected(tecINTERNAL); // Check reserve - auto const balance = (*sleDoor)[sfBalance]; - // Don't sponsor door account objects in transactions not sent by the door account - // itself - if (auto const ret = checkInsufficientReserve(psb, tx, sleDoor, balance, {}, 1, 0, j); + auto const balance = (*sleDoor)[sfBalance]->xrp(); + if (auto const ret = checkXrpBalance(psb, tx, sleDoor, balance, {}, 1, j); !isTesSuccess(ret)) - return std::unexpected(ret); // tecINSUFFICIENT_RESERVE + return std::unexpected(tecINSUFFICIENT_RESERVE); } std::vector atts; @@ -1439,14 +1437,9 @@ XChainCreateBridge::preclaim(PreclaimContext const& ctx) if (!sleAcc) return terNO_ACCOUNT; - auto const balance = (*sleAcc)[sfBalance]; - auto const sponsorSle = getTxReserveSponsor(ctx.view, ctx.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - if (auto const ret = checkInsufficientReserve( - ctx.view, ctx.tx, sleAcc, balance, *sponsorSle, 1, 0, ctx.j); + if (auto const ret = checkXrpBalance(ctx.view, ctx.tx, sleAcc, 1, XRPAmount(), ctx.j); !isTesSuccess(ret)) - return ret; + return tecINSUFFICIENT_RESERVE; } return tesSUCCESS; @@ -1489,10 +1482,8 @@ XChainCreateBridge::doApply() } auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - adjustOwnerCount(ctx_.view(), sleAcct, *sponsorSle, 1, ctx_.journal); - addSponsorToLedgerEntry(sleBridge, *sponsorSle); + adjustOwnerCount(ctx_.view(), sleAcct, sponsorSle, 1, ctx_.journal); + addSponsorToLedgerEntry(sleBridge, sponsorSle); ctx_.view().insert(sleBridge); ctx_.view().update(sleAcct); @@ -1994,14 +1985,10 @@ XChainCreateClaimID::preclaim(PreclaimContext const& ctx) if (!sleAcc) return terNO_ACCOUNT; - auto const balance = (*sleAcc)[sfBalance]; auto const sponsorSle = getTxReserveSponsor(ctx.view, ctx.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - if (auto const ret = checkInsufficientReserve( - ctx.view, ctx.tx, sleAcc, balance, *sponsorSle, 1, 0, ctx.j); + if (auto const ret = checkXrpBalance(ctx.view, ctx.tx, sleAcc, sponsorSle, 1, ctx.j); !isTesSuccess(ret)) - return ret; + return tecINSUFFICIENT_RESERVE; } return tesSUCCESS; @@ -2058,10 +2045,8 @@ XChainCreateClaimID::doApply() } auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - adjustOwnerCount(ctx_.view(), sleAcct, *sponsorSle, 1, ctx_.journal); - addSponsorToLedgerEntry(sleClaimID, *sponsorSle); + adjustOwnerCount(ctx_.view(), sleAcct, sponsorSle, 1, ctx_.journal); + addSponsorToLedgerEntry(sleClaimID, sponsorSle); ctx_.view().insert(sleClaimID); ctx_.view().update(sleBridge); diff --git a/src/libxrpl/tx/transactors/check/CheckCash.cpp b/src/libxrpl/tx/transactors/check/CheckCash.cpp index ab46f186f10..bba4ddb186c 100644 --- a/src/libxrpl/tx/transactors/check/CheckCash.cpp +++ b/src/libxrpl/tx/transactors/check/CheckCash.cpp @@ -389,17 +389,12 @@ CheckCash::doApply() optDeliverMin ? maxDeliverMin() : ctx_.tx.getFieldAmount(sfAmount)}; auto const sponsorSle = getTxReserveSponsor(psb, ctx_.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - + auto sleDst = psb.peek(keylet::account(accountID_)); // Check reserve. Return destination account SLE if enough reserve, // otherwise return nullptr. auto checkReserve = [&]() -> 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( - psb, ctx_.tx, sleDst, preFeeBalance_, *sponsorSle, 1, 0, j_); + if (auto const ret = checkXrpBalance(psb, ctx_.tx, sleDst, sponsorSle, 1, j_); !isTesSuccess(ret)) { JLOG(j_.trace()) << "Trust line does not exist. " @@ -457,7 +452,7 @@ CheckCash::doApply() Issue(currency, accountID_), // limit of zero 0, // quality in 0, // quality out - *sponsorSle, // sponsor + sponsorSle, // sponsor viewJ); // journal !isTesSuccess(ter)) { @@ -505,7 +500,7 @@ CheckCash::doApply() return tecINSUFFICIENT_RESERVE; if (auto const err = - checkCreateMPT(psb, mptID, accountID_, *sponsorSle, j_); + checkCreateMPT(psb, mptID, accountID_, sponsorSle, j_); !isTesSuccess(err)) { return err; diff --git a/src/libxrpl/tx/transactors/check/CheckCreate.cpp b/src/libxrpl/tx/transactors/check/CheckCreate.cpp index e9f3e175c64..4164b4da543 100644 --- a/src/libxrpl/tx/transactors/check/CheckCreate.cpp +++ b/src/libxrpl/tx/transactors/check/CheckCreate.cpp @@ -196,12 +196,10 @@ CheckCreate::doApply() // check the starting balance because we want to allow dipping into the // reserve to pay fees. auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - if (auto const ret = checkInsufficientReserve( - view(), ctx_.tx, sle, preFeeBalance_, *sponsorSle, 1, 0, ctx_.journal); + if (auto const ret = checkXrpBalance(view(), ctx_.tx, sle, sponsorSle, 1, ctx_.journal); !isTesSuccess(ret)) return ret; + // Note that we use the value from the sequence or ticket as the // Check sequence. For more explanation see comments in SeqProxy.h. std::uint32_t const seq = ctx_.tx.getSeqValue(); @@ -255,8 +253,8 @@ CheckCreate::doApply() } // If we succeeded, the new entry counts against the creator's reserve. - adjustOwnerCount(view(), sle, *sponsorSle, 1, viewJ); - addSponsorToLedgerEntry(sleCheck, *sponsorSle); + adjustOwnerCount(view(), sle, sponsorSle, 1, viewJ); + addSponsorToLedgerEntry(sleCheck, sponsorSle); return tesSUCCESS; } diff --git a/src/libxrpl/tx/transactors/credentials/CredentialAccept.cpp b/src/libxrpl/tx/transactors/credentials/CredentialAccept.cpp index 32563a536e9..0a647af3a4d 100644 --- a/src/libxrpl/tx/transactors/credentials/CredentialAccept.cpp +++ b/src/libxrpl/tx/transactors/credentials/CredentialAccept.cpp @@ -12,7 +12,6 @@ #include #include #include -#include #include #include #include @@ -95,12 +94,9 @@ CredentialAccept::doApply() return tefINTERNAL; // LCOV_EXCL_LINE auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - if (auto const ret = checkInsufficientReserve( - view(), ctx_.tx, sleSubject, preFeeBalance_, *sponsorSle, 1, 0, ctx_.journal); + if (auto const ret = checkXrpBalance(view(), ctx_.tx, sleSubject, sponsorSle, 1, ctx_.journal); !isTesSuccess(ret)) - return ret; + return tecINSUFFICIENT_RESERVE; auto const credType(ctx_.tx[sfCredentialType]); Keylet const credentialKey = keylet::credential(accountID_, issuer, credType); @@ -121,8 +117,8 @@ CredentialAccept::doApply() adjustOwnerCountObj(view(), sleIssuer, sleCred, -1, j_); removeSponsorFromLedgerEntry(sleCred); - adjustOwnerCount(view(), sleSubject, *sponsorSle, 1, j_); - addSponsorToLedgerEntry(sleCred, *sponsorSle); + adjustOwnerCount(view(), sleSubject, sponsorSle, 1, j_); + addSponsorToLedgerEntry(sleCred, sponsorSle); return tesSUCCESS; } diff --git a/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp b/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp index 90cfbb5ca91..4c73ad69cfa 100644 --- a/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp +++ b/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp @@ -14,7 +14,6 @@ #include #include #include -#include #include #include #include @@ -132,12 +131,9 @@ CredentialCreate::doApply() return tefINTERNAL; // LCOV_EXCL_LINE auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - if (auto const ret = checkInsufficientReserve( - view(), ctx_.tx, sleIssuer, preFeeBalance_, *sponsorSle, 1, 0, ctx_.journal); + if (auto const ret = checkXrpBalance(view(), ctx_.tx, sleIssuer, sponsorSle, 1, ctx_.journal); !isTesSuccess(ret)) - return ret; + return tecINSUFFICIENT_RESERVE; sleCred->setAccountID(sfSubject, subject); sleCred->setAccountID(sfIssuer, accountID_); @@ -155,8 +151,8 @@ CredentialCreate::doApply() return tecDIR_FULL; sleCred->setFieldU64(sfIssuerNode, *page); - adjustOwnerCount(view(), sleIssuer, *sponsorSle, 1, j_); - addSponsorToLedgerEntry(sleCred, *sponsorSle); + adjustOwnerCount(view(), sleIssuer, sponsorSle, 1, j_); + addSponsorToLedgerEntry(sleCred, sponsorSle); } if (subject == accountID_) diff --git a/src/libxrpl/tx/transactors/delegate/DelegateSet.cpp b/src/libxrpl/tx/transactors/delegate/DelegateSet.cpp index ff58e9a2c83..9a49792d44d 100644 --- a/src/libxrpl/tx/transactors/delegate/DelegateSet.cpp +++ b/src/libxrpl/tx/transactors/delegate/DelegateSet.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include #include #include @@ -96,12 +95,9 @@ DelegateSet::doApply() return tecINTERNAL; // LCOV_EXCL_LINE auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - if (auto const ret = checkInsufficientReserve( - view(), ctx_.tx, sleOwner, preFeeBalance_, *sponsorSle, 1, 0, ctx_.journal); + if (auto const ret = checkXrpBalance(view(), ctx_.tx, sleOwner, sponsorSle, 1, ctx_.journal); !isTesSuccess(ret)) - return ret; + return tecINSUFFICIENT_RESERVE; sle = std::make_shared(delegateKey); sle->setAccountID(sfAccount, accountID_); @@ -129,8 +125,8 @@ DelegateSet::doApply() (*sle)[sfDestinationNode] = *destPage; ctx_.view().insert(sle); - adjustOwnerCount(ctx_.view(), sleOwner, *sponsorSle, 1, ctx_.journal); - addSponsorToLedgerEntry(sle, *sponsorSle); + adjustOwnerCount(ctx_.view(), sleOwner, sponsorSle, 1, ctx_.journal); + addSponsorToLedgerEntry(sle, sponsorSle); return tesSUCCESS; } diff --git a/src/libxrpl/tx/transactors/dex/AMMCreate.cpp b/src/libxrpl/tx/transactors/dex/AMMCreate.cpp index bde57f75df8..12bda5905f5 100644 --- a/src/libxrpl/tx/transactors/dex/AMMCreate.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMCreate.cpp @@ -148,32 +148,35 @@ AMMCreate::preclaim(PreclaimContext const& ctx) return terNO_RIPPLE; } + // Check the reserve for LPToken trustline if (ctx.view.rules().enabled(featureSponsor)) { - auto const sponsorSle = getTxReserveSponsor(ctx.view, ctx.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - - // Check the reserve for LPToken trustline - // Insufficient reserve - auto const accountSle = ctx.view.read(keylet::account(accountID)); - if (auto const ret = checkInsufficientReserve( - ctx.view, - ctx.tx, - accountSle, - accountSle->getFieldAmount(sfBalance), - *sponsorSle, - 1, - 0, - ctx.j); + XRPAmount const balanceAdj = [&]() { + if (isXRP(amount)) + return amount.xrp(); + if (isXRP(amount2)) + return amount2.xrp(); + return XRPAmount(); + }(); + + if (auto const ret = checkXrpBalance(ctx.view, ctx.tx, accountID, 1, -balanceAdj, ctx.j); !isTesSuccess(ret)) { + if (ret == tecINSUFFICIENT_FUNDS) + { + JLOG(ctx.j.debug()) + << "AMM Instance: insufficient funds, " << amount << " " << amount2; + return tecUNFUNDED_AMM; + } + JLOG(ctx.j.debug()) << "AMM Instance: insufficient reserves"; return tecINSUF_RESERVE_LINE; } } else { + // <= beast::kZero is non standard case (standard is < beast::kZero) so we can't remove + // it until the Sponsor Amendment is fully adopted STAmount const xrpBalance = xrpLiquid(ctx.view, accountID, 1, ctx.j); // Insufficient reserve if (xrpBalance <= beast::kZero) @@ -183,18 +186,26 @@ AMMCreate::preclaim(PreclaimContext const& ctx) } } - auto const ownerCountAdj = isReserveSponsored(ctx.tx) ? 0 : 1; - STAmount const xrpBalance = xrpLiquid(ctx.view, accountID, ownerCountAdj, ctx.j); auto insufficientBalance = [&](STAmount const& amount) { if (isXRP(amount)) + { + // featureSponsor already check both amount and reserve + if (ctx.view.rules().enabled(featureSponsor)) + return false; + + auto const ownerCountAdj = isReserveSponsored(ctx.tx) ? 0 : 1; + STAmount const xrpBalance = xrpLiquid(ctx.view, accountID, ownerCountAdj, ctx.j); return xrpBalance < amount; - return accountFunds( - ctx.view, - accountID, - amount, - FreezeHandling::ZeroIfFrozen, - AuthHandling::ZeroIfUnauthorized, - ctx.j) < amount; + } + + auto const funds = accountFunds( + ctx.view, + accountID, + amount, + FreezeHandling::ZeroIfFrozen, + AuthHandling::ZeroIfUnauthorized, + ctx.j); + return funds < amount; }; if (insufficientBalance(amount) || insufficientBalance(amount2)) @@ -324,10 +335,7 @@ applyCreate(ApplyContext& ctx, Sandbox& sb, AccountID const& account, beast::Jou // Send LPT to LP. auto const sponsorSle = getTxReserveSponsor(sb, ctx.tx); - if (!sponsorSle) - return {sponsorSle.error(), false}; // LCOV_EXCL_LINE - - auto res = accountSend(sb, accountId, account, lpTokens, ctx.journal, *sponsorSle); + auto res = accountSend(sb, accountId, account, lpTokens, ctx.journal, sponsorSle); if (!isTesSuccess(res)) { JLOG(j.debug()) << "AMM Instance: failed to send LPT " << lpTokens; @@ -353,25 +361,15 @@ applyCreate(ApplyContext& ctx, Sandbox& sb, AccountID const& account, beast::Jou return err; // Don't adjust AMM owner count. // It's irrelevant for pseudo-account like AMM. + // Don't sponsor for AMM Trustline return accountSend( - sb, - account, - accountId, - amount, - ctx.journal, - {}, // don't sponsor for AMM Trustline - WaiveTransferFee::Yes); + sb, account, accountId, amount, ctx.journal, {}, WaiveTransferFee::Yes); }, // Set AMM flag on AMM trustline + // Don't sponsor for AMM Trustline [&](Issue const& issue) -> TER { if (auto const res = accountSend( - sb, - account, - accountId, - amount, - ctx.journal, - {}, // don't sponsor for AMM Trustline - WaiveTransferFee::Yes)) + sb, account, accountId, amount, ctx.journal, {}, WaiveTransferFee::Yes)) return res; // Set AMM flag on AMM trustline if (!isXRP(amount)) diff --git a/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp b/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp index 9db26c99200..48bd67e9d8b 100644 --- a/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp @@ -233,33 +233,12 @@ AMMDeposit::preclaim(PreclaimContext const& ctx) { auto const lpIssue = (*ammSle)[sfLPTokenBalance].get(); // Adjust the reserve if LP doesn't have LPToken trustline - auto const sle = - ctx.view.read(keylet::line(accountID, lpIssue.account, lpIssue.currency)); - - auto const sponsorSle = getTxReserveSponsor(ctx.view, ctx.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - auto const accountSle = ctx.view.read(keylet::account(accountID)); - auto const reserveAdj = (*sponsorSle || sle) ? 0 : 1; - - if (xrpLiquid(ctx.view, accountID, reserveAdj, ctx.j) < deposit) - { - if (sle) - return tecUNFUNDED_AMM; - return tecINSUF_RESERVE_LINE; - } - - if (auto const ret = checkInsufficientReserve( - ctx.view, - ctx.tx, - accountSle, - accountSle->getFieldAmount(sfBalance) - deposit, - *sponsorSle, - 1, - !sle, - ctx.j); - *sponsorSle && !isTesSuccess(ret)) - return tecINSUF_RESERVE_LINE; + bool const tlExists = + ctx.view.exists(keylet::line(accountID, lpIssue.account, lpIssue.currency)); + auto const ter = + checkXrpBalance(ctx.view, ctx.tx, accountID, !tlExists, -deposit.xrp(), ctx.j); + if (!isTesSuccess(ter)) + return tlExists ? tecUNFUNDED_AMM : tecINSUF_RESERVE_LINE; return tesSUCCESS; } @@ -384,28 +363,17 @@ AMMDeposit::preclaim(PreclaimContext const& ctx) { if (ctx.view.rules().enabled(featureSponsor)) { - auto const accountSle = ctx.view.read(keylet::account(accountID)); - auto const sponsorSle = getTxReserveSponsor(ctx.view, ctx.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - // Insufficient reserve - if (auto const ret = checkInsufficientReserve( - ctx.view, - ctx.tx, - accountSle, - accountSle->getFieldAmount(sfBalance), - *sponsorSle, - 1, - 0, - ctx.j); - !isTesSuccess(ret)) + if (auto const ter = checkXrpBalance(ctx.view, ctx.tx, accountID, 1, ctx.j); + !isTesSuccess(ter)) { JLOG(ctx.j.debug()) << "AMM Instance: insufficient reserves"; - return tecINSUF_RESERVE_LINE; + return tecINSUFFICIENT_RESERVE == ter ? tecINSUF_RESERVE_LINE : ter; } } else { + // <= beast::kZero is non standard case (standard is < beast::kZero) so we can't remove + // this block until the Sponsor Amendment is fully adopted STAmount const xrpBalance = xrpLiquid(ctx.view, accountID, 1, ctx.j); // Insufficient reserve if (xrpBalance <= beast::kZero) @@ -559,9 +527,6 @@ AMMDeposit::deposit( std::uint16_t tfee) { auto const sponsorSle = getTxReserveSponsor(view, ctx_.tx); - if (!sponsorSle) - return {sponsorSle.error(), STAmount{}}; // LCOV_EXCL_LINE - // Check account has sufficient funds. // Return true if it does, false otherwise. auto checkBalance = [&](auto const& depositAmount) -> TER { @@ -571,10 +536,11 @@ AMMDeposit::deposit( { auto const& lpIssue = lpTokensDeposit.get(); // Adjust the reserve if LP doesn't have LPToken trustline - auto const trustlineExists = + bool const tlExists = view.exists(keylet::line(accountID_, lpIssue.account, lpIssue.currency)); - auto const reserveAdj = (*sponsorSle || trustlineExists) ? 0 : 1; - if (xrpLiquid(view, accountID_, reserveAdj, j_) >= depositAmount) + auto const ter = checkXrpBalance( + view, ctx_.tx, accountID_, sponsorSle, !tlExists, -depositAmount.xrp(), j_); + if (isTesSuccess(ter)) return tesSUCCESS; } else if ( @@ -670,7 +636,7 @@ AMMDeposit::deposit( // Deposit LP tokens res = - accountSend(view, ammAccount, accountID_, lpTokensDepositActual, ctx_.journal, *sponsorSle); + accountSend(view, ammAccount, accountID_, lpTokensDepositActual, ctx_.journal, sponsorSle); if (!isTesSuccess(res)) { JLOG(ctx_.journal.debug()) << "AMM Deposit: failed to deposit LPTokens"; diff --git a/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp b/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp index cfbce8fc2b1..acd3a419f2b 100644 --- a/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp @@ -30,7 +30,6 @@ #include #include -#include #include #include #include @@ -611,14 +610,7 @@ AMMWithdraw::withdraw( // this is also called from AMMClawback, but only AMMWithdraw does sponsor // the new trustline - SLE::pointer sponsorSle; - if (tx[sfAccount] == account) - { - auto sle = getTxReserveSponsor(view, tx); - if (!sle) - return {sle.error(), STAmount{}, STAmount{}, STAmount{}}; // LCOV_EXCL_LINE - sponsorSle = std::move(*sle); - } + SLE::pointer sponsorSle = getTxReserveSponsor(view, tx, account); // Check the reserve in case a trustline or MPT has to be created bool const enabledFixAmMv12 = view.rules().enabled(fixAMMv1_2); @@ -646,24 +638,12 @@ AMMWithdraw::withdraw( if (!sleAccount) return tecINTERNAL; // LCOV_EXCL_LINE - auto const balance = (*sleAccount)[sfBalance]->xrp(); - std::uint32_t const count = - ownerCount(view, sponsorSle ? sponsorSle : sleAccount, journal); - // See also TrustSet::doApply() and MPTokenAuthorize::authorize() - if (count >= 2) - { - if (auto const ret = checkInsufficientReserve( - view, - tx, - sleAccount, - std::max(priorBalance, balance), - sponsorSle, - 1, - 0, - journal); - !isTesSuccess(ret)) - return ret; - } + // See also TrustSet::doApply() and authorizeMPToken() for ownerCount >=2. + // Here "true" means it + if (auto const ter = + checkXrpBalance(view, tx, sleAccount, sponsorSle, 1, true, journal); + !isTesSuccess(ter)) + return tecINSUFFICIENT_RESERVE; } return tesSUCCESS; }; diff --git a/src/libxrpl/tx/transactors/dex/OfferCreate.cpp b/src/libxrpl/tx/transactors/dex/OfferCreate.cpp index b3cc744332e..2d957dc0ff2 100644 --- a/src/libxrpl/tx/transactors/dex/OfferCreate.cpp +++ b/src/libxrpl/tx/transactors/dex/OfferCreate.cpp @@ -833,11 +833,8 @@ OfferCreate::applyGuts(Sandbox& sb, Sandbox& sbCancel) return {tefINTERNAL, false}; auto const sponsorSle = getTxReserveSponsor(sb, ctx_.tx); - if (!sponsorSle) - return {sponsorSle.error(), false}; // LCOV_EXCL_LINE - - if (auto const ret = checkInsufficientReserve( - sb, ctx_.tx, sleCreator, preFeeBalance_, *sponsorSle, 1, 0, j_); + if (auto const ret = + checkXrpBalance(sb, ctx_.tx, sleCreator, preFeeBalance_, sponsorSle, 1, j_); !isTesSuccess(ret)) { // If we are here, the signing account had an insufficient reserve @@ -870,7 +867,7 @@ OfferCreate::applyGuts(Sandbox& sb, Sandbox& sbCancel) } // Update owner count. - adjustOwnerCount(sb, sleCreator, *sponsorSle, 1, viewJ); + adjustOwnerCount(sb, sleCreator, sponsorSle, 1, viewJ); JLOG(j_.trace()) << "adding to book: " << to_string(saTakerPays.asset()) << " : " << to_string(saTakerGets.asset()) @@ -939,7 +936,7 @@ OfferCreate::applyGuts(Sandbox& sb, Sandbox& sbCancel) sleOffer->setFlag(lsfSell); if (domainID) sleOffer->setFieldH256(sfDomainID, *domainID); - addSponsorToLedgerEntry(sleOffer, *sponsorSle); + addSponsorToLedgerEntry(sleOffer, sponsorSle); // if it's a hybrid offer, set hybrid flag, and create an open dir if (bHybrid) diff --git a/src/libxrpl/tx/transactors/did/DIDSet.cpp b/src/libxrpl/tx/transactors/did/DIDSet.cpp index 094d0548f3b..65d006328d2 100644 --- a/src/libxrpl/tx/transactors/did/DIDSet.cpp +++ b/src/libxrpl/tx/transactors/did/DIDSet.cpp @@ -72,13 +72,11 @@ addSLE(ApplyContext& ctx, SLE::ref sle, AccountID const& owner) // Check reserve availability for new object creation auto const sponsorSle = getTxReserveSponsor(ctx.view(), ctx.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE auto const balance = STAmount((*sleAccount)[sfBalance]).xrp(); - if (auto const ret = checkInsufficientReserve( - ctx.view(), ctx.tx, sleAccount, balance, *sponsorSle, 1, 0, ctx.journal); + if (auto const ret = + checkXrpBalance(ctx.view(), ctx.tx, sleAccount, balance, sponsorSle, 1, ctx.journal); !isTesSuccess(ret)) - return ret; + return tecINSUFFICIENT_RESERVE; // Add ledger object to ledger ctx.view().insert(sle); @@ -91,8 +89,8 @@ addSLE(ApplyContext& ctx, SLE::ref sle, AccountID const& owner) return tecDIR_FULL; // LCOV_EXCL_LINE (*sle)[sfOwnerNode] = *page; } - adjustOwnerCount(ctx.view(), sleAccount, *sponsorSle, 1, ctx.journal); - addSponsorToLedgerEntry(sle, *sponsorSle); + adjustOwnerCount(ctx.view(), sleAccount, sponsorSle, 1, ctx.journal); + addSponsorToLedgerEntry(sle, sponsorSle); ctx.view().update(sleAccount); return tesSUCCESS; diff --git a/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp b/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp index bdb4bcddaee..735b95aacf5 100644 --- a/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp +++ b/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp @@ -435,23 +435,12 @@ EscrowCreate::doApply() // Check reserve and funds availability STAmount const amount{ctx_.tx[sfAmount]}; - auto const balance = sle->getFieldAmount(sfBalance).xrp(); + auto const balanceAdj = isXRP(amount) ? -amount.xrp() : XRPAmount(); auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - if (auto const ret = - checkInsufficientReserve(ctx_.view(), ctx_.tx, sle, balance, *sponsorSle, 1, 0, j_); + if (auto const ret = checkXrpBalance( + static_cast(ctx_.view()), ctx_.tx, sle, sponsorSle, 1, balanceAdj, j_); !isTesSuccess(ret)) - return ret; - - // Check reserve and funds availability - if (isXRP(amount)) - { - if (auto const ret = checkInsufficientReserve( - ctx_.view(), ctx_.tx, sle, balance - STAmount(amount).xrp(), {}, 1, 0, j_); - !isTesSuccess(ret)) - return tecUNFUNDED; - } + return ret == tecINSUFFICIENT_RESERVE ? tecINSUFFICIENT_RESERVE : tecUNFUNDED; // Check destination account { @@ -541,8 +530,8 @@ EscrowCreate::doApply() } // increment owner count - adjustOwnerCount(ctx_.view(), sle, *sponsorSle, 1, ctx_.journal); - addSponsorToLedgerEntry(slep, *sponsorSle); + adjustOwnerCount(ctx_.view(), sle, sponsorSle, 1, ctx_.journal); + addSponsorToLedgerEntry(slep, sponsorSle); ctx_.view().update(sle); return tesSUCCESS; } diff --git a/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp b/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp index 4bc12fbe56d..86cdd52bb59 100644 --- a/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp +++ b/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp @@ -14,7 +14,6 @@ #include #include #include -#include #include #include #include @@ -224,10 +223,6 @@ EscrowFinish::preclaim(PreclaimContext const& ctx) } } - auto const sponsorSle = getTxReserveSponsor(ctx.view, ctx.tx); - if (!sponsorSle) - return sponsorSle.error(); - return tesSUCCESS; } diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp index 826ff7f6fac..bbaa684bea3 100644 --- a/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp @@ -238,20 +238,15 @@ LoanBrokerSet::doApply() return ter; // LCOV_EXCL_LINE auto const sponsorSle = getTxReserveSponsor(view, tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - - if (auto const ret = checkInsufficientReserve( - view, tx, owner, preFeeBalance_, {}, *sponsorSle ? 1 : 2, 0, j_); + if (auto const ret = checkXrpBalance(view, tx, owner, {}, sponsorSle ? 1 : 2, j_); !isTesSuccess(ret)) - return ret; + return tecINSUFFICIENT_RESERVE; - if (*sponsorSle) + if (sponsorSle) { - if (auto const ret = checkInsufficientReserve( - view, tx, owner, preFeeBalance_, *sponsorSle, 1, 0, j_); + if (auto const ret = checkXrpBalance(view, tx, owner, sponsorSle, 1, j_); !isTesSuccess(ret)) - return ret; + return tecINSUFFICIENT_RESERVE; } // Increases the owner count by two: one for the LoanBroker object, and @@ -259,7 +254,7 @@ LoanBrokerSet::doApply() // Pseudo-account cannot be sponsored adjustOwnerCount(view, owner, {}, 1, j_); // LoanBroker object can be sponsored - adjustOwnerCount(view, owner, *sponsorSle, 1, j_); + adjustOwnerCount(view, owner, sponsorSle, 1, j_); auto maybePseudo = createPseudoAccount(view, broker->key(), sfLoanBrokerID); if (!maybePseudo) @@ -289,7 +284,7 @@ LoanBrokerSet::doApply() if (auto const coverLiq = tx[~sfCoverRateLiquidation]) broker->at(sfCoverRateLiquidation) = *coverLiq; - addSponsorToLedgerEntry(broker, *sponsorSle); + addSponsorToLedgerEntry(broker, sponsorSle); view.insert(broker); diff --git a/src/libxrpl/tx/transactors/lending/LoanSet.cpp b/src/libxrpl/tx/transactors/lending/LoanSet.cpp index 59a762206b1..384b913f721 100644 --- a/src/libxrpl/tx/transactors/lending/LoanSet.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanSet.cpp @@ -513,18 +513,15 @@ LoanSet::doApply() } } - auto const sponsorSle = getTxReserveSponsor(view, tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE + auto const sponsorSle = getTxReserveSponsor(view, tx, borrower); { auto const balance = accountID_ == borrower ? preFeeBalance_ : borrowerSle->at(sfBalance).value().xrp(); - if (auto const ret = - checkInsufficientReserve(view, tx, borrowerSle, balance, *sponsorSle, 1, 0, j_); + if (auto const ret = checkXrpBalance(view, tx, borrowerSle, balance, sponsorSle, 1, j_); !isTesSuccess(ret)) - return ret; + return tecINSUFFICIENT_RESERVE; } - adjustOwnerCount(view, borrowerSle, *sponsorSle, 1, j_); + adjustOwnerCount(view, borrowerSle, sponsorSle, 1, j_); // Account for the origination fee using two payments // @@ -624,7 +621,7 @@ LoanSet::doApply() loan->at(sfPreviousPaymentDueDate) = 0; loan->at(sfNextPaymentDueDate) = startDate + paymentInterval; loan->at(sfPaymentRemaining) = paymentTotal; - addSponsorToLedgerEntry(loan, *sponsorSle); + addSponsorToLedgerEntry(loan, sponsorSle); view.insert(loan); // Update the balances in the vault diff --git a/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp b/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp index 2298e2a8a68..8f7e1c38519 100644 --- a/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp +++ b/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp @@ -5,7 +5,6 @@ #include #include #include -#include #include #include #include @@ -22,7 +21,6 @@ #include #include -#include #include #include @@ -368,18 +366,12 @@ NFTokenAcceptOffer::transferNFToken( !isTesSuccess(ret)) return ret; - auto const sleBuyer = view().read(keylet::account(buyer)); + auto const sleBuyer = view().peek(keylet::account(buyer)); if (!sleBuyer) return tecINTERNAL; // LCOV_EXCL_LINE - std::uint32_t const buyerOwnerCountBefore = sleBuyer->getFieldU32(sfOwnerCount); - - auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - - auto const insertRet = - nft::insertToken(view(), ctx_.tx, buyer, *sponsorSle, std::move(tokenAndPage->token)); + auto const sponsorSle = + buyer == accountID_ ? getTxReserveSponsor(view(), ctx_.tx) : SLE::pointer(); // if fixNFTokenReserve is enabled, check if the buyer has sufficient // reserve to own a new object, if their OwnerCount changed. @@ -387,27 +379,17 @@ NFTokenAcceptOffer::transferNFToken( // There was an issue where the buyer accepts a sell offer, the ledger // didn't check if the buyer has enough reserve, meaning that buyer can get // NFTs free of reserve. - if (view().rules().enabled(fixNFTokenReserve)) - { - // To check if there is sufficient reserve, we cannot use preFeeBalance_ - // because NFT is sold for a price. So we must use the balance after - // the deduction of the potential offer price. A small caveat here is - // that the balance has already deducted the transaction fee, meaning - // that the reserve requirement is a few drops higher. - auto const buyerBalance = sleBuyer->getFieldAmount(sfBalance); - - auto const buyerOwnerCountAfter = sleBuyer->getFieldU32(sfOwnerCount); - if (buyerOwnerCountAfter > buyerOwnerCountBefore) - { - SLE::const_pointer buyerSponsorSle; - if (accountID_ == buyer) - buyerSponsorSle = *sponsorSle; - if (auto const ret = checkInsufficientReserve( - ctx_.view(), ctx_.tx, sleBuyer, buyerBalance, buyerSponsorSle, 0, 0, j_); - !isTesSuccess(ret)) - return ret; - } - } + + // To check if there is sufficient reserve, we cannot use preFeeBalance_ + // because NFT is sold for a price. So we must use the balance after + // the deduction of the potential offer price. A small caveat here is + // that the balance has already deducted the transaction fee, meaning + // that the reserve requirement is a few drops higher. + + auto const balance = sleBuyer->at(sfBalance)->xrp(); + bool const fixEnable = view().rules().enabled(fixNFTokenReserve); + auto const insertRet = nft::insertToken( + view(), ctx_.tx, sleBuyer, balance, sponsorSle, std::move(tokenAndPage->token), fixEnable); return insertRet; } diff --git a/src/libxrpl/tx/transactors/nft/NFTokenCreateOffer.cpp b/src/libxrpl/tx/transactors/nft/NFTokenCreateOffer.cpp index 11a2f388725..43023472e4d 100644 --- a/src/libxrpl/tx/transactors/nft/NFTokenCreateOffer.cpp +++ b/src/libxrpl/tx/transactors/nft/NFTokenCreateOffer.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -78,7 +79,7 @@ NFTokenCreateOffer::doApply() return nft::tokenOfferCreateApply( view(), ctx_.tx, - ctx_.tx[sfAccount], + view().peek(keylet::account(accountID_)), ctx_.tx[sfAmount], ctx_.tx[~sfDestination], ctx_.tx[~sfExpiration], diff --git a/src/libxrpl/tx/transactors/nft/NFTokenMint.cpp b/src/libxrpl/tx/transactors/nft/NFTokenMint.cpp index 4270f50c599..5227180ad88 100644 --- a/src/libxrpl/tx/transactors/nft/NFTokenMint.cpp +++ b/src/libxrpl/tx/transactors/nft/NFTokenMint.cpp @@ -3,7 +3,6 @@ #include #include #include -#include #include #include #include @@ -279,9 +278,6 @@ NFTokenMint::doApply() if (!tokenSeq.has_value()) return (tokenSeq.error()); - std::uint32_t const ownerCountBefore = - view().read(keylet::account(accountID_))->getFieldU32(sfOwnerCount); - // Assemble the new NFToken. SOTemplate const* nfTokenTemplate = InnerObjectFormats::getInstance().findSOTemplateBySField(sfNFToken); @@ -306,12 +302,10 @@ NFTokenMint::doApply() object.setFieldVL(sfURI, *uri); }); + auto const accSle = view().peek(keylet::account(accountID_)); auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - - if (TER const ret = - nft::insertToken(ctx_.view(), ctx_.tx, accountID_, *sponsorSle, std::move(newToken)); + if (TER const ret = nft::insertToken( + ctx_.view(), ctx_.tx, accSle, preFeeBalance_, sponsorSle, std::move(newToken)); !isTesSuccess(ret)) return ret; @@ -323,7 +317,7 @@ NFTokenMint::doApply() if (TER const ter = nft::tokenOfferCreateApply( view(), ctx_.tx, - ctx_.tx[sfAccount], + accSle, ctx_.tx[sfAmount], ctx_.tx[~sfDestination], ctx_.tx[~sfExpiration], @@ -335,26 +329,6 @@ NFTokenMint::doApply() return ter; } - // Only check the reserve if the owner count actually changed. This - // allows NFTs to be added to the page (and burn fees) without - // requiring the reserve to be met each time. The reserve is - // only managed when a new NFT page or sell offer is added. - if (auto const ownerCountAfter = - view().read(keylet::account(accountID_))->getFieldU32(sfOwnerCount); - ownerCountAfter > ownerCountBefore) - { - if (auto const ret = checkInsufficientReserve( - ctx_.view(), - ctx_.tx, - view().read(keylet::account(accountID_)), - preFeeBalance_, - *sponsorSle, - 0, - 0, - j_); - !isTesSuccess(ret)) - return ret; - } return tesSUCCESS; } diff --git a/src/libxrpl/tx/transactors/oracle/OracleSet.cpp b/src/libxrpl/tx/transactors/oracle/OracleSet.cpp index 779d312e3b3..22bb713a4f6 100644 --- a/src/libxrpl/tx/transactors/oracle/OracleSet.cpp +++ b/src/libxrpl/tx/transactors/oracle/OracleSet.cpp @@ -181,12 +181,8 @@ OracleSet::preclaim(PreclaimContext const& ctx) if (pairs.size() > kMaxOracleDataSeries) return tecARRAY_TOO_LARGE; - auto const& balance = sleSetter->getFieldAmount(sfBalance); - auto const sponsorSle = getTxReserveSponsor(ctx.view, ctx.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - if (auto const ret = checkInsufficientReserve( - ctx.view, ctx.tx, sleSetter, balance, *sponsorSle, adjustReserve, 0, ctx.j); + if (auto const ret = + checkXrpBalance(ctx.view, ctx.tx, sleSetter, adjustReserve, XRPAmount(), ctx.j); !isTesSuccess(ret)) return ret; @@ -284,15 +280,12 @@ OracleSet::doApply() // Otherwise, the sponsorship will be deleted. auto const newSponsorSle = getTxReserveSponsor(ctx_.view(), ctx_.tx); - if (!newSponsorSle) - return newSponsorSle.error(); // LCOV_EXCL_LINE - // decrease current sponsored owner count adjustOwnerCountObj(ctx_.view(), accountSle, sle, -oldCount, ctx_.journal); removeSponsorFromLedgerEntry(sle); // increase new owner count - adjustOwnerCount(ctx_.view(), accountSle, *newSponsorSle, newCount, ctx_.journal); - addSponsorToLedgerEntry(sle, *newSponsorSle); + adjustOwnerCount(ctx_.view(), accountSle, newSponsorSle, newCount, ctx_.journal); + addSponsorToLedgerEntry(sle, newSponsorSle); } else if (adjust < 0) { @@ -348,14 +341,12 @@ OracleSet::doApply() auto const count = calculateOracleReserve(series.size()); auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE auto const accountSle = ctx_.view().peek(keylet::account(ctx_.tx[sfAccount])); if (!accountSle) return tefINTERNAL; // LCOV_EXCL_LINE - adjustOwnerCount(ctx_.view(), accountSle, *sponsorSle, count, ctx_.journal); - addSponsorToLedgerEntry(sle, *sponsorSle); + adjustOwnerCount(ctx_.view(), accountSle, sponsorSle, count, ctx_.journal); + addSponsorToLedgerEntry(sle, sponsorSle); ctx_.view().insert(sle); } diff --git a/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp b/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp index 61f33661743..9bffaa52ba7 100644 --- a/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp +++ b/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp @@ -14,7 +14,6 @@ #include #include #include -#include #include #include #include @@ -162,10 +161,7 @@ DepositPreauth::doApply() // check the starting balance because we want to allow dipping into the // reserve to pay fees. auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - if (auto const ret = checkInsufficientReserve( - view(), ctx_.tx, sleOwner, preFeeBalance_, *sponsorSle, 1, 0, j_); + if (auto const ret = checkXrpBalance(view(), ctx_.tx, sleOwner, sponsorSle, 1, j_); !isTesSuccess(ret)) return ret; @@ -191,8 +187,8 @@ DepositPreauth::doApply() slePreauth->setFieldU64(sfOwnerNode, *page); // If we succeeded, the new entry counts against the creator's reserve. - adjustOwnerCount(view(), sleOwner, *sponsorSle, 1, j_); - addSponsorToLedgerEntry(slePreauth, *sponsorSle); + adjustOwnerCount(view(), sleOwner, sponsorSle, 1, j_); + addSponsorToLedgerEntry(slePreauth, sponsorSle); } else if (ctx_.tx.isFieldPresent(sfUnauthorize)) { @@ -210,10 +206,7 @@ DepositPreauth::doApply() // check the starting balance because we want to allow dipping into the // reserve to pay fees. auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - if (auto const ret = checkInsufficientReserve( - view(), ctx_.tx, sleOwner, preFeeBalance_, *sponsorSle, 1, 0, j_); + if (auto const ret = checkXrpBalance(view(), ctx_.tx, sleOwner, sponsorSle, 1, j_); !isTesSuccess(ret)) return ret; @@ -253,8 +246,8 @@ DepositPreauth::doApply() slePreauth->setFieldU64(sfOwnerNode, *page); // If we succeeded, the new entry counts against the creator's reserve. - adjustOwnerCount(view(), sleOwner, *sponsorSle, 1, j_); - addSponsorToLedgerEntry(slePreauth, *sponsorSle); + adjustOwnerCount(view(), sleOwner, sponsorSle, 1, j_); + addSponsorToLedgerEntry(slePreauth, sponsorSle); } else if (ctx_.tx.isFieldPresent(sfUnauthorizeCredentials)) { diff --git a/src/libxrpl/tx/transactors/payment/Payment.cpp b/src/libxrpl/tx/transactors/payment/Payment.cpp index 08632158ac2..aef332d3b91 100644 --- a/src/libxrpl/tx/transactors/payment/Payment.cpp +++ b/src/libxrpl/tx/transactors/payment/Payment.cpp @@ -471,7 +471,7 @@ Payment::doApply() sponsor->getFieldU32(sfSponsoringAccountCount); if (currentSponsoringAccountCount == std::numeric_limits::max()) { - JLOG(j_.fatal()) << "Sponsoring account count overflow for account " + JLOG(j_.error()) << "Sponsoring account count overflow for account " << to_string(accountID_); return tecINTERNAL; // LCOV_EXCL_LINE } @@ -651,9 +651,9 @@ Payment::doApply() // reserve. auto const reserve = accountReserve(view(), sleSrc, j_); - // In a delegated payment, the fee payer is the delegated account, + // In a delegated or sponsored payment, the fee payer can be another account, // not the source account (accountID_). - bool const accountIsPayer = (ctx_.tx.getFeePayer() == accountID_); + bool const accountIsPayer = (getFeePayer(view(), ctx_.tx).id == 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 04d8e6280b9..61a0a4a55b0 100644 --- a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp +++ b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp @@ -79,19 +79,12 @@ PaymentChannelCreate::preclaim(PreclaimContext const& ctx) // Check reserve and funds availability { - auto const balance = (*sle)[sfBalance]; auto const sponsorSle = getTxReserveSponsor(ctx.view, ctx.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE + auto const xrpAmount = ctx.tx[sfAmount].xrp(); if (auto const ret = - checkInsufficientReserve(ctx.view, ctx.tx, sle, balance, *sponsorSle, 1, 0, ctx.j); + checkXrpBalance(ctx.view, ctx.tx, sle, sponsorSle, 1, -xrpAmount, ctx.j); !isTesSuccess(ret)) - return ret; - - if (auto const ret = checkInsufficientReserve( - ctx.view, ctx.tx, sle, balance - ctx.tx[sfAmount], *sponsorSle, 1, 0, ctx.j); - !isTesSuccess(ret)) - return tecUNFUNDED; + return ret == tecINSUFFICIENT_RESERVE ? tecINSUFFICIENT_RESERVE : tecUNFUNDED; } auto const dst = ctx.tx[sfDestination]; @@ -185,10 +178,8 @@ PaymentChannelCreate::doApply() // Deduct owner's balance, increment owner count (*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount]; auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - adjustOwnerCount(ctx_.view(), sle, *sponsorSle, 1, ctx_.journal); - addSponsorToLedgerEntry(slep, *sponsorSle); + adjustOwnerCount(ctx_.view(), sle, sponsorSle, 1, ctx_.journal); + addSponsorToLedgerEntry(slep, sponsorSle); ctx_.view().update(sle); return tesSUCCESS; diff --git a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp index 1a19c466ad4..675396ed6bb 100644 --- a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp +++ b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp @@ -81,19 +81,11 @@ PaymentChannelFund::doApply() { // Check reserve and funds availability - auto const balance = (*sle)[sfBalance]; + auto const balance = (sle->at(sfBalance) - ctx_.tx[sfAmount]).xrp(); auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - if (auto const ret = - checkInsufficientReserve(ctx_.view(), ctx_.tx, sle, balance, *sponsorSle, 0, 0, j_); + if (auto const ret = checkXrpBalance(ctx_.view(), ctx_.tx, sle, balance, sponsorSle, 0, j_); !isTesSuccess(ret)) - return ret; - - if (auto const ret = checkInsufficientReserve( - ctx_.view(), ctx_.tx, sle, balance - ctx_.tx[sfAmount], {}, 0, 0, j_); - !isTesSuccess(ret)) - return tecUNFUNDED; + return ret == tecINSUFFICIENT_RESERVE ? tecINSUFFICIENT_RESERVE : tecUNFUNDED; } // do not allow adding funds if dst does not exist diff --git a/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.cpp b/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.cpp index d485ec22e99..704cd566a9d 100644 --- a/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.cpp +++ b/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.cpp @@ -106,14 +106,12 @@ PermissionedDomainSet::doApply() { // Create new permissioned domain. // Check reserve availability for new object creation - auto const balance = STAmount((*ownerSle)[sfBalance]).xrp(); + auto const balance = ownerSle->at(sfBalance)->xrp(); auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - if (auto const ret = checkInsufficientReserve( - ctx_.view(), ctx_.tx, ownerSle, balance, *sponsorSle, 1, 0, j_); + if (auto const ret = + checkXrpBalance(ctx_.view(), ctx_.tx, ownerSle, balance, sponsorSle, 1, j_); !isTesSuccess(ret)) - return ret; + return tecINSUFFICIENT_RESERVE; bool const fixEnabled = view().rules().enabled(fixCleanup3_1_3); auto const seq = fixEnabled ? ctx_.tx.getSeqValue() : ctx_.tx.getFieldU32(sfSequence); @@ -130,8 +128,8 @@ PermissionedDomainSet::doApply() slePd->setFieldU64(sfOwnerNode, *page); // If we succeeded, the new entry counts against the creator's reserve. - adjustOwnerCount(view(), ownerSle, *sponsorSle, 1, ctx_.journal); - addSponsorToLedgerEntry(slePd, *sponsorSle); + adjustOwnerCount(view(), ownerSle, sponsorSle, 1, ctx_.journal); + addSponsorToLedgerEntry(slePd, sponsorSle); view().insert(slePd); } diff --git a/src/libxrpl/tx/transactors/system/TicketCreate.cpp b/src/libxrpl/tx/transactors/system/TicketCreate.cpp index 3d301ebd77b..ac4cfe0fff1 100644 --- a/src/libxrpl/tx/transactors/system/TicketCreate.cpp +++ b/src/libxrpl/tx/transactors/system/TicketCreate.cpp @@ -77,10 +77,8 @@ TicketCreate::doApply() // reserve to pay fees. std::uint32_t const ticketCount = ctx_.tx[sfTicketCount]; auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - if (auto const ret = checkInsufficientReserve( - view(), ctx_.tx, sleAccountRoot, preFeeBalance_, *sponsorSle, ticketCount, 0, j_); + if (auto const ret = + checkXrpBalance(view(), ctx_.tx, sleAccountRoot, sponsorSle, ticketCount, j_); !isTesSuccess(ret)) return ret; @@ -119,7 +117,7 @@ TicketCreate::doApply() return tecDIR_FULL; // LCOV_EXCL_LINE sleTicket->setFieldU64(sfOwnerNode, *page); - addSponsorToLedgerEntry(sleTicket, *sponsorSle); + addSponsorToLedgerEntry(sleTicket, sponsorSle); } // Update the record of the number of Tickets this account owns. @@ -128,7 +126,7 @@ TicketCreate::doApply() sleAccountRoot->setFieldU32(sfTicketCount, oldTicketCount + ticketCount); // Every added Ticket counts against the creator's reserve. - adjustOwnerCount(view(), sleAccountRoot, *sponsorSle, ticketCount, viewJ); + adjustOwnerCount(view(), sleAccountRoot, sponsorSle, ticketCount, viewJ); // TicketCreate is the only transaction that can cause an account root's // Sequence field to increase by more than one. October 2018. diff --git a/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp b/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp index 6f041980f13..0587665d351 100644 --- a/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp +++ b/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp @@ -24,7 +24,6 @@ #include #include #include -#include namespace xrpl { @@ -113,21 +112,14 @@ MPTokenIssuanceCreate::create( if (!acct) return std::unexpected(tecINTERNAL); // LCOV_EXCL_LINE - SLE::pointer sponsorSle; - if (!isPseudoAccount(acct)) - { - auto sle = getTxReserveSponsor(view, tx); - if (!sle) - return std::unexpected(sle.error()); - sponsorSle = std::move(*sle); - } + SLE::pointer const sponsorSle = + !isPseudoAccount(acct) ? getTxReserveSponsor(view, tx, args.account) : SLE::pointer(); if (args.priorBalance) { - if (auto const ret = checkInsufficientReserve( - view, tx, acct, *(args.priorBalance), sponsorSle, 1, 0, journal); + if (auto const ret = checkXrpBalance(view, tx, acct, sponsorSle, 1, journal); !isTesSuccess(ret)) - return std::unexpected(ret); // tecINSUFFICIENT_RESERVE + return std::unexpected(tecINSUFFICIENT_RESERVE); } auto const mptId = makeMptID(args.sequence, args.account); @@ -187,7 +179,6 @@ MPTokenIssuanceCreate::create( view.insert(mptIssuance); } - // Update owner count. adjustOwnerCount(view, acct, sponsorSle, 1, journal); return mptId; diff --git a/src/libxrpl/tx/transactors/token/TrustSet.cpp b/src/libxrpl/tx/transactors/token/TrustSet.cpp index 5f00bfb720c..938e4f948ce 100644 --- a/src/libxrpl/tx/transactors/token/TrustSet.cpp +++ b/src/libxrpl/tx/transactors/token/TrustSet.cpp @@ -326,8 +326,6 @@ TrustSet::preclaim(PreclaimContext const& ctx) TER TrustSet::doApply() { - TER terResult = tesSUCCESS; - STAmount const saLimitAmount(ctx_.tx.getFieldAmount(sfLimitAmount)); bool const bQualityIn(ctx_.tx.isFieldPresent(sfQualityIn)); bool const bQualityOut(ctx_.tx.isFieldPresent(sfQualityOut)); @@ -360,17 +358,6 @@ TrustSet::doApply() // well. A person with no intention of using the gateway // could use the extra XRP for their own purposes. - auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - - std::uint32_t const uOwnerCount = ownerCount(view(), *sponsorSle ? *sponsorSle : sle, j_); - - bool const isSponsoredAndPreFunded = *sponsorSle && !isSponsorReserveCoSigning(ctx_.tx); - // If PreFunded Sponsor, it must be checked whether sufficient - // ReserveCount exists. - bool const freeTrustLine = uOwnerCount < 2 && !*sponsorSle; - std::uint32_t const uQualityIn(bQualityIn ? ctx_.tx.getFieldU32(sfQualityIn) : 0); std::uint32_t uQualityOut(bQualityOut ? ctx_.tx.getFieldU32(sfQualityOut) : 0); @@ -553,74 +540,57 @@ TrustSet::doApply() bool const bLowReserved = sleRippleState->isFlag(lsfLowReserve); bool const bHighReserved = sleRippleState->isFlag(lsfHighReserve); - bool bReserveIncrease = false; - - auto const currentHighSponsor = - getLedgerEntryReserveSponsor(view(), sleRippleState, sfHighSponsor); - auto const currentLowSponsor = - getLedgerEntryReserveSponsor(view(), sleRippleState, sfLowSponsor); - if (bSetAuth) - { uFlagsOut |= (bHigh ? lsfHighAuth : lsfLowAuth); - } if (bLowReserveSet && !bLowReserved) { - // should be checked PreFunded Sponsor before adjustOwnerCount() - // For PreFunded sponsors, we need to check if there are sufficient reserves before - // calling adjustOwnerCount(). - if (auto const ret = checkInsufficientReserve( - view(), ctx_.tx, sleLowAccount, preFeeBalance_, *sponsorSle, 1, 0, j_); - isSponsoredAndPreFunded && !isTesSuccess(ret)) + auto const txLowSponsor = getTxReserveSponsor(view(), ctx_.tx, uLowAccountID); + if (auto const ter = + checkXrpBalance(view(), ctx_.tx, sleLowAccount, txLowSponsor, 1, true, viewJ); + !isTesSuccess(ter)) return tecINSUF_RESERVE_LINE; - // Set reserve for low account. - adjustOwnerCount(view(), sleLowAccount, *sponsorSle, 1, viewJ); - uFlagsOut |= lsfLowReserve; - - addSponsorToLedgerEntry(sleRippleState, *sponsorSle, sfLowSponsor); + adjustOwnerCount(view(), sleLowAccount, txLowSponsor, 1, viewJ); + addSponsorToLedgerEntry(sleRippleState, txLowSponsor, sfLowSponsor); - if (!bHigh) - bReserveIncrease = true; + uFlagsOut |= lsfLowReserve; } if (bLowReserveClear && bLowReserved) { + auto const leLowSponsor = + getLedgerEntryReserveSponsor(view(), sleRippleState, sfLowSponsor); // Clear reserve for low account. - adjustOwnerCount(view(), sleLowAccount, currentLowSponsor, -1, viewJ); - uFlagsOut &= ~lsfLowReserve; - + adjustOwnerCount(view(), sleLowAccount, leLowSponsor, -1, viewJ); removeSponsorFromLedgerEntry(sleRippleState, sfLowSponsor); + + uFlagsOut &= ~lsfLowReserve; } if (bHighReserveSet && !bHighReserved) { - // should be checked PreFunded Sponsor before adjustOwnerCount() - // For PreFunded sponsors, we need to check if there are sufficient reserves before - // calling adjustOwnerCount(). - if (auto const ret = checkInsufficientReserve( - view(), ctx_.tx, sleHighAccount, preFeeBalance_, *sponsorSle, 1, 0, j_); - isSponsoredAndPreFunded && !isTesSuccess(ret)) + auto const txHighSponsor = getTxReserveSponsor(view(), ctx_.tx, uHighAccountID); + if (auto const ter = + checkXrpBalance(view(), ctx_.tx, sleHighAccount, txHighSponsor, 1, true, viewJ); + !isTesSuccess(ter)) return tecINSUF_RESERVE_LINE; - // Set reserve for high account. - adjustOwnerCount(view(), sleHighAccount, *sponsorSle, 1, viewJ); - uFlagsOut |= lsfHighReserve; - - addSponsorToLedgerEntry(sleRippleState, *sponsorSle, sfHighSponsor); + adjustOwnerCount(view(), sleHighAccount, txHighSponsor, 1, viewJ); + addSponsorToLedgerEntry(sleRippleState, txHighSponsor, sfHighSponsor); - if (bHigh) - bReserveIncrease = true; + uFlagsOut |= lsfHighReserve; } if (bHighReserveClear && bHighReserved) { + auto const leHighSponsor = + getLedgerEntryReserveSponsor(view(), sleRippleState, sfHighSponsor); // Clear reserve for high account. - adjustOwnerCount(view(), sleHighAccount, currentHighSponsor, -1, viewJ); - uFlagsOut &= ~lsfHighReserve; - + adjustOwnerCount(view(), sleHighAccount, leHighSponsor, -1, viewJ); removeSponsorFromLedgerEntry(sleRippleState, sfHighSponsor); + + uFlagsOut &= ~lsfHighReserve; } if (uFlagsIn != uFlagsOut) @@ -629,32 +599,16 @@ TrustSet::doApply() if (bDefault || badCurrency() == currency) { // Delete. - - terResult = trustDelete(view(), sleRippleState, uLowAccountID, uHighAccountID, viewJ); - } - // Reserve is not scaled by load. - else if ( - auto const ret = checkInsufficientReserve( - view(), ctx_.tx, sle, preFeeBalance_, *sponsorSle, 0, 0, j_); - !freeTrustLine && bReserveIncrease && !isTesSuccess(ret)) - { - JLOG(j_.trace()) << "Delay transaction: Insufficent reserve to " - "add trust line."; - - // Another transaction could provide XRP to the account and then - // this transaction would succeed. - terResult = tecINSUF_RESERVE_LINE; + return trustDelete(view(), sleRippleState, uLowAccountID, uHighAccountID, viewJ); } - else - { - view().update(sleRippleState); - JLOG(j_.trace()) << "Modify ripple line"; - } + view().update(sleRippleState); + JLOG(j_.trace()) << "Modify ripple line"; + return tesSUCCESS; } + // Line does not exist. - else if ( - !saLimitAmount && // Setting default limit. + if (!saLimitAmount && // Setting default limit. (!bQualityIn || (uQualityIn == 0u)) && // Not setting quality in or // setting default quality in. (!bQualityOut || (uQualityOut == 0u)) && // Not setting quality out or @@ -664,46 +618,43 @@ TrustSet::doApply() JLOG(j_.trace()) << "Redundant: Setting non-existent ripple line to defaults."; return tecNO_LINE_REDUNDANT; } - else if ( - auto const ret = checkInsufficientReserve( - ctx_.view(), ctx_.tx, sle, preFeeBalance_, *sponsorSle, 1, 0, j_); - !freeTrustLine && !isTesSuccess(ret)) // Reserve is not scaled by load. + + auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); + if (auto const ter = checkXrpBalance(view(), ctx_.tx, sle, sponsorSle, 1, true, viewJ); + !isTesSuccess(ter)) { JLOG(j_.trace()) << "Delay transaction: Line does not exist. " "Insufficent reserve to create line."; // Another transaction could create the account and then this // transaction would succeed. - terResult = tecNO_LINE_INSUF_RESERVE; + return tecNO_LINE_INSUF_RESERVE; } - else - { - // Zero balance in currency. - STAmount const saBalance(Issue{currency, noAccount()}); - auto const k = keylet::line(accountID_, uDstAccountID, currency); - - JLOG(j_.trace()) << "doTrustSet: Creating ripple line: " << to_string(k.key); - - // Create a new ripple line. - terResult = trustCreate( - view(), - bHigh, - accountID_, - uDstAccountID, - k.key, - sle, - bSetAuth, - bSetNoRipple && !bClearNoRipple, - bSetFreeze && !bClearFreeze, - bSetDeepFreeze, - saBalance, - saLimitAllow, // Limit for who is being charged. - uQualityIn, - uQualityOut, - *sponsorSle, - viewJ); - } + // Zero balance in currency. + STAmount const saBalance(Issue{currency, noAccount()}); + auto const k = keylet::line(accountID_, uDstAccountID, currency); + + JLOG(j_.trace()) << "doTrustSet: Creating ripple line: " << to_string(k.key); + + // Create a new ripple line. + TER terResult = trustCreate( + view(), + bHigh, + accountID_, + uDstAccountID, + k.key, + sle, + bSetAuth, + bSetNoRipple && !bClearNoRipple, + bSetFreeze && !bClearFreeze, + bSetDeepFreeze, + saBalance, + saLimitAllow, // Limit for who is being charged. + uQualityIn, + uQualityOut, + sponsorSle, + viewJ); return terResult; } diff --git a/src/libxrpl/tx/transactors/vault/VaultCreate.cpp b/src/libxrpl/tx/transactors/vault/VaultCreate.cpp index 7e4a8a8156a..7f157ffff2e 100644 --- a/src/libxrpl/tx/transactors/vault/VaultCreate.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultCreate.cpp @@ -156,29 +156,13 @@ VaultCreate::doApply() if (auto ter = dirLink(view(), accountID_, vault)) return ter; + // We will create Vault and PseudoAccount, hence increase OwnerCount by 2 auto const sponsorSle = getTxReserveSponsor(view(), tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - if (!ctx_.view().rules().enabled(featureSponsor)) - { - adjustOwnerCount(view(), owner, *sponsorSle, 2, j_); - addSponsorToLedgerEntry(vault, *sponsorSle); - if (auto const ret = - checkInsufficientReserve(view(), tx, owner, preFeeBalance_, *sponsorSle, 0, 0, j_); - !isTesSuccess(ret)) - return ret; - } - else - { - // after Sponsor Amendment, check insufficient reserve first - if (auto const ret = - checkInsufficientReserve(view(), tx, owner, preFeeBalance_, *sponsorSle, 2, 0, j_); - !isTesSuccess(ret)) - return ret; - adjustOwnerCount(view(), owner, *sponsorSle, 2, j_); - addSponsorToLedgerEntry(vault, *sponsorSle); - } + if (auto const ret = checkXrpBalance(view(), tx, owner, sponsorSle, 2, j_); !isTesSuccess(ret)) + return tecINSUFFICIENT_RESERVE; + adjustOwnerCount(view(), owner, sponsorSle, 2, j_); + addSponsorToLedgerEntry(vault, sponsorSle); auto maybePseudo = createPseudoAccount(view(), vault->key(), sfVaultID); if (!maybePseudo) diff --git a/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp b/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp index 9707a923fc0..9038b78ce2b 100644 --- a/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp @@ -344,18 +344,9 @@ VaultDeposit::doApply() } auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - // Transfer shares from vault to depositor. if (auto const ter = accountSend( - view(), - vaultAccount, - accountID_, - sharesCreated, - j_, - *sponsorSle, - WaiveTransferFee::Yes); + view(), vaultAccount, accountID_, sharesCreated, j_, sponsorSle, WaiveTransferFee::Yes); !isTesSuccess(ter)) return ter; diff --git a/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp b/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp index 59f0ce0a2ce..b3f15782dd8 100644 --- a/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp @@ -327,9 +327,6 @@ VaultWithdraw::doApply() auto const& vaultAccount = vault->at(sfAccount); auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - // Transfer shares from depositor to vault. if (auto const ter = accountSend( view(), @@ -337,7 +334,7 @@ VaultWithdraw::doApply() vaultAccount, sharesRedeemed, j_, - *sponsorSle, + sponsorSle, WaiveTransferFee::Yes); !isTesSuccess(ter)) return ter; diff --git a/src/test/app/SponsorSherlock_test.cpp b/src/test/app/SponsorSherlock_test.cpp new file mode 100644 index 00000000000..9f48aa1700c --- /dev/null +++ b/src/test/app/SponsorSherlock_test.cpp @@ -0,0 +1,1284 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl::test { + +// static STAmount +// accountReserve(jtx::Env& env, std::uint32_t count = 1) +// { +// return env.current()->fees().reserve * count; +// } + +static XRPAmount +reserve(jtx::Env& env, std::uint32_t ownerCount) +{ + return baseAccountReserve(*env.current(), ownerCount); +} + +static void +adjustAccountXRPBalance(jtx::Env& env, jtx::Account const& account, STAmount const& balanceTo) +{ + using namespace test::jtx; + XRPL_ASSERT(isXRP(balanceTo), "adjustAccountXRPBalance: balanceTo must be XRP"); + auto const currentBalance = env.balance(account); + if (currentBalance == balanceTo) + return; + + auto const baseFee = env.current()->fees().base; + if (currentBalance > balanceTo) + { + env(pay(account, env.master, currentBalance - (balanceTo)), + Fee(XRP(1)), + sponsor::As(env.master, spfSponsorFee), + Sig(sfSponsorSignature, env.master)); + } + else + { + env(pay(env.master, account, balanceTo - currentBalance), Fee(baseFee)); + } + + env.close(); +} + +static void +fillQueue(jtx::Env& env, jtx::Account const& account) +{ + using namespace jtx; + auto metrics = env.app().getTxQ().getMetrics(*env.current()); + for (std::uint32_t i = metrics.txInLedger; i <= metrics.txPerLedger; ++i) + env(noop(account)); +} + +class SponsorSherlock_test : public beast::unit_test::Suite, public test::jtx::XChainBridgeObjects +{ +protected: + void + test168CoSignedBlockedWithFeeOnlySponsorship() + { + // https://github.com/sherlock-audit/2026-04-xrp-ledger-april-2026-judging/issues/168 + testcase("Co-signed SponsorshipTransfer blocked with Fee-only Sponsorship"); + using namespace test::jtx; + Env env(*this); + + auto const sponsor = Account("sponsor"); + auto const sponsee = Account("sponsee"); + + env.fund(XRP(10000), sponsor); + env.fund(XRP(1000), sponsee); + env.close(); + + // Create Fee-only sponsorship (no ReserveCount) + env(sponsor::set_fee(sponsor, 0, XRP(100)), sponsor::SponseeAcc(sponsee)); + env.close(); + + // Sponsee creates a DID + env(did::setValid(sponsee)); + env.close(); + + auto const didKey = keylet::did(sponsee.id()); + + // Sponsor tries to co-sign SponsorshipTransfer to sponsor the DID + // This SHOULD work (sponsor has 10000 XRP) but FAILS with tecINSUFFICIENT_RESERVE + + // TEAM DECISION: considered as legit behavior, no fallbacks, sponsor should look after + // their Sponsorship objects + env(sponsor::transfer(sponsee, tfSponsorshipCreate, didKey.key), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tecINSUFFICIENT_RESERVE)); + } + + void + test251AMMDepositRejectXRPDeposits() + { + // https://github.com/sherlock-audit/2026-04-xrp-ledger-april-2026-judging/issues/251 + // AMMDeposit::preclaim misroutes !sle as accountCountDelta and hardcodes ownerCountDelta=1, + // over-charging reserve check by 1 increment (2 XRP) when LP trustline exists (!sle=0). + testcase("AMMDeposit rejects XRP deposits with insufficient reserve"); + using namespace test::jtx; + + Env env(*this); + + auto const issuer = Account("issuer"); + auto const alice = Account("alice"); + + // Fund accounts with initial amounts + env.fund(XRP(1000), issuer); + env.fund(XRP(1000), alice); + env.close(); + + // Step 1: Issuer sets DefaultRipple flag + env(fset(issuer, asfDefaultRipple)); + env.close(); + + // Step 2: Alice creates trustline for TKN + auto const tkn = issuer["TKN"]; + env(trust(alice, tkn(1'000'000))); + env.close(); + + // Step 3: Issuer sends 100 TKN to Alice + env(pay(issuer, alice, tkn(100))); + env.close(); + + // Verify Alice has ownerCount=1 (TKN trustline only) + BEAST_EXPECT(env.ownerCount(alice) == 1); + + // Step 4: Alice creates AMM (100 TKN / 5 XRP) + AMM amm(env, alice, tkn(100), XRP(5), CreateArg{.tfee = 500}); + + // Verify Alice now has ownerCount=2 (TKN trustline + LP trustline) + BEAST_EXPECT(env.ownerCount(alice) == 2); + + // Step 5: Drain Alice's balance down to approximately 15 XRP total + // Target: Leave Alice with just above 15 XRP + // With ownerCount=2, reserve requirement is base(10) + 2*inc(2) = 14 XRP + // So 15 XRP + fees should be sufficient (15 >= 14) + auto currentBalance = env.balance(alice); + auto const targetBalance = drops(15'001'000); // 15.001 XRP + auto const baseFee = env.current()->fees().base; + auto const reserve2 = reserve(env, 2); // Reserve for ownerCount=2 (should be 14 XRP) + + // Calculate how much to drain + // We want final balance = targetBalance + // Payment equation: currentBalance - drainAmount - Fee = targetBalance + // So: drainAmount = currentBalance - targetBalance - Fee + if (currentBalance > targetBalance + baseFee) + { + auto const drainAmount = currentBalance - targetBalance - baseFee; + + // Sanity check: ensure Alice can actually send this + // (must keep at least reserve2) + if (currentBalance - drainAmount - baseFee >= reserve2) + { + env(pay(alice, issuer, drainAmount), Fee(baseFee)); + env.close(); + } + } + + auto const aliceBalance = env.balance(alice); + BEAST_EXPECT(env.ownerCount(alice) == 2); + // Verify Alice has a reasonable balance above reserve but not too much + // Reserve for ownerCount=2 is 14 XRP, so Alice should have >= 14 XRP + BEAST_EXPECT(aliceBalance >= reserve2); + + // Step 6: Alice attempts AMMDeposit with 1 drop XRP + // With ownerCount=2 and LP trustline exists (!sle=0 since she has LP trustline): + // Correct behavior: reserve_for(N=2, !sle=0) = base + 2*inc = 10 + 2*2 = 14 XRP + // Post-deposit balance ~= 15 XRP - 1 drop ≈ 15 XRP. 15 >= 14 -> should PASS. + // + // Bug (issue #251): reserve_for(N+1=3, accountCountDelta=0) = base + 3*inc = 10 + 6 = 16 + // XRP. + // 15 < 16 -> would fail with tecINSUF_RESERVE_LINE. + // + // This test verifies the bug is FIXED - the deposit should succeed. + amm.deposit(alice, {}, drops(1), {}, {}, tfSingleAsset, {}, {}); + } + + void + test750SponsorFeeQueueAdmissionBug() + { + // https://github.com/sherlock-audit/2026-04-xrp-ledger-april-2026-judging/issues/750 + + testcase("SponsorFeeQueueAdmissionBug"); + using namespace jtx; + + auto cfg = makeConfig({{"minimum_txn_in_ledger_standalone", "3"}}); + cfg->fees.referenceFee = 10; + cfg->fees.accountReserve = 200; + cfg->fees.ownerReserve = 50; + Env env{*this, std::move(cfg)}; + + Account const sponsorAcc{"sponsor"}; + Account const alice{"alice"}; + + env.fund(XRP(10'000), noripple(sponsorAcc)); + env.fund(XRP(1), noripple(alice)); + env.close(); + + env(sponsor::set(sponsorAcc, 0, std::nullopt, XRP(1000)), + sponsor::SponseeAcc(alice), + Fee(XRP(1))); + env.close(); + + auto const sponsorshipKey = keylet::sponsor(sponsorAcc.id(), alice.id()); + auto const sponsorshipSle = env.current()->read(sponsorshipKey); + if (!BEAST_EXPECT(sponsorshipSle)) + return; + auto const feeAmountBefore = (*sponsorshipSle)[sfFeeAmount].xrp(); + BEAST_EXPECT(feeAmountBefore == XRP(1000).value().xrp()); + + auto const aliceBalBefore = env.balance(alice).value().xrp(); + + Account const burner{"burner"}; + env.fund(XRP(1'000'000), burner); + env.close(); + fillQueue(env, burner); + fillQueue(env, burner); + + auto const queueFeePerTx = XRPAmount{150}; + auto const aliceSeq = env.seq(alice); + + env(pay(alice, sponsorAcc, drops(1)), + Fee(queueFeePerTx), + sponsor::As(sponsorAcc, spfSponsorFee), + Seq(aliceSeq), + Ter(terQUEUED)); + + env(pay(alice, sponsorAcc, drops(1)), + Fee(queueFeePerTx), + sponsor::As(sponsorAcc, spfSponsorFee), + Seq(aliceSeq + 1), + Ter(terQUEUED)); + + env(pay(alice, sponsorAcc, drops(1)), + Fee(queueFeePerTx), + sponsor::As(sponsorAcc, spfSponsorFee), + Seq(aliceSeq + 2), + Ter(terQUEUED)); // fixed, originally telCAN_NOT_QUEUE_BALANCE + + auto const aliceBalAfter = env.balance(alice).value().xrp(); + BEAST_EXPECT(aliceBalAfter == aliceBalBefore); + + auto const sponsorshipSleAfter = env.current()->read(sponsorshipKey); + if (!BEAST_EXPECT(sponsorshipSleAfter)) + return; + auto const feeAmountAfter = (*sponsorshipSleAfter)[sfFeeAmount].xrp(); + BEAST_EXPECT(feeAmountAfter == feeAmountBefore); + } + + void + test750AdversarialSponsorBlocksVictim() + { + // https://github.com/sherlock-audit/2026-04-xrp-ledger-april-2026-judging/issues/750 + + testcase("AdversarialSponsorBlocksVictim"); + using namespace jtx; + + auto cfg = makeConfig({{"minimum_txn_in_ledger_standalone", "3"}}); + cfg->fees.referenceFee = 10; + cfg->fees.accountReserve = 200; + cfg->fees.ownerReserve = 50; + Env env{*this, std::move(cfg)}; + + Account const legitSponsor{"legitSponsor"}; + Account const attacker{"attacker"}; + Account const victim{"victim"}; + + env.fund(XRP(10'000), noripple(legitSponsor)); + env.fund(XRP(10'000), noripple(attacker)); + env.fund(XRP(1), noripple(victim)); + env.close(); + + // Legitimate sponsor opens a pre-funded sponsorship for victim. + env(sponsor::set(legitSponsor, 0, std::nullopt, XRP(1000)), + sponsor::SponseeAcc(victim), + Fee(XRP(1))); + env.close(); + + // Attacker ALSO opens a pre-funded sponsorship for victim — no + // consent from victim is required; the ltSPONSORSHIP is keyed + // by (sponsor, sponsee) pairs. + env(sponsor::set(attacker, 0, std::nullopt, XRP(1000)), + sponsor::SponseeAcc(victim), + Fee(XRP(1))); + env.close(); + + auto const attackerSponsorshipKey = keylet::sponsor(attacker.id(), victim.id()); + auto const legitSponsorshipKey = keylet::sponsor(legitSponsor.id(), victim.id()); + BEAST_EXPECT(env.current()->read(attackerSponsorshipKey)); + BEAST_EXPECT(env.current()->read(legitSponsorshipKey)); + + auto const victimBalBefore = env.balance(victim).value().xrp(); + + Account const burner{"burner"}; + env.fund(XRP(1'000'000), burner); + env.close(); + fillQueue(env, burner); + fillQueue(env, burner); + + auto const queueFeePerTx = XRPAmount{150}; + auto const victimSeq = env.seq(victim); + + // Adversarial txs: victim-sender, attacker-sponsor. These fill + // the queue under victim's account-track. + env(pay(victim, attacker, drops(1)), + Fee(queueFeePerTx), + sponsor::As(attacker, spfSponsorFee), + Seq(victimSeq), + Ter(terQUEUED)); + + env(pay(victim, attacker, drops(1)), + Fee(queueFeePerTx), + sponsor::As(attacker, spfSponsorFee), + Seq(victimSeq + 1), + Ter(terQUEUED)); + + // Victim's own legitimate sponsored tx is rejected — prior + // totalFee = 150 + 150 = 300 ≥ reserve 200 — even though + // legitSponsor has 1000 XRP pre-funded. + env(pay(victim, legitSponsor, drops(1)), + Fee(queueFeePerTx), + sponsor::As(legitSponsor, spfSponsorFee), + Seq(victimSeq + 2), + Ter(terQUEUED)); // fixed, originally telCAN_NOT_QUEUE_BALANCE + + auto const victimBalAfter = env.balance(victim).value().xrp(); + BEAST_EXPECT(victimBalAfter == victimBalBefore); + } + + void + test1033SponsoredWitnessCanChargeDoorOwnedClaimObjectsToUnrelatedSponsor() + { + // https://github.com/sherlock-audit/2026-04-xrp-ledger-april-2026-judging/issues/1033 + + testcase("xchain account-create attestation redirects witness sponsorship to the door"); + using namespace test::jtx; + + Env mcEnv{*this, envconfig(), features}; + Env scEnv{*this, envconfig(), features}; + + Account const sponsor{"sponsor"}; + Account const submitter{"submitter"}; + auto const& witnessSigner = signers[0]; + + STAmount const funds{XRP(10000)}; + mcEnv.fund(funds, mcDoor, mcAlice, mcBob, mcCarol, mcGw); + scEnv.fund(funds, scDoor, scAlice, scBob, scCarol, scGw, sponsor, submitter); + for (auto const& s : signers) + { + mcEnv.fund(funds, s.account); + scEnv.fund(funds, s.account); + } + for (auto const& payee : payees) + scEnv.fund(funds, payee); + mcEnv.close(); + scEnv.close(); + + auto const& door = scEnv.master; + + mcEnv(jtx::signers(mcDoor, quorum, signers)); + mcEnv(bridgeCreate(mcDoor, jvb, reward, XRP(20))); + mcEnv.close(); + + scEnv(jtx::signers(door, quorum, signers)); + scEnv(bridgeCreate(door, jvb, reward, XRP(20))); + scEnv.close(); + + scEnv(sponsor::set_reserve(sponsor, 0, 1), sponsor::SponseeAcc(submitter)); + scEnv.close(); + + std::uint32_t constexpr redirectedClaims = 12; + auto const baseReserve = reserve(scEnv, 1); + auto const backedForRedirectedClaims = reserve(scEnv, 1 + redirectedClaims); + + adjustAccountXRPBalance(scEnv, sponsor, drops(backedForRedirectedClaims)); + + auto sponsorObj = scEnv.le(keylet::sponsor(sponsor, submitter)); + BEAST_EXPECT(sponsorObj); + BEAST_EXPECT(sponsorObj->getFieldU32(sfReserveCount) == 1); + + auto const noJournal = beast::Journal{beast::Journal::getNullSink()}; + auto const liquidBefore = xrpLiquid(*scEnv.current(), sponsor.id(), 0, noJournal); + BEAST_EXPECT(liquidBefore == backedForRedirectedClaims - baseReserve); + + auto const submitterOwnerCountBefore = scEnv.ownerCount(submitter); + auto const submitterSponsoredOwnerCountBefore = scEnv.sponsoredOwnerCount(submitter); + auto const doorOwnerCountBefore = scEnv.ownerCount(door); + auto const doorSponsoredOwnerCountBefore = scEnv.sponsoredOwnerCount(door); + auto const sponsorSponsoringOwnerCountBefore = scEnv.sponsoringOwnerCount(sponsor); + + scEnv( + createAccountAttestation( + submitter, + jvb, + mcAlice, + XRP(20), + reward, + payees[0], + true, + 1, + scuAlice, + witnessSigner), + sponsor::As(sponsor, spfSponsorReserve), + Ter(tesSUCCESS)); + scEnv.close(); + + auto const claim1 = scEnv.le(keylet::xChainCreateAccountClaimID(STXChainBridge(jvb), 1)); + sponsorObj = scEnv.le(keylet::sponsor(sponsor, submitter)); + BEAST_EXPECT(claim1); + BEAST_EXPECT(sponsorObj); + BEAST_EXPECT((*claim1)[sfAccount] == door.id()); + BEAST_EXPECT(!claim1->isFieldPresent(sfSponsor)); + BEAST_EXPECT(sponsorObj->getFieldU32(sfReserveCount) == 1); + BEAST_EXPECT(scEnv.ownerCount(submitter) == submitterOwnerCountBefore); + BEAST_EXPECT(scEnv.sponsoredOwnerCount(submitter) == submitterSponsoredOwnerCountBefore); + BEAST_EXPECT(scEnv.ownerCount(door) == doorOwnerCountBefore + 1); + BEAST_EXPECT(scEnv.sponsoredOwnerCount(door) == doorSponsoredOwnerCountBefore); + BEAST_EXPECT(scEnv.sponsoringOwnerCount(sponsor) == sponsorSponsoringOwnerCountBefore); + + auto const liquidAfterFirst = xrpLiquid(*scEnv.current(), sponsor.id(), 0, noJournal); + BEAST_EXPECT(liquidAfterFirst == backedForRedirectedClaims - reserve(scEnv, 1)); + + for (std::uint32_t i = 2; i <= redirectedClaims; ++i) + { + scEnv( + createAccountAttestation( + submitter, + jvb, + mcBob, + XRP(20), + reward, + payees[(i - 1) % payees.size()], + true, + i, + scuBob, + witnessSigner), + sponsor::As(sponsor, spfSponsorReserve), + Ter(tesSUCCESS)); + scEnv.close(); + + auto const claim = scEnv.le(keylet::xChainCreateAccountClaimID(STXChainBridge(jvb), i)); + sponsorObj = scEnv.le(keylet::sponsor(sponsor, submitter)); + BEAST_EXPECT(claim); + BEAST_EXPECT(sponsorObj); + BEAST_EXPECT((*claim)[sfAccount] == door.id()); + BEAST_EXPECT(!claim->isFieldPresent(sfSponsor)); + BEAST_EXPECT(sponsorObj->getFieldU32(sfReserveCount) == 1); + } + + BEAST_EXPECT(scEnv.ownerCount(door) == doorOwnerCountBefore + redirectedClaims); + BEAST_EXPECT(scEnv.sponsoredOwnerCount(door) == doorSponsoredOwnerCountBefore); + BEAST_EXPECT(scEnv.sponsoringOwnerCount(sponsor) == sponsorSponsoringOwnerCountBefore); + + auto const liquidAfterAll = xrpLiquid(*scEnv.current(), sponsor.id(), 0, noJournal); + BEAST_EXPECT(liquidAfterAll > reserve(scEnv, 1)); + + scEnv(pay(sponsor, scBob, drops(1)), Fee(scEnv.current()->fees().base)); + scEnv.close(); + } + + void + test1186AMMCreateUsesPreFeeReserveBalance() + { + // https://github.com/sherlock-audit/2026-04-xrp-ledger-april-2026-judging/issues/1186 + + testcase("AMMCreate creates undercollateralized LP-token trustline"); + + using namespace jtx; + + auto const run = [&](bool exploitPath) { + Env env{*this, testableAmendments()}; + auto const fee = env.current()->fees().base; + auto const ownerIncrement = env.current()->fees().increment; + + Account const gw{"gw"}; + Account const issuer{"issuer"}; + Account const alice{"alice"}; + auto const usd = issuer["USD"]; + + env.fund(XRP(100'000), gw, issuer, alice); + env(fset(issuer, asfDefaultRipple)); + env.close(); + + env.trust(usd(50'000), alice); + env(pay(issuer, alice, usd(30'000))); + env.close(); + + MPTTester const btc( + {.env = env, + .issuer = gw, + .holders = {alice}, + .pay = 30'000, + .flags = kMptDexFlags}); + + BEAST_EXPECT(ownerCount(env, alice) == 2); + BEAST_EXPECT(env.le(keylet::line(alice, usd)) != nullptr); + BEAST_EXPECT(env.le(keylet::mptoken(btc.issuanceID(), alice)) != nullptr); + + STAmount const preTrimBalance = env.balance(alice, XRP); + STAmount const targetBalance = + exploitPath ? reserve(env, 3) : reserve(env, 3) - drops(1); + STAmount const trimAmount = preTrimBalance - targetBalance - fee; + + BEAST_EXPECT(trimAmount > XRP(0)); + env(pay(alice, issuer, trimAmount)); + env.close(); + + BEAST_EXPECT(env.balance(alice, XRP) == targetBalance); + BEAST_EXPECT(ownerCount(env, alice) == 2); + + if (!exploitPath) + { + AMM const noCreate( + env, alice, usd(10'000), btc(10'000), Ter(tecINSUF_RESERVE_LINE)); + BEAST_EXPECT(!noCreate.ammExists()); + BEAST_EXPECT(ownerCount(env, alice) == 2); + BEAST_EXPECT(env.balance(alice, XRP) == targetBalance - ownerIncrement); + return; + } + + AMM const amm(env, alice, usd(10'000), btc(10'000)); + + auto const lpLine = env.le(keylet::line(alice, amm.lptIssue())); + // log << "ammcreate_balance=" << env.balance(alice, XRP) + // << " owners=" << ownerCount(env, alice) << " lpLineExists=" << (lpLine != + // nullptr) + // << std::endl; + + BEAST_EXPECT(amm.ammExists()); + BEAST_EXPECT(lpLine != nullptr); + BEAST_EXPECT(ownerCount(env, alice) == 3); + BEAST_EXPECT(env.balance(alice, XRP) == targetBalance - ownerIncrement); + BEAST_EXPECT(env.balance(alice, XRP) < reserve(env, 3)); + }; + + run(false); + run(true); + } + + void + test1186AMMDepositUsesPreFeeReserveBalance() + { + // https://github.com/sherlock-audit/2026-04-xrp-ledger-april-2026-judging/issues/1186 + // TEAM DECISION: considered as not a bug, see TicketCreate::doApply(): + // "we want to allow dipping into the reserve to pay fees" + // As designed + + testcase("AMMDeposit creates undercollateralized LP-token trustline"); + + using namespace jtx; + + auto const run = [&](bool exploitPath) { + Env env{*this, testableAmendments()}; + auto const fee = env.current()->fees().base; + + Account const gw{"gw"}; + Account const issuer{"issuer"}; + Account const alice{"alice"}; + Account const bob{"bob"}; + auto const usd = issuer["USD"]; + + env.fund(XRP(100'000), gw, issuer, alice, bob); + env(fset(issuer, asfDefaultRipple)); + env.close(); + + env.trust(usd(50'000), alice); + env.trust(usd(50'000), bob); + env(pay(issuer, alice, usd(30'000))); + env(pay(issuer, bob, usd(30'000))); + env.close(); + + MPTTester const btc( + {.env = env, + .issuer = gw, + .holders = {alice, bob}, + .pay = 30'000, + .flags = kMptDexFlags}); + + AMM amm(env, alice, usd(10'000), btc(10'000)); + BEAST_EXPECT(amm.ammExists()); + + BEAST_EXPECT(ownerCount(env, bob) == 2); + BEAST_EXPECT(env.le(keylet::line(bob, amm.lptIssue())) == nullptr); + + STAmount const preTrimBalance = env.balance(bob, XRP); + STAmount const targetBalance = + exploitPath ? reserve(env, 3) : reserve(env, 3) - drops(1); + STAmount const trimAmount = preTrimBalance - targetBalance - fee; + + BEAST_EXPECT(trimAmount > XRP(0)); + env(pay(bob, issuer, trimAmount)); + env.close(); + + BEAST_EXPECT(env.balance(bob, XRP) == targetBalance); + BEAST_EXPECT(ownerCount(env, bob) == 2); + + if (!exploitPath) + { + amm.deposit( + {.account = bob, + .asset1In = usd(1'000), + .asset2In = btc(1'000), + .flags = tfTwoAsset, + .err = Ter(tecINSUF_RESERVE_LINE)}); + + BEAST_EXPECT(env.le(keylet::line(bob, amm.lptIssue())) == nullptr); + BEAST_EXPECT(ownerCount(env, bob) == 2); + BEAST_EXPECT(env.balance(bob, XRP) == targetBalance - fee); + return; + } + + amm.deposit( + {.account = bob, + .asset1In = usd(1'000), + .asset2In = btc(1'000), + .flags = tfTwoAsset}); + + auto const lpLine = env.le(keylet::line(bob, amm.lptIssue())); + // log << "ammdeposit_balance=" << env.balance(bob, XRP) + // << " owners=" << ownerCount(env, bob) << " lpLineExists=" << (lpLine != nullptr) + // << " lpTokens=" << amm.getLPTokensBalance(bob.id()) << std::endl; + + BEAST_EXPECT(lpLine != nullptr); + BEAST_EXPECT(ownerCount(env, bob) == 3); + BEAST_EXPECT(amm.getLPTokensBalance(bob.id()) > beast::kZero); + BEAST_EXPECT(env.balance(bob, XRP) == targetBalance - fee); + BEAST_EXPECT(env.balance(bob, XRP) < reserve(env, 3)); + }; + + run(false); + run(true); + } + + void + test1350ReserveCountSilentWrap(FeatureBitset features) + { + // https://github.com/sherlock-audit/2026-04-xrp-ledger-april-2026-judging/issues/1350 + + testcase( + "adjustReserveCount uint32 wraparound silently erases pool quota at UINT32_MAX " + "boundary"); + + using namespace test::jtx; + + Env env{*this, features}; + Account const sponsor{"sponsor"}; + Account const sponsee{"sponsee"}; + env.fund(XRP(10000), sponsor, sponsee); + env.close(); + + std::uint32_t const uint32Max = std::numeric_limits::max(); + + // STEP 1: Sponsee creates a Check co-signed by sponsor BEFORE any sponsorship + // exists. adjustOwnerCount in CheckCreate peeks keylet::sponsor(S, B) + // — finds nothing — so the pool quota is NOT debited. The resulting + // ltCHECK carries sfSponsor = sponsor. + auto const sponseeSeq = env.seq(sponsee); + uint256 const checkID = keylet::check(sponsee, sponseeSeq).key; + if (!BEAST_EXPECT(checkID.isNonZero())) + return; + + env(check::create(sponsee, sponsor, XRP(1)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + // STEP 2: Sponsor (or a SponsorReserve-narrowed delegate) creates the + // ltSPONSORSHIP pool with sfReserveCount at the UINT32_MAX boundary. + // Because the Check was already created before this pool, no deduction + // has occurred — the pool starts at exactly UINT32_MAX. + env(sponsor::set(sponsor, 0, uint32Max), sponsor::SponseeAcc(sponsee)); + env.close(); + + // VERIFY 1: pool exists with sfReserveCount == UINT32_MAX + { + json::Value const poolEntry = sponsor::ledgerEntry(env, sponsor, sponsee); + auto const& node = poolEntry[jss::result][jss::node]; + BEAST_EXPECT(node.isMember(sfReserveCount.jsonName)) && + BEAST_EXPECT(node[sfReserveCount.jsonName].asUInt() == uint32Max); + } + + // STEP 3: Sponsee ends the sponsorship on the Check. The End's payback + // at SponsorshipTransfer.cpp:520-528 calls adjustReserveCount(+1) with + // sfReserveCount = UINT32_MAX. The uint32 addition wraps to 0, int32_t + // conversion gives 0 (not negative — guard NOT triggered), and the + // code calls makeFieldAbsent — silently erasing the entire quota. + + // Updated flow: sfReserveCount adjustment can only decrease. Freeing object doesn't reset + // sfReserveCount + env(sponsor::transfer(sponsee, tfSponsorshipEnd, checkID)); + env.close(); + + // VERIFY 2: silent wrap — sfReserveCount is now ABSENT + // Present after fix. + + { + json::Value const poolEntry = sponsor::ledgerEntry(env, sponsor, sponsee); + auto const& node = poolEntry[jss::result][jss::node]; + BEAST_EXPECT(node.isMember(sfReserveCount.jsonName)) && + BEAST_EXPECTS( + node[sfReserveCount.jsonName].asUInt() == uint32Max, + std::to_string(node[sfReserveCount.jsonName].asUInt())); + } + + // VERIFY 3: pool is bricked — future drawdowns fail with + // tecINSUFFICIENT_RESERVE (sfReserveCount absent = 0 < ownerCountDelta + // = 1 at checkInsufficientReserve, preclaim). The Check is now + // un-sponsored (sfSponsor removed in STEP 3); attempt to re-sponsor it. + + // ALREADY FIXED + env(sponsor::transfer(sponsee, tfSponsorshipCreate, checkID), + sponsor::As(sponsor, spfSponsorReserve)); + env.close(); + } + + void + test1365OracleReserveDecreaseRejection() + { + // https://github.com/sherlock-audit/2026-04-xrp-ledger-april-2026-judging/issues/1365 + // Oracle reserve decrease (2 -> 1) is incorrectly rejected when spfSponsorReserve is + // present The signed/unsigned comparison bug in checkInsufficientReserve would cause + // sponsored Oracle operators to be unable to execute reserve-releasing OracleSet updates + // FIX: Using std::cmp_less for safe signed/unsigned comparison in checkXrpBalanceGeneral + + testcase("Oracle reserve decrease 2->1 (FIXED)"); + + using namespace test::jtx; + using namespace std::chrono; + + // Test both cosigning and prefunded paths + for (bool cosigning : {true, false}) + { + Env env{*this, testableAmendments()}; + + Account const sponsor{"sponsor"}; + Account const alice{"alice"}; + + env.fund(XRP(10000), sponsor, alice); + env.close(); + + // Helper function to submit sponsored transactions + auto const submitSponsored = [&](json::Value const& jv, TER expected) { + if (cosigning) + { + env(jv, + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(expected)); + } + else + { + env(jv, sponsor::As(sponsor, spfSponsorReserve), Ter(expected)); + } + env.close(); + }; + + // Helper to create OracleSet transaction + auto const oracleSet = [&](Account const& account, uint8_t dataSeriesSize) { + auto const now = env.timeKeeper().now(); + env.close(now + oracle::kTestStartTime - kEpochOffset); + + json::Value jv; + jv[jss::TransactionType] = jss::OracleSet; + jv[jss::Account] = to_string(account); + jv[jss::OracleDocumentID] = 1; + jv[jss::LastUpdateTime] = to_string( + duration_cast(env.current()->header().closeTime.time_since_epoch()) + .count() + + kEpochOffset.count() + 100); + jv[jss::PriceDataSeries] = json::ValueType::Array; + jv[jss::Provider] = strHex(std::string{"provider"}); + jv[jss::AssetClass] = strHex(std::string{"currency"}); + + for (uint8_t i = 0; i < dataSeriesSize; ++i) + { + json::Value row; + row[jss::PriceData][jss::BaseAsset] = "XRP"; + row[jss::PriceData][jss::QuoteAsset] = "US" + std::to_string(i); + row[jss::PriceData][jss::AssetPrice] = to_string(740 + i); + row[jss::PriceData][jss::Scale] = 1; + jv[jss::PriceDataSeries].append(row); + } + + return jv; + }; + + // Ensure a sponsorship object exists for (sponsor -> alice) with reserve count 2 + env(sponsor::set_reserve(sponsor, 0, 2), sponsor::SponseeAcc(alice), Ter(tesSUCCESS)); + env.close(); + + // Step 1: create oracle with 6 pairs (reserve 2) + submitSponsored(oracleSet(alice, 6), tesSUCCESS); + + // Verify oracle was created with 6 pairs + { + auto const sle = env.le(keylet::oracle(alice, 1)); + if (BEAST_EXPECT(sle)) + BEAST_EXPECT(sle->getFieldArray(sfPriceDataSeries).size() == 6); + } + + // Step 2: update to effective 5 pairs by deleting US5 (last pair) + // This creates a reserve-decreasing update (6->5 pairs, reserve 2->1, adjustReserve=-1) + // Without fix: Would fail with tecINSUFFICIENT_RESERVE due to signed/unsigned + // comparison With fix (std::cmp_less): Should succeed with tesSUCCESS + auto jv = oracleSet(alice, 5); + + // Add a delete row for US5 (no AssetPrice => delete pair) + json::Value delPrice; + delPrice[jss::BaseAsset] = "XRP"; + delPrice[jss::QuoteAsset] = "US5"; + json::Value delRow; + delRow[jss::PriceData] = delPrice; // no AssetPrice => delete pair + jv[jss::PriceDataSeries].append(delRow); + + // This should succeed (and does with the fix) + submitSponsored(jv, tesSUCCESS); + + // Verify the update succeeded and we now have 5 pairs + { + auto const sle = env.le(keylet::oracle(alice, 1)); + if (BEAST_EXPECT(sle)) + { + BEAST_EXPECT(sle->getFieldArray(sfPriceDataSeries).size() == 5); + // Also verify sponsorship is still in place + BEAST_EXPECT(sle->isFieldPresent(sfSponsor)); + } + } + } + } + + void + test1364AmmWithdrawSponsoredMptBypass() + { + // https://github.com/sherlock-audit/2026-04-xrp-ledger-april-2026-judging/issues/1364 + + // AMMWithdraw::equalWithdrawTokens() misses prefunded-sponsor reserve check, + // allowing a sponsee to create a sponsored MPToken without consuming sfReserveCount + // when the sponsor's owner count is below 2. + testcase("AMMWithdraw bypasses prefunded sponsor ReserveCount for MPT creation"); + + using namespace test::jtx; + + Env env{*this, testableAmendments()}; + + Account const issuer{"issuer"}; + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const sponsor{"sponsor"}; + + // Fund all accounts with sufficient XRP + env.fund(XRP(1000), issuer, alice, bob, sponsor); + env.close(); + + // Drain sponsor (just enough for a Fee-only sponsorship but not reserve + // sponsorship) + adjustAccountXRPBalance( + env, sponsor, reserve(env, 1) + XRP(1) + env.current()->fees().base); + // auto const sponsorSle = env.le(sponsor); + // log << "Sponsor balance: " << sponsorSle->at(sfBalance) << std::endl; + + // Get initial sponsor state + auto const sponsoringOwnerCountBefore = env.sponsoringOwnerCount(sponsor); + + // Step 1: Create MPT with lsfMPTCanTrade | lsfMPTCanTransfer flags. Both are needed for AMM + // trading. MPTokenIssuanceCreate + MPTTester mpt( + {.env = env, .issuer = issuer, .flags = kMptDexFlags, .maxAmt = 1000'000'000}); + env.close(); + + // Step 2: MPTokenAuthorize and pay to alice + mpt.authorize({.account = alice, .id = mpt.issuanceID()}); + env.close(); + env(pay(issuer, alice, mpt(20'000))); + env.close(); + + // Step 3: Alice creates an AMM pool with XRP and MPT, Fee = 2XRP + AMM const amm( + env, alice, XRP(10), mpt(10'000), false, 0, env.current()->fees().increment.drops()); + env.close(); + BEAST_EXPECT(amm.expectTradingFee(0)); + + // Step 3: Bob deposits into AMM to get LP tokens, Bob needs LP tokens to be able to + // withdraw + auto const jv1 = amm.depositJv( + {.account = bob, .asset1In = XRP(1), .flags = tfSingleAsset, .assets = {{XRP, mpt}}}); + // log << jv1.toStyledString() << std::endl; + env(jv1); + env.close(); + + // Verify Bob does NOT have an MPToken yet + BEAST_EXPECT(env.le(keylet::mptoken(mpt.issuanceID(), bob)) == nullptr); + + // Step 4: Sponsor creates a Fee-only sponsorship for Bob (no ReserveCount) + // log << "Sponsor balance: " << sponsorSle->at(sfBalance) << std::endl; + env(sponsor::set_fee(sponsor, 0, drops(1'000'000)), sponsor::SponseeAcc(bob)); + env.close(); + + // Verify sponsorship exists with FeeAmount but no ReserveCount + { + auto const sle = env.le(keylet::sponsor(sponsor, bob)); + BEAST_EXPECT(sle) && BEAST_EXPECT(sle->isFieldPresent(sfFeeAmount)) && + BEAST_EXPECT(!sle->isFieldPresent(sfReserveCount)); + } + + // Control: the regular MPTokenAuthorize path correctly treats a prefunded + // sponsor as requiring ReserveCount even while the sponsor's owner count is + // below the free-object threshold. + env(mpt.authorizeJV({.account = bob, .id = mpt.issuanceID()}), + sponsor::As(sponsor, spfSponsorReserve), + Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + + // Step 6: Exploit - AMMWithdraw with prefunded sponsor succeeds + // This should also fail with tecINSUFFICIENT_RESERVE but the bug allows it to succeed + // Fixed, failed + env(amm.withdrawJv( + WithdrawArg{ + .account = bob, + .asset1Out = mpt(1), + .flags = tfSingleAsset, + .assets = {{mpt, XRP}}}), + sponsor::As(sponsor, spfSponsorReserve), + Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + + // Verify the bug: MPToken was created without consuming ReserveCount + // FIXED + auto const mptokenAfter = env.le(keylet::mptoken(mpt.issuanceID(), bob)); + BEAST_EXPECT(!mptokenAfter); + // && BEAST_EXPECT(mptokenAfter->isFieldPresent(sfSponsor)) && + // BEAST_EXPECT(mptokenAfter->getAccountID(sfSponsor) == sponsor.id()); + + // Verify sponsorship still has no ReserveCount + { + auto const sle = env.le(keylet::sponsor(sponsor, bob)); + BEAST_EXPECT(sle) && BEAST_EXPECT(!sle->isFieldPresent(sfReserveCount)); + } + + // Verify Bob's SponsoredOwnerCount increased + // fixed + { + auto const bobAfter = env.le(keylet::account(bob)); + BEAST_EXPECT(bobAfter) && + BEAST_EXPECT(bobAfter->getFieldU32(sfSponsoredOwnerCount) == 0); + } + + // Verify sponsor's SponsoringOwnerCount increased + { + auto const sponsorAfter = env.le(keylet::account(sponsor)); + BEAST_EXPECT(sponsorAfter) && + BEAST_EXPECT( + sponsorAfter->getFieldU32(sfSponsoringOwnerCount) == + sponsoringOwnerCountBefore); + + // Verify sponsor is not under-reserved + // Sponsor should need: base(10 XRP) + sponsoringOwnerCount(1) * inc(2 XRP) = 12 XRP + auto const sponsorBalance = sponsorAfter->getFieldAmount(sfBalance); + auto const requiredReserve = accountReserve(*env.current(), sponsorAfter, env.journal); + BEAST_EXPECT(sponsorBalance >= requiredReserve); + } + } + + void + test1380AmmClawbackReserveBypass() + { + // https://github.com/sherlock-audit/2026-04-xrp-ledger-april-2026-judging/issues/1380 + // AMMWithdraw's sufficientReserve lambda was reading sponsor from tx instead of target + // account This caused reserve check bypass during AMMClawback when issuer used a fee + // sponsor FIX: Line 613-614 now checks tx[sfAccount] == account before using transaction + // sponsor + + testcase("AMMClawback reserve bypass via fee sponsor (FIXED)"); + + using namespace test::jtx; + + Env env{*this, testableAmendments()}; + + Account const gw{"gateway"}; + Account const gw2{"gateway2"}; + Account const alice{"alice"}; + Account const feeSponsor{"feeSponsor"}; + + // Fund accounts + env.fund(XRP(10000), gw, gw2, alice, feeSponsor); + env.close(); + + // Enable clawback for gateway + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + + auto const usd = gw["USD"]; + auto const eur = gw2["EUR"]; + + // Gateway 1 trusts and receives EUR to fund the AMM pool + env.trust(eur(10000), gw); + env(pay(gw2, gw, eur(10000))); + env.close(); + + // Create AMM pool (USD/EUR) + AMM amm(env, gw, usd(1000), eur(1000)); + env.close(); + + // Alice makes single-asset deposit (USD only, no EUR trustline) + env.trust(usd(1000), alice); + env(pay(gw, alice, usd(400))); + env.close(); + + amm.deposit(alice, usd(400)); + env.close(); + + // Verify Alice does NOT have EUR trustline yet + BEAST_EXPECT(!env.le(keylet::line(alice, eur.issue()))); + + // Drain Alice's XRP to exact base reserve (so creating EUR trustline would under-reserve + // her) + auto const aliceLeBefore = env.le(alice); + auto const currentOwnerCount = aliceLeBefore->getFieldU32(sfOwnerCount); + auto const baseReserve = reserve(env, currentOwnerCount); + auto const drainAmount = env.balance(alice) - baseReserve - XRPAmount(100); + if (drainAmount > XRPAmount{0}) + { + env(pay(alice, gw, drainAmount)); + env.close(); + } + + // Create minimal fee sponsor account with OwnerCount = 0 (< 2) + // This is the key to the bug - fee sponsor with low owner count + // Verify fee sponsor has OwnerCount < 2 + auto const sponsorSle = env.le(feeSponsor); + BEAST_EXPECT(sponsorSle->getFieldU32(sfOwnerCount) == 0); + + // Create fee-only sponsorship (no reserve sponsorship) + env(sponsor::set_fee(feeSponsor, 0, XRP(100)), sponsor::SponseeAcc(gw)); + env.close(); + + // Verify sponsorship exists with FeeAmount only (no ReserveCount) + { + auto const sle = env.le(keylet::sponsor(feeSponsor, gw)); + BEAST_EXPECT( + sle && sle->isFieldPresent(sfFeeAmount) && !sle->isFieldPresent(sfReserveCount)); + } + + // Attempt AMMClawback with fee sponsor + // Without fix: Would succeed and create EUR trustline for Alice without reserve check + // With fix: Should fail with tecINSUFFICIENT_RESERVE because Alice lacks reserve + env(amm::ammClawback(gw, alice, usd, eur, std::nullopt), + sponsor::As(feeSponsor, spfSponsorFee), + Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + + // Verify EUR trustline was NOT created for Alice (fix working) + BEAST_EXPECT(!env.le(keylet::line(alice, eur.issue()))); + + // Verify Alice's owner count didn't increase + auto const aliceLeAfter = env.le(alice); + BEAST_EXPECT(aliceLeAfter->getFieldU32(sfOwnerCount) == currentOwnerCount); + + // Verify Alice is not under-reserved + auto const reserveAfter = reserve(env, aliceLeAfter->getFieldU32(sfOwnerCount)); + BEAST_EXPECT(env.balance(alice) >= reserveAfter); + } + + void + test1468PathPaymentExploit() + { + // https://github.com/sherlock-audit/2026-04-xrp-ledger-april-2026-judging/issues/1468 + // This test demonstrates the FULL EXPLOITATION of the bug via path payment + // + // BUG PROOF: The test triggers an assertion failure in StrandFlow.h:192 + // This assertion catches the inconsistency: During reverse pass calculation, + // sfSponsoringOwnerCount changes from 3->0, causing liquid XRP calculation to + // change from 2 XRP -> 8 XRP mid-transaction. When StrandFlow re-executes the + // limiting step for verification, it gets a different result, proving the bug. + // + // NOTE: This test will ABORT in debug builds due to assertion failure. + // This is EXPECTED and PROVES the bug. To run other tests, comment this test out. + // In production builds (without assertions), this would cause actual reserve bypass. + + testcase("PaymentSandbox sfSponsoringOwnerCount bypass - PATH PAYMENT EXPLOIT"); + + using namespace test::jtx; + + auto cfg = makeConfig(); + cfg->fees.referenceFee = 12; + cfg->fees.accountReserve = XRP(10).value().xrp(); + cfg->fees.ownerReserve = XRP(2).value().xrp(); + Env env{*this, std::move(cfg), testableAmendments()}; + + Account const sponsor{"sponsor"}; + Account const sponsee{"sponsee"}; + Account const issuer{"issuer"}; + Account const dest{"dest"}; + + // Fund accounts + env.fund(XRP(500), sponsor, sponsee, issuer, dest); + env.close(); + + // Setup trustlines + auto const usd = issuer["USD"]; + env.trust(usd(10'000), sponsee); + env.trust(usd(10'000), dest); + env.close(); + + // Issuer pays USD to sponsee + env(pay(issuer, sponsee, usd(1'000))); + env.close(); + + // Sponsor creates sponsorship with ReserveCount=5 for sponsee + env(sponsor::set_reserve(sponsor, 0, 5), sponsor::SponseeAcc(sponsee)); + env.close(); + + // Sponsee creates 3 sponsored offers: XRP -> USD + // These will be crossed by the path payment + for (int i = 0; i < 3; ++i) + { + env(offer(sponsee, XRP(50), usd(100)), sponsor::As(sponsor, spfSponsorReserve)); + env.close(); + } + + // Create 7 trustlines for sponsor to increase sfOwnerCount to 8 + for (int i = 0; i < 7; ++i) + { + auto const tempIssuer = Account("tempIssuer" + std::to_string(i)); + env.fund(XRP(30), tempIssuer); + env.close(); + auto const aaa = tempIssuer["AAA"]; + env.trust(aaa(1), sponsor); + env.close(); + } + + // Log sponsor state BEFORE adjustment + auto const sponsorSle1 = env.le(sponsor); + auto const reserve1 = accountReserve(*env.current(), sponsorSle1, env.journal); + // auto const liquid1 = xrpLiquid(*env.current(), sponsorSle1, 0, env.journal); + auto const ownerCount1 = sponsorSle1->getFieldU32(sfOwnerCount); + auto const sponsoringOC1 = sponsorSle1->getFieldU32(sfSponsoringOwnerCount); + + BEAST_EXPECT(ownerCount1 == 8); // 7 trustlines + 1 sponsorship + BEAST_EXPECT(sponsoringOC1 == 3); // 3 sponsored offers + // Reserve should be: 10 + 2*(8+3) = 32 XRP + BEAST_EXPECT(reserve1 == XRP(32)); + + // CRITICAL: Adjust sponsor balance to exactly 34 XRP + // This sets liquid = 34 - 32 = 2 XRP + adjustAccountXRPBalance(env, sponsor, XRP(34)); + + // Verify the setup + auto const sponsorSle2 = env.le(sponsor); + auto const balance2 = sponsorSle2->at(sfBalance); + auto const reserve2 = accountReserve(*env.current(), sponsorSle2, env.journal); + auto const liquid2 = xrpLiquid(*env.current(), sponsorSle2, 0, env.journal); + + BEAST_EXPECT(balance2 == XRP(34)); + BEAST_EXPECT(reserve2 == XRP(32)); + BEAST_EXPECT(liquid2 == XRP(2)); + + // "=== EXPLOITATION: Path Payment ===" + // "Sending path payment: sponsor -> dest" << std::endl; + // "Path: XRP -> [BookStep crosses sponsored offers] -> USD" << std::endl; + // "Expected: StrandFlow reverse pass will:" << std::endl; + // " 1. BookStep deletes offers, drops sfSponsoringOwnerCount 3->0" << std::endl; + // " 2. XRPEndpointStep sees sfSponsoringOwnerCount=0 (BUG!)" << std::endl; + // " 3. Calculates liquid = 34 - 26 = 8 XRP (should be 2 XRP!)" << std::endl; + // " 4. This inconsistency causes assertion failure in StrandFlow" << std::endl; + // " 5. OR allows sending MORE than 2 XRP" << std::endl; + + // EXPLOITATION: Path payment from sponsor (XRP) to dest (USD) + // The path will cross the sponsored offers via BookStep + // tfNoRippleDirect forces use of the path + // tfPartialPayment allows partial delivery + // NOTE: This may trigger an assertion in StrandFlow due to the bug! + // The assertion proves inconsistency in reserve calculations + + env(pay(sponsor, dest, usd(250)), + Sendmax(XRP(200)), + Path(~usd), + Txflags(tfNoRippleDirect | tfPartialPayment)); + env.close(); + + // Log sponsor state AFTER exploitation + auto const sponsorSle3 = env.le(sponsor); + auto const balance3 = sponsorSle3->at(sfBalance); + auto const reserve3 = accountReserve(*env.current(), sponsorSle3, env.journal); + auto const liquid3 = xrpLiquid(*env.current(), sponsorSle3, 0, env.journal); + auto const ownerCount3 = sponsorSle3->getFieldU32(sfOwnerCount); + auto const sponsoringOC3 = sponsorSle3->getFieldU32(sfSponsoringOwnerCount); + + // Calculate XRP spent + auto const xrpSpent = balance2.xrp() - balance3.xrp(); + + // BUG CONFIRMED: sfSponsoringOwnerCount dropped from 3 to 0 + // BEAST_EXPECT(sponsoringOC3 == 0); + BEAST_EXPECT(sponsoringOC3 == sponsoringOC1); + + // Reserve calculation after: 10 + 2*8 = 26 XRP (3 sponsored offers deleted) + // BEAST_EXPECT(reserve3 == XRP(26)); + BEAST_EXPECT(reserve3 == reserve1); + + // EXPLOITATION CONFIRMED: Sponsor spent MORE than the legitimate 2 XRP liquid + // BEAST_EXPECT(xrpSpent > XRP(2)); + BEAST_EXPECT(xrpSpent <= XRP(2)); + + // EXPLOITATION CONFIRMED: balance under reserve + // BEAST_EXPECT(balance3 < reserve3); + BEAST_EXPECT(balance3 >= reserve3); + BEAST_EXPECT(!liquid3.negative()); + BEAST_EXPECT(ownerCount3 == 8); + } + +public: + void + run() override + { + using namespace test::jtx; + + test168CoSignedBlockedWithFeeOnlySponsorship(); + test251AMMDepositRejectXRPDeposits(); + test750SponsorFeeQueueAdmissionBug(); + test750AdversarialSponsorBlocksVictim(); + test1033SponsoredWitnessCanChargeDoorOwnedClaimObjectsToUnrelatedSponsor(); + test1186AMMCreateUsesPreFeeReserveBalance(); + test1186AMMDepositUsesPreFeeReserveBalance(); + test1350ReserveCountSilentWrap(testableAmendments()); + test1364AmmWithdrawSponsoredMptBypass(); + test1365OracleReserveDecreaseRejection(); + test1380AmmClawbackReserveBypass(); + test1468PathPaymentExploit(); + } +}; + +BEAST_DEFINE_TESTSUITE(SponsorSherlock, app, xrpl); + +} // namespace xrpl::test diff --git a/src/test/app/Sponsor_test.cpp b/src/test/app/Sponsor_test.cpp index c5399c71cae..924f50f062b 100644 --- a/src/test/app/Sponsor_test.cpp +++ b/src/test/app/Sponsor_test.cpp @@ -149,11 +149,12 @@ class Sponsor_test : public beast::unit_test::Suite Account const alice("alice"); Account const bob("bob"); Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); Account const noFunded("noFunded"); Account const gw("gw"); auto const usd = gw["usd"]; - env.fund(XRP(10000), alice, sponsor, gw); + env.fund(XRP(10000), alice, sponsor, sponsor2, gw, bob); env.close(); // @@ -259,9 +260,14 @@ class Sponsor_test : public beast::unit_test::Suite env(sponsor::set_fee(sponsor, 0, XRP(4)), sponsor::SponseeAcc(alice), Ter(tecUNFUNDED)); env.close(); - // insufficent reserve to create sponsorship + // Fee can be borrowed from reserve + adjustAccountXRPBalance(env, sponsor2, XRP(100) + XRP(1) + reserve(env, 1)); + env(sponsor::set(sponsor2, 0, 100, XRP(101)), sponsor::SponseeAcc(bob), Fee(XRP(1))); + env.close(); + + // insufficent reserve to create sponsorship (balance like previous but -1 drop) adjustAccountXRPBalance(env, sponsor, XRP(100) + XRP(1) + reserve(env, 1) - drops(1)); - env(sponsor::set(sponsor, 0, 100, XRP(100)), + env(sponsor::set(sponsor, 0, 100, XRP(101)), sponsor::SponseeAcc(alice), Fee(XRP(1)), Ter(tecUNFUNDED)); @@ -3688,14 +3694,19 @@ class Sponsor_test : public beast::unit_test::Suite env(noop(sponsor), ticket::Use(ticketSeq)); env.close(); - // pass (free mptoken) if (cosigning) { + // Sponsor reserve full tokens (no sponsor - free mptoken, rule ownerCount < 2) adjustAccountXRPBalance(env, sponsor, reserve(env, 2) - drops(1)); env(jv, sponsor::As(sponsor, spfSponsorReserve), Sig(sfSponsorSignature, sponsor), - Ter(tesSUCCESS)); + Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + + // full reserve pass + adjustAccountXRPBalance(env, sponsor, reserve(env, 2)); + env(jv, sponsor::As(sponsor, spfSponsorReserve), Sig(sfSponsorSignature, sponsor)); env.close(); } else diff --git a/src/xrpld/app/misc/TxQ.h b/src/xrpld/app/misc/TxQ.h index d3caec55cf3..188f5d21e1e 100644 --- a/src/xrpld/app/misc/TxQ.h +++ b/src/xrpld/app/misc/TxQ.h @@ -693,6 +693,15 @@ class TxQ remove(SeqProxy seqProx); }; + // potentialSpend, totalFee + static std::pair + calcSpendAndFee( + OpenView const& view, + TxQAccount::TxMap::iterator const& begin, + TxQAccount::TxMap::iterator const& end, + PreflightResult const& pfResult, + SeqProxy const& txSeqProx); + // Helper function returns requiredFeeLevel. static FeeLevel64 getRequiredFeeLevel( diff --git a/src/xrpld/app/misc/detail/TxQ.cpp b/src/xrpld/app/misc/detail/TxQ.cpp index 0326828a708..c1438332311 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 @@ -607,6 +608,48 @@ TxQ::tryClearAccountQueueUpThruTx( return txResult; } +std::pair +TxQ::calcSpendAndFee( + OpenView const& view, + TxQAccount::TxMap::iterator const& begin, + TxQAccount::TxMap::iterator const& end, + PreflightResult const& pfResult, + SeqProxy const& txSeqProx) +{ + XRPAmount potentialSpend = beast::kZero; + XRPAmount totalFee = beast::kZero; + for (auto iter = begin; iter != end; ++iter) + { + bool accPayFee = true; + auto const& tx = iter->second.txn; + if (tx) + { + auto const feePayer = getFeePayer(view, *tx); + accPayFee = tx->at(sfAccount) == feePayer.id; + } + + // If we're replacing this transaction don't include + // the replaced transaction's XRP spend. Otherwise add + // it to potentialSpend. + if (iter->first != txSeqProx) + { + if (accPayFee) + totalFee += iter->second.consequences().fee(); + potentialSpend += iter->second.consequences().potentialSpend(); + } + else if (std::next(iter) != end) + { + // The fee for the candidate transaction _should_ be + // counted if it's replacing a transaction in the middle + // of the queue. + if (accPayFee) + totalFee += pfResult.consequences.fee(); + potentialSpend += pfResult.consequences.potentialSpend(); + } + } + + return {potentialSpend, totalFee}; +} // Overview of considerations for when a transaction is accepted into the TxQ: // // These rules apply to the transactions in the queue owned by a single @@ -1026,27 +1069,9 @@ TxQ::apply( // Sum fees and spending for all of the queued transactions // so we know how much to remove from the account balance // for the trial preclaim. - XRPAmount potentialSpend = beast::kZero; - XRPAmount totalFee = beast::kZero; - for (auto iter = txIter->first; iter != txIter->end; ++iter) - { - // If we're replacing this transaction don't include - // the replaced transaction's XRP spend. Otherwise add - // it to potentialSpend. - if (iter->first != txSeqProx) - { - totalFee += iter->second.consequences().fee(); - potentialSpend += iter->second.consequences().potentialSpend(); - } - else if (std::next(iter) != txIter->end) - { - // The fee for the candidate transaction _should_ be - // counted if it's replacing a transaction in the middle - // of the queue. - totalFee += pfResult.consequences.fee(); - potentialSpend += pfResult.consequences.potentialSpend(); - } - } + auto [potentialSpend, totalFee] = + calcSpendAndFee(view, txIter->first, txIter->end, pfResult, txSeqProx); + // NOLINTEND(bugprone-unchecked-optional-access) /* Check if the total fees in flight are greater diff --git a/src/xrpld/rpc/detail/TransactionSign.cpp b/src/xrpld/rpc/detail/TransactionSign.cpp index 8c3a5ea245b..b8731a22894 100644 --- a/src/xrpld/rpc/detail/TransactionSign.cpp +++ b/src/xrpld/rpc/detail/TransactionSign.cpp @@ -1256,7 +1256,7 @@ transactionSignFor( // The array must be sorted and validated. // For delegated transactions, the delegate account is // the one forbidden from appearing in its own Signers array. - auto err = sortAndValidateSigners(signers, sttx->getFeePayer()); + auto err = sortAndValidateSigners(signers, sttx->getInitiator()); if (RPC::containsError(err)) return err; } @@ -1423,9 +1423,9 @@ transactionSubmitMultiSigned( } // The array must be sorted and validated. - // For delegated transactions, getFeePayer() returns sfDelegate, + // For delegated transactions, getInitiator() returns sfDelegate, // that account is the one forbidden from appearing in its own Signers array. - auto err = sortAndValidateSigners(signers, stTx->getFeePayer()); + auto err = sortAndValidateSigners(signers, stTx->getInitiator()); if (RPC::containsError(err)) return err;