From fd2cc6dcb308fb811960380eba7bf4309934d05d Mon Sep 17 00:00:00 2001 From: Olek <115580134+oleks-rip@users.noreply.github.com> Date: Fri, 10 Jul 2026 17:58:19 -0400 Subject: [PATCH] feat: XLS-68: Sponsor, #5887 continuation (#7350) Co-authored-by: tequ Co-authored-by: yinyiqian1 Co-authored-by: Mayukha Vadari Co-authored-by: Mayukha Vadari Co-authored-by: xrplf-ai-reviewer[bot] <266832837+xrplf-ai-reviewer[bot]@users.noreply.github.com> Co-authored-by: Peter Chen <34582813+PeterChen13579@users.noreply.github.com> Co-authored-by: Zhiyuan Wang <96991820+Kassaking7@users.noreply.github.com> Co-authored-by: Ayaz Salikhov Co-authored-by: Ayaz Salikhov Co-authored-by: Zhiyuan Wang <1830604455@qq.com> --- .cspell.config.yaml | 3 + include/xrpl/basics/base_uint.h | 2 +- include/xrpl/ledger/ApplyView.h | 21 +- include/xrpl/ledger/OwnerCounts.h | 70 + include/xrpl/ledger/PaymentSandbox.h | 14 +- include/xrpl/ledger/ReadView.h | 6 +- include/xrpl/ledger/View.h | 3 +- .../xrpl/ledger/helpers/AccountRootHelpers.h | 295 +- include/xrpl/ledger/helpers/EscrowHelpers.h | 77 +- include/xrpl/ledger/helpers/MPTokenHelpers.h | 10 +- include/xrpl/ledger/helpers/OracleHelpers.h | 31 + .../xrpl/ledger/helpers/RippleStateHelpers.h | 6 +- include/xrpl/ledger/helpers/SponsorHelpers.h | 184 + include/xrpl/ledger/helpers/TokenHelpers.h | 6 +- include/xrpl/protocol/Book.h | 4 +- include/xrpl/protocol/Fees.h | 9 +- include/xrpl/protocol/Indexes.h | 4 + include/xrpl/protocol/Issue.h | 2 +- include/xrpl/protocol/LedgerFormats.h | 6 +- include/xrpl/protocol/STObject.h | 4 +- include/xrpl/protocol/STTx.h | 3 + include/xrpl/protocol/TER.h | 2 + include/xrpl/protocol/TxFlags.h | 25 +- include/xrpl/protocol/detail/features.macro | 1 + .../xrpl/protocol/detail/ledger_entries.macro | 66 +- include/xrpl/protocol/detail/sfields.macro | 15 + .../xrpl/protocol/detail/transactions.macro | 29 + include/xrpl/protocol/jss.h | 3 + .../ledger_entries/AccountRoot.h | 105 + .../ledger_entries/RippleState.h | 70 + .../ledger_entries/Sponsorship.h | 344 + .../transactions/SponsorshipSet.h | 290 + .../transactions/SponsorshipTransfer.h | 179 + include/xrpl/tx/ApplyContext.h | 9 + include/xrpl/tx/Transactor.h | 24 + include/xrpl/tx/invariants/InvariantCheck.h | 5 +- .../xrpl/tx/invariants/SponsorshipInvariant.h | 61 + include/xrpl/tx/paths/AMMOffer.h | 6 +- include/xrpl/tx/paths/Offer.h | 3 +- .../tx/transactors/sponsor/SponsorshipSet.h | 52 + .../transactors/sponsor/SponsorshipTransfer.h | 49 + .../transactors/token/MPTokenIssuanceCreate.h | 2 +- src/libxrpl/ledger/PaymentSandbox.cpp | 15 +- src/libxrpl/ledger/View.cpp | 24 +- .../ledger/helpers/AccountRootHelpers.cpp | 409 +- .../ledger/helpers/CredentialHelpers.cpp | 2 +- src/libxrpl/ledger/helpers/MPTokenHelpers.cpp | 91 +- src/libxrpl/ledger/helpers/NFTokenHelpers.cpp | 43 +- src/libxrpl/ledger/helpers/OfferHelpers.cpp | 2 +- .../ledger/helpers/PaymentChannelHelpers.cpp | 2 +- .../ledger/helpers/RippleStateHelpers.cpp | 83 +- src/libxrpl/ledger/helpers/SponsorHelpers.cpp | 345 ++ src/libxrpl/ledger/helpers/TokenHelpers.cpp | 44 +- src/libxrpl/protocol/Indexes.cpp | 7 + src/libxrpl/protocol/InnerObjectFormats.cpp | 8 + src/libxrpl/protocol/LedgerFormats.cpp | 1 + src/libxrpl/protocol/STObject.cpp | 6 +- src/libxrpl/protocol/STTx.cpp | 19 +- src/libxrpl/protocol/TER.cpp | 2 + src/libxrpl/protocol/TxFormats.cpp | 3 + src/libxrpl/tx/Transactor.cpp | 337 +- src/libxrpl/tx/applySteps.cpp | 3 + src/libxrpl/tx/invariants/InvariantCheck.cpp | 42 +- .../tx/invariants/SponsorshipInvariant.cpp | 158 + src/libxrpl/tx/invariants/VaultInvariant.cpp | 9 +- src/libxrpl/tx/paths/BookStep.cpp | 2 +- src/libxrpl/tx/paths/MPTEndpointStep.cpp | 3 +- .../tx/transactors/account/AccountDelete.cpp | 39 +- .../tx/transactors/account/SignerListSet.cpp | 34 +- .../tx/transactors/bridge/XChainBridge.cpp | 19 +- .../tx/transactors/check/CheckCancel.cpp | 3 +- .../tx/transactors/check/CheckCash.cpp | 37 +- .../tx/transactors/check/CheckCreate.cpp | 16 +- .../credentials/CredentialAccept.cpp | 33 +- .../credentials/CredentialCreate.cpp | 15 +- .../tx/transactors/delegate/DelegateSet.cpp | 20 +- src/libxrpl/tx/transactors/dex/AMMCreate.cpp | 19 +- src/libxrpl/tx/transactors/dex/AMMDeposit.cpp | 9 +- .../tx/transactors/dex/AMMWithdraw.cpp | 17 +- .../tx/transactors/dex/OfferCreate.cpp | 6 +- src/libxrpl/tx/transactors/did/DIDDelete.cpp | 3 +- src/libxrpl/tx/transactors/did/DIDSet.cpp | 5 +- .../tx/transactors/escrow/EscrowCancel.cpp | 5 +- .../tx/transactors/escrow/EscrowCreate.cpp | 33 +- .../tx/transactors/escrow/EscrowFinish.cpp | 19 +- .../lending/LoanBrokerCoverClawback.cpp | 2 +- .../lending/LoanBrokerCoverDeposit.cpp | 2 +- .../lending/LoanBrokerCoverWithdraw.cpp | 9 +- .../transactors/lending/LoanBrokerDelete.cpp | 10 +- .../tx/transactors/lending/LoanBrokerSet.cpp | 8 +- .../tx/transactors/lending/LoanDelete.cpp | 7 +- .../tx/transactors/lending/LoanManage.cpp | 1 + .../tx/transactors/lending/LoanPay.cpp | 6 +- .../tx/transactors/lending/LoanSet.cpp | 26 +- .../tx/transactors/nft/NFTokenAcceptOffer.cpp | 4 +- .../tx/transactors/nft/NFTokenMint.cpp | 11 +- .../tx/transactors/oracle/OracleDelete.cpp | 9 +- .../tx/transactors/oracle/OracleSet.cpp | 40 +- .../tx/transactors/payment/DepositPreauth.cpp | 34 +- .../tx/transactors/payment/Payment.cpp | 77 +- .../payment_channel/PaymentChannelCreate.cpp | 37 +- .../payment_channel/PaymentChannelFund.cpp | 18 +- .../PermissionedDomainDelete.cpp | 2 +- .../PermissionedDomainSet.cpp | 5 +- .../tx/transactors/sponsor/SponsorshipSet.cpp | 420 ++ .../sponsor/SponsorshipTransfer.cpp | 542 ++ src/libxrpl/tx/transactors/system/Batch.cpp | 29 +- .../tx/transactors/system/TicketCreate.cpp | 16 +- .../tx/transactors/token/MPTokenAuthorize.cpp | 2 +- .../token/MPTokenIssuanceCreate.cpp | 36 +- .../token/MPTokenIssuanceDestroy.cpp | 3 +- src/libxrpl/tx/transactors/token/TrustSet.cpp | 170 +- .../tx/transactors/vault/VaultClawback.cpp | 9 +- .../tx/transactors/vault/VaultCreate.cpp | 23 +- .../tx/transactors/vault/VaultDelete.cpp | 11 +- .../tx/transactors/vault/VaultDeposit.cpp | 18 +- .../tx/transactors/vault/VaultWithdraw.cpp | 9 +- src/test/app/AMMExtendedMPT_test.cpp | 2 +- src/test/app/AMMExtended_test.cpp | 2 +- src/test/app/AMMMPT_test.cpp | 2 +- src/test/app/AccountSet_test.cpp | 3 +- src/test/app/CheckMPT_test.cpp | 2 +- src/test/app/Check_test.cpp | 2 +- src/test/app/Credentials_test.cpp | 4 +- src/test/app/Delegate_test.cpp | 22 +- src/test/app/DepositAuth_test.cpp | 4 +- src/test/app/EscrowToken_test.cpp | 2 +- src/test/app/FlowMPT_test.cpp | 2 +- src/test/app/Flow_test.cpp | 2 +- src/test/app/Invariants_test.cpp | 190 +- src/test/app/LoanBroker_test.cpp | 3 +- src/test/app/Loan_test.cpp | 41 +- src/test/app/MPToken_test.cpp | 11 +- src/test/app/NFToken_test.cpp | 2 +- src/test/app/OfferMPT_test.cpp | 18 +- src/test/app/Offer_test.cpp | 18 +- src/test/app/Oracle_test.cpp | 6 +- src/test/app/Sponsor_test.cpp | 5511 +++++++++++++++++ src/test/app/Ticket_test.cpp | 10 +- src/test/app/TrustSet_test.cpp | 2 +- src/test/app/TxQ_test.cpp | 40 + src/test/app/Vault_test.cpp | 8 +- src/test/app/XChain_test.cpp | 4 +- src/test/jtx/Env.h | 18 + src/test/jtx/JTx.h | 2 +- src/test/jtx/TestHelpers.h | 12 + src/test/jtx/impl/AMMTest.cpp | 2 +- src/test/jtx/impl/Env.cpp | 27 + src/test/jtx/impl/TestHelpers.cpp | 18 + src/test/jtx/impl/multisign.cpp | 3 +- src/test/jtx/impl/owners.cpp | 18 + src/test/jtx/impl/sig.cpp | 2 +- src/test/jtx/impl/sponsor.cpp | 98 + src/test/jtx/owners.h | 54 + src/test/jtx/sponsor.h | 105 + src/test/ledger/PaymentSandbox_test.cpp | 228 +- src/test/rpc/AccountObjects_test.cpp | 269 +- src/test/rpc/AccountTx_test.cpp | 80 +- src/test/rpc/LedgerEntry_test.cpp | 59 +- src/test/rpc/Simulate_test.cpp | 153 + .../ledger_entries/AccountRootTests.cpp | 81 + .../ledger_entries/RippleStateTests.cpp | 54 + .../ledger_entries/SponsorshipTests.cpp | 329 + .../transactions/SponsorshipSetTests.cpp | 261 + .../transactions/SponsorshipTransferTests.cpp | 198 + src/xrpld/app/misc/detail/TxQ.cpp | 4 + src/xrpld/rpc/detail/TransactionSign.cpp | 6 +- .../rpc/handlers/account/AccountObjects.cpp | 55 +- src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp | 22 + .../rpc/handlers/transaction/Simulate.cpp | 52 +- 170 files changed, 13151 insertions(+), 660 deletions(-) create mode 100644 include/xrpl/ledger/OwnerCounts.h create mode 100644 include/xrpl/ledger/helpers/OracleHelpers.h create mode 100644 include/xrpl/ledger/helpers/SponsorHelpers.h create mode 100644 include/xrpl/protocol_autogen/ledger_entries/Sponsorship.h create mode 100644 include/xrpl/protocol_autogen/transactions/SponsorshipSet.h create mode 100644 include/xrpl/protocol_autogen/transactions/SponsorshipTransfer.h create mode 100644 include/xrpl/tx/invariants/SponsorshipInvariant.h create mode 100644 include/xrpl/tx/transactors/sponsor/SponsorshipSet.h create mode 100644 include/xrpl/tx/transactors/sponsor/SponsorshipTransfer.h create mode 100644 src/libxrpl/ledger/helpers/SponsorHelpers.cpp create mode 100644 src/libxrpl/tx/invariants/SponsorshipInvariant.cpp create mode 100644 src/libxrpl/tx/transactors/sponsor/SponsorshipSet.cpp create mode 100644 src/libxrpl/tx/transactors/sponsor/SponsorshipTransfer.cpp create mode 100644 src/test/app/Sponsor_test.cpp create mode 100644 src/test/jtx/impl/sponsor.cpp create mode 100644 src/test/jtx/sponsor.h create mode 100644 src/tests/libxrpl/protocol_autogen/ledger_entries/SponsorshipTests.cpp create mode 100644 src/tests/libxrpl/protocol_autogen/transactions/SponsorshipSetTests.cpp create mode 100644 src/tests/libxrpl/protocol_autogen/transactions/SponsorshipTransferTests.cpp diff --git a/.cspell.config.yaml b/.cspell.config.yaml index c558fc0984..dc24bf833b 100644 --- a/.cspell.config.yaml +++ b/.cspell.config.yaml @@ -279,6 +279,8 @@ words: - sles - soci - socidb + - sponsee + - sponsees - SRPMS - sslws - statsd @@ -327,6 +329,7 @@ words: - unserviced - unshareable - unshares + - unsponsored - unsquelch - unsquelched - unsquelching diff --git a/include/xrpl/basics/base_uint.h b/include/xrpl/basics/base_uint.h index 481a7dbd77..f75100f862 100644 --- a/include/xrpl/basics/base_uint.h +++ b/include/xrpl/basics/base_uint.h @@ -595,7 +595,7 @@ template [[nodiscard]] constexpr bool operator==(BaseUInt const& lhs, BaseUInt const& rhs) { - return (lhs <=> rhs) == 0; + return (lhs <=> rhs) == 0; // NOLINT(modernize-use-nullptr) } //------------------------------------------------------------------------------ diff --git a/include/xrpl/ledger/ApplyView.h b/include/xrpl/ledger/ApplyView.h index e519013d9a..afe6b291b2 100644 --- a/include/xrpl/ledger/ApplyView.h +++ b/include/xrpl/ledger/ApplyView.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include // IWYU pragma: keep @@ -11,6 +12,7 @@ #include #include #include +#include #include #include @@ -288,7 +290,7 @@ public: // 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) { } @@ -411,6 +413,23 @@ public: emptyDirDelete(Keylet const& directory); }; +/** Bundles the mutable ledger view and the transaction being applied. + + Passed together to avoid threading two separate parameters through every + helper that needs both the view (for state reads/writes) and the + transaction (for field inspection and metadata). + + Both members are non-owning references; the caller is responsible for + ensuring that the referenced objects outlive the ApplyViewContext. + + TODO: replace with ApplyContext after it's untangled with xrpl/tx +*/ +struct ApplyViewContext +{ + ApplyView& view; + STTx const& tx; +}; + namespace directory { /** Helper functions for managing low-level directory operations. These are not part of the ApplyView interface. diff --git a/include/xrpl/ledger/OwnerCounts.h b/include/xrpl/ledger/OwnerCounts.h new file mode 100644 index 0000000000..74d60014b5 --- /dev/null +++ b/include/xrpl/ledger/OwnerCounts.h @@ -0,0 +1,70 @@ +#pragma once + +#include +#include +#include +#include // IWYU pragma: keep +#include + +#include +#include + +namespace xrpl { + +struct OwnerCounts +{ + std::uint32_t owner = 0; + std::uint32_t sponsored = 0; + std::uint32_t sponsoring = 0; + + OwnerCounts() = default; + OwnerCounts(SLE::const_ref sle) + : owner(sle->at(sfOwnerCount)) + , sponsored(sle->at(sfSponsoredOwnerCount)) + , sponsoring(sle->at(sfSponsoringOwnerCount)) + { + XRPL_ASSERT( + owner >= sponsored, + "xrpl::OwnerCounts : OwnerCount must be greater than or equal to " + "SponsoredOwnerCount"); + XRPL_ASSERT(sle->getType() == ltACCOUNT_ROOT, "xrpl::OwnerCounts : sle is AccountRoot"); + } + + [[nodiscard]] std::uint32_t + count() const + { + std::int64_t const x = static_cast(owner) - sponsored + sponsoring; + if (x < 0) + { + // LCOV_EXCL_START + UNREACHABLE("xrpl::OwnerCounts::count : count less than zero"); + return 0; + // LCOV_EXCL_STOP + } + + if (x > std::numeric_limits::max()) + return std::numeric_limits::max(); // LCOV_EXCL_LINE + return static_cast(x); + } + + auto + operator<=>(OwnerCounts const& o) const + { + if (auto cmp = count() <=> o.count(); cmp != 0) // NOLINT(modernize-use-nullptr) + return cmp; + if (auto cmp = owner <=> o.owner; cmp != 0) // NOLINT(modernize-use-nullptr) + return cmp; + if (auto cmp = sponsored <=> o.sponsored; cmp != 0) // NOLINT(modernize-use-nullptr) + return cmp; + return sponsoring <=> o.sponsoring; + } + + bool + operator==(OwnerCounts const& o) const + { + return this == &o || + (owner == o.owner && sponsored == o.sponsored && sponsoring == o.sponsoring); + } +}; + +} // namespace xrpl diff --git a/include/xrpl/ledger/PaymentSandbox.h b/include/xrpl/ledger/PaymentSandbox.h index 0117a962ff..8afe21b397 100644 --- a/include/xrpl/ledger/PaymentSandbox.h +++ b/include/xrpl/ledger/PaymentSandbox.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -107,12 +108,12 @@ public: 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 @@ -124,7 +125,7 @@ private: std::map creditsIOU_; std::map creditsMPT_; - std::map ownerCounts_; + std::map ownerCounts_; }; } // namespace detail @@ -218,10 +219,11 @@ public: 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 724533039b..da92e0b510 100644 --- a/include/xrpl/ledger/ReadView.h +++ b/include/xrpl/ledger/ReadView.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -13,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -189,8 +191,8 @@ public: // 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/View.h b/include/xrpl/ledger/View.h index c89764df1d..7fad61d407 100644 --- a/include/xrpl/ledger/View.h +++ b/include/xrpl/ledger/View.h @@ -210,8 +210,7 @@ canWithdraw(ReadView const& view, STTx const& tx); [[nodiscard]] TER doWithdraw( - ApplyView& view, - STTx const& tx, + ApplyViewContext ctx, AccountID const& senderAcct, AccountID const& dstAcct, AccountID const& sourceAcct, diff --git a/include/xrpl/ledger/helpers/AccountRootHelpers.h b/include/xrpl/ledger/helpers/AccountRootHelpers.h index d0cfc175a3..ae8cf53f2f 100644 --- a/include/xrpl/ledger/helpers/AccountRootHelpers.h +++ b/include/xrpl/ledger/helpers/AccountRootHelpers.h @@ -14,6 +14,7 @@ #include #include +#include #include #include @@ -26,21 +27,293 @@ namespace xrpl { [[nodiscard]] bool isGlobalFrozen(ReadView const& view, AccountID const& issuer); -// Calculate liquid XRP balance for an account. -// This function may be used to calculate the amount of XRP that -// the holder is able to freely spend. It subtracts reserve requirements. -// -// ownerCountAdj adjusts the owner count in case the caller calculates -// before ledger entries are added or removed. Positive to add, negative -// to subtract. -// -// @param ownerCountAdj positive to add to count, negative to reduce count. +/** Calculate liquid XRP balance for an account. + * + * This function may be used to calculate the amount of XRP that + * the holder is able to freely spend. It subtracts reserve requirements. + * + * ownerCountAdj adjusts the owner count in case the caller calculates + * before ledger entries are added or removed. Positive to add, negative + * to subtract. + * + * @param view The ledger view to read from + * @param id The account ID to check + * @param ownerCountAdj Positive to add to count, negative to reduce count + * @param j Journal for logging + * @return The liquid XRP amount available to the account + */ [[nodiscard]] XRPAmount xrpLiquid(ReadView const& view, AccountID const& id, std::int32_t ownerCountAdj, beast::Journal j); -/** Adjust the owner count up or down. */ +struct Adjustment +{ + std::int32_t ownerCountDelta = 0; + std::int32_t accountCountDelta = 0; +}; + +/** Returns the account reserve, in drops. + * + * Actual owner count can be adjusted by delta in ownerCountAdj + * Actual reserve count can be adjusted by delta in accountCountAdj + * The reserve is calculated as: + * (ownerCount + "sponsoring object count" - "sponsored object count" + additionalOwnerCount) * + * increment + (1 if not sponsored account + sponsoringAccountCount) * "reserve base" + * + * @param view The ledger view to read from + * @param sle The ledger entry for the account + * @param j Journal for logging + * @param adj Adjustment to the owner/account count (default: 0/0). Positive to add, negative to + * subtract. + * @return The account reserve amount in drops + */ +[[nodiscard]] XRPAmount +accountReserve(ReadView const& view, SLE::const_ref sle, beast::Journal j, Adjustment adj = {}); + +/** Convenience overload that accepts AccountID instead of SLE. + * + * @param view The ledger view to read from + * @param id The account ID + * @param j Journal for logging + * @param adj Adjustment to the owner/account count (default: 0/0). Positive to add, negative to + * subtract. + * @return The account reserve amount in drops + */ +[[nodiscard]] inline XRPAmount +accountReserve(ReadView const& view, AccountID const& id, beast::Journal j, Adjustment adj = {}) +{ + return accountReserve(view, view.read(keylet::account(id)), j, adj); +} + +/** Check if an account has sufficient reserve. + * + * @param view The ledger view to read from + * @param tx The transaction being processed + * @param accSle The account's ledger entry + * @param accBalance The account's balance + * @param sponsorSle The sponsor's ledger entry (if applicable) + * @param adj Adjustment to the owner/account count (default: 0/0). Positive to add, negative to + * subtract. + * @param j Journal for logging (default: null sink) + * @param insufReserveCode The transaction result code to return if the reserve is insufficient + * (default: tecINSUFFICIENT_RESERVE). + * @return Transaction result code + */ +[[nodiscard]] TER +checkReserve( + ApplyViewContext ctx, + SLE::const_ref accSle, + XRPAmount accBalance, + SLE::const_ref sponsorSle, + Adjustment adj, + beast::Journal j, + TER insufReserveCode = tecINSUFFICIENT_RESERVE); + +/** Check if an account has sufficient reserve, deriving the sponsor internally. + * + * Equivalent to the overload above, but resolves the sponsor via + * getEffectiveTxReserveSponsor(ctx, accSle) instead of taking it explicitly. Use this + * in the common case where the sponsor is simply the transaction's reserve + * sponsor for accSle. Callers that must force the account's-own-reserve branch + * (passing a null sponsor) or supply a different sponsor should use the + * explicit overload above. + * + * @param ctx The apply-view context (view + tx) + * @param accSle The account's ledger entry + * @param accBalance The account's balance + * @param adj Reserve adjustments (owner/account count deltas) + * @param j Journal for logging (default: null sink) + * @return Transaction result code + */ +[[nodiscard]] TER +checkReserve( + ApplyViewContext ctx, + SLE::const_ref accSle, + XRPAmount accBalance, + Adjustment adj, + beast::Journal j = beast::Journal{beast::Journal::getNullSink()}); + +/** Return number of the objects which reserve is covered by the account(sle) (so called "owner + * count"). Actual owner count can be adjusted by delta in ownerCountAdj. + * + * @param sle The account's ledger entry + * @param j Journal for logging + * @param ownerCountAdj Adjustment to the owner count (default: 0) + * @return The adjusted owner count + */ +std::uint32_t +ownerCount(SLE::const_ref sle, beast::Journal j, std::int32_t ownerCountAdj = 0); + +/** Increase owner-count fields when the caller supplies the sponsor. + * + * This helper does not create a ledger object. It updates reserve accounting + * after the caller has created/updated an object. + * If sponsorSle is provided, this also adjusts the account's sponsored count + * and the sponsor's sponsoring count. + * + * @param view The apply view for making changes + * @param accountSle The account's ledger entry + * @param sponsorSle The sponsor's ledger entry (if applicable) + * @param count Amount to add to the owner count + * @param j Journal for logging + */ void -adjustOwnerCount(ApplyView& view, SLE::ref sle, std::int32_t amount, beast::Journal j); +increaseOwnerCount( + ApplyView& view, + SLE::ref accountSle, + SLE::ref sponsorSle, + std::uint32_t count, + beast::Journal j); + +/** Increase owner-count fields, deriving the tx reserve sponsor internally. + * + * Equivalent to the overload above, but resolves the sponsor via + * getEffectiveTxReserveSponsor(ctx, accountSle) instead of taking it explicitly. Use + * this when the sponsor is the transaction's reserve sponsor for accountSle + * (the common create path). Deletion paths, which derive the sponsor from an + * object's sfSponsor field, should keep using the explicit overload. + * + * @param ctx The apply-view context (view + tx) + * @param accountSle The account's ledger entry + * @param count Amount to add to the owner count + * @param j Journal for logging + */ +void +increaseOwnerCount( + ApplyViewContext ctx, + SLE::ref accountSle, + std::uint32_t count, + beast::Journal j); + +/** Convenience overload that accepts AccountID instead of SLE references. + * + * @param view The apply view for making changes + * @param account The account ID + * @param sponsor The optional sponsor account ID + * @param count Amount to add to the owner count + * @param j Journal for logging + */ +inline void +increaseOwnerCount( + ApplyView& view, + AccountID const& account, + std::optional const& sponsor, + std::uint32_t count, + beast::Journal j) +{ + increaseOwnerCount( + view, + view.peek(keylet::account(account)), + sponsor ? view.peek(keylet::account(*sponsor)) : SLE::pointer(), + count, + j); +} + +/** Decrease owner-count fields when the caller supplies the sponsor. + * + * This helper does not delete a ledger object. It updates reserve accounting + * after the caller has removed an owner-counted reserve, or for special + * owner-count changes whose sponsor cannot be derived from an object's + * sfSponsor field. + * + * @param view The apply view for making changes + * @param accountSle The account's ledger entry + * @param sponsorSle The sponsor's ledger entry (if applicable) + * @param count Amount to remove from the owner count + * @param j Journal for logging + */ +void +decreaseOwnerCount( + ApplyView& view, + SLE::ref accountSle, + SLE::ref sponsorSle, + std::uint32_t count, + beast::Journal j); + +/** Convenience overload that accepts AccountID instead of SLE references. + * + * @param view The apply view for making changes + * @param account The account ID + * @param sponsor The optional sponsor account ID + * @param count Amount to remove from the owner count + * @param j Journal for logging + */ +inline void +decreaseOwnerCount( + ApplyView& view, + AccountID const& account, + std::optional const& sponsor, + std::uint32_t count, + beast::Journal j) +{ + decreaseOwnerCount( + view, + view.peek(keylet::account(account)), + sponsor ? view.peek(keylet::account(*sponsor)) : SLE::pointer(), + count, + j); +} + +/** Decrease owner-count fields for an existing ledger object. + * + * This helper derives the reserve sponsor from objectSle's sfSponsor field, + * then updates the same owner-count fields as decreaseOwnerCount. Use this + * when removing an existing object whose reserve sponsor is stored on that + * object. + * + * @param view The apply view for making changes + * @param accountSle The account's ledger entry + * @param objectSle The object's ledger entry + * @param count Amount to remove from the owner count + * @param j Journal for logging + */ +void +decreaseOwnerCountForObject( + ApplyView& view, + SLE::ref accountSle, + SLE::ref objectSle, + std::uint32_t count, + beast::Journal j); + +/** Convenience overload that accepts AccountID instead of account SLE reference. + * + * @param view The apply view for making changes + * @param account The account ID + * @param objectSle The object's ledger entry + * @param count Amount to remove from the owner count + * @param j Journal for logging + */ +inline void +decreaseOwnerCountForObject( + ApplyView& view, + AccountID const& account, + SLE::ref objectSle, + std::uint32_t count, + beast::Journal j) +{ + SLE::ref accountSle = view.peek(keylet::account(account)); + decreaseOwnerCountForObject(view, accountSle, objectSle, count, j); +} + +/** Adjust a LoanBroker's owner count. + * + * A LoanBroker's sfOwnerCount tracks the number of outstanding loans on + * that broker; it is not a reserve-backed owner count and is distinct + * from the broker's pseudo-account's owner count. Loans can never carry a + * reserve sponsor (LoanSet rejects reserve sponsorship at preflight), so + * this never involves sponsor accounting and never invokes the + * ownerCountHook used for ACCOUNT_ROOT reserve tracking. + * + * @param view The apply view for making changes + * @param brokerSle The LoanBroker's ledger entry + * @param delta Amount to add (positive) or remove (negative) from the count + * @param j Journal for logging + */ +void +adjustLoanBrokerOwnerCount( + ApplyView& view, + SLE::ref brokerSle, + std::int32_t delta, + beast::Journal j); /** Returns IOU issuer transfer fee as Rate. Rate specifies * the fee as fractions of 1 billion. For example, 1% transfer rate diff --git a/include/xrpl/ledger/helpers/EscrowHelpers.h b/include/xrpl/ledger/helpers/EscrowHelpers.h index d173c33c56..9f54e53769 100644 --- a/include/xrpl/ledger/helpers/EscrowHelpers.h +++ b/include/xrpl/ledger/helpers/EscrowHelpers.h @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -22,17 +23,15 @@ #include #include -#include - namespace xrpl { template TER escrowUnlockApplyHelper( - ApplyView& view, + ApplyViewContext ctx, Rate lockedRate, SLE::ref sleDest, - STAmount const& xrpBalance, + XRPAmount xrpBalance, STAmount const& amount, AccountID const& issuer, AccountID const& sender, @@ -43,10 +42,10 @@ escrowUnlockApplyHelper( template <> inline TER escrowUnlockApplyHelper( - ApplyView& view, + ApplyViewContext ctx, Rate lockedRate, SLE::ref sleDest, - STAmount const& xrpBalance, + XRPAmount xrpBalance, STAmount const& amount, AccountID const& issuer, AccountID const& sender, @@ -66,16 +65,26 @@ escrowUnlockApplyHelper( if (receiverIssuer) return tesSUCCESS; - if (!view.exists(trustLineKey) && createAsset) + if (!ctx.view.exists(trustLineKey) && createAsset) { // Can the account cover the trust line's reserve? - if (std::uint32_t const ownerCount = {sleDest->at(sfOwnerCount)}; - xrpBalance < view.fees().accountReserve(ownerCount + 1)) + auto const sponsorSle = getEffectiveTxReserveSponsor(ctx, sleDest); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + + if (auto const ret = checkReserve( + ctx, + sleDest, + xrpBalance, + *sponsorSle, + {.ownerCountDelta = 1}, + journal, + tecNO_LINE_INSUF_RESERVE); + !isTesSuccess(ret)) { JLOG(journal.trace()) << "Trust line does not exist. " "Insufficient reserve to create line."; - - return tecNO_LINE_INSUF_RESERVE; + return ret; } Currency const currency = issue.currency; @@ -83,7 +92,7 @@ escrowUnlockApplyHelper( initialBalance.get().account = noAccount(); if (TER const ter = trustCreate( - view, // payment sandbox + ctx.view, // payment sandbox recvLow, // is dest low? issuer, // source receiver, // destination @@ -97,19 +106,20 @@ escrowUnlockApplyHelper( Issue(currency, receiver), // limit of zero 0, // quality in 0, // quality out + *sponsorSle, // sponsor journal); // journal !isTesSuccess(ter)) { return ter; // LCOV_EXCL_LINE } - view.update(sleDest); + ctx.view.update(sleDest); } - if (!view.exists(trustLineKey) && !receiverIssuer) + if (!ctx.view.exists(trustLineKey) && !receiverIssuer) return tecNO_LINE; - auto const xferRate = transferRate(view, amount); + auto const xferRate = transferRate(ctx.view, amount); // update if issuer rate is less than locked rate if (xferRate < lockedRate) lockedRate = xferRate; @@ -137,7 +147,7 @@ escrowUnlockApplyHelper( // of the funds if (!createAsset) { - auto const sleRippleState = view.peek(trustLineKey); + auto const sleRippleState = ctx.view.peek(trustLineKey); if (!sleRippleState) return tecINTERNAL; // LCOV_EXCL_LINE @@ -163,7 +173,7 @@ escrowUnlockApplyHelper( // if destination is not the issuer then transfer funds if (!receiverIssuer) { - auto const ter = directSendNoFee(view, issuer, receiver, finalAmt, true, journal); + auto const ter = directSendNoFee(ctx.view, issuer, receiver, finalAmt, true, journal); if (!isTesSuccess(ter)) return ter; // LCOV_EXCL_LINE } @@ -173,10 +183,10 @@ escrowUnlockApplyHelper( template <> inline TER escrowUnlockApplyHelper( - ApplyView& view, + ApplyViewContext ctx, Rate lockedRate, SLE::ref sleDest, - STAmount const& xrpBalance, + XRPAmount xrpBalance, STAmount const& amount, AccountID const& issuer, AccountID const& sender, @@ -189,27 +199,32 @@ escrowUnlockApplyHelper( auto const mptID = amount.get().getMptID(); auto const issuanceKey = keylet::mptokenIssuance(mptID); - if (!view.exists(keylet::mptoken(issuanceKey.key, receiver)) && createAsset && !receiverIssuer) + auto const mptKeylet = keylet::mptoken(issuanceKey.key, receiver); + if (!ctx.view.exists(mptKeylet) && createAsset && !receiverIssuer) { - if (std::uint32_t const ownerCount = {sleDest->at(sfOwnerCount)}; - xrpBalance < view.fees().accountReserve(ownerCount + 1)) - { - return tecINSUFFICIENT_RESERVE; - } + auto const sponsorSle = getEffectiveTxReserveSponsor(ctx, sleDest); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE - if (auto const ter = createMPToken(view, mptID, receiver, 0); !isTesSuccess(ter)) + if (auto const ret = checkReserve( + ctx, sleDest, xrpBalance, *sponsorSle, {.ownerCountDelta = 1}, journal); + !isTesSuccess(ret)) + return ret; + + if (auto const ter = createMPToken(ctx.view, mptID, receiver, *sponsorSle, 0); + !isTesSuccess(ter)) { return ter; // LCOV_EXCL_LINE } // update owner count. - adjustOwnerCount(view, sleDest, 1, journal); + increaseOwnerCount(ctx.view, sleDest, *sponsorSle, 1, journal); } - if (!view.exists(keylet::mptoken(issuanceKey.key, receiver)) && !receiverIssuer) + if (!ctx.view.exists(mptKeylet) && !receiverIssuer) return tecNO_PERMISSION; - auto const xferRate = transferRate(view, amount); + auto const xferRate = transferRate(ctx.view, amount); // update if issuer rate is less than locked rate if (xferRate < lockedRate) lockedRate = xferRate; @@ -232,11 +247,11 @@ escrowUnlockApplyHelper( finalAmt = amount.value() - xferFee; } return unlockEscrowMPT( - view, + ctx.view, sender, receiver, finalAmt, - view.rules().enabled(fixTokenEscrowV1) ? amount : finalAmt, + ctx.view.rules().enabled(fixTokenEscrowV1) ? amount : finalAmt, journal); } diff --git a/include/xrpl/ledger/helpers/MPTokenHelpers.h b/include/xrpl/ledger/helpers/MPTokenHelpers.h index a725871231..8a2a4a5b84 100644 --- a/include/xrpl/ledger/helpers/MPTokenHelpers.h +++ b/include/xrpl/ledger/helpers/MPTokenHelpers.h @@ -86,7 +86,7 @@ canAddHolding(ReadView const& view, MPTIssue const& mptIssue); [[nodiscard]] TER authorizeMPToken( - ApplyView& view, + ApplyViewContext ctx, XRPAmount const& priorBalance, MPTID const& mptIssuanceID, AccountID const& account, @@ -117,7 +117,7 @@ requireAuth( */ [[nodiscard]] TER enforceMPTokenAuthorization( - ApplyView& view, + ApplyViewContext ctx, MPTID const& mptIssuanceID, AccountID const& account, XRPAmount const& priorBalance, @@ -203,7 +203,7 @@ canMPTTradeAndTransfer( [[nodiscard]] TER addEmptyHolding( - ApplyView& view, + ApplyViewContext ctx, AccountID const& accountID, XRPAmount priorBalance, MPTIssue const& mptIssue, @@ -211,7 +211,7 @@ addEmptyHolding( [[nodiscard]] TER removeEmptyHolding( - ApplyView& view, + ApplyViewContext ctx, AccountID const& accountID, MPTIssue const& mptIssue, beast::Journal journal); @@ -243,6 +243,7 @@ createMPToken( ApplyView& view, MPTID const& mptIssuanceID, AccountID const& account, + SLE::ref sponsorSle, std::uint32_t const flags); TER @@ -250,6 +251,7 @@ checkCreateMPT( xrpl::ApplyView& view, xrpl::MPTIssue const& mptIssue, xrpl::AccountID const& holder, + SLE::ref sponsorSle, beast::Journal j); //------------------------------------------------------------------------------ diff --git a/include/xrpl/ledger/helpers/OracleHelpers.h b/include/xrpl/ledger/helpers/OracleHelpers.h new file mode 100644 index 0000000000..da04618e46 --- /dev/null +++ b/include/xrpl/ledger/helpers/OracleHelpers.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include // IWYU pragma: keep +#include + +#include +#include + +namespace xrpl { + +constexpr std::uint32_t kMinOracleReserveCount = 1; +constexpr std::uint32_t kMaxOracleReserveCount = 2; +constexpr std::size_t kOracleReserveCountThreshold = 5; + +template + requires requires(T const& t) { t.size(); } +inline std::uint32_t +calculateOracleReserve(T const& priceDataSeries) +{ + return priceDataSeries.size() > kOracleReserveCountThreshold ? kMaxOracleReserveCount + : kMinOracleReserveCount; +} + +inline std::uint32_t +calculateOracleReserve(SLE::const_ref oracleSle) +{ + return calculateOracleReserve(oracleSle->getFieldArray(sfPriceDataSeries)); +} + +} // namespace xrpl diff --git a/include/xrpl/ledger/helpers/RippleStateHelpers.h b/include/xrpl/ledger/helpers/RippleStateHelpers.h index ab09b931cc..398ec86bb7 100644 --- a/include/xrpl/ledger/helpers/RippleStateHelpers.h +++ b/include/xrpl/ledger/helpers/RippleStateHelpers.h @@ -156,6 +156,7 @@ trustCreate( // Issuer should be the account being set. std::uint32_t uQualityIn, std::uint32_t uQualityOut, + SLE::ref sponsorSle, beast::Journal j); [[nodiscard]] TER @@ -178,6 +179,7 @@ issueIOU( AccountID const& account, STAmount const& amount, Issue const& issue, + SLE::ref sponsorSle, beast::Journal j); [[nodiscard]] TER @@ -235,7 +237,7 @@ canTransfer(ReadView const& view, Issue const& issue, AccountID const& from, Acc /// canAddHolding() in preflight with the same View and Asset [[nodiscard]] TER addEmptyHolding( - ApplyView& view, + ApplyViewContext ctx, AccountID const& accountID, XRPAmount priorBalance, Issue const& issue, @@ -243,7 +245,7 @@ addEmptyHolding( [[nodiscard]] TER removeEmptyHolding( - ApplyView& view, + ApplyViewContext ctx, AccountID const& accountID, Issue const& issue, beast::Journal journal); diff --git a/include/xrpl/ledger/helpers/SponsorHelpers.h b/include/xrpl/ledger/helpers/SponsorHelpers.h new file mode 100644 index 0000000000..fd2315b024 --- /dev/null +++ b/include/xrpl/ledger/helpers/SponsorHelpers.h @@ -0,0 +1,184 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace xrpl { + +/** Whether the given transaction type may use reserve sponsorship (v1). + * + * Reserve sponsorship is restricted to an explicit allow-list of transaction + * types; all others reject spfSponsorReserve at preflight. + */ +bool +isReserveSponsorAllowed(TxType txType); + +/** Whether the transaction's fee is sponsored (sfSponsor present + spfSponsorFee set). */ +inline bool +isFeeSponsored(STTx const& tx) +{ + return tx.isFieldPresent(sfSponsor) && ((tx.getFieldU32(sfSponsorFlags) & spfSponsorFee) != 0u); +} + +/** Whether the transaction's reserve is sponsored (sfSponsor present + spfSponsorReserve set). */ +inline bool +isReserveSponsored(STTx const& tx) +{ + return tx.isFieldPresent(sfSponsor) && + ((tx.getFieldU32(sfSponsorFlags) & spfSponsorReserve) != 0u); +} + +/** Return the AccountID of the transaction's reserve sponsor, or nullopt if unsponsored. */ +std::optional +getTxReserveSponsorID(STTx const& tx); + +/** Return a mutable SLE for the transaction's reserve sponsor account. + * + * @param ctx The apply-view context (view + tx) + * @return The sponsor account SLE, a null pointer if the tx is not + * reserve-sponsored, or tecINTERNAL if the sponsor account cannot + * be loaded (an already-checked invariant). + */ +std::expected +getTxReserveSponsor(ApplyViewContext ctx); + +/** Return a read-only SLE for the transaction's reserve sponsor account. + * + * @param view The ledger read view + * @param tx The transaction to inspect + * @return The sponsor account SLE, a null pointer if the tx is not + * reserve-sponsored, or tecINTERNAL if the sponsor account cannot + * be loaded (an already-checked invariant). + */ +std::expected +getTxReserveSponsor(ReadView const& view, STTx const& tx); + +/** The transaction's reserve sponsor for the given account, if applicable. + * + * A reserve sponsor only covers the transaction submitter's own objects, so + * this returns the tx reserve sponsor SLE only when accountSle is the tx's own + * (non-pseudo) account; otherwise it returns a null sponsor pointer. This is + * the single source of truth for the "sponsor applies to tx.Account only" rule + * that the sponsor-deriving helper overloads in AccountRootHelpers rely on. + * + * @param ctx The apply-view context (view + tx) + * @param accountSle The account whose sponsor is being resolved + * @return The sponsor SLE (nullptr if unsponsored), or tecINTERNAL if the + * sponsor account cannot be loaded (an already-checked invariant) + */ +[[nodiscard]] std::expected +getEffectiveTxReserveSponsor(ApplyViewContext ctx, SLE::const_ref accountSle); + +/** Return the AccountID stored in the given sponsor field of a ledger entry, or nullopt if absent. + */ +std::optional +getLedgerEntryReserveSponsorID(SLE::const_ref sle, SF_ACCOUNT const& field = sfSponsor); + +/** Return a mutable SLE for the reserve sponsor recorded on a ledger entry. + * + * Reads the sponsor AccountID from @p field on @p sle and peeks the + * corresponding account root in @p view. + * + * @param view The mutable apply view + * @param sle The ledger entry whose sponsor field is inspected + * @param field The field that holds the sponsor AccountID (defaults to sfSponsor) + * @return The sponsor account SLE, or a null pointer if the entry is unsponsored. + */ +SLE::pointer +getLedgerEntryReserveSponsor( + ApplyView& view, + SLE::const_ref sle, + SF_ACCOUNT const& field = sfSponsor); + +/** Stamp a reserve sponsor onto a ledger entry using an explicit sponsor SLE. + * + * Sets @p field on @p sle to the AccountID from @p sponsorSle. A no-op when + * @p sponsorSle is null (unsponsored). For RippleState entries the field must + * be sfHighSponsor or sfLowSponsor; for all other entry types it must be + * sfSponsor. + * + * @param sle The ledger entry to stamp + * @param sponsorSle The sponsor's account root SLE (null → no-op) + * @param field The sponsor field to set (defaults to sfSponsor) + */ +void +addSponsorToLedgerEntry( + SLE::ref sle, + SLE::const_ref sponsorSle, + SF_ACCOUNT const& field = sfSponsor); + +/** Stamp the transaction's reserve sponsor onto a newly-created ledger entry. + * + * Equivalent to the overload above, but resolves the sponsor via + * getTxReserveSponsor(ctx) instead of taking it explicitly. A no-op when the + * transaction is not reserve-sponsored. The entry is assumed to be owned by + * the transaction submitter, which is the only account a tx reserve sponsor + * can cover. + */ +void +addSponsorToLedgerEntry(ApplyViewContext ctx, SLE::ref sle, SF_ACCOUNT const& field = sfSponsor); + +/** Remove the reserve sponsor field from a ledger entry. + * + * A no-op when @p field is not present on @p sle. For RippleState entries + * the field must be sfHighSponsor or sfLowSponsor; for all other entry types + * it must be sfSponsor. + * + * @param sle The ledger entry to modify + * @param field The sponsor field to clear (defaults to sfSponsor) + */ +void +removeSponsorFromLedgerEntry(SLE::ref sle, SF_ACCOUNT const& field = sfSponsor); + +/** Whether @p account is the owner of a ledger entry for sponsorship purposes. + * + * Ownership rules vary by entry type. For RippleState entries the owner is + * whichever side of the trust line holds the reserve. For credentials, the + * owner is the subject once accepted and the issuer before acceptance. + * + * @param view The ledger read view (used for SignerList lookup) + * @param sle The ledger entry whose owner is checked + * @param account The candidate account to match against + * @return true if @p account owns @p sle, false otherwise. + */ +bool +isLedgerEntryOwner(ReadView const& view, SLE const& sle, AccountID const& account); + +/** Whether this ledger entry type can have a reserve sponsor attached to it. */ +bool +isLedgerEntrySupportedBySponsorship(SLE const& sle); + +/** Return the number of owner-count units the ledger entry consumes. + * + * Most entries cost 1. Exceptions: Oracles scale with their price-data series + * size, Vaults cost 2 (vault + pseudo-account), and legacy SignerList entries + * (pre-MultiSignReserve) cost 2 + signer count. + */ +std::uint32_t +getLedgerEntryOwnerCount(SLE const& sle); + +/** Return the SField used to store the reserve sponsor for @p owner on @p sle. + * + * For most entry types this is sfSponsor. RippleState entries use + * sfHighSponsor or sfLowSponsor depending on which side of the trust line + * @p owner holds. + * + * @param sle The ledger entry + * @param owner The account whose sponsor field is needed + * @return sfHighSponsor, sfLowSponsor, or sfSponsor as appropriate. + */ +SF_ACCOUNT const& +getLedgerEntrySponsorField(SLE const& sle, AccountID const& owner); + +} // namespace xrpl diff --git a/include/xrpl/ledger/helpers/TokenHelpers.h b/include/xrpl/ledger/helpers/TokenHelpers.h index 32f785a0d6..33d9761d84 100644 --- a/include/xrpl/ledger/helpers/TokenHelpers.h +++ b/include/xrpl/ledger/helpers/TokenHelpers.h @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -302,7 +303,7 @@ canAddHolding(ReadView const& view, Asset const& asset); [[nodiscard]] TER addEmptyHolding( - ApplyView& view, + ApplyViewContext ctx, AccountID const& accountID, XRPAmount priorBalance, Asset const& asset, @@ -310,7 +311,7 @@ addEmptyHolding( [[nodiscard]] TER removeEmptyHolding( - ApplyView& view, + ApplyViewContext ctx, AccountID const& accountID, Asset const& asset, beast::Journal journal); @@ -371,6 +372,7 @@ accountSend( AccountID const& to, STAmount const& saAmount, beast::Journal j, + SLE::ref sponsorSle = {}, WaiveTransferFee waiveFee = WaiveTransferFee::No, AllowMPTOverflow allowOverflow = AllowMPTOverflow::No); diff --git a/include/xrpl/protocol/Book.h b/include/xrpl/protocol/Book.h index 476bdba35a..92d1353929 100644 --- a/include/xrpl/protocol/Book.h +++ b/include/xrpl/protocol/Book.h @@ -74,9 +74,9 @@ operator==(Book const& lhs, Book const& rhs) [[nodiscard]] constexpr std::weak_ordering operator<=>(Book const& lhs, Book const& rhs) { - if (auto const c{lhs.in <=> rhs.in}; c != 0) + if (auto const c{lhs.in <=> rhs.in}; c != 0) // NOLINT(modernize-use-nullptr) return c; - if (auto const c{lhs.out <=> rhs.out}; c != 0) + if (auto const c{lhs.out <=> rhs.out}; c != 0) // NOLINT(modernize-use-nullptr) return c; // Manually compare optionals diff --git a/include/xrpl/protocol/Fees.h b/include/xrpl/protocol/Fees.h index 4e79204f4c..2f9159cd9b 100644 --- a/include/xrpl/protocol/Fees.h +++ b/include/xrpl/protocol/Fees.h @@ -2,7 +2,6 @@ #include -#include #include namespace xrpl { @@ -39,13 +38,13 @@ struct Fees /** Returns the account reserve given the owner count, in drops. - The reserve is calculated as the reserve base plus - the reserve increment times the number of increments. + The reserve is calculated as the reserve base times the number of accounts plus the reserve + increment times the number of increments. */ [[nodiscard]] XRPAmount - accountReserve(std::size_t ownerCount) const + accountReserve(std::uint32_t ownerCount, std::uint32_t accountCount) const { - return reserve + ownerCount * increment; + return (reserve * accountCount) + (increment * ownerCount); } }; diff --git a/include/xrpl/protocol/Indexes.h b/include/xrpl/protocol/Indexes.h index 053a66787f..79ecd05ad5 100644 --- a/include/xrpl/protocol/Indexes.h +++ b/include/xrpl/protocol/Indexes.h @@ -142,6 +142,10 @@ ticket(uint256 const& key) Keylet signerList(AccountID const& account) noexcept; +/** A Sponsorship */ +Keylet +sponsorship(AccountID const& sponsor, AccountID const& sponsee) noexcept; + /** A Check */ /** @{ */ Keylet diff --git a/include/xrpl/protocol/Issue.h b/include/xrpl/protocol/Issue.h index 3d556e83eb..a4980404cd 100644 --- a/include/xrpl/protocol/Issue.h +++ b/include/xrpl/protocol/Issue.h @@ -84,7 +84,7 @@ operator==(Issue const& lhs, Issue const& rhs) [[nodiscard]] constexpr std::weak_ordering operator<=>(Issue const& lhs, Issue const& rhs) { - if (auto const c{lhs.currency <=> rhs.currency}; c != 0) + if (auto const c{lhs.currency <=> rhs.currency}; c != 0) // NOLINT(modernize-use-nullptr) return c; if (isXRP(lhs.currency)) diff --git a/include/xrpl/protocol/LedgerFormats.h b/include/xrpl/protocol/LedgerFormats.h index 5b8a8cc2c5..142e5763be 100644 --- a/include/xrpl/protocol/LedgerFormats.h +++ b/include/xrpl/protocol/LedgerFormats.h @@ -208,7 +208,11 @@ enum LedgerEntryType : std::uint16_t { LEDGER_OBJECT(Loan, \ LSF_FLAG(lsfLoanDefault, 0x00010000) \ LSF_FLAG(lsfLoanImpaired, 0x00020000) \ - LSF_FLAG(lsfLoanOverpayment, 0x00040000)) /* True, loan allows overpayments */ + LSF_FLAG(lsfLoanOverpayment, 0x00040000)) /* True, loan allows overpayments */ \ + \ + LEDGER_OBJECT(Sponsorship, \ + LSF_FLAG(lsfSponsorshipRequireSignForFee, 0x00010000) \ + LSF_FLAG(lsfSponsorshipRequireSignForReserve, 0x00020000)) // clang-format on diff --git a/include/xrpl/protocol/STObject.h b/include/xrpl/protocol/STObject.h index fe5c611648..9abf56e91b 100644 --- a/include/xrpl/protocol/STObject.h +++ b/include/xrpl/protocol/STObject.h @@ -229,10 +229,10 @@ public: [[nodiscard]] AccountID getAccountID(SField const& field) const; - /** The account responsible for the fee and authorization: the delegate when + /** The account responsible for the authorization: the delegate when sfDelegate is present, otherwise the account. */ [[nodiscard]] AccountID - getFeePayer() const; + getInitiator() const; [[nodiscard]] Blob getFieldVL(SField const& field) const; diff --git a/include/xrpl/protocol/STTx.h b/include/xrpl/protocol/STTx.h index b36207bf61..5d5424e623 100644 --- a/include/xrpl/protocol/STTx.h +++ b/include/xrpl/protocol/STTx.h @@ -141,6 +141,9 @@ public: [[nodiscard]] std::vector const& getBatchTransactionIDs() const; + [[nodiscard]] AccountID + getFeePayerID() const; + private: /** Check the signature. @param rules The current ledger rules. diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index 54b081f358..730d021254 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -225,6 +225,7 @@ enum TERcodes : TERUnderlyingType { // create a pseudo-account terNO_DELEGATE_PERMISSION, // Delegate does not have permission terLOCKED, // MPT is locked + terNO_PERMISSION, // No permission but retry }; //------------------------------------------------------------------------------ @@ -368,6 +369,7 @@ enum TECcodes : TERUnderlyingType { // reclaimed after those networks reset. tecNO_DELEGATE_PERMISSION = 198, tecBAD_PROOF = 199, + tecNO_SPONSOR_PERMISSION = 200, }; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index 461afd24e7..1d4f33a39d 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -102,7 +102,8 @@ inline constexpr FlagValue tfUniversalMask = ~tfUniversal; TRANSACTION(Payment, \ TF_FLAG(tfNoRippleDirect, 0x00010000) \ TF_FLAG(tfPartialPayment, 0x00020000) \ - TF_FLAG(tfLimitQuality, 0x00040000), \ + TF_FLAG(tfLimitQuality, 0x00040000) \ + TF_FLAG(tfSponsorCreatedAccount, 0x00080000), \ MASK_ADJ(0)) \ \ TRANSACTION(TrustSet, \ @@ -141,7 +142,7 @@ inline constexpr FlagValue tfUniversalMask = ~tfUniversal; TF_FLAG(tfMPTCanTrade, lsfMPTCanTrade) \ TF_FLAG(tfMPTCanTransfer, lsfMPTCanTransfer) \ TF_FLAG(tfMPTCanClawback, lsfMPTCanClawback) \ - TF_FLAG(tfMPTCanHoldConfidentialBalance, lsfMPTCanHoldConfidentialBalance), \ + TF_FLAG(tfMPTCanHoldConfidentialBalance, lsfMPTCanHoldConfidentialBalance), \ MASK_ADJ(0)) \ \ TRANSACTION(MPTokenAuthorize, \ @@ -215,6 +216,20 @@ inline constexpr FlagValue tfUniversalMask = ~tfUniversal; TF_FLAG(tfLoanDefault, 0x00010000) \ TF_FLAG(tfLoanImpair, 0x00020000) \ TF_FLAG(tfLoanUnimpair, 0x00040000), \ + MASK_ADJ(0)) \ + \ + TRANSACTION(SponsorshipSet, \ + TF_FLAG(tfSponsorshipSetRequireSignForFee, 0x00010000) \ + TF_FLAG(tfSponsorshipClearRequireSignForFee, 0x00020000) \ + TF_FLAG(tfSponsorshipSetRequireSignForReserve, 0x00040000) \ + TF_FLAG(tfSponsorshipClearRequireSignForReserve, 0x00080000) \ + TF_FLAG(tfDeleteObject, 0x00100000), \ + MASK_ADJ(0)) \ + \ + TRANSACTION(SponsorshipTransfer, \ + TF_FLAG(tfSponsorshipEnd, 0x00010000) \ + TF_FLAG(tfSponsorshipCreate, 0x00020000) \ + TF_FLAG(tfSponsorshipReassign, 0x00040000), \ MASK_ADJ(0)) // clang-format on @@ -444,6 +459,12 @@ getAsfFlagMap() #pragma pop_macro("ACCOUNTSET_FLAG_TO_MAP") #pragma pop_macro("ACCOUNTSET_FLAGS") +// Sponsor flags (spf) + +inline constexpr FlagValue spfSponsorFee = 1; +inline constexpr FlagValue spfSponsorReserve = 2; +inline constexpr FlagValue spfSponsorFlagMask = ~(spfSponsorFee | spfSponsorReserve); + } // namespace xrpl // NOLINTEND(readability-identifier-naming) diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 3f078561a3..bfe03a6303 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -15,6 +15,7 @@ // Add new amendments to the top of this list. // Keep it sorted in reverse chronological order. +XRPL_FEATURE(Sponsor, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(BatchV1_1, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(LendingProtocolV1_1, Supported::No, VoteBehavior::DefaultNo) XRPL_FEATURE(ConfidentialTransfer, Supported::Yes, VoteBehavior::DefaultNo) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 2f403708c3..90810e06d2 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -127,29 +127,32 @@ LEDGER_ENTRY(ltTICKET, 0x0054, Ticket, ticket, ({ \sa keylet::account */ LEDGER_ENTRY(ltACCOUNT_ROOT, 0x0061, AccountRoot, account, ({ - {sfAccount, SoeRequired}, - {sfSequence, SoeRequired}, - {sfBalance, SoeRequired}, - {sfOwnerCount, SoeRequired}, - {sfPreviousTxnID, SoeRequired}, - {sfPreviousTxnLgrSeq, SoeRequired}, - {sfAccountTxnID, SoeOptional}, - {sfRegularKey, SoeOptional}, - {sfEmailHash, SoeOptional}, - {sfWalletLocator, SoeOptional}, - {sfWalletSize, SoeOptional}, - {sfMessageKey, SoeOptional}, - {sfTransferRate, SoeOptional}, - {sfDomain, SoeOptional}, - {sfTickSize, SoeOptional}, - {sfTicketCount, SoeOptional}, - {sfNFTokenMinter, SoeOptional}, - {sfMintedNFTokens, SoeDefault}, - {sfBurnedNFTokens, SoeDefault}, - {sfFirstNFTokenSequence, SoeOptional}, - {sfAMMID, SoeOptional}, // pseudo-account designator - {sfVaultID, SoeOptional}, // pseudo-account designator - {sfLoanBrokerID, SoeOptional}, // pseudo-account designator + {sfAccount, SoeRequired}, + {sfSequence, SoeRequired}, + {sfBalance, SoeRequired}, + {sfOwnerCount, SoeRequired}, + {sfPreviousTxnID, SoeRequired}, + {sfPreviousTxnLgrSeq, SoeRequired}, + {sfAccountTxnID, SoeOptional}, + {sfRegularKey, SoeOptional}, + {sfEmailHash, SoeOptional}, + {sfWalletLocator, SoeOptional}, + {sfWalletSize, SoeOptional}, + {sfMessageKey, SoeOptional}, + {sfTransferRate, SoeOptional}, + {sfDomain, SoeOptional}, + {sfTickSize, SoeOptional}, + {sfTicketCount, SoeOptional}, + {sfNFTokenMinter, SoeOptional}, + {sfMintedNFTokens, SoeDefault}, + {sfBurnedNFTokens, SoeDefault}, + {sfFirstNFTokenSequence, SoeOptional}, + {sfSponsoredOwnerCount, SoeDefault}, + {sfSponsoringOwnerCount, SoeDefault}, + {sfSponsoringAccountCount, SoeDefault}, + {sfAMMID, SoeOptional}, // pseudo-account designator + {sfVaultID, SoeOptional}, // pseudo-account designator + {sfLoanBrokerID, SoeOptional}, // pseudo-account designator })) /** A ledger object which contains a list of object identifiers. @@ -286,6 +289,8 @@ LEDGER_ENTRY(ltRIPPLE_STATE, 0x0072, RippleState, state, ({ {sfHighNode, SoeOptional}, {sfHighQualityIn, SoeOptional}, {sfHighQualityOut, SoeOptional}, + {sfHighSponsor, SoeOptional}, + {sfLowSponsor, SoeOptional}, })) /** The ledger object which lists the network's fee settings. @@ -616,5 +621,20 @@ LEDGER_ENTRY(ltLOAN, 0x0089, Loan, loan, ({ {sfLoanScale, SoeDefault}, })) +/** A ledger object representing a sponsorship. + \sa keylet::sponsorship + */ +LEDGER_ENTRY(ltSPONSORSHIP, 0x0090, Sponsorship, sponsorship, ({ + {sfPreviousTxnID, SoeRequired}, + {sfPreviousTxnLgrSeq, SoeRequired}, + {sfOwner, SoeRequired}, + {sfSponsee, SoeRequired}, + {sfFeeAmount, SoeOptional}, + {sfMaxFee, SoeOptional}, + {sfRemainingOwnerCount, SoeDefault}, + {sfOwnerNode, SoeRequired}, + {sfSponseeNode, SoeRequired}, +})) + #undef EXPAND #undef LEDGER_ENTRY_DUPLICATE diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 0d453eea11..4ef76c8b75 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -114,6 +114,11 @@ TYPED_SFIELD(sfLateInterestRate, UINT32, 66) // 1/10 basis points (bi TYPED_SFIELD(sfCloseInterestRate, UINT32, 67) // 1/10 basis points (bips) TYPED_SFIELD(sfOverpaymentInterestRate, UINT32, 68) // 1/10 basis points (bips) TYPED_SFIELD(sfConfidentialBalanceVersion, UINT32, 69) +TYPED_SFIELD(sfSponsoredOwnerCount, UINT32, 70) +TYPED_SFIELD(sfSponsoringOwnerCount, UINT32, 71) +TYPED_SFIELD(sfSponsoringAccountCount, UINT32, 72) +TYPED_SFIELD(sfRemainingOwnerCount, UINT32, 73) +TYPED_SFIELD(sfSponsorFlags, UINT32, 74) // 64-bit integers (common) TYPED_SFIELD(sfIndexNext, UINT64, 1) @@ -148,6 +153,7 @@ TYPED_SFIELD(sfLockedAmount, UINT64, 29, SField::kSmdBaseTen|SFie TYPED_SFIELD(sfVaultNode, UINT64, 30) TYPED_SFIELD(sfLoanBrokerNode, UINT64, 31) TYPED_SFIELD(sfConfidentialOutstandingAmount, UINT64, 32, SField::kSmdBaseTen|SField::kSmdDefault) +TYPED_SFIELD(sfSponseeNode, UINT64, 33) // 128-bit TYPED_SFIELD(sfEmailHash, UINT128, 1) @@ -209,6 +215,7 @@ TYPED_SFIELD(sfLoanBrokerID, UINT256, 37, TYPED_SFIELD(sfLoanID, UINT256, 38) TYPED_SFIELD(sfReferenceHolding, UINT256, 39) TYPED_SFIELD(sfBlindingFactor, UINT256, 40) +TYPED_SFIELD(sfObjectID, UINT256, 41) // number (common) TYPED_SFIELD(sfNumber, NUMBER, 1) @@ -268,6 +275,8 @@ TYPED_SFIELD(sfPrice, AMOUNT, 28) TYPED_SFIELD(sfSignatureReward, AMOUNT, 29) TYPED_SFIELD(sfMinAccountCreateAmount, AMOUNT, 30) TYPED_SFIELD(sfLPTokenBalance, AMOUNT, 31) +TYPED_SFIELD(sfFeeAmount, AMOUNT, 32) +TYPED_SFIELD(sfMaxFee, AMOUNT, 33) // variable length (common) TYPED_SFIELD(sfPublicKey, VL, 1) @@ -343,6 +352,11 @@ TYPED_SFIELD(sfIssuingChainDoor, ACCOUNT, 23) TYPED_SFIELD(sfSubject, ACCOUNT, 24) TYPED_SFIELD(sfBorrower, ACCOUNT, 25) TYPED_SFIELD(sfCounterparty, ACCOUNT, 26) +TYPED_SFIELD(sfSponsor, ACCOUNT, 27) +TYPED_SFIELD(sfHighSponsor, ACCOUNT, 28) +TYPED_SFIELD(sfLowSponsor, ACCOUNT, 29) +TYPED_SFIELD(sfCounterpartySponsor, ACCOUNT, 30) +TYPED_SFIELD(sfSponsee, ACCOUNT, 31) // vector of 256-bit TYPED_SFIELD(sfIndexes, VECTOR256, 1, SField::kSmdNever) @@ -407,6 +421,7 @@ UNTYPED_SFIELD(sfRawTransaction, OBJECT, 34) UNTYPED_SFIELD(sfBatchSigner, OBJECT, 35) UNTYPED_SFIELD(sfBook, OBJECT, 36) UNTYPED_SFIELD(sfCounterpartySignature, OBJECT, 37, SField::kSmdDefault, SField::kNotSigning) +UNTYPED_SFIELD(sfSponsorSignature, OBJECT, 38, SField::kSmdDefault, SField::kNotSigning) // array of objects (common) // ARRAY/1 is reserved for end of array diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index b0c4f66bae..e805596c00 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -1165,6 +1165,35 @@ TRANSACTION(ttCONFIDENTIAL_MPT_CLAWBACK, 89, ConfidentialMPTClawback, {sfZKProof, SoeRequired}, })) +/** This transaction transfers sponsorship on an object/account. */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttSPONSORSHIP_TRANSFER, 90, SponsorshipTransfer, + Delegation::NotDelegable, + featureSponsor, + NoPriv, + ({ + {sfObjectID, SoeOptional}, + {sfSponsee, SoeOptional}, +})) + +/** This transaction creates a Sponsorship object. */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttSPONSORSHIP_SET, 91, SponsorshipSet, + Delegation::Delegable, + featureSponsor, + NoPriv, + ({ + {sfCounterpartySponsor, SoeOptional}, + {sfSponsee, SoeOptional}, + {sfFeeAmount, SoeOptional}, + {sfMaxFee, SoeOptional}, + {sfRemainingOwnerCount, SoeOptional}, +})) + /** This system-generated transaction type is used to update the status of the various amendments. For details, see: https://xrpl.org/amendments.html diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index 191ed385f3..b20b71661a 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -558,6 +558,9 @@ JSS(source_account); // in: PathRequest, RipplePathFind JSS(source_amount); // in: PathRequest, RipplePathFind JSS(source_currencies); // in: PathRequest, RipplePathFind JSS(source_tag); // out: AccountChannels +JSS(sponsee); // in: LedgerEntry +JSS(sponsor); // in: LedgerEntry +JSS(sponsored); // in: AccountObjects JSS(stand_alone); // out: NetworkOPs JSS(standard_deviation); // out: get_aggregate_price JSS(start); // in: TxHistory diff --git a/include/xrpl/protocol_autogen/ledger_entries/AccountRoot.h b/include/xrpl/protocol_autogen/ledger_entries/AccountRoot.h index f9a12a027f..725f46437e 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/AccountRoot.h +++ b/include/xrpl/protocol_autogen/ledger_entries/AccountRoot.h @@ -447,6 +447,78 @@ public: return this->sle_->isFieldPresent(sfFirstNFTokenSequence); } + /** + * @brief Get sfSponsoredOwnerCount (SoeDefault) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getSponsoredOwnerCount() const + { + if (hasSponsoredOwnerCount()) + return this->sle_->at(sfSponsoredOwnerCount); + return std::nullopt; + } + + /** + * @brief Check if sfSponsoredOwnerCount is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasSponsoredOwnerCount() const + { + return this->sle_->isFieldPresent(sfSponsoredOwnerCount); + } + + /** + * @brief Get sfSponsoringOwnerCount (SoeDefault) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getSponsoringOwnerCount() const + { + if (hasSponsoringOwnerCount()) + return this->sle_->at(sfSponsoringOwnerCount); + return std::nullopt; + } + + /** + * @brief Check if sfSponsoringOwnerCount is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasSponsoringOwnerCount() const + { + return this->sle_->isFieldPresent(sfSponsoringOwnerCount); + } + + /** + * @brief Get sfSponsoringAccountCount (SoeDefault) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getSponsoringAccountCount() const + { + if (hasSponsoringAccountCount()) + return this->sle_->at(sfSponsoringAccountCount); + return std::nullopt; + } + + /** + * @brief Check if sfSponsoringAccountCount is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasSponsoringAccountCount() const + { + return this->sle_->isFieldPresent(sfSponsoringAccountCount); + } + /** * @brief Get sfAMMID (SoeOptional) * @return The field value, or std::nullopt if not present. @@ -786,6 +858,39 @@ public: return *this; } + /** + * @brief Set sfSponsoredOwnerCount (SoeDefault) + * @return Reference to this builder for method chaining. + */ + AccountRootBuilder& + setSponsoredOwnerCount(std::decay_t const& value) + { + object_[sfSponsoredOwnerCount] = value; + return *this; + } + + /** + * @brief Set sfSponsoringOwnerCount (SoeDefault) + * @return Reference to this builder for method chaining. + */ + AccountRootBuilder& + setSponsoringOwnerCount(std::decay_t const& value) + { + object_[sfSponsoringOwnerCount] = value; + return *this; + } + + /** + * @brief Set sfSponsoringAccountCount (SoeDefault) + * @return Reference to this builder for method chaining. + */ + AccountRootBuilder& + setSponsoringAccountCount(std::decay_t const& value) + { + object_[sfSponsoringAccountCount] = value; + return *this; + } + /** * @brief Set sfAMMID (SoeOptional) * @return Reference to this builder for method chaining. diff --git a/include/xrpl/protocol_autogen/ledger_entries/RippleState.h b/include/xrpl/protocol_autogen/ledger_entries/RippleState.h index e8debfe792..388a062ec1 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/RippleState.h +++ b/include/xrpl/protocol_autogen/ledger_entries/RippleState.h @@ -243,6 +243,54 @@ public: { return this->sle_->isFieldPresent(sfHighQualityOut); } + + /** + * @brief Get sfHighSponsor (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getHighSponsor() const + { + if (hasHighSponsor()) + return this->sle_->at(sfHighSponsor); + return std::nullopt; + } + + /** + * @brief Check if sfHighSponsor is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasHighSponsor() const + { + return this->sle_->isFieldPresent(sfHighSponsor); + } + + /** + * @brief Get sfLowSponsor (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getLowSponsor() const + { + if (hasLowSponsor()) + return this->sle_->at(sfLowSponsor); + return std::nullopt; + } + + /** + * @brief Check if sfLowSponsor is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasLowSponsor() const + { + return this->sle_->isFieldPresent(sfLowSponsor); + } }; /** @@ -410,6 +458,28 @@ public: return *this; } + /** + * @brief Set sfHighSponsor (SoeOptional) + * @return Reference to this builder for method chaining. + */ + RippleStateBuilder& + setHighSponsor(std::decay_t const& value) + { + object_[sfHighSponsor] = value; + return *this; + } + + /** + * @brief Set sfLowSponsor (SoeOptional) + * @return Reference to this builder for method chaining. + */ + RippleStateBuilder& + setLowSponsor(std::decay_t const& value) + { + object_[sfLowSponsor] = value; + return *this; + } + /** * @brief Build and return the completed RippleState wrapper. * @param index The ledger entry index. diff --git a/include/xrpl/protocol_autogen/ledger_entries/Sponsorship.h b/include/xrpl/protocol_autogen/ledger_entries/Sponsorship.h new file mode 100644 index 0000000000..c309a38aef --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/Sponsorship.h @@ -0,0 +1,344 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::ledger_entries { + +class SponsorshipBuilder; + +/** + * @brief Ledger Entry: Sponsorship + * + * Type: ltSPONSORSHIP (0x0090) + * RPC Name: sponsorship + * + * Immutable wrapper around SLE providing type-safe field access. + * Use SponsorshipBuilder to construct new ledger entries. + */ +class Sponsorship : public LedgerEntryBase +{ +public: + static constexpr LedgerEntryType entryType = ltSPONSORSHIP; + + /** + * @brief Construct a Sponsorship ledger entry wrapper from an existing SLE object. + * @throws std::runtime_error if the ledger entry type doesn't match. + */ + explicit Sponsorship(SLE::const_pointer sle) + : LedgerEntryBase(std::move(sle)) + { + // Verify ledger entry type + if (sle_->getType() != entryType) + { + throw std::runtime_error("Invalid ledger entry type for Sponsorship"); + } + } + + // Ledger entry-specific field getters + + /** + * @brief Get sfPreviousTxnID (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT256::type::value_type + getPreviousTxnID() const + { + return this->sle_->at(sfPreviousTxnID); + } + + /** + * @brief Get sfPreviousTxnLgrSeq (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT32::type::value_type + getPreviousTxnLgrSeq() const + { + return this->sle_->at(sfPreviousTxnLgrSeq); + } + + /** + * @brief Get sfOwner (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_ACCOUNT::type::value_type + getOwner() const + { + return this->sle_->at(sfOwner); + } + + /** + * @brief Get sfSponsee (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_ACCOUNT::type::value_type + getSponsee() const + { + return this->sle_->at(sfSponsee); + } + + /** + * @brief Get sfFeeAmount (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getFeeAmount() const + { + if (hasFeeAmount()) + return this->sle_->at(sfFeeAmount); + return std::nullopt; + } + + /** + * @brief Check if sfFeeAmount is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasFeeAmount() const + { + return this->sle_->isFieldPresent(sfFeeAmount); + } + + /** + * @brief Get sfMaxFee (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getMaxFee() const + { + if (hasMaxFee()) + return this->sle_->at(sfMaxFee); + return std::nullopt; + } + + /** + * @brief Check if sfMaxFee is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasMaxFee() const + { + return this->sle_->isFieldPresent(sfMaxFee); + } + + /** + * @brief Get sfRemainingOwnerCount (SoeDefault) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getRemainingOwnerCount() const + { + if (hasRemainingOwnerCount()) + return this->sle_->at(sfRemainingOwnerCount); + return std::nullopt; + } + + /** + * @brief Check if sfRemainingOwnerCount is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasRemainingOwnerCount() const + { + return this->sle_->isFieldPresent(sfRemainingOwnerCount); + } + + /** + * @brief Get sfOwnerNode (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT64::type::value_type + getOwnerNode() const + { + return this->sle_->at(sfOwnerNode); + } + + /** + * @brief Get sfSponseeNode (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT64::type::value_type + getSponseeNode() const + { + return this->sle_->at(sfSponseeNode); + } +}; + +/** + * @brief Builder for Sponsorship ledger entries. + * + * Provides a fluent interface for constructing ledger entries with method chaining. + * Uses STObject internally for flexible ledger entry construction. + * Inherits common field setters from LedgerEntryBuilderBase. + */ +class SponsorshipBuilder : public LedgerEntryBuilderBase +{ +public: + /** + * @brief Construct a new SponsorshipBuilder with required fields. + * @param previousTxnID The sfPreviousTxnID field value. + * @param previousTxnLgrSeq The sfPreviousTxnLgrSeq field value. + * @param owner The sfOwner field value. + * @param sponsee The sfSponsee field value. + * @param ownerNode The sfOwnerNode field value. + * @param sponseeNode The sfSponseeNode field value. + */ + SponsorshipBuilder(std::decay_t const& previousTxnID,std::decay_t const& previousTxnLgrSeq,std::decay_t const& owner,std::decay_t const& sponsee,std::decay_t const& ownerNode,std::decay_t const& sponseeNode) + : LedgerEntryBuilderBase(ltSPONSORSHIP) + { + setPreviousTxnID(previousTxnID); + setPreviousTxnLgrSeq(previousTxnLgrSeq); + setOwner(owner); + setSponsee(sponsee); + setOwnerNode(ownerNode); + setSponseeNode(sponseeNode); + } + + /** + * @brief Construct a SponsorshipBuilder from an existing SLE object. + * @param sle The existing ledger entry to copy from. + * @throws std::runtime_error if the ledger entry type doesn't match. + */ + SponsorshipBuilder(SLE::const_pointer sle) + { + if (sle->at(sfLedgerEntryType) != ltSPONSORSHIP) + { + throw std::runtime_error("Invalid ledger entry type for Sponsorship"); + } + object_ = *sle; + } + + /** @brief Ledger entry-specific field setters */ + + /** + * @brief Set sfPreviousTxnID (SoeRequired) + * @return Reference to this builder for method chaining. + */ + SponsorshipBuilder& + setPreviousTxnID(std::decay_t const& value) + { + object_[sfPreviousTxnID] = value; + return *this; + } + + /** + * @brief Set sfPreviousTxnLgrSeq (SoeRequired) + * @return Reference to this builder for method chaining. + */ + SponsorshipBuilder& + setPreviousTxnLgrSeq(std::decay_t const& value) + { + object_[sfPreviousTxnLgrSeq] = value; + return *this; + } + + /** + * @brief Set sfOwner (SoeRequired) + * @return Reference to this builder for method chaining. + */ + SponsorshipBuilder& + setOwner(std::decay_t const& value) + { + object_[sfOwner] = value; + return *this; + } + + /** + * @brief Set sfSponsee (SoeRequired) + * @return Reference to this builder for method chaining. + */ + SponsorshipBuilder& + setSponsee(std::decay_t const& value) + { + object_[sfSponsee] = value; + return *this; + } + + /** + * @brief Set sfFeeAmount (SoeOptional) + * @return Reference to this builder for method chaining. + */ + SponsorshipBuilder& + setFeeAmount(std::decay_t const& value) + { + object_[sfFeeAmount] = value; + return *this; + } + + /** + * @brief Set sfMaxFee (SoeOptional) + * @return Reference to this builder for method chaining. + */ + SponsorshipBuilder& + setMaxFee(std::decay_t const& value) + { + object_[sfMaxFee] = value; + return *this; + } + + /** + * @brief Set sfRemainingOwnerCount (SoeDefault) + * @return Reference to this builder for method chaining. + */ + SponsorshipBuilder& + setRemainingOwnerCount(std::decay_t const& value) + { + object_[sfRemainingOwnerCount] = value; + return *this; + } + + /** + * @brief Set sfOwnerNode (SoeRequired) + * @return Reference to this builder for method chaining. + */ + SponsorshipBuilder& + setOwnerNode(std::decay_t const& value) + { + object_[sfOwnerNode] = value; + return *this; + } + + /** + * @brief Set sfSponseeNode (SoeRequired) + * @return Reference to this builder for method chaining. + */ + SponsorshipBuilder& + setSponseeNode(std::decay_t const& value) + { + object_[sfSponseeNode] = value; + return *this; + } + + /** + * @brief Build and return the completed Sponsorship wrapper. + * @param index The ledger entry index. + * @return The constructed ledger entry wrapper. + */ + Sponsorship + build(uint256 const& index) + { + return Sponsorship{std::make_shared(std::move(object_), index)}; + } +}; + +} // namespace xrpl::ledger_entries diff --git a/include/xrpl/protocol_autogen/transactions/SponsorshipSet.h b/include/xrpl/protocol_autogen/transactions/SponsorshipSet.h new file mode 100644 index 0000000000..2831c8a077 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/SponsorshipSet.h @@ -0,0 +1,290 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::transactions { + +class SponsorshipSetBuilder; + +/** + * @brief Transaction: SponsorshipSet + * + * Type: ttSPONSORSHIP_SET (91) + * Delegable: Delegation::Delegable + * Amendment: featureSponsor + * Privileges: NoPriv + * + * Immutable wrapper around STTx providing type-safe field access. + * Use SponsorshipSetBuilder to construct new transactions. + */ +class SponsorshipSet : public TransactionBase +{ +public: + static constexpr xrpl::TxType txType = ttSPONSORSHIP_SET; + + /** + * @brief Construct a SponsorshipSet transaction wrapper from an existing STTx object. + * @throws std::runtime_error if the transaction type doesn't match. + */ + explicit SponsorshipSet(std::shared_ptr tx) + : TransactionBase(std::move(tx)) + { + // Verify transaction type + if (tx_->getTxnType() != txType) + { + throw std::runtime_error("Invalid transaction type for SponsorshipSet"); + } + } + + // Transaction-specific field getters + + /** + * @brief Get sfCounterpartySponsor (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getCounterpartySponsor() const + { + if (hasCounterpartySponsor()) + { + return this->tx_->at(sfCounterpartySponsor); + } + return std::nullopt; + } + + /** + * @brief Check if sfCounterpartySponsor is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasCounterpartySponsor() const + { + return this->tx_->isFieldPresent(sfCounterpartySponsor); + } + + /** + * @brief Get sfSponsee (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getSponsee() const + { + if (hasSponsee()) + { + return this->tx_->at(sfSponsee); + } + return std::nullopt; + } + + /** + * @brief Check if sfSponsee is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasSponsee() const + { + return this->tx_->isFieldPresent(sfSponsee); + } + + /** + * @brief Get sfFeeAmount (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getFeeAmount() const + { + if (hasFeeAmount()) + { + return this->tx_->at(sfFeeAmount); + } + return std::nullopt; + } + + /** + * @brief Check if sfFeeAmount is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasFeeAmount() const + { + return this->tx_->isFieldPresent(sfFeeAmount); + } + + /** + * @brief Get sfMaxFee (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getMaxFee() const + { + if (hasMaxFee()) + { + return this->tx_->at(sfMaxFee); + } + return std::nullopt; + } + + /** + * @brief Check if sfMaxFee is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasMaxFee() const + { + return this->tx_->isFieldPresent(sfMaxFee); + } + + /** + * @brief Get sfRemainingOwnerCount (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getRemainingOwnerCount() const + { + if (hasRemainingOwnerCount()) + { + return this->tx_->at(sfRemainingOwnerCount); + } + return std::nullopt; + } + + /** + * @brief Check if sfRemainingOwnerCount is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasRemainingOwnerCount() const + { + return this->tx_->isFieldPresent(sfRemainingOwnerCount); + } +}; + +/** + * @brief Builder for SponsorshipSet transactions. + * + * Provides a fluent interface for constructing transactions with method chaining. + * Uses STObject internally for flexible transaction construction. + * Inherits common field setters from TransactionBuilderBase. + */ +class SponsorshipSetBuilder : public TransactionBuilderBase +{ +public: + /** + * @brief Construct a new SponsorshipSetBuilder with required fields. + * @param account The account initiating the transaction. + * @param sequence Optional sequence number for the transaction. + * @param fee Optional fee for the transaction. + */ + SponsorshipSetBuilder(SF_ACCOUNT::type::value_type account, + std::optional sequence = std::nullopt, + std::optional fee = std::nullopt +) + : TransactionBuilderBase(ttSPONSORSHIP_SET, account, sequence, fee) + { + } + + /** + * @brief Construct a SponsorshipSetBuilder from an existing STTx object. + * @param tx The existing transaction to copy from. + * @throws std::runtime_error if the transaction type doesn't match. + */ + SponsorshipSetBuilder(std::shared_ptr tx) + { + if (tx->getTxnType() != ttSPONSORSHIP_SET) + { + throw std::runtime_error("Invalid transaction type for SponsorshipSetBuilder"); + } + object_ = *tx; + } + + /** @brief Transaction-specific field setters */ + + /** + * @brief Set sfCounterpartySponsor (SoeOptional) + * @return Reference to this builder for method chaining. + */ + SponsorshipSetBuilder& + setCounterpartySponsor(std::decay_t const& value) + { + object_[sfCounterpartySponsor] = value; + return *this; + } + + /** + * @brief Set sfSponsee (SoeOptional) + * @return Reference to this builder for method chaining. + */ + SponsorshipSetBuilder& + setSponsee(std::decay_t const& value) + { + object_[sfSponsee] = value; + return *this; + } + + /** + * @brief Set sfFeeAmount (SoeOptional) + * @return Reference to this builder for method chaining. + */ + SponsorshipSetBuilder& + setFeeAmount(std::decay_t const& value) + { + object_[sfFeeAmount] = value; + return *this; + } + + /** + * @brief Set sfMaxFee (SoeOptional) + * @return Reference to this builder for method chaining. + */ + SponsorshipSetBuilder& + setMaxFee(std::decay_t const& value) + { + object_[sfMaxFee] = value; + return *this; + } + + /** + * @brief Set sfRemainingOwnerCount (SoeOptional) + * @return Reference to this builder for method chaining. + */ + SponsorshipSetBuilder& + setRemainingOwnerCount(std::decay_t const& value) + { + object_[sfRemainingOwnerCount] = value; + return *this; + } + + /** + * @brief Build and return the SponsorshipSet wrapper. + * @param publicKey The public key for signing. + * @param secretKey The secret key for signing. + * @return The constructed transaction wrapper. + */ + SponsorshipSet + build(PublicKey const& publicKey, SecretKey const& secretKey) + { + sign(publicKey, secretKey); + return SponsorshipSet{std::make_shared(std::move(object_))}; + } +}; + +} // namespace xrpl::transactions diff --git a/include/xrpl/protocol_autogen/transactions/SponsorshipTransfer.h b/include/xrpl/protocol_autogen/transactions/SponsorshipTransfer.h new file mode 100644 index 0000000000..bc27f1603b --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/SponsorshipTransfer.h @@ -0,0 +1,179 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::transactions { + +class SponsorshipTransferBuilder; + +/** + * @brief Transaction: SponsorshipTransfer + * + * Type: ttSPONSORSHIP_TRANSFER (90) + * Delegable: Delegation::NotDelegable + * Amendment: featureSponsor + * Privileges: NoPriv + * + * Immutable wrapper around STTx providing type-safe field access. + * Use SponsorshipTransferBuilder to construct new transactions. + */ +class SponsorshipTransfer : public TransactionBase +{ +public: + static constexpr xrpl::TxType txType = ttSPONSORSHIP_TRANSFER; + + /** + * @brief Construct a SponsorshipTransfer transaction wrapper from an existing STTx object. + * @throws std::runtime_error if the transaction type doesn't match. + */ + explicit SponsorshipTransfer(std::shared_ptr tx) + : TransactionBase(std::move(tx)) + { + // Verify transaction type + if (tx_->getTxnType() != txType) + { + throw std::runtime_error("Invalid transaction type for SponsorshipTransfer"); + } + } + + // Transaction-specific field getters + + /** + * @brief Get sfObjectID (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getObjectID() const + { + if (hasObjectID()) + { + return this->tx_->at(sfObjectID); + } + return std::nullopt; + } + + /** + * @brief Check if sfObjectID is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasObjectID() const + { + return this->tx_->isFieldPresent(sfObjectID); + } + + /** + * @brief Get sfSponsee (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getSponsee() const + { + if (hasSponsee()) + { + return this->tx_->at(sfSponsee); + } + return std::nullopt; + } + + /** + * @brief Check if sfSponsee is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasSponsee() const + { + return this->tx_->isFieldPresent(sfSponsee); + } +}; + +/** + * @brief Builder for SponsorshipTransfer transactions. + * + * Provides a fluent interface for constructing transactions with method chaining. + * Uses STObject internally for flexible transaction construction. + * Inherits common field setters from TransactionBuilderBase. + */ +class SponsorshipTransferBuilder : public TransactionBuilderBase +{ +public: + /** + * @brief Construct a new SponsorshipTransferBuilder with required fields. + * @param account The account initiating the transaction. + * @param sequence Optional sequence number for the transaction. + * @param fee Optional fee for the transaction. + */ + SponsorshipTransferBuilder(SF_ACCOUNT::type::value_type account, + std::optional sequence = std::nullopt, + std::optional fee = std::nullopt +) + : TransactionBuilderBase(ttSPONSORSHIP_TRANSFER, account, sequence, fee) + { + } + + /** + * @brief Construct a SponsorshipTransferBuilder from an existing STTx object. + * @param tx The existing transaction to copy from. + * @throws std::runtime_error if the transaction type doesn't match. + */ + SponsorshipTransferBuilder(std::shared_ptr tx) + { + if (tx->getTxnType() != ttSPONSORSHIP_TRANSFER) + { + throw std::runtime_error("Invalid transaction type for SponsorshipTransferBuilder"); + } + object_ = *tx; + } + + /** @brief Transaction-specific field setters */ + + /** + * @brief Set sfObjectID (SoeOptional) + * @return Reference to this builder for method chaining. + */ + SponsorshipTransferBuilder& + setObjectID(std::decay_t const& value) + { + object_[sfObjectID] = value; + return *this; + } + + /** + * @brief Set sfSponsee (SoeOptional) + * @return Reference to this builder for method chaining. + */ + SponsorshipTransferBuilder& + setSponsee(std::decay_t const& value) + { + object_[sfSponsee] = value; + return *this; + } + + /** + * @brief Build and return the SponsorshipTransfer wrapper. + * @param publicKey The public key for signing. + * @param secretKey The secret key for signing. + * @return The constructed transaction wrapper. + */ + SponsorshipTransfer + build(PublicKey const& publicKey, SecretKey const& secretKey) + { + sign(publicKey, secretKey); + return SponsorshipTransfer{std::make_shared(std::move(object_))}; + } +}; + +} // namespace xrpl::transactions diff --git a/include/xrpl/tx/ApplyContext.h b/include/xrpl/tx/ApplyContext.h index ca778387cd..f64957dd35 100644 --- a/include/xrpl/tx/ApplyContext.h +++ b/include/xrpl/tx/ApplyContext.h @@ -127,6 +127,15 @@ public: TER checkInvariants(TER const result, XRPAmount const fee); + ApplyViewContext + getApplyViewContext() + { + XRPL_ASSERT( + view_.has_value(), + "xrpl::ApplyContext::getApplyViewContext : view_ emplaced in constructor"); + return {.view = *view_, .tx = tx}; + } + private: static TER failInvariantCheck(TER const result); diff --git a/include/xrpl/tx/Transactor.h b/include/xrpl/tx/Transactor.h index bc5e8c80e7..bdb51b06ec 100644 --- a/include/xrpl/tx/Transactor.h +++ b/include/xrpl/tx/Transactor.h @@ -10,8 +10,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -126,6 +128,21 @@ struct PreflightResult; // Needed for preflight specialization class Change; +enum class FeePayerType { + Account, + Delegate, + SponsorCoSigned, + SponsorPreFunded, +}; + +struct FeePayer +{ + AccountID id; + Keylet keylet; + SF_AMOUNT const& balanceField; + FeePayerType type{FeePayerType::Account}; +}; + class Transactor { protected: @@ -298,6 +315,10 @@ public: return T::checkGranularSemantics(view, tx, heldGranularPermissions); } + + static NotTEC + checkSponsor(ReadView const& view, STTx const& tx); + ///////////////////////////////////////////////////// // Interface used by AccountDelete @@ -459,6 +480,9 @@ private: std::pair reset(XRPAmount fee); + static FeePayer + getFeePayer(ReadView const& view, STTx const& tx); + TER consumeSeqProxy(SLE::pointer const& sleAccount); TER diff --git a/include/xrpl/tx/invariants/InvariantCheck.h b/include/xrpl/tx/invariants/InvariantCheck.h index d19f34635d..0e6b2a361d 100644 --- a/include/xrpl/tx/invariants/InvariantCheck.h +++ b/include/xrpl/tx/invariants/InvariantCheck.h @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -441,7 +442,9 @@ using InvariantChecks = std::tuple< ValidMPTPayment, ValidAmounts, ValidMPTTransfer, - ObjectHasPseudoAccount>; + ObjectHasPseudoAccount, + SponsorshipOwnerCountsMatch, + SponsorshipAccountCountMatchesField>; /** * @brief get a tuple of all invariant checks diff --git a/include/xrpl/tx/invariants/SponsorshipInvariant.h b/include/xrpl/tx/invariants/SponsorshipInvariant.h new file mode 100644 index 0000000000..e664685c16 --- /dev/null +++ b/include/xrpl/tx/invariants/SponsorshipInvariant.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +namespace xrpl { + +/** + * @brief Invariant: Sponsored owner counts are balanced. + * + * The following checks are made for every transaction: + * - The sum of all per-account deltas of `sfSponsoredOwnerCount` equals + * the sum of all per-account deltas of `sfSponsoringOwnerCount`. + * - Account OwnerCount must be greater than or equal to SponsoredOwnerCount. + * - The net delta of sponsored object owner counts (the owner-count + * magnitude of sponsored ledger entries) equals the net delta of + * `sfSponsoredOwnerCount`. + */ +class SponsorshipOwnerCountsMatch +{ + std::int64_t deltaSponsoredOwnerCount_ = 0; + std::int64_t deltaSponsoringOwnerCount_ = 0; + std::int64_t deltaSponsoredObjectOwnerCount_ = 0; + std::uint64_t ownerCountBelowSponsored_ = 0; + +public: + void + visitEntry(bool, SLE::const_ref, SLE::const_ref); + + [[nodiscard]] bool + finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const; +}; + +/** + * @brief Invariant: Sponsoring account relationships tracked consistently. + * + * The following check is made for every transaction: + * - The net delta of `sfSponsoringAccountCount` across all accounts equals + * the net delta of the count of ltACCOUNT_ROOT entries having + * `sfSponsor` present (presence transitions only: add/remove). + */ +class SponsorshipAccountCountMatchesField +{ + std::int64_t deltaSponsoringAccountCount_ = 0; + std::int64_t deltaSponsorFieldPresence_ = 0; + +public: + void + visitEntry(bool, SLE::const_ref, SLE::const_ref); + + [[nodiscard]] bool + finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const; +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/paths/AMMOffer.h b/include/xrpl/tx/paths/AMMOffer.h index 40a9cf40b3..6e7a5ceca5 100644 --- a/include/xrpl/tx/paths/AMMOffer.h +++ b/include/xrpl/tx/paths/AMMOffer.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -110,7 +111,10 @@ public: send(Args&&... args) { return accountSend( - std::forward(args)..., WaiveTransferFee::Yes, AllowMPTOverflow::Yes); + std::forward(args)..., + SLE::pointer(), + WaiveTransferFee::Yes, + AllowMPTOverflow::Yes); } [[nodiscard]] bool diff --git a/include/xrpl/tx/paths/Offer.h b/include/xrpl/tx/paths/Offer.h index 164a933ba1..d7daa30cab 100644 --- a/include/xrpl/tx/paths/Offer.h +++ b/include/xrpl/tx/paths/Offer.h @@ -234,7 +234,8 @@ template TER TOffer::send(Args&&... args) { - return accountSend(std::forward(args)..., WaiveTransferFee::No, AllowMPTOverflow::Yes); + return accountSend( + std::forward(args)..., SLE::pointer(), WaiveTransferFee::No, AllowMPTOverflow::Yes); } template diff --git a/include/xrpl/tx/transactors/sponsor/SponsorshipSet.h b/include/xrpl/tx/transactors/sponsor/SponsorshipSet.h new file mode 100644 index 0000000000..3310c995ae --- /dev/null +++ b/include/xrpl/tx/transactors/sponsor/SponsorshipSet.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace xrpl { + +class SponsorshipSet : public Transactor +{ +public: + static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal; + + explicit SponsorshipSet(ApplyContext& ctx) : Transactor(ctx) + { + } + + static TxConsequences + makeTxConsequences(PreflightContext const& ctx); + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; + + void + visitInvariantEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) override; + + [[nodiscard]] bool + finalizeInvariants( + STTx const& tx, + TER result, + XRPAmount fee, + ReadView const& view, + beast::Journal const& j) override; +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/transactors/sponsor/SponsorshipTransfer.h b/include/xrpl/tx/transactors/sponsor/SponsorshipTransfer.h new file mode 100644 index 0000000000..cf0022ab72 --- /dev/null +++ b/include/xrpl/tx/transactors/sponsor/SponsorshipTransfer.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace xrpl { + +class SponsorshipTransfer : public Transactor +{ +public: + static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal; + + explicit SponsorshipTransfer(ApplyContext& ctx) : Transactor(ctx) + { + } + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; + + void + visitInvariantEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) override; + + [[nodiscard]] bool + finalizeInvariants( + STTx const& tx, + TER result, + XRPAmount fee, + ReadView const& view, + beast::Journal const& j) override; +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h b/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h index 045911c7ae..5d35f65f44 100644 --- a/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h +++ b/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h @@ -76,7 +76,7 @@ public: beast::Journal const& j) override; static std::expected - create(ApplyView& view, beast::Journal journal, MPTCreateArgs const& args); + create(ApplyViewContext ctx, beast::Journal journal, MPTCreateArgs const& args); }; } // namespace xrpl diff --git a/src/libxrpl/ledger/PaymentSandbox.cpp b/src/libxrpl/ledger/PaymentSandbox.cpp index 97e3e53cbf..a7a8962095 100644 --- a/src/libxrpl/ledger/PaymentSandbox.cpp +++ b/src/libxrpl/ledger/PaymentSandbox.cpp @@ -3,6 +3,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 ebac76a754..8116f4f641 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -431,8 +432,7 @@ canWithdraw(ReadView const& view, STTx const& tx) TER doWithdraw( - ApplyView& view, - STTx const& tx, + ApplyViewContext ctx, AccountID const& senderAcct, AccountID const& dstAcct, AccountID const& sourceAcct, @@ -440,23 +440,24 @@ doWithdraw( STAmount const& amount, beast::Journal j) { + auto const dstSle = ctx.view.read(keylet::account(dstAcct)); + // Create trust line or MPToken for the receiving account if (dstAcct == senderAcct) { - if (auto const ter = addEmptyHolding(view, senderAcct, priorBalance, amount.asset(), j); + if (auto const ter = addEmptyHolding(ctx, senderAcct, priorBalance, amount.asset(), j); !isTesSuccess(ter) && ter != tecDUPLICATE) return ter; } else { - auto dstSle = view.read(keylet::account(dstAcct)); - if (auto err = verifyDepositPreauth(tx, view, senderAcct, dstAcct, dstSle, j)) + if (auto err = verifyDepositPreauth(ctx.tx, ctx.view, senderAcct, dstAcct, dstSle, j)) return err; } // Sanity check if (accountHolds( - view, + ctx.view, sourceAcct, amount.asset(), FreezeHandling::IgnoreFreeze, @@ -469,9 +470,18 @@ doWithdraw( // LCOV_EXCL_STOP } + // A reserve sponsor only covers tx.Account's own objects, so resolve the + // sponsor against the destination. accountSend can auto-create a holding + // for dstAcct; keying on the destination ensures a third-party destination's + // holding is never stamped with the tx's reserve sponsor. + auto const sponsorSle = getEffectiveTxReserveSponsor(ctx, dstSle); + 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, WaiveTransferFee::Yes); + return accountSend( + ctx.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 1c4acc7bc4..faca4ebfb6 100644 --- a/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp +++ b/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp @@ -7,7 +7,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -15,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -41,46 +44,226 @@ isGlobalFrozen(ReadView const& view, AccountID const& issuer) return false; } +namespace { + // An owner count cannot be negative. If adjustment 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. // If id != std::nullopt, then do error reporting. // // Returns adjusted owner count. -static std::uint32_t +std::uint32_t confineOwnerCount( - std::uint32_t current, - std::int32_t adjustment, + std::uint32_t currentOwnerCount, + 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 totalOwnerCount{currentOwnerCount + ownerCountAdj}; + if (ownerCountAdj > 0) { // Overflow is well defined on unsigned - if (adjusted < current) + if (totalOwnerCount < currentOwnerCount) { + // LCOV_EXCL_START if (id) { JLOG(j.fatal()) << "Account " << *id << " owner count exceeds max!"; } - adjusted = std::numeric_limits::max(); + totalOwnerCount = std::numeric_limits::max(); + // LCOV_EXCL_STOP } } else { // Underflow is well defined on unsigned - if (adjusted > current) + if (totalOwnerCount > currentOwnerCount) { + // LCOV_EXCL_START if (id) { JLOG(j.fatal()) << "Account " << *id << " owner count set below 0!"; } - adjusted = 0; + totalOwnerCount = 0; XRPL_ASSERT(!id, "xrpl::confineOwnerCount : id is not set"); + // LCOV_EXCL_STOP } } - return adjusted; + return totalOwnerCount; +} + +// Returns the number of account reserves funded by this account: 1 for itself (0 if sponsored by +// another account) plus the count of accounts it sponsors. +std::uint32_t +accountCountImpl(SLE::const_ref sle, std::int32_t accountCountAdj, beast::Journal j) +{ + bool const isSponsored = sle->isFieldPresent(sfSponsor); + std::int64_t const sponsoringAccountCount = sle->getFieldU32(sfSponsoringAccountCount); + std::int64_t const currentAccountCount = (isSponsored ? 0 : 1) + sponsoringAccountCount; + + std::int64_t totalAccountCount{currentAccountCount + accountCountAdj}; + if (totalAccountCount > std::numeric_limits::max()) + { + // LCOV_EXCL_START + JLOG(j.fatal()) << "Reserve count exceeds max!"; + totalAccountCount = std::numeric_limits::max(); + // LCOV_EXCL_STOP + } + else if (totalAccountCount < 0) + { + // LCOV_EXCL_START + UNREACHABLE("xrpl::accountCountImpl : Reserve count set below 0"); + JLOG(j.fatal()) << "Reserve count set below 0"; + totalAccountCount = 0; + // LCOV_EXCL_STOP + } + + return totalAccountCount; +} + +std::uint32_t +adjustOwnerCountImpl( + ApplyView& view, + SLE::ref sle, + SF_UINT32 const& sfield, + AccountID const& accID, + std::int32_t ownerCountAdj, + beast::Journal j) +{ + std::uint32_t const currentOwnerCount = sle->at(sfield); + std::uint32_t const totalOwnerCount = + confineOwnerCount(currentOwnerCount, ownerCountAdj, accID, j); + sle->at(sfield) = totalOwnerCount; + view.update(sle); + return totalOwnerCount; +} + +void +adjustOwnerCountSigned( + ApplyView& view, + SLE::ref accountSle, + SLE::ref sponsorSle, + std::int32_t adjustment, + beast::Journal j) +{ + if (view.rules().enabled(featureSponsor)) + { + XRPL_ASSERT(accountSle, "xrpl::adjustOwnerCountSigned : valid account sle"); + if (!accountSle) + return; // LCOV_EXCL_LINE + + auto const accountID = accountSle->getAccountID(sfAccount); + bool const validType = accountSle->getType() == ltACCOUNT_ROOT; + XRPL_ASSERT(validType, "xrpl::adjustOwnerCountSigned : valid account sle type"); + if (!validType) + return; // LCOV_EXCL_LINE + + XRPL_ASSERT(adjustment, "xrpl::adjustOwnerCountSigned : nonzero adjustment input"); + + OwnerCounts const currentOwnerCount(accountSle); + OwnerCounts totalOwnerCount(currentOwnerCount); + + if (sponsorSle) + { + bool const validSponsorType = sponsorSle->getType() == ltACCOUNT_ROOT; + XRPL_ASSERT(validSponsorType, "xrpl::adjustOwnerCountSigned : valid sponsor sle type"); + if (!validSponsorType) + return; // LCOV_EXCL_LINE + auto const sponsorID = sponsorSle->getAccountID(sfAccount); + + totalOwnerCount.sponsored = adjustOwnerCountImpl( + view, accountSle, sfSponsoredOwnerCount, accountID, adjustment, j); + + { + OwnerCounts const sponsorCurrent(sponsorSle); + OwnerCounts sponsorAdjustment(sponsorCurrent); + sponsorAdjustment.sponsoring = adjustOwnerCountImpl( + view, sponsorSle, sfSponsoringOwnerCount, sponsorID, adjustment, j); + view.adjustOwnerCountHook(sponsorID, sponsorCurrent, sponsorAdjustment); + } + + auto sponsorshipSle = view.peek(keylet::sponsorship(sponsorID, accountID)); + if (sponsorshipSle && adjustment > 0) + { + // Only decrease the pre-funded ReserveCount on Sponsorship if we assign new + // objects. Removing/reassigning ownership of the object doesn't increase + // RemainingOwnerCount back. Don't call hook because this counter is not something + // that requires reserve (like other sf...OwnerCounts do). + adjustOwnerCountImpl( + view, sponsorshipSle, sfRemainingOwnerCount, sponsorID, -adjustment, j); + } + } + + totalOwnerCount.owner = + adjustOwnerCountImpl(view, accountSle, sfOwnerCount, accountID, adjustment, j); + view.adjustOwnerCountHook(accountID, currentOwnerCount, totalOwnerCount); + } + else + { + XRPL_ASSERT(accountSle, "xrpl::adjustOwnerCountSigned : valid account sle"); + if (!accountSle) + return; + // the remaining are only asserts to preserve existing behavior + XRPL_ASSERT(sponsorSle == nullptr, "xrpl::adjustOwnerCountSigned : sponsor not enabled"); + XRPL_ASSERT( + accountSle->getType() == ltACCOUNT_ROOT, + "xrpl::adjustOwnerCountSigned : valid account sle type"); + XRPL_ASSERT(adjustment, "xrpl::adjustOwnerCount : nonzero adjustment input"); + std::uint32_t const current{accountSle->getFieldU32(sfOwnerCount)}; + AccountID const id = (*accountSle)[sfAccount]; + std::uint32_t const adjusted = confineOwnerCount(current, adjustment, id, j); + + OwnerCounts const currentOwnerCount(accountSle); + OwnerCounts finalOwnerCount(currentOwnerCount); + finalOwnerCount.owner = adjusted; + + view.adjustOwnerCountHook(id, currentOwnerCount, finalOwnerCount); + accountSle->at(sfOwnerCount) = adjusted; + view.update(accountSle); + } +} + +} // namespace + +std::uint32_t +ownerCount(SLE::const_ref sle, beast::Journal j, std::int32_t ownerCountAdj) +{ + XRPL_ASSERT(sle && sle->getType() == ltACCOUNT_ROOT, "xrpl::ownerCount : sle is account root"); + + AccountID const id = sle->getAccountID(sfAccount); + std::uint32_t const currentOwnerCount = sle->at(sfOwnerCount); + std::uint32_t const sponsoredOwnerCount = sle->at(sfSponsoredOwnerCount); + std::uint32_t const sponsoringOwnerCount = sle->at(sfSponsoringOwnerCount); + + XRPL_ASSERT( + currentOwnerCount >= sponsoredOwnerCount, + "xrpl::ownerCount : OwnerCount must be greater than or equal to SponsoredOwnerCount"); + + std::int64_t deltaCount = + static_cast(ownerCountAdj) - sponsoredOwnerCount + sponsoringOwnerCount; + + if (deltaCount > std::numeric_limits::max()) + { + // LCOV_EXCL_START + deltaCount = std::numeric_limits::max(); + JLOG(j.fatal()) << "Account " << id << " delta count exceeds max, " + << "adjustment: " << ownerCountAdj + << ", sponsoredCount: " << sponsoredOwnerCount + << ", sponsoringOwnerCount: " << sponsoringOwnerCount; + // LCOV_EXCL_STOP + } + else if (deltaCount < std::numeric_limits::min()) + { + // LCOV_EXCL_START + deltaCount = std::numeric_limits::min(); + JLOG(j.fatal()) << "Account " << id << " delta count is below min, " + << "adjustment: " << ownerCountAdj + << ", sponsoredCount: " << sponsoredOwnerCount + << ", sponsoringCount: " << sponsoringOwnerCount; + // LCOV_EXCL_STOP + } + + return confineOwnerCount(currentOwnerCount, deltaCount); } XRPAmount @@ -91,12 +274,14 @@ xrpLiquid(ReadView const& view, AccountID const& id, std::int32_t ownerCountAdj, return beast::kZero; // Return balance minus reserve - std::uint32_t const ownerCount = - confineOwnerCount(view.ownerCountHook(id, sle->getFieldU32(sfOwnerCount)), ownerCountAdj); + std::uint32_t const currentOwnerCount = + confineOwnerCount(view.ownerCountHook(id, OwnerCounts(sle)).count(), ownerCountAdj); + std::uint32_t const currentAccountCount = accountCountImpl(sle, 0, j); // Pseudo-accounts have no reserve requirement - auto const reserve = - isPseudoAccount(sle) ? XRPAmount{0} : view.fees().accountReserve(ownerCount); + auto const reserve = isPseudoAccount(sle) + ? XRPAmount{0} + : view.fees().accountReserve(currentOwnerCount, currentAccountCount); auto const fullBalance = sle->getFieldAmount(sfBalance); @@ -108,7 +293,7 @@ xrpLiquid(ReadView const& view, AccountID const& id, std::int32_t ownerCountAdj, << " amount=" << amount.getFullText() << " fullBalance=" << fullBalance.getFullText() << " balance=" << balance.getFullText() << " reserve=" << reserve - << " ownerCount=" << ownerCount << " ownerCountAdj=" << ownerCountAdj; + << " ownerCount=" << currentOwnerCount << " ownerCountAdj=" << ownerCountAdj; return amount.xrp(); } @@ -125,19 +310,193 @@ transferRate(ReadView const& view, AccountID const& issuer) } void -adjustOwnerCount(ApplyView& view, SLE::ref sle, std::int32_t amount, beast::Journal j) +increaseOwnerCount( + ApplyView& view, + SLE::ref accountSle, + SLE::ref sponsorSle, + std::uint32_t count, + beast::Journal j) { - if (!sle) - return; - XRPL_ASSERT(amount, "xrpl::adjustOwnerCount : nonzero amount input"); - std::uint32_t const current{sle->getFieldU32(sfOwnerCount)}; - AccountID const id = (*sle)[sfAccount]; - std::uint32_t const adjusted = confineOwnerCount(current, amount, id, j); - view.adjustOwnerCountHook(id, current, adjusted); - sle->at(sfOwnerCount) = adjusted; - view.update(sle); + XRPL_ASSERT( + count != 0 && count <= std::numeric_limits::max(), + "xrpl::increaseOwnerCount : count in signed delta range"); + if (count == 0 || count > std::numeric_limits::max()) + return; // LCOV_EXCL_LINE + + adjustOwnerCountSigned(view, accountSle, sponsorSle, static_cast(count), j); } +void +increaseOwnerCount(ApplyViewContext ctx, SLE::ref accountSle, std::uint32_t count, beast::Journal j) +{ + auto const sponsorExp = getEffectiveTxReserveSponsor(ctx, accountSle); + + // The sponsor's existence is validated by checkReserve/checkSponsor before + // any owner-count mutation, so loading it here cannot fail. + XRPL_ASSERT( + sponsorExp.has_value(), "xrpl::increaseOwnerCount : sponsor validated before mutation"); + + increaseOwnerCount(ctx.view, accountSle, sponsorExp ? *sponsorExp : SLE::pointer(), count, j); +} + +void +decreaseOwnerCount( + ApplyView& view, + SLE::ref accountSle, + SLE::ref sponsorSle, + std::uint32_t count, + beast::Journal j) +{ + XRPL_ASSERT( + count != 0 && count <= std::numeric_limits::max(), + "xrpl::decreaseOwnerCount : count in signed delta range"); + if (count == 0 || count > std::numeric_limits::max()) + return; // LCOV_EXCL_LINE + + adjustOwnerCountSigned(view, accountSle, sponsorSle, -static_cast(count), j); +} + +void +decreaseOwnerCountForObject( + ApplyView& view, + SLE::ref accountSle, + SLE::ref objectSle, + std::uint32_t count, + beast::Journal j) +{ + XRPL_ASSERT(objectSle, "xrpl::decreaseOwnerCountForObject : valid object sle"); + if (!objectSle) + return; // LCOV_EXCL_LINE + + bool const validObjectType = objectSle->getType() != ltACCOUNT_ROOT; + XRPL_ASSERT(validObjectType, "xrpl::decreaseOwnerCountForObject : valid object sle type"); + if (!validObjectType) + return; // LCOV_EXCL_LINE + + SLE::ref sponsorSle = getLedgerEntryReserveSponsor(view, objectSle); + decreaseOwnerCount(view, accountSle, sponsorSle, count, j); +} + +void +adjustLoanBrokerOwnerCount( + ApplyView& view, + SLE::ref brokerSle, + std::int32_t delta, + beast::Journal j) +{ + XRPL_ASSERT( + brokerSle && brokerSle->getType() == ltLOAN_BROKER, + "xrpl::adjustLoanBrokerOwnerCount : valid loan broker sle"); + if (!brokerSle || brokerSle->getType() != ltLOAN_BROKER) + return; // LCOV_EXCL_LINE + + XRPL_ASSERT(delta != 0, "xrpl::adjustLoanBrokerOwnerCount : nonzero delta input"); + if (delta == 0) + return; // LCOV_EXCL_LINE + + adjustOwnerCountImpl( + view, brokerSle, sfOwnerCount, brokerSle->getAccountID(sfAccount), delta, j); +} + +XRPAmount +accountReserve(ReadView const& view, SLE::const_ref sle, beast::Journal j, Adjustment adj) +{ + XRPL_ASSERT(sle && sle->getType() == ltACCOUNT_ROOT, "xrpl::accountReserve : valid sle"); + + if (!view.rules().enabled(featureSponsor)) + { + XRPL_ASSERT(adj.accountCountDelta == 0, "xrpl::accountReserve : no account count delta"); + return view.fees().accountReserve(sle->getFieldU32(sfOwnerCount) + adj.ownerCountDelta, 1); + } + std::uint32_t const currentOwnerCount = ownerCount(sle, j, adj.ownerCountDelta); + std::uint32_t const currentAccountCount = accountCountImpl(sle, adj.accountCountDelta, j); + + return view.fees().accountReserve(currentOwnerCount, currentAccountCount); +} + +TER +checkReserve( + ApplyViewContext ctx, + SLE::const_ref accSle, + XRPAmount accBalance, + SLE::const_ref sponsorSle, + Adjustment adj, + beast::Journal j, + TER insufReserveCode) +{ + // TODO: swap to assert after fixCleanup3_2_0 is retired + if (!accSle || accSle->getType() != ltACCOUNT_ROOT) + return tefINTERNAL; // LCOV_EXCL_LINE + XRPL_ASSERT( + !isTesSuccess(insufReserveCode), "xrpl::checkReserve : insufReserveCode is not tesSUCCESS"); + if (ctx.view.rules().enabled(featureSponsor)) + { + if (sponsorSle) + { + if (sponsorSle->getType() != ltACCOUNT_ROOT) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto const sle = ctx.view.read( + keylet::sponsorship( + sponsorSle->getAccountID(sfAccount), accSle->getAccountID(sfAccount))); + + // A reserve-sponsored tx must carry a sponsor signature + // (cosigning path) and/or have a pre-existing sponsorship SLE + // (prefunded path). Absence of both is an internal invariant break. + if (isReserveSponsored(ctx.tx) && !sle && !ctx.tx.isFieldPresent(sfSponsorSignature)) + return tecINTERNAL; // LCOV_EXCL_LINE + + if (sle) + { + auto const ownerCountAllowed = sle->getFieldU32(sfRemainingOwnerCount); + if (adj.ownerCountDelta > 0 && + ownerCountAllowed < static_cast(adj.ownerCountDelta)) + return insufReserveCode; + } + + auto const sponsorBalance = sponsorSle->getFieldAmount(sfBalance).xrp(); + XRPAmount const sponsorReserve = accountReserve(ctx.view, sponsorSle, j, adj); + + if (sponsorBalance < sponsorReserve) + return insufReserveCode; + } + else + { + XRPAmount const reserve = accountReserve(ctx.view, accSle, j, adj); + if (accBalance < reserve) + return insufReserveCode; + } + } + else + { + XRPL_ASSERT( + !sponsorSle, + "xrpl::checkReserve : featureSponsor disabled and sponsorSle not provided"); + XRPL_ASSERT(adj.accountCountDelta == 0, "xrpl::checkReserve : accountCountDelta is 0"); + auto const reserve = ctx.view.fees().accountReserve( + accSle->getFieldU32(sfOwnerCount) + adj.ownerCountDelta, 1); + if (accBalance < reserve) + return insufReserveCode; + } + return tesSUCCESS; +} + +TER +checkReserve( + ApplyViewContext ctx, + SLE::const_ref accSle, + XRPAmount accBalance, + Adjustment adj, + beast::Journal j) +{ + auto const sponsorExp = getEffectiveTxReserveSponsor(ctx, accSle); + if (!sponsorExp) + return sponsorExp.error(); // LCOV_EXCL_LINE + return checkReserve(ctx, accSle, accBalance, *sponsorExp, adj, j); +} + +// ---------------------------------------------------- + AccountID pseudoAccountAddress(ReadView const& view, uint256 const& pseudoOwnerKey) { diff --git a/src/libxrpl/ledger/helpers/CredentialHelpers.cpp b/src/libxrpl/ledger/helpers/CredentialHelpers.cpp index 6fc2faf03e..226ea100e9 100644 --- a/src/libxrpl/ledger/helpers/CredentialHelpers.cpp +++ b/src/libxrpl/ledger/helpers/CredentialHelpers.cpp @@ -97,7 +97,7 @@ deleteSLE(ApplyView& view, SLE::ref sleCredential, beast::Journal j) } if (isOwner) - adjustOwnerCount(view, sleAccount, -1, j); + decreaseOwnerCountForObject(view, sleAccount, sleCredential, 1, j); return tesSUCCESS; }; diff --git a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp index 3c4e55a16e..ac14c3b66d 100644 --- a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp @@ -3,7 +3,6 @@ #include #include #include -#include #include #include #include @@ -11,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -125,29 +125,29 @@ canAddHolding(ReadView const& view, MPTIssue const& mptIssue) [[nodiscard]] TER addEmptyHolding( - ApplyView& view, + ApplyViewContext ctx, AccountID const& accountID, XRPAmount priorBalance, MPTIssue const& mptIssue, beast::Journal journal) { auto const& mptID = mptIssue.getMptID(); - auto const mpt = view.peek(keylet::mptokenIssuance(mptID)); + auto const mpt = ctx.view.peek(keylet::mptokenIssuance(mptID)); if (!mpt) return tefINTERNAL; // LCOV_EXCL_LINE if (mpt->isFlag(lsfMPTLocked)) return tefINTERNAL; // LCOV_EXCL_LINE - if (view.peek(keylet::mptoken(mptID, accountID))) + if (ctx.view.peek(keylet::mptoken(mptID, accountID))) return tecDUPLICATE; if (accountID == mptIssue.getIssuer()) return tesSUCCESS; - return authorizeMPToken(view, priorBalance, mptID, accountID, journal); + return authorizeMPToken(ctx, priorBalance, mptID, accountID, journal); } [[nodiscard]] TER authorizeMPToken( - ApplyView& view, + ApplyViewContext ctx, XRPAmount const& priorBalance, MPTID const& mptIssuanceID, AccountID const& account, @@ -155,7 +155,7 @@ authorizeMPToken( std::uint32_t flags, std::optional holderID) { - auto const sleAcct = view.peek(keylet::account(account)); + auto const sleAcct = ctx.view.peek(keylet::account(account)); if (!sleAcct) return tecINTERNAL; // LCOV_EXCL_LINE @@ -170,19 +170,19 @@ authorizeMPToken( if ((flags & tfMPTUnauthorize) != 0u) { auto const mptokenKey = keylet::mptoken(mptIssuanceID, account); - auto const sleMpt = view.peek(mptokenKey); + auto const sleMpt = ctx.view.peek(mptokenKey); if (!sleMpt || (*sleMpt)[sfMPTAmount] != 0 || - (view.rules().enabled(fixCleanup3_1_3) && + (ctx.view.rules().enabled(fixCleanup3_1_3) && (*sleMpt)[~sfLockedAmount].valueOr(0) != 0)) return tecINTERNAL; // LCOV_EXCL_LINE - if (!view.dirRemove( + if (!ctx.view.dirRemove( keylet::ownerDir(account), (*sleMpt)[sfOwnerNode], sleMpt->key(), false)) return tecINTERNAL; // LCOV_EXCL_LINE - adjustOwnerCount(view, sleAcct, -1, journal); + decreaseOwnerCountForObject(ctx.view, sleAcct, sleMpt, 1, journal); - view.erase(sleMpt); + ctx.view.erase(sleMpt); return tesSUCCESS; } @@ -190,47 +190,57 @@ authorizeMPToken( // - add the new mptokenKey to the owner directory // - create the MPToken object for the holder + // A reserve sponsor only covers tx.Account's own objects. + auto const sponsorExp = getEffectiveTxReserveSponsor(ctx, sleAcct); + if (!sponsorExp) + return sponsorExp.error(); // LCOV_EXCL_LINE + auto const sponsorSle = *sponsorExp; + // 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. - std::uint32_t const uOwnerCount = sleAcct->getFieldU32(sfOwnerCount); - XRPAmount const reserveCreate( - (uOwnerCount < 2) ? XRPAmount(beast::kZero) - : view.fees().accountReserve(uOwnerCount + 1)); - - if (priorBalance < reserveCreate) - return tecINSUFFICIENT_RESERVE; + // The "free-tier" shortcut (ownerCount < 2) does not apply once a sponsor is on + // the tx — the sponsor must always cover the reserve (via balance or prefunded + // budget), so this check always runs for sponsored transactions. + if (sponsorSle || ownerCount(sleAcct, journal) >= 2) + { + if (auto const ret = checkReserve( + ctx, sleAcct, priorBalance, sponsorSle, {.ownerCountDelta = 1}, journal); + !isTesSuccess(ret)) + return ret; + } // Defensive check before we attempt to create MPToken for the issuer - auto const mpt = view.read(keylet::mptokenIssuance(mptIssuanceID)); + auto const mpt = ctx.view.read(keylet::mptokenIssuance(mptIssuanceID)); if (!mpt || mpt->getAccountID(sfIssuer) == account) { // LCOV_EXCL_START UNREACHABLE("xrpl::authorizeMPToken : invalid issuance or issuers token"); - if (view.rules().enabled(featureLendingProtocol)) + if (ctx.view.rules().enabled(featureLendingProtocol)) return tecINTERNAL; // LCOV_EXCL_STOP } auto const mptokenKey = keylet::mptoken(mptIssuanceID, account); auto mptoken = std::make_shared(mptokenKey); - if (auto ter = dirLink(view, account, mptoken)) + if (auto ter = dirLink(ctx.view, account, mptoken)) return ter; // LCOV_EXCL_LINE (*mptoken)[sfAccount] = account; (*mptoken)[sfMPTokenIssuanceID] = mptIssuanceID; (*mptoken)[sfFlags] = 0; - view.insert(mptoken); + ctx.view.insert(mptoken); // Update owner count. - adjustOwnerCount(view, sleAcct, 1, journal); + increaseOwnerCount(ctx.view, sleAcct, sponsorSle, 1, journal); + addSponsorToLedgerEntry(mptoken, sponsorSle); return tesSUCCESS; } - auto const sleMptIssuance = view.read(keylet::mptokenIssuance(mptIssuanceID)); + auto const sleMptIssuance = ctx.view.read(keylet::mptokenIssuance(mptIssuanceID)); if (!sleMptIssuance) return tecINTERNAL; // LCOV_EXCL_LINE @@ -240,7 +250,7 @@ authorizeMPToken( if (account != (*sleMptIssuance)[sfIssuer]) return tecINTERNAL; // LCOV_EXCL_LINE - auto const sleMpt = view.peek(keylet::mptoken(mptIssuanceID, *holderID)); + auto const sleMpt = ctx.view.peek(keylet::mptoken(mptIssuanceID, *holderID)); if (!sleMpt) return tecINTERNAL; // LCOV_EXCL_LINE @@ -263,13 +273,13 @@ authorizeMPToken( if (flagsIn != flagsOut) sleMpt->setFieldU32(sfFlags, flagsOut); - view.update(sleMpt); + ctx.view.update(sleMpt); return tesSUCCESS; } [[nodiscard]] TER removeEmptyHolding( - ApplyView& view, + ApplyViewContext ctx, AccountID const& accountID, MPTIssue const& mptIssue, beast::Journal journal) @@ -279,7 +289,7 @@ removeEmptyHolding( // a token does exist, it will get deleted. If not, return success. bool const accountIsIssuer = accountID == mptIssue.getIssuer(); auto const& mptID = mptIssue.getMptID(); - auto const mptoken = view.peek(keylet::mptoken(mptID, accountID)); + auto const mptoken = ctx.view.peek(keylet::mptoken(mptID, accountID)); if (!mptoken) return accountIsIssuer ? (TER)tesSUCCESS : (TER)tecOBJECT_NOT_FOUND; // Unlike a trust line, if the account is the issuer, and the token has a @@ -287,7 +297,7 @@ removeEmptyHolding( // accounting out of balance, so fail. Since this should be impossible // anyway, I'm not going to put any effort into it. if (mptoken->at(sfMPTAmount) != 0 || - (view.rules().enabled(fixCleanup3_1_3) && (*mptoken)[~sfLockedAmount].valueOr(0) != 0)) + (ctx.view.rules().enabled(fixCleanup3_1_3) && (*mptoken)[~sfLockedAmount].valueOr(0) != 0)) return tecHAS_OBLIGATIONS; // Don't delete if the token still has confidential balances @@ -300,7 +310,7 @@ removeEmptyHolding( } return authorizeMPToken( - view, + ctx, {}, // priorBalance mptID, accountID, @@ -419,13 +429,13 @@ requireAuth( [[nodiscard]] TER enforceMPTokenAuthorization( - ApplyView& view, + ApplyViewContext ctx, MPTID const& mptIssuanceID, AccountID const& account, XRPAmount const& priorBalance, // for MPToken authorization beast::Journal j) { - auto const sleIssuance = view.read(keylet::mptokenIssuance(mptIssuanceID)); + auto const sleIssuance = ctx.view.read(keylet::mptokenIssuance(mptIssuanceID)); if (!sleIssuance) return tefINTERNAL; // LCOV_EXCL_LINE @@ -437,7 +447,7 @@ enforceMPTokenAuthorization( return tefINTERNAL; // LCOV_EXCL_LINE auto const keylet = keylet::mptoken(mptIssuanceID, account); - auto const sleToken = view.read(keylet); // NOTE: might be null + auto const sleToken = ctx.view.read(keylet); // NOTE: might be null auto const maybeDomainID = sleIssuance->at(~sfDomainID); bool expired = false; bool const authorizedByDomain = [&]() -> bool { @@ -445,7 +455,7 @@ enforceMPTokenAuthorization( if (!maybeDomainID.has_value()) return false; // LCOV_EXCL_LINE - auto const ter = verifyValidDomain(view, account, *maybeDomainID, j); + auto const ter = verifyValidDomain(ctx.view, account, *maybeDomainID, j); if (isTesSuccess(ter)) return true; if (ter == tecEXPIRED) @@ -500,7 +510,7 @@ enforceMPTokenAuthorization( maybeDomainID.has_value() && sleToken == nullptr, "xrpl::enforceMPTokenAuthorization : new MPToken for domain"); if (auto const err = authorizeMPToken( - view, + ctx, priorBalance, // priorBalance mptIssuanceID, // mptIssuanceID account, // account @@ -915,6 +925,7 @@ createMPToken( ApplyView& view, MPTID const& mptIssuanceID, AccountID const& account, + SLE::ref sponsorSle, std::uint32_t const flags) { auto const mptokenKey = keylet::mptoken(mptIssuanceID, account); @@ -931,6 +942,8 @@ createMPToken( (*mptoken)[sfFlags] = flags; (*mptoken)[sfOwnerNode] = *ownerNode; + addSponsorToLedgerEntry(mptoken, sponsorSle); + view.insert(mptoken); return tesSUCCESS; @@ -941,6 +954,7 @@ checkCreateMPT( xrpl::ApplyView& view, xrpl::MPTIssue const& mptIssue, xrpl::AccountID const& holder, + SLE::ref sponsorSle, beast::Journal j) { if (mptIssue.getIssuer() == holder) @@ -950,7 +964,7 @@ checkCreateMPT( auto const mptokenID = keylet::mptoken(mptIssuanceID.key, holder); if (!view.exists(mptokenID)) { - if (auto const err = createMPToken(view, mptIssue.getMptID(), holder, 0); + if (auto const err = createMPToken(view, mptIssue.getMptID(), holder, sponsorSle, 0); !isTesSuccess(err)) { return err; @@ -960,7 +974,8 @@ checkCreateMPT( { return tecINTERNAL; } - adjustOwnerCount(view, sleAcct, 1, j); + + increaseOwnerCount(view, sleAcct, sponsorSle, 1, j); } return tesSUCCESS; } diff --git a/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp b/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp index 6dca715a8c..c67ab6775d 100644 --- a/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp @@ -269,11 +269,7 @@ insertToken(ApplyView& view, AccountID owner, STObject&& nft) // the NFT. SLE::pointer const page = getPageForToken(view, owner, nft[sfNFTokenID], [](ApplyView& view, AccountID const& owner) { - adjustOwnerCount( - view, - view.peek(keylet::account(owner)), - 1, - beast::Journal{beast::Journal::getNullSink()}); + increaseOwnerCount(view, owner, {}, 1, beast::Journal{beast::Journal::getNullSink()}); }); if (!page) @@ -411,21 +407,17 @@ removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID, S curr->setFieldArray(sfNFTokens, arr); view.update(curr); - int cnt = 0; + std::uint32_t cnt = 0; if (prev && mergePages(view, prev, curr)) - cnt--; + ++cnt; if (next && mergePages(view, curr, next)) - cnt--; + ++cnt; if (cnt != 0) { - adjustOwnerCount( - view, - view.peek(keylet::account(owner)), - cnt, - beast::Journal{beast::Journal::getNullSink()}); + decreaseOwnerCount(view, owner, {}, cnt, beast::Journal{beast::Journal::getNullSink()}); } return tesSUCCESS; @@ -460,11 +452,7 @@ removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID, S curr->makeFieldAbsent(sfPreviousPageMin); } - adjustOwnerCount( - view, - view.peek(keylet::account(owner)), - -1, - beast::Journal{beast::Journal::getNullSink()}); + decreaseOwnerCount(view, owner, {}, 1, beast::Journal{beast::Journal::getNullSink()}); view.update(curr); view.erase(prev); @@ -502,7 +490,7 @@ removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID, S view.erase(curr); - int cnt = 1; + uint32_t cnt = 1; // Since we're here, try to consolidate the previous and current pages // of the page we removed (if any) into one. mergePages() _should_ @@ -519,11 +507,7 @@ removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID, S view.peek(Keylet(ltNFTOKEN_PAGE, next->key())))) cnt++; - adjustOwnerCount( - view, - view.peek(keylet::account(owner)), - -1 * cnt, - beast::Journal{beast::Journal::getNullSink()}); + decreaseOwnerCount(view, owner, {}, cnt, beast::Journal{beast::Journal::getNullSink()}); return tesSUCCESS; } @@ -639,8 +623,7 @@ deleteTokenOffer(ApplyView& view, SLE::ref offer) false)) return false; - adjustOwnerCount( - view, view.peek(keylet::account(owner)), -1, beast::Journal{beast::Journal::getNullSink()}); + decreaseOwnerCount(view, owner, {}, 1, beast::Journal{beast::Journal::getNullSink()}); view.erase(offer); return true; @@ -743,9 +726,11 @@ repairNFTokenDirectoryLinks(ApplyView& view, AccountID const& owner) auto const newPrev = view.peek(Keylet(ltNFTOKEN_PAGE, *prevLink)); if (!newPrev) { + // LCOV_EXCL_START Throw( "NFTokenPage directory for " + to_string(owner) + - " cannot be repaired. Unexpected link problem."); + " cannot be repaired. Unexpected link problem."); + // LCOV_EXCL_STOP } newPrev->at(sfNextPageMin) = nextPage->key(); view.update(newPrev); @@ -931,7 +916,7 @@ tokenOfferCreateApply( { Keylet const acctKeylet = keylet::account(acctID); if (auto const acct = view.read(acctKeylet); - priorBalance < view.fees().accountReserve((*acct)[sfOwnerCount] + 1)) + priorBalance < accountReserve(view, acct, j, {.ownerCountDelta = 1})) return tecINSUFFICIENT_RESERVE; auto const offerID = keylet::nftokenOffer(acctID, seqProxy.value()); @@ -983,7 +968,7 @@ tokenOfferCreateApply( } // Update owner count. - adjustOwnerCount(view, view.peek(acctKeylet), 1, j); + increaseOwnerCount(view, acctID, {}, 1, j); return tesSUCCESS; } diff --git a/src/libxrpl/ledger/helpers/OfferHelpers.cpp b/src/libxrpl/ledger/helpers/OfferHelpers.cpp index 6e72b71564..caa6ffb8ae 100644 --- a/src/libxrpl/ledger/helpers/OfferHelpers.cpp +++ b/src/libxrpl/ledger/helpers/OfferHelpers.cpp @@ -55,7 +55,7 @@ offerDelete(ApplyView& view, SLE::ref sle, beast::Journal j) } } - adjustOwnerCount(view, view.peek(keylet::account(owner)), -1, j); + decreaseOwnerCountForObject(view, owner, sle, 1, j); view.erase(sle); diff --git a/src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp b/src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp index 03b68e8860..d5a0b25681 100644 --- a/src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp +++ b/src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp @@ -58,7 +58,7 @@ closeChannel(SLE::ref slep, ApplyView& view, uint256 const& key, beast::Journal XRPL_ASSERT( (*slep)[sfAmount] >= (*slep)[sfBalance], "xrpl::closeChannel : minimum channel amount"); (*sle)[sfBalance] = (*sle)[sfBalance] + (*slep)[sfAmount] - (*slep)[sfBalance]; - adjustOwnerCount(view, sle, -1, j); + decreaseOwnerCountForObject(view, sle, slep, 1, j); view.update(sle); // Remove PayChan from ledger diff --git a/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp b/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp index 5a2995c030..868c9fb26d 100644 --- a/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp +++ b/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -195,6 +196,7 @@ trustCreate( // Issuer should be the account being set. std::uint32_t uQualityIn, std::uint32_t uQualityOut, + SLE::ref sponsorSle, beast::Journal j) { JLOG(j.trace()) << "trustCreate: " << to_string(uSrcAccountID) << ", " @@ -281,7 +283,9 @@ trustCreate( } sleRippleState->setFieldU32(sfFlags, uFlags); - adjustOwnerCount(view, sleAccount, 1, j); + increaseOwnerCount(view, sleAccount, sponsorSle, 1, j); + + addSponsorToLedgerEntry(sleRippleState, sponsorSle, bSetHigh ? sfHighSponsor : sfLowSponsor); // ONLY: Create ripple balance. sleRippleState->setFieldAmount(sfBalance, bSetHigh ? -saBalance : saBalance); @@ -317,6 +321,9 @@ trustDelete( return tefBAD_LEDGER; // LCOV_EXCL_LINE } + removeSponsorFromLedgerEntry(sleRippleState, sfHighSponsor); + removeSponsorFromLedgerEntry(sleRippleState, sfLowSponsor); + JLOG(j.trace()) << "trustDelete: Deleting ripple line: state"; view.erase(sleRippleState); @@ -369,11 +376,15 @@ updateTrustLine( { // VFALCO Where is the line being deleted? // Clear the reserve of the sender, possibly delete the line! - adjustOwnerCount(view, sle, -1, j); + auto const currentSponsor = + getLedgerEntryReserveSponsor(view, state, bSenderHigh ? sfHighSponsor : sfLowSponsor); + decreaseOwnerCount(view, sle, currentSponsor, 1, j); // Clear reserve flag. state->clearFlag(senderReserveFlag); + removeSponsorFromLedgerEntry(state, !bSenderHigh ? sfLowSponsor : sfHighSponsor); + // Balance is zero, receiver reserve is clear. if (!after && !state->isFlag(receiverReserveFlag)) return true; @@ -381,12 +392,14 @@ updateTrustLine( return false; } +// Only used in tests TER issueIOU( ApplyView& view, AccountID const& account, STAmount const& amount, Issue const& issue, + SLE::ref sponsorSle, beast::Journal j) { XRPL_ASSERT( @@ -472,6 +485,7 @@ issueIOU( limit, 0, 0, + sponsorSle, j); } @@ -620,7 +634,7 @@ canTransfer(ReadView const& view, Issue const& issue, AccountID const& from, Acc TER addEmptyHolding( - ApplyView& view, + ApplyViewContext ctx, AccountID const& accountID, XRPAmount priorBalance, Issue const& issue, @@ -632,30 +646,45 @@ addEmptyHolding( auto const& issuerId = issue.getIssuer(); auto const& currency = issue.currency; - if (isGlobalFrozen(view, issuerId)) + if (isGlobalFrozen(ctx.view, issuerId)) return tecFROZEN; // LCOV_EXCL_LINE auto const& srcId = issuerId; auto const& dstId = accountID; auto const high = srcId > dstId; auto const index = keylet::trustLine(srcId, dstId, currency); - auto const sleSrc = view.peek(keylet::account(srcId)); - auto const sleDst = view.peek(keylet::account(dstId)); + auto const sleSrc = ctx.view.peek(keylet::account(srcId)); + auto const sleDst = ctx.view.peek(keylet::account(dstId)); if (!sleDst || !sleSrc) return tefINTERNAL; // LCOV_EXCL_LINE if (!sleSrc->isFlag(lsfDefaultRipple)) return tecINTERNAL; // LCOV_EXCL_LINE // If the line already exists, don't create it again. - if (view.read(index)) + if (ctx.view.read(index)) return tecDUPLICATE; + // A reserve sponsor only covers tx.Account's own objects. + auto const sponsorExp = getEffectiveTxReserveSponsor(ctx, sleDst); + if (!sponsorExp) + return sponsorExp.error(); // LCOV_EXCL_LINE + auto const sponsorSle = *sponsorExp; + // Can the account cover the trust line reserve ? - std::uint32_t const ownerCount = sleDst->at(sfOwnerCount); - if (priorBalance < view.fees().accountReserve(ownerCount + 1)) - return tecNO_LINE_INSUF_RESERVE; + if (auto const ret = checkReserve( + ctx, + sleDst, + priorBalance, + sponsorSle, + {.ownerCountDelta = 1}, + journal, + tecNO_LINE_INSUF_RESERVE); + !isTesSuccess(ret)) + { + return ret; + } return trustCreate( - view, + ctx.view, high, srcId, dstId, @@ -669,19 +698,20 @@ addEmptyHolding( /*saLimit=*/STAmount{Issue{currency, dstId}}, /*uQualityIn=*/0, /*uQualityOut=*/0, + sponsorSle, journal); } TER removeEmptyHolding( - ApplyView& view, + ApplyViewContext ctx, AccountID const& accountID, Issue const& issue, beast::Journal journal) { if (issue.native()) { - auto const sle = view.read(keylet::account(accountID)); + auto const sle = ctx.view.read(keylet::account(accountID)); if (!sle) return tecINTERNAL; // LCOV_EXCL_LINE @@ -696,7 +726,7 @@ removeEmptyHolding( // If the account is the issuer, then no line should exist. Check anyway. // If a line does exist, it will get deleted. If not, return success. bool const accountIsIssuer = accountID == issue.account; - auto const line = view.peek(keylet::trustLine(accountID, issue)); + auto const line = ctx.view.peek(keylet::trustLine(accountID, issue)); if (!line) return accountIsIssuer ? (TER)tesSUCCESS : (TER)tecOBJECT_NOT_FOUND; if (!accountIsIssuer && line->at(sfBalance)->iou() != beast::kZero) @@ -706,33 +736,43 @@ removeEmptyHolding( if (line->isFlag(lsfLowReserve)) { // Clear reserve for low account. - auto sleLowAccount = view.peek(keylet::account(line->at(sfLowLimit)->getIssuer())); + auto sleLowAccount = ctx.view.peek(keylet::account(line->at(sfLowLimit)->getIssuer())); if (!sleLowAccount) return tecINTERNAL; // LCOV_EXCL_LINE - adjustOwnerCount(view, sleLowAccount, -1, journal); + auto const currentLowSponsor = getLedgerEntryReserveSponsor(ctx.view, line, sfLowSponsor); + + decreaseOwnerCount(ctx.view, sleLowAccount, currentLowSponsor, 1, journal); // It's not really necessary to clear the reserve flag, since the line // is about to be deleted, but this will make the metadata reflect an // accurate state at the time of deletion. line->clearFlag(lsfLowReserve); + removeSponsorFromLedgerEntry(line, sfLowSponsor); } if (line->isFlag(lsfHighReserve)) { // Clear reserve for high account. - auto sleHighAccount = view.peek(keylet::account(line->at(sfHighLimit)->getIssuer())); + auto sleHighAccount = ctx.view.peek(keylet::account(line->at(sfHighLimit)->getIssuer())); if (!sleHighAccount) return tecINTERNAL; // LCOV_EXCL_LINE - adjustOwnerCount(view, sleHighAccount, -1, journal); + auto const currentHighSponsor = getLedgerEntryReserveSponsor(ctx.view, line, sfHighSponsor); + + decreaseOwnerCount(ctx.view, sleHighAccount, currentHighSponsor, 1, journal); // It's not really necessary to clear the reserve flag, since the line // is about to be deleted, but this will make the metadata reflect an // accurate state at the time of deletion. line->clearFlag(lsfHighReserve); + removeSponsorFromLedgerEntry(line, sfHighSponsor); } return trustDelete( - view, line, line->at(sfLowLimit)->getIssuer(), line->at(sfHighLimit)->getIssuer(), journal); + ctx.view, + line, + line->at(sfLowLimit)->getIssuer(), + line->at(sfHighLimit)->getIssuer(), + journal); } TER @@ -768,6 +808,9 @@ deleteAMMTrustLine( if (ammAccountID && (low != *ammAccountID && high != *ammAccountID)) return terNO_AMM; + auto const sponsorSle = + getLedgerEntryReserveSponsor(view, sleState, !ammLow ? sfLowSponsor : sfHighSponsor); + if (auto const ter = trustDelete(view, sleState, low, high, j); !isTesSuccess(ter)) { JLOG(j.error()) << "deleteAMMTrustLine: failed to delete the trustline."; @@ -778,7 +821,7 @@ deleteAMMTrustLine( if (!sleState->isFlag(uFlags)) return tecINTERNAL; // LCOV_EXCL_LINE - adjustOwnerCount(view, !ammLow ? sleLow : sleHigh, -1, j); + decreaseOwnerCount(view, !ammLow ? sleLow : sleHigh, sponsorSle, 1, j); return tesSUCCESS; } diff --git a/src/libxrpl/ledger/helpers/SponsorHelpers.cpp b/src/libxrpl/ledger/helpers/SponsorHelpers.cpp new file mode 100644 index 0000000000..7e0c041854 --- /dev/null +++ b/src/libxrpl/ledger/helpers/SponsorHelpers.cpp @@ -0,0 +1,345 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace xrpl { + +bool +isReserveSponsorAllowed(TxType txType) +{ + // Transaction types explicitly allow-listed for reserve sponsorship, for + // v1. Lazily-initialized function-local static: constructed once on first + // use, with no startup cost paid by clients that never call this. + static std::unordered_set const kReserveSponsorAllowed = { + ttDELEGATE_SET, + ttDEPOSIT_PREAUTH, + ttPAYMENT, + ttSIGNER_LIST_SET, + ttCHECK_CANCEL, + ttCHECK_CASH, + ttCHECK_CREATE, + ttESCROW_CANCEL, + ttESCROW_CREATE, + ttESCROW_FINISH, + ttPAYCHAN_CLAIM, + ttPAYCHAN_CREATE, + ttPAYCHAN_FUND, + ttCLAWBACK, + ttMPTOKEN_AUTHORIZE, + ttMPTOKEN_ISSUANCE_CREATE, + ttMPTOKEN_ISSUANCE_DESTROY, + ttMPTOKEN_ISSUANCE_SET, + ttTRUST_SET, + ttCREDENTIAL_ACCEPT, + ttCREDENTIAL_CREATE, + ttCREDENTIAL_DELETE, + ttACCOUNT_SET, + ttREGULAR_KEY_SET, + ttSPONSORSHIP_TRANSFER, + }; + return kReserveSponsorAllowed.contains(txType); +} + +std::optional +getTxReserveSponsorID(STTx const& tx) +{ + if (tx.isFieldPresent(sfSponsor) && isReserveSponsored(tx)) + { + XRPL_ASSERT( + getCurrentTransactionRules()->enabled( // NOLINT(bugprone-unchecked-optional-access) + featureSponsor), + "xrpl::getTxReserveSponsorID : sponsor exists + Sponsor enabled"); + return tx.getAccountID(sfSponsor); + } + return {}; +} + +std::expected +getTxReserveSponsor(ApplyViewContext ctx) +{ + auto const sponsorID = getTxReserveSponsorID(ctx.tx); + if (sponsorID) + { + XRPL_ASSERT( + ctx.view.rules().enabled(featureSponsor), + "xrpl::getTxReserveSponsor : sponsor exists + Sponsor enabled"); + auto sle = ctx.view.peek(keylet::account(*sponsorID)); + + // already checked in Transactor::checkSponsor + if (!sle) + return std::unexpected(tecINTERNAL); // LCOV_EXCL_LINE + return sle; + } + return SLE::pointer(); +} + +std::expected +getTxReserveSponsor(ReadView const& view, STTx const& tx) +{ + auto const sponsorID = getTxReserveSponsorID(tx); + if (sponsorID) + { + XRPL_ASSERT( + view.rules().enabled(featureSponsor), + "xrpl::getTxReserveSponsor : sponsor exists + Sponsor enabled"); + auto sle = view.read(keylet::account(*sponsorID)); + + // already checked in Transactor::checkSponsor + if (!sle) + return std::unexpected(tecINTERNAL); // LCOV_EXCL_LINE + return sle; + } + return SLE::pointer(); +} + +std::expected +getEffectiveTxReserveSponsor(ApplyViewContext ctx, SLE::const_ref accountSle) +{ + // A reserve sponsor only covers tx.Account's own objects. + if (ctx.view.rules().enabled(fixCleanup3_2_0)) + { + XRPL_ASSERT( + accountSle && accountSle->getType() == ltACCOUNT_ROOT, + "xrpl::getEffectiveTxReserveSponsor : accountSle exists and is account type"); + } + else + { + XRPL_ASSERT( + accountSle && + ((accountSle->getType() == ltACCOUNT_ROOT) || (accountSle->getType() == ltESCROW)), + "xrpl::getEffectiveTxReserveSponsor : accountSle exists and is account type"); + } + + if (isPseudoAccount(accountSle) || accountSle->getAccountID(sfAccount) != ctx.tx[sfAccount]) + return SLE::pointer(); + return getTxReserveSponsor(ctx); +} + +std::optional +getLedgerEntryReserveSponsorID(SLE::const_ref sle, SF_ACCOUNT const& field) +{ + XRPL_ASSERT( + (sle && + ((sle->getType() == ltRIPPLE_STATE && (field == sfHighSponsor || field == sfLowSponsor)) || + (sle->getType() != ltRIPPLE_STATE && field == sfSponsor))), + "xrpl::getLedgerEntryReserveSponsorID : correct sfield"); + + if (sle->isFieldPresent(field)) + return sle->getAccountID(field); + return {}; +} + +SLE::pointer +getLedgerEntryReserveSponsor(ApplyView& view, SLE::const_ref sle, SF_ACCOUNT const& field) +{ + auto const sponsorID = getLedgerEntryReserveSponsorID(sle, field); + if (sponsorID) + { + XRPL_ASSERT( + view.rules().enabled(featureSponsor), + "xrpl::getLedgerEntryReserveSponsor : sponsor exists + Sponsor enabled"); + return view.peek(keylet::account(*sponsorID)); + } + return {}; +} + +void +addSponsorToLedgerEntry(SLE::ref sle, SLE::const_ref sponsorSle, SF_ACCOUNT const& field) +{ + XRPL_ASSERT( + (sle->getType() == ltRIPPLE_STATE && (field == sfHighSponsor || field == sfLowSponsor)) || + (sle->getType() != ltRIPPLE_STATE && field == sfSponsor), + "addSponsorToLedgerEntry : Invalid field to the LedgerEntry"); + if (sponsorSle) + { + XRPL_ASSERT( + getCurrentTransactionRules()->enabled( // NOLINT(bugprone-unchecked-optional-access) + featureSponsor), + "xrpl::addSponsorToLedgerEntry : sponsor exists + Sponsor enabled"); + sle->setAccountID(field, sponsorSle->getAccountID(sfAccount)); + } +} + +void +addSponsorToLedgerEntry(ApplyViewContext ctx, SLE::ref sle, SF_ACCOUNT const& field) +{ + // getTxReserveSponsor yields a null pointer when the tx is not + // reserve-sponsored, so addSponsorToLedgerEntry becomes a no-op then. The + // error case (tecINTERNAL) is an already-checked invariant; skip stamping. + auto const sponsorSle = getTxReserveSponsor(ctx); + if (sponsorSle && *sponsorSle) + { + XRPL_ASSERT( + ctx.view.rules().enabled(featureSponsor), + "xrpl::addSponsorToLedgerEntry : sponsor exists + Sponsor enabled"); + addSponsorToLedgerEntry(sle, *sponsorSle, field); + } +} + +void +removeSponsorFromLedgerEntry(SLE::ref sle, SF_ACCOUNT const& field) +{ + XRPL_ASSERT( + (sle->getType() == ltRIPPLE_STATE && (field == sfHighSponsor || field == sfLowSponsor)) || + (sle->getType() != ltRIPPLE_STATE && field == sfSponsor), + "removeSponsorFromLedgerEntry : Invalid field to the LedgerEntry"); + if (sle->isFieldPresent(field)) + { + XRPL_ASSERT( + getCurrentTransactionRules()->enabled( // NOLINT(bugprone-unchecked-optional-access) + featureSponsor), + "xrpl::removeSponsorFromLedgerEntry : sponsor exists + Sponsor enabled"); + sle->makeFieldAbsent(field); + } +} + +bool +isLedgerEntryOwner(ReadView const& view, SLE const& sle, AccountID const& account) +{ + switch (sle.getType()) + { + case ltCHECK: + case ltESCROW: + case ltPAYCHAN: + case ltMPTOKEN: + case ltDELEGATE: + case ltDEPOSIT_PREAUTH: + return sle.getAccountID(sfAccount) == account; + case ltMPTOKEN_ISSUANCE: + return sle.getAccountID(sfIssuer) == account; + case ltSIGNER_LIST: { + auto const signerList = view.read(keylet::signerList(account)); + if (!signerList) + return false; + return signerList->key() == sle.key(); + } + case ltCREDENTIAL: { + auto const& ownerField = sle.isFlag(lsfAccepted) ? sfSubject : sfIssuer; + return sle.getAccountID(ownerField) == account; + } + case ltRIPPLE_STATE: { + if (sle.isFlag(lsfHighReserve)) + { + auto const highAccount = sle.getFieldAmount(sfHighLimit).getIssuer(); + if (highAccount == account) + return true; + } + if (sle.isFlag(lsfLowReserve)) + { + auto const lowAccount = sle.getFieldAmount(sfLowLimit).getIssuer(); + if (lowAccount == account) + return true; + } + // Reachable: the sponsee may be a third party or the side of the + // line that holds no reserve (e.g. the issuer). Callers map this + // to tecNO_PERMISSION. + return false; + } + default: + // LCOV_EXCL_START + UNREACHABLE("xrpl::isLedgerEntryOwner : object is not supported by sponsorship."); + return false; + // LCOV_EXCL_STOP + }; +} + +bool +isLedgerEntrySupportedBySponsorship(SLE const& sle) +{ + switch (sle.getType()) + { + case ltCHECK: + case ltESCROW: + case ltPAYCHAN: + case ltMPTOKEN: + case ltDELEGATE: + case ltDEPOSIT_PREAUTH: + case ltMPTOKEN_ISSUANCE: + case ltSIGNER_LIST: + case ltCREDENTIAL: + case ltRIPPLE_STATE: + return true; + default: + return false; + }; +} + +std::uint32_t +getLedgerEntryOwnerCount(SLE const& sle) +{ + switch (sle.getType()) + { + case ltORACLE: { + return calculateOracleReserve(sle.getFieldArray(sfPriceDataSeries)); + } + // Vaults require 2 owner counts (the vault and a pseudo-account) + case ltVAULT: + return 2; + case ltSIGNER_LIST: { + // Mirror SignerListSet's owner-count accounting so that create and + // delete agree. Modern lists (post-MultiSignReserve) carry the + // lsfOneOwnerCount flag and cost a single owner count. Legacy + // pre-MultiSignReserve lists cost 2 + signer_count owner counts + if (sle.isFlag(lsfOneOwnerCount)) + return 1; + return 2 + static_cast(sle.getFieldArray(sfSignerEntries).size()); + } + case ltACCOUNT_ROOT: + // LCOV_EXCL_START + UNREACHABLE("AccountRoots are not supported by object sponsorship."); + return 0; + // LCOV_EXCL_STOP + default: + return 1; + } +} + +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("xrpl::getLedgerEntrySponsorField : unknown owner for RippleState"); + return sfSponsor; + // LCOV_EXCL_STOP + } + default: + return sfSponsor; + } +} + +} // namespace xrpl diff --git a/src/libxrpl/ledger/helpers/TokenHelpers.cpp b/src/libxrpl/ledger/helpers/TokenHelpers.cpp index 0ab97a4b60..06e366847d 100644 --- a/src/libxrpl/ledger/helpers/TokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/TokenHelpers.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -571,7 +572,7 @@ canAddHolding(ReadView const& view, Asset const& asset) TER addEmptyHolding( - ApplyView& view, + ApplyViewContext ctx, AccountID const& accountID, XRPAmount priorBalance, Asset const& asset, @@ -579,21 +580,21 @@ addEmptyHolding( { return std::visit( [&](TIss const& issue) -> TER { - return addEmptyHolding(view, accountID, priorBalance, issue, journal); + return addEmptyHolding(ctx, accountID, priorBalance, issue, journal); }, asset.value()); } TER removeEmptyHolding( - ApplyView& view, + ApplyViewContext ctx, AccountID const& accountID, Asset const& asset, beast::Journal journal) { return std::visit( [&](TIss const& issue) -> TER { - return removeEmptyHolding(view, accountID, issue, journal); + return removeEmptyHolding(ctx, accountID, issue, journal); }, asset.value()); } @@ -647,6 +648,7 @@ directSendNoFeeIOU( AccountID const& uReceiverID, STAmount const& saAmount, bool bCheckIssuer, + SLE::ref sponsorSle, beast::Journal j) { AccountID const& issuer = saAmount.getIssuer(); @@ -717,7 +719,12 @@ directSendNoFeeIOU( // Sender quality out is 0. { // Clear the reserve of the sender, possibly delete the line! - adjustOwnerCount(view, view.peek(keylet::account(uSenderID)), -1, j); + auto const currentSponsor = getLedgerEntryReserveSponsor( + view, sleRippleState, !bSenderHigh ? sfLowSponsor : sfHighSponsor); + decreaseOwnerCount(view, view.peek(keylet::account(uSenderID)), currentSponsor, 1, j); + + removeSponsorFromLedgerEntry( + sleRippleState, !bSenderHigh ? sfLowSponsor : sfHighSponsor); // Clear reserve flag. sleRippleState->clearFlag(senderReserveFlag); @@ -780,6 +787,7 @@ directSendNoFeeIOU( saReceiverLimit, 0, 0, + sponsorSle, j); } @@ -794,6 +802,7 @@ directSendNoLimitIOU( STAmount const& saAmount, STAmount& saActual, beast::Journal j, + SLE::ref sponsorSle, WaiveTransferFee waiveFee) { auto const& issuer = saAmount.getIssuer(); @@ -806,7 +815,8 @@ directSendNoLimitIOU( if (uSenderID == issuer || uReceiverID == issuer || issuer == noAccount()) { // Direct send: redeeming IOUs and/or sending own IOUs. - auto const ter = directSendNoFeeIOU(view, uSenderID, uReceiverID, saAmount, false, j); + auto const ter = + directSendNoFeeIOU(view, uSenderID, uReceiverID, saAmount, false, sponsorSle, j); if (!isTesSuccess(ter)) return ter; saActual = saAmount; @@ -824,10 +834,12 @@ directSendNoLimitIOU( << to_string(uReceiverID) << " : deliver=" << saAmount.getFullText() << " cost=" << saActual.getFullText(); - TER terResult = directSendNoFeeIOU(view, issuer, uReceiverID, saAmount, true, j); + TER terResult = directSendNoFeeIOU(view, issuer, uReceiverID, saAmount, true, sponsorSle, j); if (tesSUCCESS == terResult) - terResult = directSendNoFeeIOU(view, uSenderID, issuer, saActual, true, j); + { + terResult = directSendNoFeeIOU(view, uSenderID, issuer, saActual, true, sponsorSle, j); + } return terResult; } @@ -870,7 +882,8 @@ directSendNoLimitMultiIOU( if (senderID == issuer || receiverID == issuer || issuer == noAccount()) { // Direct send: redeeming IOUs and/or sending own IOUs. - if (auto const ter = directSendNoFeeIOU(view, senderID, receiverID, amount, false, j); + if (auto const ter = + directSendNoFeeIOU(view, senderID, receiverID, amount, false, {}, j); !isTesSuccess(ter)) return ter; actual += amount; @@ -894,14 +907,14 @@ directSendNoLimitMultiIOU( << to_string(receiverID) << " : deliver=" << amount.getFullText() << " cost=" << actual.getFullText(); - if (TER const terResult = directSendNoFeeIOU(view, issuer, receiverID, amount, true, j)) + if (TER const terResult = directSendNoFeeIOU(view, issuer, receiverID, amount, true, {}, j)) return terResult; } if (senderID != issuer && takeFromSender) { if (TER const terResult = - directSendNoFeeIOU(view, senderID, issuer, takeFromSender, true, j)) + directSendNoFeeIOU(view, senderID, issuer, takeFromSender, true, {}, j)) return terResult; } @@ -915,6 +928,7 @@ accountSendIOU( AccountID const& uReceiverID, STAmount const& saAmount, beast::Journal j, + SLE::ref sponsorSle, WaiveTransferFee waiveFee) { if (view.rules().enabled(fixAMMv1_1)) @@ -946,7 +960,8 @@ accountSendIOU( JLOG(j.trace()) << "accountSendIOU: " << to_string(uSenderID) << " -> " << to_string(uReceiverID) << " : " << saAmount.getFullText(); - return directSendNoLimitIOU(view, uSenderID, uReceiverID, saAmount, saActual, j, waiveFee); + return directSendNoLimitIOU( + view, uSenderID, uReceiverID, saAmount, saActual, j, sponsorSle, waiveFee); } /* XRP send which does not check reserve and can do pure adjustment. @@ -1475,7 +1490,7 @@ directSendNoFee( { return saAmount.asset().visit( [&](Issue const&) { - return directSendNoFeeIOU(view, uSenderID, uReceiverID, saAmount, bCheckIssuer, j); + return directSendNoFeeIOU(view, uSenderID, uReceiverID, saAmount, bCheckIssuer, {}, j); }, [&](MPTIssue const&) { XRPL_ASSERT(!bCheckIssuer, "xrpl::directSendNoFee : not checking issuer"); @@ -1490,12 +1505,13 @@ accountSend( AccountID const& uReceiverID, STAmount const& saAmount, beast::Journal j, + SLE::ref sponsorSle, WaiveTransferFee waiveFee, AllowMPTOverflow allowOverflow) { return saAmount.asset().visit( [&](Issue const&) { - return accountSendIOU(view, uSenderID, uReceiverID, saAmount, j, waiveFee); + return accountSendIOU(view, uSenderID, uReceiverID, saAmount, j, sponsorSle, waiveFee); }, [&](MPTIssue const&) { return accountSendMPT( diff --git a/src/libxrpl/protocol/Indexes.cpp b/src/libxrpl/protocol/Indexes.cpp index 76ff868f9e..ced18d4db6 100644 --- a/src/libxrpl/protocol/Indexes.cpp +++ b/src/libxrpl/protocol/Indexes.cpp @@ -84,6 +84,7 @@ enum class LedgerNameSpace : std::uint16_t { Vault = 'V', LoanBroker = 'l', // lower-case L Loan = 'L', + Sponsorship = '>', // No longer used or supported. Left here to reserve the space to avoid accidental reuse. Contract [[deprecated]] = 'c', @@ -326,6 +327,12 @@ signerList(AccountID const& account) noexcept return signerList(account, 0); } +Keylet +sponsorship(AccountID const& sponsor, AccountID const& sponsee) noexcept +{ + return {ltSPONSORSHIP, indexHash(LedgerNameSpace::Sponsorship, sponsor, sponsee)}; +} + Keylet check(AccountID const& id, std::uint32_t seq) noexcept { diff --git a/src/libxrpl/protocol/InnerObjectFormats.cpp b/src/libxrpl/protocol/InnerObjectFormats.cpp index 66b2822a42..0bdb217771 100644 --- a/src/libxrpl/protocol/InnerObjectFormats.cpp +++ b/src/libxrpl/protocol/InnerObjectFormats.cpp @@ -160,6 +160,14 @@ InnerObjectFormats::InnerObjectFormats() {sfTxnSignature, SoeOptional}, {sfSigners, SoeOptional}, }); + + add(sfSponsorSignature.jsonName.cStr(), + sfSponsorSignature.getCode(), + { + {sfSigningPubKey, SoeOptional}, + {sfTxnSignature, SoeOptional}, + {sfSigners, SoeOptional}, + }); } InnerObjectFormats const& diff --git a/src/libxrpl/protocol/LedgerFormats.cpp b/src/libxrpl/protocol/LedgerFormats.cpp index 8b91bb7930..a29ea49266 100644 --- a/src/libxrpl/protocol/LedgerFormats.cpp +++ b/src/libxrpl/protocol/LedgerFormats.cpp @@ -15,6 +15,7 @@ LedgerFormats::getCommonFields() {sfLedgerIndex, SoeOptional}, {sfLedgerEntryType, SoeRequired}, {sfFlags, SoeRequired}, + {sfSponsor, SoeOptional}, }; return kCommonFields; } diff --git a/src/libxrpl/protocol/STObject.cpp b/src/libxrpl/protocol/STObject.cpp index 869700baed..e86b3fb6e6 100644 --- a/src/libxrpl/protocol/STObject.cpp +++ b/src/libxrpl/protocol/STObject.cpp @@ -634,16 +634,16 @@ STObject::getAccountID(SField const& field) const } AccountID -STObject::getFeePayer() const +STObject::getInitiator() const { - // If sfDelegate is present, the delegate account is the payer + // If sfDelegate is present, the delegate account is the initiator // note: if a delegate is specified, its authorization to act on behalf of the account is // enforced in `Transactor::invokeCheckPermission` // cryptographic signature validity is checked separately (e.g., in `Transactor::checkSign`) if (isFieldPresent(sfDelegate)) return getAccountID(sfDelegate); - // Default payer + // Default initiator return getAccountID(sfAccount); } diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp index 8438d3498b..be4ba2e8e5 100644 --- a/src/libxrpl/protocol/STTx.cpp +++ b/src/libxrpl/protocol/STTx.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include @@ -271,6 +272,13 @@ STTx::checkSign(Rules const& rules) const return std::unexpected("Counterparty: " + ret.error()); } + if (isFieldPresent(sfSponsorSignature)) + { + auto const sponsorSignatureObj = getFieldObject(sfSponsorSignature); + if (auto const ret = checkSign(rules, sponsorSignatureObj); !ret) + return std::unexpected("Sponsor: " + ret.error()); + } + // Verify the batch signer signatures here too, so they are cached with the // rest of signature checking (checkValidity / SF_SIGGOOD) and stay out of // the transaction engine. Gated on a batch (batchTxnIds_ seated) that @@ -551,7 +559,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 @@ -603,6 +611,15 @@ STTx::getBatchTransactionIDs() const return *batchTxnIds_; } +AccountID +STTx::getFeePayerID() const +{ + if (isFieldPresent(sfSponsor) && ((getFieldU32(sfSponsorFlags) & spfSponsorFee) != 0u)) + return at(sfSponsor); + + return getInitiator(); +} + //------------------------------------------------------------------------------ static bool diff --git a/src/libxrpl/protocol/TER.cpp b/src/libxrpl/protocol/TER.cpp index a6f8192a2f..c2167d58ce 100644 --- a/src/libxrpl/protocol/TER.cpp +++ b/src/libxrpl/protocol/TER.cpp @@ -107,6 +107,7 @@ transResults() MAKE_ERROR(tecPSEUDO_ACCOUNT, "This operation is not allowed against a pseudo-account."), MAKE_ERROR(tecPRECISION_LOSS, "The amounts used by the transaction cannot interact."), MAKE_ERROR(tecBAD_PROOF, "Proof cannot be verified"), + MAKE_ERROR(tecNO_SPONSOR_PERMISSION, "Sponsor has not authorized this transaction."), MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."), MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."), @@ -220,6 +221,7 @@ transResults() MAKE_ERROR(terADDRESS_COLLISION, "Failed to allocate an unique account address."), MAKE_ERROR(terNO_DELEGATE_PERMISSION, "Delegated account lacks permission to perform this transaction."), MAKE_ERROR(terLOCKED, "Fund is locked."), + MAKE_ERROR(terNO_PERMISSION, "No permission to perform requested operation."), MAKE_ERROR(tesSUCCESS, "The transaction was applied. Only final in a validated ledger."), }; diff --git a/src/libxrpl/protocol/TxFormats.cpp b/src/libxrpl/protocol/TxFormats.cpp index b926bdf0e5..e4d4c4b03c 100644 --- a/src/libxrpl/protocol/TxFormats.cpp +++ b/src/libxrpl/protocol/TxFormats.cpp @@ -30,6 +30,9 @@ TxFormats::getCommonFields() {sfSigners, SoeOptional}, // submit_multisigned {sfNetworkID, SoeOptional}, {sfDelegate, SoeOptional}, + {sfSponsor, SoeOptional}, + {sfSponsorFlags, SoeOptional}, + {sfSponsorSignature, SoeOptional}, }; return kCommonFields; } diff --git a/src/libxrpl/tx/Transactor.cpp b/src/libxrpl/tx/Transactor.cpp index 0f6988543e..065dead1fd 100644 --- a/src/libxrpl/tx/Transactor.cpp +++ b/src/libxrpl/tx/Transactor.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -41,6 +42,7 @@ #include #include +#include #include #include #include @@ -167,6 +169,58 @@ preflightCheckSimulateKeys(ApplyFlags flags, STObject const& sigObject, beast::J } // namespace detail +static NotTEC +preflight1Sponsor(PreflightContext const& ctx) +{ + bool const hasSponsor = ctx.tx.isFieldPresent(sfSponsor); + bool const hasSponsorFlags = ctx.tx.isFieldPresent(sfSponsorFlags); + bool const hasSponsorSig = ctx.tx.isFieldPresent(sfSponsorSignature); + + if ((hasSponsor || hasSponsorFlags || hasSponsorSig) && !ctx.rules.enabled(featureSponsor)) + return temDISABLED; + + if (hasSponsor != hasSponsorFlags) + { + JLOG(ctx.j.debug()) << "preflight1: sponsor and sponsor flags mismatch"; + return temINVALID_FLAG; + } + if (hasSponsorSig && (!hasSponsor || !hasSponsorFlags)) + { + JLOG(ctx.j.debug()) << "preflight1: sponsor signature without sponsor definition"; + return temMALFORMED; + } + + if (hasSponsorFlags) + { + auto const sponsorFlags = ctx.tx.getFieldU32(sfSponsorFlags); + if (((sponsorFlags & spfSponsorFlagMask) != 0u) || sponsorFlags == 0) + { + JLOG(ctx.j.debug()) << "preflight1: invalid sponsor flags"; + return temINVALID_FLAG; + } + + // Reserve sponsorship is only permitted for an explicit allow-list of + // transaction types, for v1. All other tx types reject spfSponsorReserve here. + if (isReserveSponsored(ctx.tx)) + { + if (!isReserveSponsorAllowed(ctx.tx.getTxnType())) + { + JLOG(ctx.j.debug()) + << "preflight1: spfSponsorReserve not allowed for this transaction type"; + return temINVALID_FLAG; + } + } + } + + if (hasSponsor && ctx.tx.getAccountID(sfSponsor) == ctx.tx.getAccountID(sfAccount)) + { + JLOG(ctx.j.debug()) << "preflight1: Sponsor account cannot be the same as the account"; + return temMALFORMED; + } + + return tesSUCCESS; +} + /** Performs early sanity checks on the account and fee fields */ NotTEC Transactor::preflight1(PreflightContext const& ctx, std::uint32_t flagMask) @@ -230,6 +284,9 @@ Transactor::preflight1(PreflightContext const& ctx, std::uint32_t flagMask) if (ctx.tx.isFlag(tfInnerBatchTxn) != ctx.parentBatchId.has_value()) return temINVALID_INNER_BATCH; + if (auto const ter = preflight1Sponsor(ctx); !isTesSuccess(ter)) + return ter; + return tesSUCCESS; } @@ -343,6 +400,44 @@ Transactor::checkPermission( return tesSUCCESS; } +NotTEC +Transactor::checkSponsor(ReadView const& view, STTx const& tx) +{ + if (!tx.isFieldPresent(sfSponsor)) + return tesSUCCESS; + + // Reserve sponsorship with permissioned delegation is disallowed. + if (tx.isFieldPresent(sfDelegate) && isReserveSponsored(tx)) + return temINVALID; + + if (!view.exists(keylet::account(tx.getAccountID(sfSponsor)))) + return terNO_ACCOUNT; + + // Skip Sponsorship existence checks if the sponsor has signed the transaction - this + // transaction is valid regardless of the Sponsorship object. + // The use of the Sponsorship object is properly handled in + // getFeePayer/checkReserve/increaseOwnerCount/decreaseOwnerCount. + if (tx.isFieldPresent(sfSponsorSignature)) + return tesSUCCESS; + + // If the transaction contains sfDelegate, the Sponsorship object should be + // between the sponsor and the delegate. + auto const sponsorshipSle = + view.read(keylet::sponsorship(tx.getAccountID(sfSponsor), tx.getInitiator())); + + // sponsorship object missing for pre-funded (no co-signing) tx + if (!sponsorshipSle) + return terNO_PERMISSION; + + if (isFeeSponsored(tx) && sponsorshipSle->isFlag(lsfSponsorshipRequireSignForFee)) + return terNO_PERMISSION; + + if (isReserveSponsored(tx) && sponsorshipSle->isFlag(lsfSponsorshipRequireSignForReserve)) + return terNO_PERMISSION; + + return tesSUCCESS; +} + XRPAmount Transactor::calculateBaseFee(ReadView const& view, STTx const& tx) { @@ -351,6 +446,7 @@ Transactor::calculateBaseFee(ReadView const& view, STTx const& tx) // The computation has two parts: // * The base fee, which is the same for most transactions. // * The additional cost of each multisignature on the transaction. + // * The additional cost of each multisignature on the sponsor. XRPAmount const baseFee = view.fees().base; // Each signer adds one more baseFee to the minimum required fee @@ -358,7 +454,15 @@ Transactor::calculateBaseFee(ReadView const& view, STTx const& tx) std::size_t const signerCount = tx.isFieldPresent(sfSigners) ? tx.getFieldArray(sfSigners).size() : 0; - return baseFee + (signerCount * baseFee); + std::size_t sponsorSignerCount = 0; + if (tx.isFieldPresent(sfSponsorSignature)) + { + auto const sponsorObj = tx.getFieldObject(sfSponsorSignature); + if (sponsorObj.isFieldPresent(sfSigners)) + sponsorSignerCount += sponsorObj.getFieldArray(sfSigners).size(); + } + + return baseFee + ((signerCount + sponsorSignerCount) * baseFee); } XRPAmount @@ -437,12 +541,51 @@ Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee) if (feePaid == beast::kZero) return tesSUCCESS; - auto const id = ctx.tx.getFeePayer(); - auto const sle = ctx.view.read(keylet::account(id)); - if (!sle) - return terNO_ACCOUNT; + auto const feePayer = getFeePayer(ctx.view, ctx.tx); + auto const payerSle = ctx.view.read(feePayer.keylet); - auto const balance = (*sle)[sfBalance].xrp(); + if (!payerSle) + { + if (feePayer.type == FeePayerType::SponsorPreFunded) + { + // Sanity check: already checked in checkSponsor + return tefINTERNAL; // LCOV_EXCL_LINE + } + + return terNO_ACCOUNT; + } + + XRPAmount maxSpendable = beast::kZero; + + if (feePayer.type == FeePayerType::SponsorPreFunded) + { + if (payerSle->getType() != ltSPONSORSHIP) + return tefINTERNAL; // LCOV_EXCL_LINE + + if (payerSle->isFieldPresent(feePayer.balanceField)) + maxSpendable = payerSle->getFieldAmount(feePayer.balanceField).xrp(); + + if (payerSle->isFieldPresent(sfMaxFee)) + { + auto const cap = payerSle->getFieldAmount(sfMaxFee).xrp(); + maxSpendable = std::min(maxSpendable, cap); + } + } + else + { + if (payerSle->getType() != ltACCOUNT_ROOT) + return tefINTERNAL; // LCOV_EXCL_LINE + + if (feePayer.type == FeePayerType::SponsorCoSigned) + { + auto const sponsorReserve = accountReserve(ctx.view, payerSle, ctx.j); + maxSpendable = payerSle->getFieldAmount(sfBalance).xrp() - sponsorReserve; + } + else + { + maxSpendable = payerSle->getFieldAmount(feePayer.balanceField).xrp(); + } + } // NOTE: Because preclaim evaluates against a static readview, it // does not reflect fee deductions from other transactions paid by @@ -451,12 +594,12 @@ Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee) // transactions, this check may pass optimistically. // The fee shortfall will be handled by the Transactor::reset mechanism, // which caps the fee to the remaining actual balance. - if (balance < feePaid) + if (maxSpendable < feePaid) { - JLOG(ctx.j.trace()) << "Insufficient balance:" << " balance=" << to_string(balance) + JLOG(ctx.j.trace()) << "Insufficient balance:" << " balance=" << to_string(maxSpendable) << " paid=" << to_string(feePaid); - if ((balance > beast::kZero) && !ctx.view.open()) + if ((maxSpendable > beast::kZero) && !ctx.view.open()) { // Closed ledger, non-zero balance, less than fee return tecINSUFF_FEE; @@ -473,16 +616,72 @@ Transactor::payFee() { auto const feePaid = ctx_.tx[sfFee].xrp(); - auto const feePayer = ctx_.tx.getFeePayer(); - auto const sle = view().peek(keylet::account(feePayer)); + auto const feePayer = getFeePayer(view(), ctx_.tx); + auto const sle = view().peek(feePayer.keylet); + + JLOG(j_.trace()) << "Fee payer: " + to_string(feePayer.id); + if (!sle) return tefINTERNAL; // LCOV_EXCL_LINE - // Deduct the fee, so it's not available during the transaction. - // Will only write the account back if the transaction succeeds. - sle->setFieldAmount(sfBalance, sle->getFieldAmount(sfBalance) - feePaid); - if (feePayer != accountID_) - view().update(sle); // done in `apply()` for the account + if (feePaid == beast::kZero) + return tesSUCCESS; + + XRPAmount balance = beast::kZero; + if (sle->isFieldPresent(feePayer.balanceField)) + { + balance = sle->getFieldAmount(feePayer.balanceField).xrp(); + } + else if (feePayer.balanceField != sfFeeAmount) + { + return tefINTERNAL; // LCOV_EXCL_LINE + } + + // A co-signed sponsor pays the fee out of its own account balance, but must + // never be charged into its account reserve, and a pre-funded sponsorship's + // fee is capped by sfMaxFee. Mirror the spendable amount computed in + // checkFee() so both limits are enforced on the apply path too. + XRPAmount spendable = balance; + if (feePayer.type == FeePayerType::SponsorCoSigned) + { + auto const sponsorReserve = accountReserve(view(), sle, j_); + // max(balance - reserve, 0) with overflow handling + spendable = balance > sponsorReserve ? balance - sponsorReserve : beast::kZero; + } + else if (feePayer.type == FeePayerType::SponsorPreFunded && sle->isFieldPresent(sfMaxFee)) + { + auto const cap = sle->getFieldAmount(sfMaxFee).xrp(); + spendable = std::min(spendable, cap); + } + + // Only sponsor fee-payers reject here on insufficient funds. For an + // ordinary account, the fee falls through and is capped by reset(), which + // caps to the account's balance. That capping is wrong for sponsors: a + // co-signed sponsor would be charged into its own reserve, and a prefunded + // sponsorship's fee amount should be rejected rather than partially spent. + if (feePaid > spendable && + (feePayer.type == FeePayerType::SponsorPreFunded || + feePayer.type == FeePayerType::SponsorCoSigned)) + { + if ((spendable > beast::kZero) && !view().open()) + return tecINSUFF_FEE; + + return terINSUF_FEE_B; + } + + auto const feeAmountAfter = balance - feePaid; + + if (feeAmountAfter == beast::kZero && feePayer.balanceField == sfFeeAmount) + { + // Because ltSponsorship.sfFeeAmount is soeOptional + sle->makeFieldAbsent(feePayer.balanceField); + } + else + { + sle->setFieldAmount(feePayer.balanceField, feeAmountAfter); + } + + view().update(sle); // VFALCO Should we call view().rawDestroyXRP() here as well? return tesSUCCESS; @@ -656,7 +855,7 @@ Transactor::ticketDelete( } // Update the Ticket owner's reserve. - adjustOwnerCount(view, sleAccount, -1, j); + decreaseOwnerCountForObject(view, sleAccount, sleTicket, 1, j); // Remove Ticket from ledger. view.erase(sleTicket); @@ -750,6 +949,21 @@ Transactor::checkSign( return tesSUCCESS; } + if (sigObject.isFieldPresent(sfSponsorSignature)) + { + // Co-signed sponsorship + + // Sanity check: already checked in preflight1 + if (!sigObject.isFieldPresent(sfSponsor)) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto const sponsorID = sigObject.getAccountID(sfSponsor); + auto const sponsorSignature = sigObject.getFieldObject(sfSponsorSignature); + if (auto const ret = checkSign(view, flags, std::nullopt, sponsorID, sponsorSignature, j); + !isTesSuccess(ret)) + return ret; + } + // If the pk is empty and not simulate or simulate and signers, // then we must be multi-signing. if (sigObject.isFieldPresent(sfSigners)) @@ -1079,11 +1293,38 @@ Transactor::reset(XRPAmount fee) if (!txnAcct) return {tefINTERNAL, beast::kZero}; - auto const payerSle = view().peek(keylet::account(ctx_.tx.getFeePayer())); + auto const feePayer = getFeePayer(view(), ctx_.tx); + auto const payerSle = view().peek(feePayer.keylet); + if (!payerSle) return {tefINTERNAL, beast::kZero}; // LCOV_EXCL_LINE - auto const balance = payerSle->getFieldAmount(sfBalance).xrp(); + XRPAmount balance = beast::kZero; + if (payerSle->isFieldPresent(feePayer.balanceField)) + { + balance = payerSle->getFieldAmount(feePayer.balanceField).xrp(); + } + else if (feePayer.balanceField != sfFeeAmount) + { + return {tefINTERNAL, beast::kZero}; // LCOV_EXCL_LINE + } + + if (feePayer.type == FeePayerType::SponsorPreFunded && payerSle->isFieldPresent(sfMaxFee)) + { + auto const cap = payerSle->getFieldAmount(sfMaxFee).xrp(); + fee = std::min(fee, cap); + } + + // A co-signed sponsor must never be charged into its own account reserve, + // so the fee is capped to the balance above the reserve rather than to the + // full balance. + XRPAmount spendable = balance; + if (feePayer.type == FeePayerType::SponsorCoSigned) + { + auto const sponsorReserve = accountReserve(view(), payerSle, j_); + // max(balance - reserve, 0) with overflow handling + spendable = balance > sponsorReserve ? balance - sponsorReserve : beast::kZero; + } // balance should have already been checked in checkFee / preFlight. XRPL_ASSERT( @@ -1093,8 +1334,8 @@ Transactor::reset(XRPAmount fee) // We retry/reject the transaction if the account balance is zero or // we're applying against an open ledger and the balance is less than // the fee - if (fee > balance) - fee = balance; + if (fee > spendable) + fee = spendable; // Since we reset the context, we need to charge the fee and update // the account's sequence number (or consume the Ticket) again. @@ -1102,7 +1343,17 @@ Transactor::reset(XRPAmount fee) // If for some reason we are unable to consume the ticket or sequence // then the ledger is corrupted. Rather than make things worse we // reject the transaction. - payerSle->setFieldAmount(sfBalance, balance - fee); + auto const feeAmountAfter = balance - fee; + if (feeAmountAfter == beast::kZero && feePayer.balanceField == sfFeeAmount) + { + // Because ltSponsorship.sfFeeAmount is soeOptional + payerSle->makeFieldAbsent(feePayer.balanceField); + } + else + { + payerSle->setFieldAmount(feePayer.balanceField, feeAmountAfter); + } + TER const ter{consumeSeqProxy(txnAcct)}; XRPL_ASSERT(isTesSuccess(ter), "xrpl::Transactor::reset : result is tesSUCCESS"); @@ -1116,6 +1367,48 @@ Transactor::reset(XRPAmount fee) return {ter, fee}; } +FeePayer +Transactor::getFeePayer(ReadView const& view, STTx const& tx) +{ + if (tx.isFieldPresent(sfSponsor) && isFeeSponsored(tx)) + { + auto const sponsorID = tx.getAccountID(sfSponsor); + auto const sponseeID = tx.getInitiator(); + auto const sponsorshipKeylet = keylet::sponsorship(sponsorID, sponseeID); + + // if pre-funded sponsorship exists, prefer it + if (view.exists(sponsorshipKeylet)) + { + // pre funded + return FeePayer{ + .id = sponsorID, + .keylet = sponsorshipKeylet, + .balanceField = sfFeeAmount, + .type = FeePayerType::SponsorPreFunded}; + } + + // Checked in Transactor::checkSponsor + XRPL_ASSERT( + tx.isFieldPresent(sfSponsorSignature), + "xrpl::getFeePayer has sponsor signature without a sponsorship object"); + + // co-signed + return FeePayer{ + .id = sponsorID, + .keylet = keylet::account(sponsorID), + .balanceField = sfBalance, + .type = FeePayerType::SponsorCoSigned}; + } + + AccountID const payerID = tx.getInitiator(); + auto const payerAccountKeylet = keylet::account(payerID); + auto const payerType = + tx.isFieldPresent(sfDelegate) ? FeePayerType::Delegate : FeePayerType::Account; + + return FeePayer{ + .id = payerID, .keylet = 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/applySteps.cpp b/src/libxrpl/tx/applySteps.cpp index caaacfd010..5af4f621a7 100644 --- a/src/libxrpl/tx/applySteps.cpp +++ b/src/libxrpl/tx/applySteps.cpp @@ -181,6 +181,9 @@ invokePreclaim(PreclaimContext const& ctx) if (NotTEC const result = T::checkPriorTxAndLastLedger(ctx)) return result; + if (NotTEC const result = T::checkSponsor(ctx.view, ctx.tx)) + return result; + if (NotTEC const result = Transactor::invokeCheckPermission(ctx.view, ctx.tx)) return result; diff --git a/src/libxrpl/tx/invariants/InvariantCheck.cpp b/src/libxrpl/tx/invariants/InvariantCheck.cpp index 32df44a96b..220a7a8e41 100644 --- a/src/libxrpl/tx/invariants/InvariantCheck.cpp +++ b/src/libxrpl/tx/invariants/InvariantCheck.cpp @@ -148,6 +148,15 @@ XRPNotCreated::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref a if (isXRP((*before)[sfAmount])) drops_ -= (*before)[sfAmount].xrp().drops(); break; + case ltSPONSORSHIP: + if (before->isFieldPresent(sfFeeAmount)) + { + XRPL_ASSERT( + isXRP((*before)[sfFeeAmount]), + "XRPNotCreated::visitEntry : Sponsorship.FeeAmount is XRP"); + drops_ -= (*before)[sfFeeAmount].xrp().drops(); + } + break; default: break; } @@ -168,6 +177,15 @@ XRPNotCreated::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref a if (!isDelete && isXRP((*after)[sfAmount])) drops_ += (*after)[sfAmount].xrp().drops(); break; + case ltSPONSORSHIP: + if (!isDelete && after->isFieldPresent(sfFeeAmount)) + { + XRPL_ASSERT( + isXRP((*after)[sfFeeAmount]), + "XRPNotCreated::visitEntry : Sponsorship.FeeAmount is XRP"); + drops_ += (*after)[sfFeeAmount].xrp().drops(); + } + break; default: break; } @@ -515,6 +533,20 @@ AccountRootsDeletedClean::finalize( if (enforce) return false; } + // An account should not be deleted with sponsorship fields + if (after->isFieldPresent(sfSponsoredOwnerCount) || + after->isFieldPresent(sfSponsoringOwnerCount) || + after->isFieldPresent(sfSponsoringAccountCount) || after->isFieldPresent(sfSponsor)) + { + JLOG(j.fatal()) << "Invariant failed: account deletion left " + "behind a sponsorship field"; + XRPL_ASSERT( + enforce, + "xrpl::AccountRootsDeletedClean::finalize : " + "deleted account has no sponsorship fields"); + if (enforce) + return false; + } // Simple types for (auto const& [keyletfunc, _1, _2] : kDirectAccountKeylets) { @@ -881,8 +913,10 @@ ValidPseudoAccounts::visitEntry(bool isDelete, SLE::const_ref before, SLE::const // 1. Exactly one of the pseudo-account fields is set. // 2. The sequence number is not changed. // 3. The lsfDisableMaster, lsfDefaultRipple, and lsfDepositAuth - // flags are set. + // flags are set. // 4. The RegularKey is not set. + // 5. The SponsoredOwnerCount, SponsoringOwnerCount, SponsoringAccountCount, Sponsor + // fields are not set. { std::vector const& fields = getPseudoAccountFields(); @@ -908,6 +942,12 @@ ValidPseudoAccounts::visitEntry(bool isDelete, SLE::const_ref before, SLE::const { errors_.emplace_back("pseudo-account has a regular key"); } + if (after->isFieldPresent(sfSponsoredOwnerCount) || + after->isFieldPresent(sfSponsoringOwnerCount) || after->isFieldPresent(sfSponsor) || + after->isFieldPresent(sfSponsoringAccountCount)) + { + errors_.emplace_back("pseudo-account has a sponsorship field"); + } } } } diff --git a/src/libxrpl/tx/invariants/SponsorshipInvariant.cpp b/src/libxrpl/tx/invariants/SponsorshipInvariant.cpp new file mode 100644 index 0000000000..bdf37492f3 --- /dev/null +++ b/src/libxrpl/tx/invariants/SponsorshipInvariant.cpp @@ -0,0 +1,158 @@ +#include +// +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace xrpl { + +// Add new sponsorship-related invariants implementations +void +SponsorshipOwnerCountsMatch::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) +{ + auto getSponsored = [](SLE::const_ref sle) -> std::uint32_t { + if (sle && sle->getType() == ltACCOUNT_ROOT) + return sle->getFieldU32(sfSponsoredOwnerCount); + return 0; + }; + auto getSponsoring = [](SLE::const_ref sle) -> std::uint32_t { + if (sle && sle->getType() == ltACCOUNT_ROOT) + return sle->getFieldU32(sfSponsoringOwnerCount); + return 0; + }; + + auto getOwnerCount = [](SLE::const_ref sle) -> std::uint32_t { + if (sle && sle->getType() == ltACCOUNT_ROOT) + return sle->getFieldU32(sfOwnerCount); + return 0; + }; + + auto getSponsoredObjectOwnerCount = [&](SLE::const_ref sle) -> std::uint32_t { + if (!sle) + return 0; + switch (sle->getType()) + { + case ltACCOUNT_ROOT: + return 0; + case ltRIPPLE_STATE: { + // A trust line can be reserve-sponsored independently on each + // side, so it may contribute up to two sponsored owner counts. + uint32_t ownerCount = 0; + if (sle->isFieldPresent(sfHighSponsor)) + ownerCount++; + if (sle->isFieldPresent(sfLowSponsor)) + ownerCount++; + return ownerCount; + } + default: + // Every other supported type carries a single sfSponsor field + // and contributes its full owner-count magnitude only when it is + // sponsored. + if (!sle->isFieldPresent(sfSponsor)) + return 0; + return getLedgerEntryOwnerCount(*sle); + } + }; + + // The values are implicitly casted to std::int64_t to calculate deltas. + std::int64_t const beforeSponsored = getSponsored(before); + std::int64_t const afterSponsored = getSponsored(after); + std::int64_t const beforeSponsoring = getSponsoring(before); + std::int64_t const afterSponsoring = getSponsoring(after); + + std::int64_t const beforeSponsoredObjectOwnerCount = getSponsoredObjectOwnerCount(before); + std::int64_t const afterSponsoredObjectOwnerCount = + isDelete ? 0 : getSponsoredObjectOwnerCount(after); + + deltaSponsoredOwnerCount_ += (afterSponsored - beforeSponsored); + deltaSponsoringOwnerCount_ += (afterSponsoring - beforeSponsoring); + + deltaSponsoredObjectOwnerCount_ += + (afterSponsoredObjectOwnerCount - beforeSponsoredObjectOwnerCount); + + if (getOwnerCount(after) < getSponsored(after)) + ownerCountBelowSponsored_ += 1; +} + +bool +SponsorshipOwnerCountsMatch::finalize( + STTx const&, + TER const, + XRPAmount const, + ReadView const&, + beast::Journal const& j) const +{ + if (deltaSponsoredOwnerCount_ != deltaSponsoringOwnerCount_) + { + JLOG(j.fatal()) << "Invariant failed: SponsoredOwnerCount does not " + "equal SponsoringOwnerCount delta."; + return false; + } + + if (ownerCountBelowSponsored_ > 0) + { + JLOG(j.fatal()) + << "Invariant failed: OwnerCount must be greater than or equal to SponsoredOwnerCount."; + return false; + } + + if (deltaSponsoredObjectOwnerCount_ != deltaSponsoredOwnerCount_) + { + JLOG(j.fatal()) << "Invariant failed: SponsoredObjectOwnerCount does not " + "equal SponsoredOwnerCount delta."; + return false; + } + + return true; +} + +void +SponsorshipAccountCountMatchesField::visitEntry(bool, SLE::const_ref before, SLE::const_ref after) +{ + auto getSponsoringAccountCount = [](SLE::const_ref sle) -> std::uint32_t { + if (sle && sle->getType() == ltACCOUNT_ROOT) + return sle->getFieldU32(sfSponsoringAccountCount); + return 0; + }; + + auto hasSponsorField = [](SLE::const_ref sle) -> bool { + return sle && sle->getType() == ltACCOUNT_ROOT && sle->isFieldPresent(sfSponsor); + }; + + std::int64_t const beforeCount = getSponsoringAccountCount(before); + std::int64_t const afterCount = getSponsoringAccountCount(after); + deltaSponsoringAccountCount_ += (afterCount - beforeCount); + + int const beforePresent = hasSponsorField(before) ? 1 : 0; + int const afterPresent = hasSponsorField(after) ? 1 : 0; + deltaSponsorFieldPresence_ += (afterPresent - beforePresent); +} + +bool +SponsorshipAccountCountMatchesField::finalize( + STTx const&, + TER const, + XRPAmount const, + ReadView const&, + beast::Journal const& j) const +{ + if (deltaSponsoringAccountCount_ != deltaSponsorFieldPresence_) + { + JLOG(j.fatal()) << "Invariant failed: Net delta of SponsoringAccountCount does not " + "match net delta of sfSponsor presence."; + return false; + } + + return true; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/invariants/VaultInvariant.cpp b/src/libxrpl/tx/invariants/VaultInvariant.cpp index 84814977db..a9ba0ec874 100644 --- a/src/libxrpl/tx/invariants/VaultInvariant.cpp +++ b/src/libxrpl/tx/invariants/VaultInvariant.cpp @@ -222,7 +222,10 @@ ValidVault::deltaAssetsTxAccount(STTx const& tx, XRPAmount fee) const if (!ret.has_value() || !vaultAsset.native()) return ret; - if (auto const delegate = tx[~sfDelegate]; delegate.has_value() && *delegate != tx[sfAccount]) + // Only add the fee back if tx[sfAccount] actually paid it. When the fee is + // paid by someone else (a delegate or a fee sponsor), the + // account's XRP balance moved only by the vault amount. + if (tx.getFeePayerID() != tx[sfAccount]) return ret; ret->delta += fee.drops(); @@ -1044,10 +1047,8 @@ ValidVault::finalize( case ttLOAN_SET: case ttLOAN_MANAGE: - case ttLOAN_PAY: { - // TBD + case ttLOAN_PAY: return true; - } default: // LCOV_EXCL_START diff --git a/src/libxrpl/tx/paths/BookStep.cpp b/src/libxrpl/tx/paths/BookStep.cpp index adb73f126e..bf24c66ee1 100644 --- a/src/libxrpl/tx/paths/BookStep.cpp +++ b/src/libxrpl/tx/paths/BookStep.cpp @@ -731,7 +731,7 @@ BookStep::forEachOffer( // Create MPToken for the offer's owner. No need to check // for the reserve since the offer is removed if it is consumed. // Therefore, the owner count remains the same. - if (auto const err = checkCreateMPT(sb, assetIn.get(), owner, j_); + if (auto const err = checkCreateMPT(sb, assetIn.get(), owner, {}, j_); !isTesSuccess(err)) { return true; diff --git a/src/libxrpl/tx/paths/MPTEndpointStep.cpp b/src/libxrpl/tx/paths/MPTEndpointStep.cpp index a47cfa15a5..0a0f6a9f27 100644 --- a/src/libxrpl/tx/paths/MPTEndpointStep.cpp +++ b/src/libxrpl/tx/paths/MPTEndpointStep.cpp @@ -410,7 +410,8 @@ MPTEndpointOfferCrossingStep::checkCreateMPT(ApplyView& view, xrpl::DebtDirectio // for the reserve since the offer doesn't go on the books // if crossed. Insufficient reserve is allowed if the offer // crossed. See CreateOffer::applyGuts() for reserve check. - if (auto const err = xrpl::checkCreateMPT(view, mptIssue_, dst_, j_); !isTesSuccess(err)) + if (auto const err = xrpl::checkCreateMPT(view, mptIssue_, dst_, {}, j_); + !isTesSuccess(err)) { JLOG(j_.trace()) << "MPTEndpointStep::checkCreateMPT: failed create MPT"; resetCache(srcDebtDir); diff --git a/src/libxrpl/tx/transactors/account/AccountDelete.cpp b/src/libxrpl/tx/transactors/account/AccountDelete.cpp index 254733a618..0055fce403 100644 --- a/src/libxrpl/tx/transactors/account/AccountDelete.cpp +++ b/src/libxrpl/tx/transactors/account/AccountDelete.cpp @@ -35,7 +35,6 @@ #include namespace xrpl { - bool AccountDelete::checkExtraFeatures(PreflightContext const& ctx) { @@ -268,6 +267,15 @@ AccountDelete::preclaim(PreclaimContext const& ctx) if (cp) return tecHAS_OBLIGATIONS; + if (sleAccount->isFieldPresent(sfSponsor)) + { + if (dst != sleAccount->getAccountID(sfSponsor)) + return tecNO_SPONSOR_PERMISSION; + } + if (sleAccount->isFieldPresent(sfSponsoringOwnerCount) || + sleAccount->isFieldPresent(sfSponsoringAccountCount)) + return tecHAS_OBLIGATIONS; + // We don't allow an account to be deleted if its sequence number // is within 256 of the current ledger. This prevents replay of old // transactions if this account is resurrected after it is deleted. @@ -394,6 +402,35 @@ AccountDelete::doApply() (*src)[sfBalance] = (*src)[sfBalance] - remainingBalance; ctx_.deliver(remainingBalance); + if (src->isFieldPresent(sfSponsor)) + { + auto const sponsorID = src->getAccountID(sfSponsor); + auto sponsorSle = view().peek(keylet::account(sponsorID)); + + if (!sponsorSle) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto const sponsoringAccountCount = sponsorSle->getFieldU32(sfSponsoringAccountCount); + + XRPL_ASSERT( + sponsoringAccountCount != 0, + "xrpl::AccountDelete::doApply : sponsoring account count is present"); + if (sponsoringAccountCount == 0) + { + // sanity check + // Since sfSponsoringAccountCount is set to soeDEFAULT, the field will not be + // present with a value of 0. + return tefINTERNAL; // LCOV_EXCL_LINE + } + sponsorSle->at(sfSponsoringAccountCount) = sponsoringAccountCount - 1; + view().update(sponsorSle); + + // Following line might look redundant, but without it, sfSponsor + // would end up remaining in after-ltAccountRoot during the + // InvariantCheck. + src->makeFieldAbsent(sfSponsor); + } + XRPL_ASSERT( (*src)[sfBalance] == XRPAmount(0), "xrpl::AccountDelete::doApply : source balance is zero"); diff --git a/src/libxrpl/tx/transactors/account/SignerListSet.cpp b/src/libxrpl/tx/transactors/account/SignerListSet.cpp index 2f95d32664..799c292bfc 100644 --- a/src/libxrpl/tx/transactors/account/SignerListSet.cpp +++ b/src/libxrpl/tx/transactors/account/SignerListSet.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -147,9 +148,7 @@ SignerListSet::preCompute() Transactor::preCompute(); } -// The return type is signed so it is compatible with the 3rd argument -// of adjustOwnerCount() (which must be signed). -static int +static std::uint32_t signerCountBasedOwnerCountDelta(std::size_t entryCount, Rules const& rules) { // We always compute the full change in OwnerCount, taking into account: @@ -165,7 +164,7 @@ signerCountBasedOwnerCountDelta(std::size_t entryCount, Rules const& rules) // units. A SignerList with 8 entries would cost 10 OwnerCount units. // // The static_cast should always be safe since entryCount should always - // be in the range from 1 to 32. + // be in the range from 1 to 32, so the result is always positive. // We've got a lot of room to grow. XRPL_ASSERT( entryCount >= STTx::kMinMultiSigners, @@ -196,12 +195,11 @@ removeSignersFromLedger( // There are two different ways that the OwnerCount could be managed. // If the lsfOneOwnerCount bit is set then remove just one owner count. // Otherwise use the pre-MultiSignReserve amendment calculation. - int removeFromOwnerCount = -1; + std::uint32_t removeFromOwnerCount = 1; if (!signers->isFlag(lsfOneOwnerCount)) { STArray const& actualList = signers->getFieldArray(sfSignerEntries); - removeFromOwnerCount = - signerCountBasedOwnerCountDelta(actualList.size(), view.rules()) * -1; + removeFromOwnerCount = signerCountBasedOwnerCountDelta(actualList.size(), view.rules()); } // Remove the node from the account directory. @@ -215,8 +213,8 @@ removeSignersFromLedger( // LCOV_EXCL_STOP } - adjustOwnerCount( - view, view.peek(accountKeylet), removeFromOwnerCount, registry.getJournal("View")); + decreaseOwnerCountForObject( + view, view.peek(accountKeylet), signers, removeFromOwnerCount, registry.getJournal("View")); view.erase(signers); @@ -315,19 +313,20 @@ SignerListSet::replaceSignerList() if (!sle) return tefINTERNAL; // LCOV_EXCL_LINE - // Compute new reserve. Verify the account has funds to meet the reserve. - std::uint32_t const oldOwnerCount{(*sle)[sfOwnerCount]}; - static constexpr int kAddedOwnerCount = 1; std::uint32_t const flags{lsfOneOwnerCount}; - XRPAmount const newReserve{view().fees().accountReserve(oldOwnerCount + kAddedOwnerCount)}; - // We check the reserve against the starting balance because we want to // allow dipping into the reserve to pay fees. This behavior is consistent // with TicketCreate. - if (preFeeBalance_ < newReserve) - return tecINSUFFICIENT_RESERVE; + if (auto const ret = checkReserve( + ctx_.getApplyViewContext(), + sle, + preFeeBalance_, + {.ownerCountDelta = kAddedOwnerCount}, + ctx_.journal); + !isTesSuccess(ret)) + return ret; // Everything's ducky. Add the ltSIGNER_LIST to the ledger. auto signerList = std::make_shared(signerListKeylet); @@ -349,7 +348,8 @@ SignerListSet::replaceSignerList() // If we succeeded, the new entry counts against the // creator's reserve. - adjustOwnerCount(view(), sle, kAddedOwnerCount, viewJ); + increaseOwnerCount(ctx_.getApplyViewContext(), sle, kAddedOwnerCount, viewJ); + addSponsorToLedgerEntry(ctx_.getApplyViewContext(), signerList); return tesSUCCESS; } diff --git a/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp b/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp index f3ea97a341..92a8dccdb9 100644 --- a/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp +++ b/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp @@ -435,8 +435,7 @@ transferHelper( return tecINTERNAL; // LCOV_EXCL_LINE { - auto const ownerCount = sleSrc->getFieldU32(sfOwnerCount); - auto const reserve = psb.fees().accountReserve(ownerCount); + auto const reserve = accountReserve(psb, sleSrc, j); auto const availableBalance = [&]() -> STAmount { STAmount curBal = (*sleSrc)[sfBalance]; @@ -726,9 +725,8 @@ finalizeClaimHelper( } // Remove the claim id from the ledger + decreaseOwnerCountForObject(outerSb, sleOwner, sleClaimID, 1, j); outerSb.erase(sleClaimID); - - adjustOwnerCount(outerSb, sleOwner, -1, j); } } @@ -1028,7 +1026,7 @@ applyCreateAccountAttestations( // Check reserve auto const balance = (*sleDoor)[sfBalance]; - auto const reserve = psb.fees().accountReserve((*sleDoor)[sfOwnerCount] + 1); + auto const reserve = accountReserve(psb, sleDoor, j, {.ownerCountDelta = 1}); if (balance < reserve) return std::unexpected(tecINSUFFICIENT_RESERVE); @@ -1137,7 +1135,7 @@ applyCreateAccountAttestations( return tecINTERNAL; // LCOV_EXCL_LINE // Reserve was already checked - adjustOwnerCount(psb, sleDoor, 1, j); + increaseOwnerCount(psb, sleDoor, {}, 1, j); psb.insert(createdSleClaimID); psb.update(sleDoor); } @@ -1439,7 +1437,7 @@ XChainCreateBridge::preclaim(PreclaimContext const& ctx) return terNO_ACCOUNT; auto const balance = (*sleAcc)[sfBalance]; - auto const reserve = ctx.view.fees().accountReserve((*sleAcc)[sfOwnerCount] + 1); + auto const reserve = accountReserve(ctx.view, sleAcc, ctx.j, {.ownerCountDelta = 1}); if (balance < reserve) return tecINSUFFICIENT_RESERVE; @@ -1484,7 +1482,7 @@ XChainCreateBridge::doApply() (*sleBridge)[sfOwnerNode] = *page; } - adjustOwnerCount(ctx_.view(), sleAcct, 1, ctx_.journal); + increaseOwnerCount(ctx_.view(), sleAcct, {}, 1, ctx_.journal); ctx_.view().insert(sleBridge); ctx_.view().update(sleAcct); @@ -1987,8 +1985,7 @@ XChainCreateClaimID::preclaim(PreclaimContext const& ctx) return terNO_ACCOUNT; auto const balance = (*sleAcc)[sfBalance]; - auto const reserve = ctx.view.fees().accountReserve((*sleAcc)[sfOwnerCount] + 1); - + auto const reserve = accountReserve(ctx.view, sleAcc, ctx.j, {.ownerCountDelta = 1}); if (balance < reserve) return tecINSUFFICIENT_RESERVE; } @@ -2046,7 +2043,7 @@ XChainCreateClaimID::doApply() (*sleClaimID)[sfOwnerNode] = *page; } - adjustOwnerCount(ctx_.view(), sleAcct, 1, ctx_.journal); + increaseOwnerCount(ctx_.view(), sleAcct, {}, 1, ctx_.journal); ctx_.view().insert(sleClaimID); ctx_.view().update(sleBridge); diff --git a/src/libxrpl/tx/transactors/check/CheckCancel.cpp b/src/libxrpl/tx/transactors/check/CheckCancel.cpp index d966772191..ed60817224 100644 --- a/src/libxrpl/tx/transactors/check/CheckCancel.cpp +++ b/src/libxrpl/tx/transactors/check/CheckCancel.cpp @@ -91,8 +91,7 @@ CheckCancel::doApply() } // If we succeeded, update the check owner's reserve. - auto const sleSrc = view().peek(keylet::account(srcId)); - adjustOwnerCount(view(), sleSrc, -1, viewJ); + decreaseOwnerCountForObject(view(), srcId, sleCheck, 1, viewJ); // Remove check from ledger. view().erase(sleCheck); diff --git a/src/libxrpl/tx/transactors/check/CheckCash.cpp b/src/libxrpl/tx/transactors/check/CheckCash.cpp index 80ef742f52..4e810d94cf 100644 --- a/src/libxrpl/tx/transactors/check/CheckCash.cpp +++ b/src/libxrpl/tx/transactors/check/CheckCash.cpp @@ -3,11 +3,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -30,7 +32,7 @@ #include #include -#include +#include #include namespace xrpl { @@ -175,7 +177,7 @@ CheckCash::preclaim(PreclaimContext const& ctx) // once the check is cashed, since the check's reserve will no // longer be required. So, if we're dealing in XRP, we add one // reserve's worth to the available funds. - if (value.native()) + if (value.native() && !sleCheck->isFieldPresent(sfSponsor)) availableFunds += XRPAmount{ctx.view.fees().increment}; if (value > availableFunds) @@ -307,6 +309,8 @@ CheckCash::doApply() // LCOV_EXCL_STOP } + auto const sponsorCheckSle = getLedgerEntryReserveSponsor(psb, sleCheck); + // Preclaim already checked that source has at least the requested // funds. // @@ -335,7 +339,7 @@ CheckCash::doApply() // from src's directory, we allow them to send that additional // incremental reserve amount in the transfer. Hence the -1 // argument. - STAmount const srcLiquid{xrpLiquid(psb, srcId, -1, viewJ)}; + STAmount const srcLiquid{xrpLiquid(psb, srcId, sponsorCheckSle ? 0 : -1, viewJ)}; // Now, how much do they need in order to be successful? STAmount const xrpDeliver{ @@ -385,14 +389,25 @@ CheckCash::doApply() STAmount const flowDeliver{ optDeliverMin ? maxDeliverMin() : ctx_.tx.getFieldAmount(sfAmount)}; + auto applyViewContext = ApplyViewContext({.view = psb, .tx = ctx_.tx}); + auto const sponsorSle = getTxReserveSponsor(applyViewContext); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + // Check reserve. Return destination account SLE if enough reserve, // otherwise return nullptr. - auto checkReserve = [&]() -> SLE::pointer { + auto checkDstReserve = [&]() -> SLE::pointer { auto sleDst = psb.peek(keylet::account(accountID_)); // Can the account cover the trust line's or MPT reserve? - if (std::uint32_t const ownerCount = {sleDst->at(sfOwnerCount)}; - preFeeBalance_ < psb.fees().accountReserve(ownerCount + 1)) + if (auto const ret = checkReserve( + applyViewContext, + sleDst, + preFeeBalance_, + *sponsorSle, + {.ownerCountDelta = 1}, + j_); + !isTesSuccess(ret)) { JLOG(j_.trace()) << "Trust line does not exist. " "Insufficient reserve to create line."; @@ -426,7 +441,7 @@ CheckCash::doApply() // a. this (destination) account and // b. issuing account (not sending account). - auto const sleDst = checkReserve(); + auto const sleDst = checkDstReserve(); if (sleDst == nullptr) return tecNO_LINE_INSUF_RESERVE; @@ -449,6 +464,7 @@ CheckCash::doApply() Issue(currency, accountID_), // limit of zero 0, // quality in 0, // quality out + *sponsorSle, // sponsor viewJ); // journal !isTesSuccess(ter)) { @@ -491,11 +507,12 @@ CheckCash::doApply() auto const mptokenKey = keylet::mptoken(mptID, accountID_); if (!psb.exists(mptokenKey)) { - auto sleDst = checkReserve(); + auto sleDst = checkDstReserve(); if (sleDst == nullptr) return tecINSUFFICIENT_RESERVE; - if (auto const err = checkCreateMPT(psb, mptID, accountID_, j_); + if (auto const err = + checkCreateMPT(psb, mptID, accountID_, *sponsorSle, j_); !isTesSuccess(err)) { return err; @@ -581,7 +598,7 @@ CheckCash::doApply() } // If we succeeded, update the check owner's reserve. - adjustOwnerCount(psb, psb.peek(keylet::account(srcId)), -1, viewJ); + decreaseOwnerCountForObject(psb, srcId, sleCheck, 1, viewJ); // Remove check from ledger. psb.erase(sleCheck); diff --git a/src/libxrpl/tx/transactors/check/CheckCreate.cpp b/src/libxrpl/tx/transactors/check/CheckCreate.cpp index 595bcc4ab1..cb1d81ba4a 100644 --- a/src/libxrpl/tx/transactors/check/CheckCreate.cpp +++ b/src/libxrpl/tx/transactors/check/CheckCreate.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -194,13 +195,10 @@ CheckCreate::doApply() // A check counts against the reserve of the issuing account, but we // check the starting balance because we want to allow dipping into the // reserve to pay fees. - { - STAmount const reserve{view().fees().accountReserve(sle->getFieldU32(sfOwnerCount) + 1)}; - - if (preFeeBalance_ < reserve) - return tecINSUFFICIENT_RESERVE; - } - + if (auto const ret = checkReserve( + ctx_.getApplyViewContext(), sle, preFeeBalance_, {.ownerCountDelta = 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(); @@ -253,7 +251,9 @@ CheckCreate::doApply() sleCheck->setFieldU64(sfOwnerNode, *page); } // If we succeeded, the new entry counts against the creator's reserve. - adjustOwnerCount(view(), sle, 1, viewJ); + + increaseOwnerCount(ctx_.getApplyViewContext(), sle, 1, viewJ); + addSponsorToLedgerEntry(ctx_.getApplyViewContext(), sleCheck); return tesSUCCESS; } diff --git a/src/libxrpl/tx/transactors/credentials/CredentialAccept.cpp b/src/libxrpl/tx/transactors/credentials/CredentialAccept.cpp index e0ebdd893a..c0ef7ea9e8 100644 --- a/src/libxrpl/tx/transactors/credentials/CredentialAccept.cpp +++ b/src/libxrpl/tx/transactors/credentials/CredentialAccept.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -11,7 +12,6 @@ #include #include #include -#include #include #include #include @@ -93,12 +93,19 @@ CredentialAccept::doApply() if (!sleSubject || !sleIssuer) return tefINTERNAL; // LCOV_EXCL_LINE - { - STAmount const reserve{ - view().fees().accountReserve(sleSubject->getFieldU32(sfOwnerCount) + 1)}; - if (preFeeBalance_ < reserve) - return tecINSUFFICIENT_RESERVE; - } + auto txSponsorSle = getTxReserveSponsor(ctx_.getApplyViewContext()); + if (!txSponsorSle) + return txSponsorSle.error(); // LCOV_EXCL_LINE + + if (auto const ret = checkReserve( + ctx_.getApplyViewContext(), + sleSubject, + preFeeBalance_, + *txSponsorSle, + {.ownerCountDelta = 1}, + j_); + !isTesSuccess(ret)) + return ret; auto const credType(ctx_.tx[sfCredentialType]); Keylet const credentialKey = keylet::credential(accountID_, issuer, credType); @@ -115,10 +122,16 @@ CredentialAccept::doApply() } sleCred->setFieldU32(sfFlags, lsfAccepted); - view().update(sleCred); - adjustOwnerCount(view(), sleIssuer, -1, j_); - adjustOwnerCount(view(), sleSubject, 1, j_); + // Release the original creation sponsor from the credential (it covered + // the issuer's reserve), then assign the accept tx's sponsor (if any) so + // the credential reflects whoever is now covering the subject's reserve. + decreaseOwnerCountForObject(view(), sleIssuer, sleCred, 1, j_); + removeSponsorFromLedgerEntry(sleCred); + + addSponsorToLedgerEntry(ctx_.getApplyViewContext(), sleCred); + increaseOwnerCount(ctx_.getApplyViewContext(), sleSubject, 1, j_); + view().update(sleCred); return tesSUCCESS; } diff --git a/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp b/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp index acca408a95..e902ee73a6 100644 --- a/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp +++ b/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp @@ -7,13 +7,13 @@ #include #include // IWYU pragma: keep #include +#include #include #include #include #include #include #include -#include #include #include #include @@ -130,12 +130,10 @@ CredentialCreate::doApply() if (!sleIssuer) return tefINTERNAL; // LCOV_EXCL_LINE - { - STAmount const reserve{ - view().fees().accountReserve(sleIssuer->getFieldU32(sfOwnerCount) + 1)}; - if (preFeeBalance_ < reserve) - return tecINSUFFICIENT_RESERVE; - } + if (auto const ret = checkReserve( + ctx_.getApplyViewContext(), sleIssuer, preFeeBalance_, {.ownerCountDelta = 1}, j_); + !isTesSuccess(ret)) + return ret; sleCred->setAccountID(sfSubject, subject); sleCred->setAccountID(sfIssuer, accountID_); @@ -153,7 +151,8 @@ CredentialCreate::doApply() return tecDIR_FULL; sleCred->setFieldU64(sfIssuerNode, *page); - adjustOwnerCount(view(), sleIssuer, 1, j_); + increaseOwnerCount(ctx_.getApplyViewContext(), sleIssuer, 1, j_); + addSponsorToLedgerEntry(ctx_.getApplyViewContext(), sleCred); } if (subject == accountID_) diff --git a/src/libxrpl/tx/transactors/delegate/DelegateSet.cpp b/src/libxrpl/tx/transactors/delegate/DelegateSet.cpp index 32a51555b1..96e6c9e443 100644 --- a/src/libxrpl/tx/transactors/delegate/DelegateSet.cpp +++ b/src/libxrpl/tx/transactors/delegate/DelegateSet.cpp @@ -5,10 +5,10 @@ #include #include #include +#include #include #include #include -#include #include #include #include @@ -98,11 +98,14 @@ DelegateSet::doApply() if (permissions.empty()) return tecINTERNAL; // LCOV_EXCL_LINE - STAmount const reserve{ - ctx_.view().fees().accountReserve(sleOwner->getFieldU32(sfOwnerCount) + 1)}; - - if (preFeeBalance_ < reserve) - return tecINSUFFICIENT_RESERVE; + if (auto const ret = checkReserve( + ctx_.getApplyViewContext(), + sleOwner, + preFeeBalance_, + {.ownerCountDelta = 1}, + ctx_.journal); + !isTesSuccess(ret)) + return ret; sle = std::make_shared(delegateKey); sle->setAccountID(sfAccount, accountID_); @@ -130,7 +133,8 @@ DelegateSet::doApply() (*sle)[sfDestinationNode] = *destPage; ctx_.view().insert(sle); - adjustOwnerCount(ctx_.view(), sleOwner, 1, ctx_.journal); + increaseOwnerCount(ctx_.getApplyViewContext(), sleOwner, 1, ctx_.journal); + addSponsorToLedgerEntry(ctx_.getApplyViewContext(), sle); return tesSUCCESS; } @@ -170,7 +174,7 @@ DelegateSet::deleteDelegate(ApplyView& view, SLE::ref sle, beast::Journal j) if (!sleOwner) return tecINTERNAL; // LCOV_EXCL_LINE - adjustOwnerCount(view, sleOwner, -1, j); + decreaseOwnerCountForObject(view, sleOwner, sle, 1, j); view.erase(sle); diff --git a/src/libxrpl/tx/transactors/dex/AMMCreate.cpp b/src/libxrpl/tx/transactors/dex/AMMCreate.cpp index 66d059c54d..7c7d35497a 100644 --- a/src/libxrpl/tx/transactors/dex/AMMCreate.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMCreate.cpp @@ -332,17 +332,30 @@ applyCreate(ApplyContext& ctx, Sandbox& sb, AccountID const& account, beast::Jou return err; } - if (auto const err = createMPToken(sb, mptID, accountId, flags); !isTesSuccess(err)) + if (auto const err = createMPToken(sb, mptID, accountId, {}, flags); + !isTesSuccess(err)) return err; // Don't adjust AMM owner count. // It's irrelevant for pseudo-account like AMM. return accountSend( - sb, account, accountId, amount, ctx.journal, WaiveTransferFee::Yes); + sb, + account, + accountId, + amount, + ctx.journal, + {}, // don't sponsor for AMM Trustline + WaiveTransferFee::Yes); }, // Set AMM flag on AMM trustline [&](Issue const& issue) -> TER { if (auto const res = accountSend( - sb, account, accountId, amount, ctx.journal, WaiveTransferFee::Yes)) + sb, + account, + accountId, + amount, + ctx.journal, + {}, // don't sponsor for AMM Trustline + 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 ff7fec019a..6a9600d060 100644 --- a/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp @@ -618,7 +618,13 @@ AMMDeposit::deposit( } auto res = accountSend( - view, accountID_, ammAccount, amountDepositActual, ctx_.journal, WaiveTransferFee::Yes); + view, + accountID_, + ammAccount, + amountDepositActual, + ctx_.journal, + {}, // don't sponsor for AMM Trustline + WaiveTransferFee::Yes); if (!isTesSuccess(res)) { JLOG(ctx_.journal.debug()) << "AMM Deposit: failed to deposit " << amountDepositActual; @@ -642,6 +648,7 @@ AMMDeposit::deposit( ammAccount, *amount2DepositActual, ctx_.journal, + {}, // don't sponsor for AMM Trustline WaiveTransferFee::Yes); if (!isTesSuccess(res)) { diff --git a/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp b/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp index 14ed5a4646..ff27a58ad7 100644 --- a/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -658,15 +659,15 @@ AMMWithdraw::withdraw( auto sleAccount = view.peek(keylet::account(account)); if (!sleAccount) return tecINTERNAL; // LCOV_EXCL_LINE - STAmount const balance = (*sleAccount)[sfBalance]; - std::uint32_t const ownerCount = sleAccount->at(sfOwnerCount); + auto const balance = (*sleAccount)[sfBalance]->xrp(); // See also TrustSet::doApply() and MPTokenAuthorize::authorize() XRPAmount const reserve( - (ownerCount < 2) ? XRPAmount(beast::kZero) - : view.fees().accountReserve(ownerCount + 1)); + (ownerCount(sleAccount, journal) < 2) + ? XRPAmount(beast::kZero) + : accountReserve(view, sleAccount, journal, {.ownerCountDelta = 1})); - auto const balanceAdj = isIssue ? std::max(priorBalance, balance.xrp()) : priorBalance; + auto const balanceAdj = isIssue ? std::max(priorBalance, balance) : priorBalance; if (balanceAdj < reserve) return tecINSUFFICIENT_RESERVE; } @@ -683,7 +684,7 @@ AMMWithdraw::withdraw( !isTesSuccess(err)) return err; - if (auto const err = checkCreateMPT(view, mptIssue, account, journal); + if (auto const err = checkCreateMPT(view, mptIssue, account, {}, journal); !isTesSuccess(err)) { return err; @@ -700,7 +701,7 @@ AMMWithdraw::withdraw( // Withdraw amountWithdraw auto res = accountSend( - view, ammAccount, account, amountWithdrawActual, journal, WaiveTransferFee::Yes); + view, ammAccount, account, amountWithdrawActual, journal, {}, WaiveTransferFee::Yes); if (!isTesSuccess(res)) { // LCOV_EXCL_START @@ -719,7 +720,7 @@ AMMWithdraw::withdraw( return {res, STAmount{}, STAmount{}, STAmount{}}; res = accountSend( - view, ammAccount, account, *amount2WithdrawActual, journal, WaiveTransferFee::Yes); + view, ammAccount, account, *amount2WithdrawActual, journal, {}, WaiveTransferFee::Yes); if (!isTesSuccess(res)) { // LCOV_EXCL_START diff --git a/src/libxrpl/tx/transactors/dex/OfferCreate.cpp b/src/libxrpl/tx/transactors/dex/OfferCreate.cpp index a915bb60d1..fb47cf0f97 100644 --- a/src/libxrpl/tx/transactors/dex/OfferCreate.cpp +++ b/src/libxrpl/tx/transactors/dex/OfferCreate.cpp @@ -833,9 +833,7 @@ OfferCreate::applyGuts(Sandbox& sb, Sandbox& sbCancel) return {tefINTERNAL, false}; { - XRPAmount const reserve = - sb.fees().accountReserve(sleCreator->getFieldU32(sfOwnerCount) + 1); - + XRPAmount const reserve = accountReserve(sb, sleCreator, viewJ, {.ownerCountDelta = 1}); if (preFeeBalance_ < reserve) { // If we are here, the signing account had an insufficient reserve @@ -869,7 +867,7 @@ OfferCreate::applyGuts(Sandbox& sb, Sandbox& sbCancel) } // Update owner count. - adjustOwnerCount(sb, sleCreator, 1, viewJ); + increaseOwnerCount(sb, sleCreator, {}, 1, viewJ); JLOG(j_.trace()) << "adding to book: " << to_string(saTakerPays.asset()) << " : " << to_string(saTakerGets.asset()) diff --git a/src/libxrpl/tx/transactors/did/DIDDelete.cpp b/src/libxrpl/tx/transactors/did/DIDDelete.cpp index 90aa21d8a1..a2af4c1100 100644 --- a/src/libxrpl/tx/transactors/did/DIDDelete.cpp +++ b/src/libxrpl/tx/transactors/did/DIDDelete.cpp @@ -50,8 +50,7 @@ DIDDelete::deleteSLE(ApplyView& view, SLE::pointer sle, AccountID const owner, b if (!sleOwner) return tecINTERNAL; // LCOV_EXCL_LINE - adjustOwnerCount(view, sleOwner, -1, j); - view.update(sleOwner); + decreaseOwnerCountForObject(view, sleOwner, sle, 1, j); // Remove object from ledger view.erase(sle); diff --git a/src/libxrpl/tx/transactors/did/DIDSet.cpp b/src/libxrpl/tx/transactors/did/DIDSet.cpp index 1392581bb0..2659460b8d 100644 --- a/src/libxrpl/tx/transactors/did/DIDSet.cpp +++ b/src/libxrpl/tx/transactors/did/DIDSet.cpp @@ -72,7 +72,8 @@ addSLE(ApplyContext& ctx, SLE::ref sle, AccountID const& owner) // Check reserve availability for new object creation { auto const balance = STAmount((*sleAccount)[sfBalance]).xrp(); - auto const reserve = ctx.view().fees().accountReserve((*sleAccount)[sfOwnerCount] + 1); + auto const reserve = + accountReserve(ctx.view(), sleAccount, ctx.journal, {.ownerCountDelta = 1}); if (balance < reserve) return tecINSUFFICIENT_RESERVE; @@ -89,7 +90,7 @@ addSLE(ApplyContext& ctx, SLE::ref sle, AccountID const& owner) return tecDIR_FULL; // LCOV_EXCL_LINE (*sle)[sfOwnerNode] = *page; } - adjustOwnerCount(ctx.view(), sleAccount, 1, ctx.journal); + increaseOwnerCount(ctx.view(), sleAccount, {}, 1, ctx.journal); ctx.view().update(sleAccount); return tesSUCCESS; diff --git a/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp b/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp index 55a9133e1e..feed43d410 100644 --- a/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp +++ b/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp @@ -181,7 +181,7 @@ EscrowCancel::doApply() if (auto const ret = std::visit( [&](T const&) { return escrowUnlockApplyHelper( - ctx_.view(), + ctx_.getApplyViewContext(), kParityRate, ctx_.view().rules().enabled(fixCleanup3_2_0) ? sle : slep, preFeeBalance_, @@ -209,8 +209,7 @@ EscrowCancel::doApply() } } - adjustOwnerCount(ctx_.view(), sle, -1, ctx_.journal); - ctx_.view().update(sle); + decreaseOwnerCountForObject(ctx_.view(), sle, slep, 1, ctx_.journal); // Remove escrow from ledger ctx_.view().erase(slep); diff --git a/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp b/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp index 531098bd1c..50f2e8b859 100644 --- a/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp +++ b/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -434,16 +435,33 @@ EscrowCreate::doApply() // Check reserve and funds availability STAmount const amount{ctx_.tx[sfAmount]}; - auto const reserve = ctx_.view().fees().accountReserve((*sle)[sfOwnerCount] + 1); - auto const balance = sle->getFieldAmount(sfBalance).xrp(); - if (balance < reserve) - return tecINSUFFICIENT_RESERVE; + // First check: whoever is on the hook for the new owner increment + // can cover it. When sponsored this hits the sponsor branch and + // validates the sponsor's reserve + remaining credit. When + // unsponsored this hits the source branch and validates the + // source's pre-lock balance against base + (currentOC+1)*increment. + if (auto const ret = + checkReserve(ctx_.getApplyViewContext(), sle, balance, {.ownerCountDelta = 1}, j_); + !isTesSuccess(ret)) + return ret; - // Check reserve and funds availability if (isXRP(amount)) { - if (balance < reserve + STAmount(amount).xrp()) + // Second check (XRP escrow only): after locking the escrowed + // amount, the source must still meet its own reserve floor. This is + // always the source's own balance against the source's own reserve — + // the sponsor's reserve was already validated above, and a sponsor + // never covers the locked funds. We compare directly (rather than via + // checkReserve) because that helper diverts to the sponsor's balance + // when a sponsor is present and would ignore the source's post-lock + // balance entirely. ownerCountDelta differs by case: + // - sponsored: 0 — sponsor covers the new owner increment, so the + // source only owes reserve for its current owners. + // - unsponsored: 1 — source owes reserve including the new increment. + auto const sourceReserve = accountReserve( + ctx_.view(), sle, j_, {.ownerCountDelta = getTxReserveSponsorID(ctx_.tx) ? 0 : 1}); + if (balance - STAmount(amount).xrp() < sourceReserve) return tecUNFUNDED; } @@ -535,7 +553,8 @@ EscrowCreate::doApply() } // increment owner count - adjustOwnerCount(ctx_.view(), sle, 1, ctx_.journal); + increaseOwnerCount(ctx_.getApplyViewContext(), sle, 1, ctx_.journal); + addSponsorToLedgerEntry(ctx_.getApplyViewContext(), slep); 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 7f1f9ec078..8bc98c7aa8 100644 --- a/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp +++ b/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp @@ -340,6 +340,16 @@ EscrowFinish::doApply() } } + // With the Sponsor amendment, release the escrow reserve before delivery. + // Token delivery can auto-create a destination holding, and the same + // sponsor (or the same account, for a self-escrow) may cover both the + // escrow being removed and the holding being created. Without the + // amendment, keep the legacy order: releasing early changes the reserve + // arithmetic for self-escrows and would break consensus if not gated. + bool const sponsorEnabled = ctx_.view().rules().enabled(featureSponsor); + if (sponsorEnabled) + decreaseOwnerCountForObject(ctx_.view(), account, slep, 1, ctx_.journal); + STAmount const amount = slep->getFieldAmount(sfAmount); // Transfer amount to destination if (isXRP(amount)) @@ -359,7 +369,7 @@ EscrowFinish::doApply() if (auto const ret = std::visit( [&](T const&) { return escrowUnlockApplyHelper( - ctx_.view(), + ctx_.getApplyViewContext(), lockedRate, sled, preFeeBalance_, @@ -389,10 +399,9 @@ EscrowFinish::doApply() ctx_.view().update(sled); - // Adjust source owner count - auto const sle = ctx_.view().peek(keylet::account(account)); - adjustOwnerCount(ctx_.view(), sle, -1, ctx_.journal); - ctx_.view().update(sle); + // Adjust source owner count (legacy position, pre-Sponsor) + if (!sponsorEnabled) + decreaseOwnerCountForObject(ctx_.view(), account, slep, 1, ctx_.journal); // Remove escrow from ledger ctx_.view().erase(slep); diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverClawback.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverClawback.cpp index 9bb84e878e..b914f3cf24 100644 --- a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverClawback.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverClawback.cpp @@ -372,7 +372,7 @@ LoanBrokerCoverClawback::doApply() associateAsset(*sleBroker, vaultAsset); // Transfer assets from pseudo-account to depositor. - return accountSend(view(), brokerPseudoID, account, clawAmount, j_, WaiveTransferFee::Yes); + return accountSend(view(), brokerPseudoID, account, clawAmount, j_, {}, WaiveTransferFee::Yes); } void diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp index 69b28b57af..09ab03347a 100644 --- a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp @@ -176,7 +176,7 @@ LoanBrokerCoverDeposit::doApply() // Transfer assets from depositor to pseudo-account. if (auto ter = - accountSend(view(), accountID_, brokerPseudoID, amount, j_, WaiveTransferFee::Yes)) + accountSend(view(), accountID_, brokerPseudoID, amount, j_, {}, WaiveTransferFee::Yes)) return ter; // Increase the LoanBroker's CoverAvailable by Amount diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp index 008857a4ad..498f3c99eb 100644 --- a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp @@ -211,7 +211,14 @@ LoanBrokerCoverWithdraw::doApply() associateAsset(*broker, vaultAsset); - return doWithdraw(view(), tx, accountID_, dstAcct, brokerPseudoID, preFeeBalance_, amount, j_); + return doWithdraw( + ctx_.getApplyViewContext(), + accountID_, + dstAcct, + brokerPseudoID, + preFeeBalance_, + amount, + j_); } void diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp index 141cc2cf56..b36977d225 100644 --- a/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp @@ -155,11 +155,11 @@ LoanBrokerDelete::doApply() { auto const coverAvailable = STAmount{vaultAsset, broker->at(sfCoverAvailable)}; if (auto const ter = accountSend( - view(), brokerPseudoID, accountID_, coverAvailable, j_, WaiveTransferFee::Yes)) + view(), brokerPseudoID, accountID_, coverAvailable, j_, {}, WaiveTransferFee::Yes)) return ter; } - if (auto ter = removeEmptyHolding(view(), brokerPseudoID, vaultAsset, j_)) + if (auto ter = removeEmptyHolding(ctx_.getApplyViewContext(), brokerPseudoID, vaultAsset, j_)) return ter; auto brokerPseudoSLE = view().peek(keylet::account(brokerPseudoID)); @@ -186,8 +186,6 @@ LoanBrokerDelete::doApply() view().erase(brokerPseudoSLE); - view().erase(broker); - { auto owner = view().peek(keylet::account(accountID_)); if (!owner) @@ -195,9 +193,11 @@ LoanBrokerDelete::doApply() // Decreases the owner count by two: one for the LoanBroker object, and // one for the pseudo-account. - adjustOwnerCount(view(), owner, -2, j_); + decreaseOwnerCountForObject(view(), owner, broker, 2, j_); } + view().erase(broker); + associateAsset(*broker, vaultAsset); return tesSUCCESS; diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp index ab1c8f22dc..e9c153404c 100644 --- a/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp @@ -238,9 +238,8 @@ LoanBrokerSet::doApply() // Increases the owner count by two: one for the LoanBroker object, and // one for the pseudo-account. - adjustOwnerCount(view, owner, 2, j_); - auto const ownerCount = owner->at(sfOwnerCount); - if (preFeeBalance_ < view.fees().accountReserve(ownerCount)) + increaseOwnerCount(view, owner, {}, 2, j_); + if (preFeeBalance_ < accountReserve(view, owner, j_)) return tecINSUFFICIENT_RESERVE; auto maybePseudo = createPseudoAccount(view, broker->key(), sfLoanBrokerID); @@ -249,7 +248,8 @@ LoanBrokerSet::doApply() auto& pseudo = *maybePseudo; auto pseudoId = pseudo->at(sfAccount); - if (auto ter = addEmptyHolding(view, pseudoId, preFeeBalance_, sleVault->at(sfAsset), j_)) + if (auto ter = addEmptyHolding( + ctx_.getApplyViewContext(), pseudoId, preFeeBalance_, sleVault->at(sfAsset), j_)) return ter; // Initialize data fields: diff --git a/src/libxrpl/tx/transactors/lending/LoanDelete.cpp b/src/libxrpl/tx/transactors/lending/LoanDelete.cpp index 3459119379..1a77489b4b 100644 --- a/src/libxrpl/tx/transactors/lending/LoanDelete.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanDelete.cpp @@ -107,9 +107,8 @@ LoanDelete::doApply() view.erase(loanSle); // Decrement the LoanBroker's owner count. - // The broker's owner count is solely for the number of outstanding loans, - // and is distinct from the broker's pseudo-account's owner count - adjustOwnerCount(view, brokerSle, -1, j_); + adjustLoanBrokerOwnerCount(view, brokerSle, -1, j_); + // If there are no loans left, then any remaining debt must be forgiven, // because there is no other way to pay it back. if (brokerSle->at(sfOwnerCount) == 0) @@ -129,7 +128,7 @@ LoanDelete::doApply() } } // Decrement the borrower's owner count - adjustOwnerCount(view, borrowerSle, -1, j_); + decreaseOwnerCountForObject(view, borrowerSle, loanSle, 1, j_); // These associations shouldn't do anything, but do them just to be safe associateAsset(*loanSle, vaultAsset); diff --git a/src/libxrpl/tx/transactors/lending/LoanManage.cpp b/src/libxrpl/tx/transactors/lending/LoanManage.cpp index 830eb30272..a0aa948876 100644 --- a/src/libxrpl/tx/transactors/lending/LoanManage.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanManage.cpp @@ -292,6 +292,7 @@ LoanManage::defaultLoan( vaultSle->at(sfAccount), STAmount{vaultAsset, defaultCovered}, j, + {}, WaiveTransferFee::Yes); } diff --git a/src/libxrpl/tx/transactors/lending/LoanPay.cpp b/src/libxrpl/tx/transactors/lending/LoanPay.cpp index fcda2a9fff..54ee85b186 100644 --- a/src/libxrpl/tx/transactors/lending/LoanPay.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanPay.cpp @@ -625,7 +625,11 @@ LoanPay::doApply() { // The broker may have deleted their holding. Recreate it if needed if (auto const ter = addEmptyHolding( - view, brokerPayee, brokerPayeeSle->at(sfBalance).value().xrp(), asset, j_); + ctx_.getApplyViewContext(), + brokerPayee, + brokerPayeeSle->at(sfBalance).value().xrp(), + asset, + j_); ter && ter != tecDUPLICATE) { // ignore tecDUPLICATE. That means the holding already exists, diff --git a/src/libxrpl/tx/transactors/lending/LoanSet.cpp b/src/libxrpl/tx/transactors/lending/LoanSet.cpp index 1ac387d1b1..694d01c69f 100644 --- a/src/libxrpl/tx/transactors/lending/LoanSet.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanSet.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -56,6 +57,12 @@ LoanSet::preflight(PreflightContext const& ctx) auto const& tx = ctx.tx; + if (tx.isFieldPresent(sfSponsorFlags) && isReserveSponsored(tx)) + { + JLOG(ctx.j.debug()) << "LoanSet: reserve sponsorship is not allowed."; + return temINVALID_FLAG; + } + // Special case for Batch inner transactions if (tx.isFlag(tfInnerBatchTxn) && ctx.rules.enabled(featureBatchV1_1) && !tx.isFieldPresent(sfCounterparty)) @@ -512,12 +519,12 @@ LoanSet::doApply() } } - adjustOwnerCount(view, borrowerSle, 1, j_); + increaseOwnerCount(view, borrowerSle, {}, 1, j_); + { - auto const ownerCount = borrowerSle->at(sfOwnerCount); auto const balance = accountID_ == borrower ? preFeeBalance_ : borrowerSle->at(sfBalance).value().xrp(); - if (balance < view.fees().accountReserve(ownerCount)) + if (balance < accountReserve(view, borrowerSle, j_)) return tecINSUFFICIENT_RESERVE; } @@ -531,8 +538,9 @@ LoanSet::doApply() borrower == accountID_ || borrower == counterparty, "xrpl::LoanSet::doApply", "borrower signed transaction"); + auto applyViewContext = ctx_.getApplyViewContext(); if (auto const ter = addEmptyHolding( - view, borrower, borrowerSle->at(sfBalance).value().xrp(), vaultAsset, j_); + applyViewContext, borrower, borrowerSle->at(sfBalance).value().xrp(), vaultAsset, j_); ter && ter != tecDUPLICATE) { // ignore tecDUPLICATE. That means the holding already exists, and @@ -555,7 +563,11 @@ LoanSet::doApply() "broker owner signed transaction"); if (auto const ter = addEmptyHolding( - view, brokerOwner, brokerOwnerSle->at(sfBalance).value().xrp(), vaultAsset, j_); + applyViewContext, + brokerOwner, + brokerOwnerSle->at(sfBalance).value().xrp(), + vaultAsset, + j_); ter && ter != tecDUPLICATE) { // ignore tecDUPLICATE. That means the holding already exists, @@ -631,9 +643,7 @@ LoanSet::doApply() // Update the balances in the loan broker adjustImpreciseNumber(brokerSle->at(sfDebtTotal), newDebtDelta, vaultAsset, vaultScale); - // The broker's owner count is solely for the number of outstanding loans, - // and is distinct from the broker's pseudo-account's owner count - adjustOwnerCount(view, brokerSle, 1, j_); + adjustLoanBrokerOwnerCount(view, brokerSle, 1, j_); loanSequenceProxy += 1; // The sequence should be extremely unlikely to roll over, but fail if it // does diff --git a/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp b/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp index 14bf3646c0..41bb051768 100644 --- a/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp +++ b/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -387,8 +388,7 @@ NFTokenAcceptOffer::transferNFToken( auto const buyerOwnerCountAfter = sleBuyer->getFieldU32(sfOwnerCount); if (buyerOwnerCountAfter > buyerOwnerCountBefore) { - if (auto const reserve = view().fees().accountReserve(buyerOwnerCountAfter); - buyerBalance < reserve) + if (buyerBalance < accountReserve(view(), sleBuyer, j_)) return tecINSUFFICIENT_RESERVE; } diff --git a/src/libxrpl/tx/transactors/nft/NFTokenMint.cpp b/src/libxrpl/tx/transactors/nft/NFTokenMint.cpp index d8e5a7b235..5490c2de3a 100644 --- a/src/libxrpl/tx/transactors/nft/NFTokenMint.cpp +++ b/src/libxrpl/tx/transactors/nft/NFTokenMint.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -331,12 +332,14 @@ NFTokenMint::doApply() // 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); + auto const sleAccount = view().read(keylet::account(accountID_)); + if (!sleAccount) + return tefINTERNAL; // LCOV_EXCL_LINE + + if (auto const ownerCountAfter = sleAccount->getFieldU32(sfOwnerCount); ownerCountAfter > ownerCountBefore) { - if (auto const reserve = view().fees().accountReserve(ownerCountAfter); - preFeeBalance_ < reserve) + if (preFeeBalance_ < accountReserve(view(), sleAccount, j_)) return tecINSUFFICIENT_RESERVE; } return tesSUCCESS; diff --git a/src/libxrpl/tx/transactors/oracle/OracleDelete.cpp b/src/libxrpl/tx/transactors/oracle/OracleDelete.cpp index 9a26816155..b4c12b9514 100644 --- a/src/libxrpl/tx/transactors/oracle/OracleDelete.cpp +++ b/src/libxrpl/tx/transactors/oracle/OracleDelete.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -13,6 +14,8 @@ #include #include +#include + namespace xrpl { NotTEC @@ -68,10 +71,8 @@ OracleDelete::deleteOracle( if (!sleOwner) return tecINTERNAL; // LCOV_EXCL_LINE - auto const count = sle->getFieldArray(sfPriceDataSeries).size() > 5 ? -2 : -1; - - adjustOwnerCount(view, sleOwner, count, j); - + std::uint32_t const count = calculateOracleReserve(sle); + decreaseOwnerCountForObject(view, sleOwner, sle, count, j); view.erase(sle); return tesSUCCESS; diff --git a/src/libxrpl/tx/transactors/oracle/OracleSet.cpp b/src/libxrpl/tx/transactors/oracle/OracleSet.cpp index 12d826e54d..ead26824e5 100644 --- a/src/libxrpl/tx/transactors/oracle/OracleSet.cpp +++ b/src/libxrpl/tx/transactors/oracle/OracleSet.cpp @@ -1,9 +1,11 @@ #include #include +#include #include #include #include +#include #include #include #include @@ -22,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -150,8 +153,9 @@ OracleSet::preclaim(PreclaimContext const& ctx) if (!pairsDel.empty()) return tecTOKEN_PAIR_NOT_FOUND; - auto const oldCount = sle->getFieldArray(sfPriceDataSeries).size() > 5 ? 2 : 1; - auto const newCount = pairs.size() > 5 ? 2 : 1; + auto const oldCount = calculateOracleReserve(sle); + auto const newCount = calculateOracleReserve(pairs); + adjustReserve = newCount - oldCount; } else @@ -160,7 +164,7 @@ OracleSet::preclaim(PreclaimContext const& ctx) if (!ctx.tx.isFieldPresent(sfProvider) || !ctx.tx.isFieldPresent(sfAssetClass)) return temMALFORMED; - adjustReserve = pairs.size() > 5 ? 2 : 1; + adjustReserve = calculateOracleReserve(pairs); } if (pairs.empty()) @@ -169,7 +173,7 @@ OracleSet::preclaim(PreclaimContext const& ctx) return tecARRAY_TOO_LARGE; auto const reserve = - ctx.view.fees().accountReserve(sleSetter->getFieldU32(sfOwnerCount) + adjustReserve); + accountReserve(ctx.view, sleSetter, ctx.j, {.ownerCountDelta = adjustReserve}); auto const& balance = sleSetter->getFieldAmount(sfBalance); if (balance < reserve) @@ -179,11 +183,22 @@ OracleSet::preclaim(PreclaimContext const& ctx) } static bool -adjustOwnerCount(ApplyContext& ctx, int count) +adjustOracleOwnerCount(ApplyContext& ctx, int count) { + XRPL_ASSERT(std::abs(count) <= 2, "xrpl::adjustOracleOwnerCount abs(counter) <= 2"); + if (auto const sleAccount = ctx.view().peek(keylet::account(ctx.tx[sfAccount]))) { - adjustOwnerCount(ctx.view(), sleAccount, count, ctx.journal); + if (count > 0) + { + increaseOwnerCount( + ctx.view(), sleAccount, {}, static_cast(count), ctx.journal); + } + else if (count < 0) + { + decreaseOwnerCount( + ctx.view(), sleAccount, {}, static_cast(-count), ctx.journal); + } return true; } @@ -228,7 +243,7 @@ OracleSet::doApply() priceData.setFieldCurrency(sfQuoteAsset, entry.getFieldCurrency(sfQuoteAsset)); pairs.emplace(tokenPairKey(entry), std::move(priceData)); } - auto const oldCount = pairs.size() > 5 ? 2 : 1; + auto const oldCount = calculateOracleReserve(pairs); // update/add/delete pairs for (auto const& entry : ctx_.tx.getFieldArray(sfPriceDataSeries)) { @@ -266,9 +281,10 @@ OracleSet::doApply() (*sle)[sfOracleDocumentID] = ctx_.tx[sfOracleDocumentID]; } - auto const newCount = pairs.size() > 5 ? 2 : 1; - auto const adjust = newCount - oldCount; - if (adjust != 0 && !adjustOwnerCount(ctx_, adjust)) + auto const newCount = calculateOracleReserve(pairs); + int32_t const adjust = newCount - oldCount; + + if (adjust != 0 && !adjustOracleOwnerCount(ctx_, adjust)) return tefINTERNAL; // LCOV_EXCL_LINE ctx_.view().update(sle); @@ -317,8 +333,8 @@ OracleSet::doApply() (*sle)[sfOwnerNode] = *page; - auto const count = series.size() > 5 ? 2 : 1; - if (!adjustOwnerCount(ctx_, count)) + auto const count = calculateOracleReserve(series); + if (!adjustOracleOwnerCount(ctx_, count)) return tefINTERNAL; // LCOV_EXCL_LINE ctx_.view().insert(sle); diff --git a/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp b/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp index 7e950dc743..d3e2af86ef 100644 --- a/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp +++ b/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp @@ -7,13 +7,13 @@ #include #include #include +#include #include #include #include #include #include #include -#include #include #include #include @@ -151,6 +151,7 @@ DepositPreauth::preclaim(PreclaimContext const& ctx) TER DepositPreauth::doApply() { + auto applyViewContext = ctx_.getApplyViewContext(); if (ctx_.tx.isFieldPresent(sfAuthorize)) { auto const sleOwner = view().peek(keylet::account(accountID_)); @@ -160,13 +161,10 @@ DepositPreauth::doApply() // A preauth counts against the reserve of the issuing account, but we // check the starting balance because we want to allow dipping into the // reserve to pay fees. - { - STAmount const reserve{ - view().fees().accountReserve(sleOwner->getFieldU32(sfOwnerCount) + 1)}; - - if (preFeeBalance_ < reserve) - return tecINSUFFICIENT_RESERVE; - } + if (auto const ret = checkReserve( + applyViewContext, sleOwner, preFeeBalance_, {.ownerCountDelta = 1}, j_); + !isTesSuccess(ret)) + return ret; // Preclaim already verified that the Preauth entry does not yet exist. // Create and populate the Preauth entry. @@ -190,7 +188,8 @@ DepositPreauth::doApply() slePreauth->setFieldU64(sfOwnerNode, *page); // If we succeeded, the new entry counts against the creator's reserve. - adjustOwnerCount(view(), sleOwner, 1, j_); + increaseOwnerCount(applyViewContext, sleOwner, 1, j_); + addSponsorToLedgerEntry(applyViewContext, slePreauth); } else if (ctx_.tx.isFieldPresent(sfUnauthorize)) { @@ -207,13 +206,10 @@ DepositPreauth::doApply() // A preauth counts against the reserve of the issuing account, but we // check the starting balance because we want to allow dipping into the // reserve to pay fees. - { - STAmount const reserve{ - view().fees().accountReserve(sleOwner->getFieldU32(sfOwnerCount) + 1)}; - - if (preFeeBalance_ < reserve) - return tecINSUFFICIENT_RESERVE; - } + if (auto const ret = checkReserve( + applyViewContext, sleOwner, preFeeBalance_, {.ownerCountDelta = 1}, j_); + !isTesSuccess(ret)) + return ret; // Preclaim already verified that the Preauth entry does not yet exist. // Create and populate the Preauth entry. @@ -251,7 +247,8 @@ DepositPreauth::doApply() slePreauth->setFieldU64(sfOwnerNode, *page); // If we succeeded, the new entry counts against the creator's reserve. - adjustOwnerCount(view(), sleOwner, 1, j_); + increaseOwnerCount(applyViewContext, sleOwner, 1, j_); + addSponsorToLedgerEntry(applyViewContext, slePreauth); } else if (ctx_.tx.isFieldPresent(sfUnauthorizeCredentials)) { @@ -289,8 +286,7 @@ DepositPreauth::removeFromLedger(ApplyView& view, uint256 const& preauthIndex, b if (!sleOwner) return tefINTERNAL; // LCOV_EXCL_LINE - adjustOwnerCount(view, sleOwner, -1, j); - + decreaseOwnerCountForObject(view, sleOwner, slePreauth, 1, j); // Remove DepositPreauth from ledger. view.erase(slePreauth); diff --git a/src/libxrpl/tx/transactors/payment/Payment.cpp b/src/libxrpl/tx/transactors/payment/Payment.cpp index 6e3883572a..17c96a1919 100644 --- a/src/libxrpl/tx/transactors/payment/Payment.cpp +++ b/src/libxrpl/tx/transactors/payment/Payment.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -37,6 +38,7 @@ #include #include +#include #include #include #include @@ -120,6 +122,21 @@ Payment::preflight(PreflightContext const& ctx) if (!ctx.rules.enabled(featureMPTokensV1) && isDstMPT) return temDISABLED; + if (tx.isFlag(tfSponsorCreatedAccount)) + { + if (!ctx.rules.enabled(featureSponsor)) + return temDISABLED; + + if (tx.isFlag(tfNoRippleDirect) || tx.isFlag(tfPartialPayment) || tx.isFlag(tfLimitQuality)) + return temINVALID_FLAG; + + if (tx.isFieldPresent(sfSendMax) || tx.isFieldPresent(sfPaths)) + return temINVALID; + + if (!dstAmount.native()) + return temBAD_AMOUNT; + } + if (!mpTokensV2 && isDstMPT && ctx.tx.isFieldPresent(sfPaths)) return temMALFORMED; @@ -383,15 +400,28 @@ Payment::preclaim(PreclaimContext const& ctx) { // accountReserve is the minimum amount that an account can have. // Reserve is not scaled by load. - JLOG(ctx.j.trace()) << "Delay transaction: Destination account does not exist. " - << "Insufficent payment to create account."; + if (!ctx.tx.isFlag(tfSponsorCreatedAccount)) + { + // The minimum amount when creating a Sponsored Account is 1 drop. + // Since the reserve is covered by the sponsor, you don't need to hold the + // 1-increment reserve yourself. + JLOG(ctx.j.trace()) << "Delay transaction: Destination account does not exist. " + << "Insufficient payment to create account."; - // TODO: de-dupe - // Another transaction could create the account and then this - // transaction would succeed. - return tecNO_DST_INSUF_XRP; + // TODO: de-dupe + // Another transaction could create the account and then this + // transaction would succeed. + return tecNO_DST_INSUF_XRP; + } } } + else if (ctx.tx.isFlag(tfSponsorCreatedAccount)) + { + // The tfSponsorCreatedAccount flag is specific to account creation via + // sponsorship. If the destination account already exists, applying this + // flag is invalid. + return tecNO_SPONSOR_PERMISSION; + } else if (sleDst->isFlag(lsfRequireDestTag) && !ctx.tx.isFieldPresent(sfDestinationTag)) { // The tag is basically account-specific information we don't @@ -470,6 +500,27 @@ Payment::doApply() sleDst->setFieldU32(sfSequence, view().seq()); sleDst->setFieldAmount(sfBalance, XRPAmount(beast::kZero)); + if (ctx_.tx.isFlag(tfSponsorCreatedAccount)) + { + auto const sponsor = view().peek(keylet::account(accountID_)); + if (!sponsor) + return tefINTERNAL; // LCOV_EXCL_LINE + auto const currentSponsoringAccountCount = + sponsor->getFieldU32(sfSponsoringAccountCount); + if (currentSponsoringAccountCount == std::numeric_limits::max()) + { + // LCOV_EXCL_START + JLOG(j_.fatal()) << "Sponsoring account count overflow for account " + << to_string(accountID_); + return tecINTERNAL; + // LCOV_EXCL_STOP + } + sponsor->setFieldU32(sfSponsoringAccountCount, currentSponsoringAccountCount + 1); + + addSponsorToLedgerEntry(sleDst, sponsor); + view().update(sponsor); + } + view().insert(sleDst); } else @@ -636,16 +687,12 @@ Payment::doApply() if (!sleSrc) return tefINTERNAL; // LCOV_EXCL_LINE - // ownerCount is the number of entries in this ledger for this - // account that require a reserve. - auto const ownerCount = sleSrc->getFieldU32(sfOwnerCount); + // the number of reserves in this ledger for this account that require a + // reserve. + auto const reserve = accountReserve(view(), sleSrc, j_); - // This is the total reserve in drops. - auto const reserve = view().fees().accountReserve(ownerCount); - - // In a delegated payment, the fee payer is the delegated account, - // not the source account (accountID_). - bool const accountIsPayer = (ctx_.tx.getFeePayer() == accountID_); + // In a delegated / fee sponsored payment, the fee payer is not the source account (accountID_). + bool const accountIsPayer = ctx_.tx.getFeePayerID() == accountID_; // preFeeBalance_ is the balance on the source account (accountID_) BEFORE the fees // were charged. If source account is the fee payer, it must also cover the fee. diff --git a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp index 977da56861..b17430948a 100644 --- a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp +++ b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -77,9 +78,10 @@ PaymentChannelCreate::preclaim(PreclaimContext const& ctx) return terNO_ACCOUNT; // Check reserve and funds availability + if (!ctx.view.rules().enabled(featureSponsor)) { auto const balance = (*sle)[sfBalance]; - auto const reserve = ctx.view.fees().accountReserve((*sle)[sfOwnerCount] + 1); + auto const reserve = ctx.view.fees().accountReserve((*sle)[sfOwnerCount] + 1, 1); if (balance < reserve) return tecINSUFFICIENT_RESERVE; @@ -131,6 +133,36 @@ PaymentChannelCreate::doApply() return tecEXPIRED; } + if (ctx_.view().rules().enabled(featureSponsor)) + { + // First check: whoever is on the hook for the new owner increment + // can cover it. When sponsored this hits the sponsor branch and + // validates the sponsor's reserve + remaining credit. When + // unsponsored this hits the source branch and validates the + // source's pre-lock balance against base + (currentOC+1)*increment. + if (auto const ret = checkReserve( + ctx_.getApplyViewContext(), sle, preFeeBalance_, {.ownerCountDelta = 1}, j_); + !isTesSuccess(ret)) + return ret; + + // Second check: after locking sfAmount in the channel, the source + // must still meet its own reserve floor. This is always the + // source's own balance against the source's own reserve — the + // sponsor's reserve was already validated above, and a sponsor + // never covers the locked funds. We compare directly (rather than + // via checkReserve) because that helper diverts to the + // sponsor's balance when a sponsor is present and would ignore the + // source's post-lock balance entirely. ownerCountDelta differs by + // case: + // - sponsored: 0 — sponsor covers the new owner increment, so + // the source only owes reserve for its current owners. + // - unsponsored: 1 — source owes reserve including the new increment. + auto const sourceReserve = accountReserve( + ctx_.view(), sle, j_, {.ownerCountDelta = getTxReserveSponsorID(ctx_.tx) ? 0 : 1}); + if (preFeeBalance_ - ctx_.tx[sfAmount].xrp() < sourceReserve) + return tecUNFUNDED; + } + auto const dst = ctx_.tx[sfDestination]; // Create PayChan in ledger. @@ -178,7 +210,8 @@ PaymentChannelCreate::doApply() // Deduct owner's balance, increment owner count (*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount]; - adjustOwnerCount(ctx_.view(), sle, 1, ctx_.journal); + increaseOwnerCount(ctx_.getApplyViewContext(), sle, 1, ctx_.journal); + addSponsorToLedgerEntry(ctx_.getApplyViewContext(), slep); 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 4e3c5dd638..1d1cf07e25 100644 --- a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp +++ b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -87,13 +88,18 @@ PaymentChannelFund::doApply() { // Check reserve and funds availability - auto const balance = (*sle)[sfBalance]; - auto const reserve = ctx_.view().fees().accountReserve((*sle)[sfOwnerCount]); + STAmount const balance = (*sle)[sfBalance]; + if (auto const ret = checkReserve(ctx_.getApplyViewContext(), sle, balance.xrp(), {}, j_); + !isTesSuccess(ret)) + return ret; - if (balance < reserve) - return tecINSUFFICIENT_RESERVE; - - if (balance < reserve + ctx_.tx[sfAmount]) + // After locking sfAmount in the channel, the source must still meet + // its own reserve floor. We compare directly (rather than via + // checkReserve) because that helper diverts to the sponsor's balance + // when a sponsor is present and would ignore the source's post-lock + // balance entirely. Funding an existing channel adds no owned object, + // so there is no owner-count delta. + if (balance < accountReserve(ctx_.view(), sle, j_) + ctx_.tx[sfAmount]) return tecUNFUNDED; } diff --git a/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.cpp b/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.cpp index 7eb3f282b9..9978d3e260 100644 --- a/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.cpp +++ b/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.cpp @@ -65,7 +65,7 @@ PermissionedDomainDelete::doApply() XRPL_ASSERT( ownerSle && ownerSle->getFieldU32(sfOwnerCount) > 0, "xrpl::PermissionedDomainDelete::doApply : nonzero owner count"); - adjustOwnerCount(view(), ownerSle, -1, ctx_.journal); + decreaseOwnerCountForObject(view(), ownerSle, slePd, 1, ctx_.journal); view().erase(slePd); return tesSUCCESS; diff --git a/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.cpp b/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.cpp index 0e71ceada1..c2df114e48 100644 --- a/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.cpp +++ b/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.cpp @@ -106,7 +106,8 @@ PermissionedDomainSet::doApply() // Create new permissioned domain. // Check reserve availability for new object creation auto const balance = STAmount((*ownerSle)[sfBalance]).xrp(); - auto const reserve = ctx_.view().fees().accountReserve((*ownerSle)[sfOwnerCount] + 1); + auto const reserve = + accountReserve(ctx_.view(), ownerSle, ctx_.journal, {.ownerCountDelta = 1}); if (balance < reserve) return tecINSUFFICIENT_RESERVE; @@ -125,7 +126,7 @@ PermissionedDomainSet::doApply() slePd->setFieldU64(sfOwnerNode, *page); // If we succeeded, the new entry counts against the creator's reserve. - adjustOwnerCount(view(), ownerSle, 1, ctx_.journal); + increaseOwnerCount(view(), ownerSle, {}, 1, ctx_.journal); view().insert(slePd); } diff --git a/src/libxrpl/tx/transactors/sponsor/SponsorshipSet.cpp b/src/libxrpl/tx/transactors/sponsor/SponsorshipSet.cpp new file mode 100644 index 0000000000..2b6ab8cf15 --- /dev/null +++ b/src/libxrpl/tx/transactors/sponsor/SponsorshipSet.cpp @@ -0,0 +1,420 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace xrpl { + +static bool +hasSponsorshipBudget( + SLE::const_ref sponsorshipSle, + std::optional const& feeAmount, + std::optional const& remainingOwnerCount) +{ + // A field the transaction omits keeps whatever the existing object holds, + // so fall back to the current SLE value when the tx does not set it. + bool const hasFeeAmount = feeAmount + ? *feeAmount > beast::kZero + : sponsorshipSle && (*sponsorshipSle)[~sfFeeAmount].value_or(STAmount{0}) > beast::kZero; + + bool const hasRemainingOwnerCount = remainingOwnerCount + ? *remainingOwnerCount > 0 + : sponsorshipSle && (*sponsorshipSle)[~sfRemainingOwnerCount].value_or(0) > 0; + + return hasFeeAmount || hasRemainingOwnerCount; +} + +TxConsequences +SponsorshipSet::makeTxConsequences(PreflightContext const& ctx) +{ + auto const feeAmount = ctx.tx[~sfFeeAmount]; + return TxConsequences{ctx.tx, feeAmount.has_value() ? feeAmount->xrp() : beast::kZero}; +} + +std::uint32_t +SponsorshipSet::getFlagsMask(PreflightContext const& ctx) +{ + return tfSponsorshipSetMask; +} + +NotTEC +SponsorshipSet::preflight(PreflightContext const& ctx) +{ + if (ctx.tx.isFlag(tfSponsorshipSetRequireSignForFee) && + ctx.tx.isFlag(tfSponsorshipClearRequireSignForFee)) + return temINVALID_FLAG; + if (ctx.tx.isFlag(tfSponsorshipSetRequireSignForReserve) && + ctx.tx.isFlag(tfSponsorshipClearRequireSignForReserve)) + return temINVALID_FLAG; + + auto const account = ctx.tx.getAccountID(sfAccount); + bool const hasSponsor = ctx.tx.isFieldPresent(sfCounterpartySponsor); + bool const hasSponsee = ctx.tx.isFieldPresent(sfSponsee); + + // The transaction must specify either Sponsor or Sponsee, but not both. + if (hasSponsor == hasSponsee) + return temMALFORMED; + + auto const sponsorID = ctx.tx[~sfCounterpartySponsor].value_or(account); + auto const sponseeID = ctx.tx[~sfSponsee].value_or(account); + + if (sponsorID == sponseeID) + return temMALFORMED; + + if (ctx.tx.isFlag(tfDeleteObject)) + { + // Transactions deleting `Sponsorship` cannot set modification flags. + constexpr std::uint32_t kModifyFlags = tfSponsorshipSetRequireSignForFee | + tfSponsorshipSetRequireSignForReserve | tfSponsorshipClearRequireSignForFee | + tfSponsorshipClearRequireSignForReserve; + + if ((ctx.tx.getFlags() & kModifyFlags) != 0u) + return temINVALID_FLAG; + + // Transactions deleting `Sponsorship` cannot include modification fields. + if (ctx.tx.isFieldPresent(sfFeeAmount) || ctx.tx.isFieldPresent(sfRemainingOwnerCount) || + ctx.tx.isFieldPresent(sfMaxFee)) + return temMALFORMED; + } + else + { + // Both sponsor and sponsee can delete a Sponsorship object, but only + // the sponsor can create or update one. + if (account != sponsorID) + return temMALFORMED; + + // FeeAmount and MaxFee must be non-negative XRP amounts when present. + auto const checkOptionalAmountField = [&](SField const& field) -> NotTEC { + if (!ctx.tx.isFieldPresent(field)) + return tesSUCCESS; + + auto const amount = ctx.tx.getFieldAmount(field); + + if (!isXRP(amount)) + return temBAD_AMOUNT; + + if (amount.xrp() < beast::kZero) + return temBAD_AMOUNT; + + return tesSUCCESS; + }; + + if (auto const ret = checkOptionalAmountField(sfFeeAmount); !isTesSuccess(ret)) + return ret; + + if (auto const ret = checkOptionalAmountField(sfMaxFee); !isTesSuccess(ret)) + return ret; + } + + return tesSUCCESS; +} + +TER +SponsorshipSet::preclaim(PreclaimContext const& ctx) +{ + auto const sponsorID = ctx.tx[~sfCounterpartySponsor].value_or(ctx.tx[sfAccount]); + auto const sponseeID = ctx.tx[~sfSponsee].value_or(ctx.tx[sfAccount]); + + if (sponseeID == sponsorID) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const sponsorAccSle = ctx.view.read(keylet::account(sponsorID)); + if (!sponsorAccSle) + return tecNO_DST; + + auto const sponseeSle = ctx.view.read(keylet::account(sponseeID)); + if (!sponseeSle) + return tecNO_DST; + + // Pseudo-accounts cannot participate in sponsorship. + if (isPseudoAccount(sponsorAccSle) || isPseudoAccount(sponseeSle)) + return tecNO_PERMISSION; + + auto const sponsorshipSle = ctx.view.read(keylet::sponsorship(sponsorID, sponseeID)); + + // Deleting a Sponsorship object requires the object to already exist. + if (ctx.tx.isFlag(tfDeleteObject) && !sponsorshipSle) + return tecNO_ENTRY; + + // Reject creating or updating a Sponsorship that would be left with no + // budget (neither a positive FeeAmount nor a positive RemainingOwnerCount). + // Such an object is unusable yet still consumes the sponsor's reserve. + if (!ctx.tx.isFlag(tfDeleteObject) && + !hasSponsorshipBudget(sponsorshipSle, ctx.tx[~sfFeeAmount], ctx.tx[~sfRemainingOwnerCount])) + return tecNO_PERMISSION; + + return tesSUCCESS; +} + +static TER +deleteSponsorship(ApplyView& view, SLE::ref sle, beast::Journal j) +{ + if (!sle) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const sponsorID = (*sle)[sfOwner]; + auto const sponseeID = (*sle)[sfSponsee]; + + // The sponsor owns the Sponsorship object, so deletion releases the + // sponsor's owner reserve. + auto sponsorAccSle = view.peek(keylet::account(sponsorID)); + if (!sponsorAccSle) + return tecINTERNAL; // LCOV_EXCL_LINE + + if (!view.dirRemove(keylet::ownerDir(sponsorID), (*sle)[sfOwnerNode], sle->key(), false)) + { + // LCOV_EXCL_START + JLOG(j.fatal()) << "Unable to delete Sponsorship from sponsor."; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } + if (!view.dirRemove(keylet::ownerDir(sponseeID), (*sle)[sfSponseeNode], sle->key(), false)) + { + // LCOV_EXCL_START + JLOG(j.fatal()) << "Unable to delete Sponsorship from sponsee."; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } + + decreaseOwnerCountForObject(view, sponsorAccSle, sle, 1, j); + + // Return any prefunded fee amount to the sponsor before erasing the object. + if (sle->isFieldPresent(sfFeeAmount)) + { + (*sponsorAccSle)[sfBalance] += sle->getFieldAmount(sfFeeAmount); + view.update(sponsorAccSle); + } + + view.erase(sle); + + return tesSUCCESS; +} + +TER +SponsorshipSet::doApply() +{ + auto const sponsorID = ctx_.tx[~sfCounterpartySponsor].value_or(accountID_); + auto const sponseeID = ctx_.tx[~sfSponsee].value_or(accountID_); + + if (sponseeID == sponsorID) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const sponsorAccSle = ctx_.view().peek(keylet::account(sponsorID)); + if (!sponsorAccSle) + return tecINTERNAL; // LCOV_EXCL_LINE + + if (!ctx_.view().exists(keylet::account(sponseeID))) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const sponsorKeylet = keylet::sponsorship(sponsorID, sponseeID); + auto const sponsorshipSle = ctx_.view().peek(sponsorKeylet); + + if (ctx_.tx.isFlag(tfDeleteObject)) + { + if (!sponsorshipSle) + return tecINTERNAL; // LCOV_EXCL_LINE + + return deleteSponsorship(ctx_.view(), sponsorshipSle, ctx_.journal); + } + + auto const feeAmount = ctx_.tx[~sfFeeAmount]; + auto const maxFee = ctx_.tx[~sfMaxFee]; + auto const remainingOwnerCount = ctx_.tx[~sfRemainingOwnerCount]; + + bool const hasPositiveFeeAmount = feeAmount.has_value() && *feeAmount > beast::kZero; + + auto reserveSponsorAccSle = getTxReserveSponsor(ctx_.getApplyViewContext()); + if (!reserveSponsorAccSle) + return reserveSponsorAccSle.error(); // LCOV_EXCL_LINE + + if (!sponsorshipSle) + { + // Create a new Sponsorship object between the sponsor and sponsee. + auto newSle = std::make_shared(sponsorKeylet); + + (*newSle)[sfOwner] = sponsorID; + (*newSle)[sfSponsee] = sponseeID; + if (feeAmount && (*feeAmount).xrp() > (*sponsorAccSle)[sfBalance]) + return tecUNFUNDED; + + STAmount sponsorBalanceAfterFee = (*sponsorAccSle)[sfBalance]; + if (hasPositiveFeeAmount) + sponsorBalanceAfterFee -= *feeAmount; + + if (auto const ret = checkReserve( + ctx_.getApplyViewContext(), + sponsorAccSle, + sponsorBalanceAfterFee.xrp(), + *reserveSponsorAccSle, + {.ownerCountDelta = 1}, + ctx_.journal, + tecUNFUNDED); + !isTesSuccess(ret)) + { + return ret; + } + + if (hasPositiveFeeAmount) + { + // New object: FeeAmount starts absent, so deduct and record the full amount + (*newSle)[sfFeeAmount] = *feeAmount; + (*sponsorAccSle)[sfBalance] -= *feeAmount; + } + + if (maxFee && *maxFee > beast::kZero) + (*newSle)[sfMaxFee] = *maxFee; + if (remainingOwnerCount && *remainingOwnerCount > 0) + (*newSle)[sfRemainingOwnerCount] = *remainingOwnerCount; + + std::uint32_t flags = 0; + if (ctx_.tx.isFlag(tfSponsorshipSetRequireSignForFee)) + flags |= lsfSponsorshipRequireSignForFee; + + if (ctx_.tx.isFlag(tfSponsorshipSetRequireSignForReserve)) + flags |= lsfSponsorshipRequireSignForReserve; + + (*newSle)[sfFlags] = flags; + + auto const sponsorPage = view().dirInsert( + keylet::ownerDir(sponsorID), sponsorKeylet, describeOwnerDir(sponsorID)); + if (!sponsorPage) + return tecDIR_FULL; // LCOV_EXCL_LINE + (*newSle)[sfOwnerNode] = *sponsorPage; + + auto const sponseePage = view().dirInsert( + keylet::ownerDir(sponseeID), sponsorKeylet, describeOwnerDir(sponseeID)); + if (!sponseePage) + return tecDIR_FULL; // LCOV_EXCL_LINE + (*newSle)[sfSponseeNode] = *sponseePage; + + // NOLINTNEXTLINE(readability-suspicious-call-argument) + increaseOwnerCount(view(), sponsorAccSle, *reserveSponsorAccSle, 1, ctx_.journal); + addSponsorToLedgerEntry(newSle, *reserveSponsorAccSle); + + ctx_.view().insert(newSle); + return tesSUCCESS; + } + + // Update the existing Sponsorship object. + if (feeAmount) + { + auto const currentFeeAmount = (*sponsorshipSle)[~sfFeeAmount].valueOr(XRPAmount{0}); + auto const feeAmountDelta = XRPAmount(*feeAmount - currentFeeAmount); + + if (feeAmountDelta > beast::kZero && feeAmountDelta > (*sponsorAccSle)[sfBalance]) + return tecUNFUNDED; + + // Move the FeeAmount delta between the sponsor balance and Sponsorship + // object. + if (feeAmountDelta != beast::kZero) + { + STAmount sponsorBalanceAfterFee = (*sponsorAccSle)[sfBalance]; + sponsorBalanceAfterFee -= feeAmountDelta; + + if (auto const ret = checkReserve( + ctx_.getApplyViewContext(), + sponsorAccSle, + sponsorBalanceAfterFee.xrp(), + *reserveSponsorAccSle, + {}, + ctx_.journal, + tecUNFUNDED); + !isTesSuccess(ret)) + { + return ret; + } + + (*sponsorAccSle)[sfBalance] -= feeAmountDelta; + if (*feeAmount == beast::kZero) + { + (*sponsorshipSle).makeFieldAbsent(sfFeeAmount); + } + else + { + (*sponsorshipSle).setFieldAmount(sfFeeAmount, *feeAmount); + } + ctx_.view().update(sponsorAccSle); + } + } + + if (maxFee) + { + if (*maxFee == beast::kZero) + { + (*sponsorshipSle).makeFieldAbsent(sfMaxFee); + } + else + { + (*sponsorshipSle)[sfMaxFee] = *maxFee; + } + } + + if (remainingOwnerCount) + { + if (*remainingOwnerCount == 0) + { + sponsorshipSle->makeFieldAbsent(sfRemainingOwnerCount); + } + else + { + sponsorshipSle->at(sfRemainingOwnerCount) = *remainingOwnerCount; + } + } + + // Apply requested flag changes. + auto flags = sponsorshipSle->getFieldU32(sfFlags); + if (ctx_.tx.isFlag(tfSponsorshipSetRequireSignForFee)) + flags |= lsfSponsorshipRequireSignForFee; + + if (ctx_.tx.isFlag(tfSponsorshipClearRequireSignForFee)) + flags &= ~lsfSponsorshipRequireSignForFee; + + if (ctx_.tx.isFlag(tfSponsorshipSetRequireSignForReserve)) + flags |= lsfSponsorshipRequireSignForReserve; + + if (ctx_.tx.isFlag(tfSponsorshipClearRequireSignForReserve)) + flags &= ~lsfSponsorshipRequireSignForReserve; + + if (flags != (*sponsorshipSle)[sfFlags]) + (*sponsorshipSle)[sfFlags] = flags; + + view().update(sponsorshipSle); + + return tesSUCCESS; +} + +void +SponsorshipSet::visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref) +{ +} + +bool +SponsorshipSet::finalizeInvariants( + STTx const&, + TER, + XRPAmount, + ReadView const&, + beast::Journal const&) +{ + return true; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/sponsor/SponsorshipTransfer.cpp b/src/libxrpl/tx/transactors/sponsor/SponsorshipTransfer.cpp new file mode 100644 index 0000000000..0e036649fd --- /dev/null +++ b/src/libxrpl/tx/transactors/sponsor/SponsorshipTransfer.cpp @@ -0,0 +1,542 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace xrpl { + +// Increment an uint32 sponsor count field and update the SLE. +static TER +incrementSponsorCount( + ApplyView& view, + SLE::ref sle, + SF_UINT32 const& field, + std::uint32_t const delta) +{ + auto const currentValue = sle->getFieldU32(field); + if (std::numeric_limits::max() - currentValue < delta) + { + // LCOV_EXCL_START + UNREACHABLE("xrpl::incrementSponsorCount : sponsor field overflow"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } + + sle->at(field) = currentValue + delta; + view.update(sle); + return tesSUCCESS; +} + +// Decrement an uint32 sponsor count field and update the SLE. +static TER +decrementSponsorCount( + ApplyView& view, + SLE::ref sle, + SF_UINT32 const& field, + std::uint32_t const delta) +{ + auto const currentValue = sle->getFieldU32(field); + if (currentValue < delta) + { + // LCOV_EXCL_START + UNREACHABLE("xrpl::decrementSponsorCount : sponsor field underflow"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } + + sle->at(field) = currentValue - delta; + view.update(sle); + return tesSUCCESS; +} + +// Consume the sponsor's pre-funded reserve budget and lowers the Sponsorship +// object's RemainingOwnerCount. +static TER +decrementPrefundedReserveCount(ApplyView& view, SLE::ref sponsorshipSle, std::uint32_t const delta) +{ + if (delta == 0) + return tesSUCCESS; // LCOV_EXCL_LINE + + auto const currentReserveCount = sponsorshipSle->getFieldU32(sfRemainingOwnerCount); + if (currentReserveCount < delta) + { + // LCOV_EXCL_START + // Already verified by checkReserve (sufficient RemainingOwnerCount) + UNREACHABLE("xrpl::decrementPrefundedReserveCount : invalid reserve count"); + return tefINTERNAL; + // LCOV_EXCL_STOP + } + + sponsorshipSle->at(sfRemainingOwnerCount) = currentReserveCount - delta; + view.update(sponsorshipSle); + return tesSUCCESS; +} + +std::uint32_t +SponsorshipTransfer::getFlagsMask(PreflightContext const& ctx) +{ + return tfSponsorshipTransferMask; +} + +NotTEC +SponsorshipTransfer::preflight(PreflightContext const& ctx) +{ + static constexpr auto transferFlags = + tfSponsorshipCreate | tfSponsorshipReassign | tfSponsorshipEnd; + if (std::popcount(ctx.tx.getFlags() & transferFlags) != 1) + { + JLOG(ctx.j.debug()) << "preflight: Only one SponsorshipTransfer flag can be set per tx."; + return temINVALID_FLAG; + } + + if (ctx.tx.isFlag(tfSponsorshipCreate)) + { + // Creating sponsorship transfers an unsponsored target from the sponsee + // to a reserve sponsor identified by sfSponsor + spfSponsorReserve. + if (!ctx.tx.isFieldPresent(sfSponsor)) + { + JLOG(ctx.j.debug()) << "preflight: sfSponsor must be present when creating sponsorship"; + return temMALFORMED; + } + + if (!isReserveSponsored(ctx.tx)) + { + JLOG(ctx.j.debug()) + << "preflight: spfSponsorReserve must be set when creating sponsorship"; + return temINVALID_FLAG; + } + + if (ctx.tx.isFieldPresent(sfSponsee)) + { + JLOG(ctx.j.debug()) + << "preflight: sfSponsee must not be present when creating sponsorship"; + return temMALFORMED; + } + } + + if (ctx.tx.isFlag(tfSponsorshipReassign)) + { + // Reassigning sponsorship transfers an already sponsored target from its + // current reserve sponsor to the new sponsor identified by sfSponsor + + // spfSponsorReserve. + if (!ctx.tx.isFieldPresent(sfSponsor)) + { + JLOG(ctx.j.debug()) + << "preflight: sfSponsor must be present when reassigning sponsorship"; + return temMALFORMED; + } + + if (!isReserveSponsored(ctx.tx)) + { + JLOG(ctx.j.debug()) + << "preflight: spfSponsorReserve must be set when reassigning sponsorship"; + return temINVALID_FLAG; + } + if (ctx.tx.isFieldPresent(sfSponsee)) + { + JLOG(ctx.j.debug()) + << "preflight: sfSponsee must not be present when reassigning sponsorship"; + return temMALFORMED; + } + } + + if (ctx.tx.isFlag(tfSponsorshipEnd)) + { + // Ending sponsorship removes reserve sponsorship from a sponsored target. + // The target is sfSponsee when provided; otherwise it is sfAccount. + if (ctx.tx.isFieldPresent(sfSponsor)) + { + JLOG(ctx.j.debug()) + << "preflight: sfSponsor must not be present when ending sponsorship"; + return temMALFORMED; + } + + // sfSponsorFlags should not be present if it is ending sponsorship. + if (ctx.tx.isFieldPresent(sfSponsorFlags)) + { + // Unreachable: reaching here means sfSponsor is absent, which is already checked above, + // and preflight1Sponsor already rejects sfSponsorFlags present without sfSponsor with + // temINVALID_FLAG. Keep this as a defensive check. + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::SponsorshipTransfer::preflight : sfSponsorFlags present without sfSponsor " + "when ending sponsorship"); + return temINVALID_FLAG; + // LCOV_EXCL_STOP + } + + if (ctx.tx.isFieldPresent(sfSponsee) && + ctx.tx.getAccountID(sfSponsee) == ctx.tx.getAccountID(sfAccount)) + { + JLOG(ctx.j.debug()) << "preflight: sfSponsee should not be the same as the account"; + return temMALFORMED; + } + } + + // Account-level reserve sponsorship changes the reserve responsibility for + // the account itself, so the new sponsor must explicitly co-sign. Object-level + // sponsorship may use pre-funded reserve sponsorship instead. + bool const isCreateOrReassign = + ctx.tx.isFlag(tfSponsorshipCreate) || ctx.tx.isFlag(tfSponsorshipReassign); + auto const reserveSponsor = getTxReserveSponsorID(ctx.tx); + bool const isAccountReserveSponsorship = + isCreateOrReassign && reserveSponsor && !ctx.tx.isFieldPresent(sfObjectID); + + if (isAccountReserveSponsorship && !ctx.tx.isFieldPresent(sfSponsorSignature)) + { + JLOG(ctx.j.debug()) << "preflight: account sponsorship requires sfSponsorSignature"; + return temMALFORMED; + } + + return tesSUCCESS; +} + +TER +SponsorshipTransfer::preclaim(PreclaimContext const& ctx) +{ + auto const objectID = ctx.tx[~sfObjectID]; + auto const newSponsorSleExpected = getTxReserveSponsor(ctx.view, ctx.tx); + if (!newSponsorSleExpected) + return newSponsorSleExpected.error(); // LCOV_EXCL_LINE + auto const newSponsorSle = *newSponsorSleExpected; + + auto const account = ctx.tx[sfAccount]; + auto const sponseeID = ctx.tx[~sfSponsee].value_or(account); + auto const sponseeSle = ctx.view.read(keylet::account(sponseeID)); + if (!sponseeSle) + { + // If it is ending sponsorship, sfSponsee is user input, return terNO_ACCOUNT if it does not + // exist. + if (ctx.tx.isFieldPresent(sfSponsee)) + return terNO_ACCOUNT; + + // If it is creating or reassigning sponsorship, sfSponsee is the account itself, which is + // always present by the time preclaim runs. Return tecINTERNAL if it does not exist. + return tecINTERNAL; // LCOV_EXCL_LINE + } + + // Default setup with an account sponsorship transfer. If it is an object transfer, they will be + // overridden to the object SLE and its type-specific sponsor field: + // sfHighSponsor/sfLowSponsor for a RippleState, sfSponsor for other object types. + SLE::const_pointer targetSle = sponseeSle; + auto const* sponsorField = &sfSponsor; + + if (objectID.has_value()) + { + auto const objectSle = ctx.view.read(keylet::unchecked(*objectID)); + if (!objectSle) + return tecNO_ENTRY; + + if (!isLedgerEntrySupportedBySponsorship(*objectSle)) + return tecNO_PERMISSION; + + if (!isLedgerEntryOwner(ctx.view, *objectSle, sponseeID)) + return tecNO_PERMISSION; + + // Object transfer: the target is the object, and its sponsor field + // depends on the object type, a RippleState stores the sponsor in + // sfHighSponsor/sfLowSponsor, while other object type uses sfSponsor. + targetSle = objectSle; + sponsorField = &getLedgerEntrySponsorField(*objectSle, sponseeID); + } + + bool const isSponsored = targetSle->isFieldPresent(*sponsorField); + + if (ctx.tx.isFlag(tfSponsorshipCreate)) + { + // Creating a new sponsorship: needs a new reserve sponsor, and the + // target must not already be sponsored + if (!newSponsorSle || isSponsored) + return tecNO_PERMISSION; + } + else if (ctx.tx.isFlag(tfSponsorshipReassign)) + { + // Reassigning sponsorship: needs a new reserve sponsor, and the target must already + // be sponsored + if (!newSponsorSle || !isSponsored) + return tecNO_PERMISSION; + + // Reassigning to the current sponsor would change no state, but would + // still draw down the sponsor's pre-funded reserve budget (and its + // reserve headroom would be double-counted in checkReserve). + if (targetSle->getAccountID(*sponsorField) == ctx.tx.getAccountID(sfSponsor)) + return tecNO_PERMISSION; + } + else if (ctx.tx.isFlag(tfSponsorshipEnd)) + { + // Ending sponsorship: no new reserve sponsor, the target must be sponsored. + if (newSponsorSle || !isSponsored) + return tecNO_PERMISSION; + + // Only the sponsor or sponsee can end sponsorship. + auto const sponsor = targetSle->getAccountID(*sponsorField); + if (account != sponsor && account != sponseeID) + return tecNO_PERMISSION; + } + + return tesSUCCESS; +} + +TER +SponsorshipTransfer::doApply() +{ + auto const objectID = ctx_.tx[~sfObjectID]; + + auto const sponseeID = ctx_.tx[~sfSponsee].value_or(accountID_); + auto const sponseeSle = view().peek(keylet::account(sponseeID)); + if (!sponseeSle) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto const balanceBeforeFee = [&](SLE::const_ref sle) -> XRPAmount { + if (sle->getAccountID(sfAccount) == accountID_) + return preFeeBalance_; + return sle->getFieldAmount(sfBalance).xrp(); + }; + + bool const isCreate = ctx_.tx.isFlag(tfSponsorshipCreate); + bool const isReassign = ctx_.tx.isFlag(tfSponsorshipReassign); + + if (objectID.has_value()) + { + // Transfer object sponsor + auto const objectSle = view().peek(keylet::unchecked(*objectID)); + if (!objectSle) + return tefINTERNAL; // LCOV_EXCL_LINE + + // preclaim established that the sponsee owns the object. + if (!isLedgerEntryOwner(view(), *objectSle, sponseeID)) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto const ownerSle = view().peek(keylet::account(sponseeID)); + if (!ownerSle) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto const ownerCountDelta = + static_cast(getLedgerEntryOwnerCount(*objectSle)); + auto const& sponsorField = getLedgerEntrySponsorField(*objectSle, sponseeID); + + if (isCreate || isReassign) + { + auto const newSponsor = ctx_.tx[~sfSponsor]; + XRPL_ASSERT( + newSponsor.has_value(), + "xrpl::SponsorshipTransfer::doApply : sfSponsor present for object sponsor " + "create/reassign"); + if (!newSponsor) + return tefINTERNAL; // LCOV_EXCL_LINE + auto const newSponsorID = *newSponsor; + auto const newSponsorSle = view().peek(keylet::account(newSponsorID)); + if (!newSponsorSle) + return tefINTERNAL; // LCOV_EXCL_LINE + + // Check if new sponsor has sufficient balance + // NOLINTNEXTLINE(readability-suspicious-call-argument) + if (auto const ter = checkReserve( + ctx_.getApplyViewContext(), + sponseeSle, + sponseeSle->getFieldAmount(sfBalance).xrp(), + newSponsorSle, + {.ownerCountDelta = ownerCountDelta}, + ctx_.journal); + !isTesSuccess(ter)) + return ter; + + if (isCreate) + { + // Update owner's sponsored count + if (auto const ter = incrementSponsorCount( + view(), ownerSle, sfSponsoredOwnerCount, ownerCountDelta); + !isTesSuccess(ter)) + return ter; // LCOV_EXCL_LINE + } + else if (isReassign) + { + auto const oldSponsorID = objectSle->getAccountID(sponsorField); + if (!oldSponsorID) + return tefINTERNAL; // LCOV_EXCL_LINE + auto const oldSponsorSle = view().peek(keylet::account(oldSponsorID)); + if (!oldSponsorSle) + return tefINTERNAL; // LCOV_EXCL_LINE + + // Decrement old sponsor's sponsoring count + if (auto const ter = decrementSponsorCount( + view(), oldSponsorSle, sfSponsoringOwnerCount, ownerCountDelta); + !isTesSuccess(ter)) + return ter; // LCOV_EXCL_LINE + } + + // Increment new sponsor's sponsoring count + if (auto const ter = incrementSponsorCount( + view(), newSponsorSle, sfSponsoringOwnerCount, ownerCountDelta); + !isTesSuccess(ter)) + return ter; // LCOV_EXCL_LINE + + // Object is now sponsored by new sponsor + objectSle->setAccountID(sponsorField, newSponsorID); + view().update(objectSle); + + auto const sponsorshipSle = view().peek(keylet::sponsorship(newSponsorID, sponseeID)); + if (sponsorshipSle) + { + // Update ReserveCount for sponsorship object if it exists + if (auto const ter = + decrementPrefundedReserveCount(view(), sponsorshipSle, ownerCountDelta); + !isTesSuccess(ter)) + return ter; // LCOV_EXCL_LINE + } + } + else if (ctx_.tx.isFlag(tfSponsorshipEnd)) + { + // End object sponsor + auto const oldSponsorID = objectSle->getAccountID(sponsorField); + if (!oldSponsorID) + return tefINTERNAL; // LCOV_EXCL_LINE + auto const oldSponsorSle = view().peek(keylet::account(oldSponsorID)); + if (!oldSponsorSle) + return tefINTERNAL; // LCOV_EXCL_LINE + + // The owner reclaims the reserve burden when the object is no longer sponsored. + // We do not check the sponsee's reserve here (via `checkReserve`) so that a sponsor can + // always end a sponsorship, even if the sponsee lacks sufficient reserve. + + // Decrement sponsored count + if (auto const ter = decrementSponsorCount( + view(), sponseeSle, sfSponsoredOwnerCount, ownerCountDelta); + !isTesSuccess(ter)) + return ter; // LCOV_EXCL_LINE + + // Decrement old sponsoring count + if (auto const ter = decrementSponsorCount( + view(), oldSponsorSle, sfSponsoringOwnerCount, ownerCountDelta); + !isTesSuccess(ter)) + return ter; // LCOV_EXCL_LINE + + // Remove sponsor from object + objectSle->makeFieldAbsent(sponsorField); + view().update(objectSle); + } + } + else + { + // Account-level sponsorship is always co-signed (preflight requires + // sfSponsorSignature), so there is no pre-funded budget to draw down here. + if (isCreate || isReassign) + { + auto const newSponsor = ctx_.tx[~sfSponsor]; + XRPL_ASSERT( + newSponsor.has_value(), + "xrpl::SponsorshipTransfer::doApply : sfSponsor present for account sponsor " + "create/reassign"); + if (!newSponsor) + return tefINTERNAL; // LCOV_EXCL_LINE + auto const newSponsorID = *newSponsor; + auto const newSponsorSle = view().peek(keylet::account(newSponsorID)); + if (!newSponsorSle) + return tefINTERNAL; // LCOV_EXCL_LINE + + if (auto const ter = checkReserve( + ctx_.getApplyViewContext(), + sponseeSle, + sponseeSle->getFieldAmount(sfBalance).xrp(), + newSponsorSle, + {.accountCountDelta = 1}, + ctx_.journal); + !isTesSuccess(ter)) + return ter; + + if (isReassign) + { + auto const oldSponsorID = sponseeSle->getAccountID(sfSponsor); + if (!oldSponsorID) + return tefINTERNAL; // LCOV_EXCL_LINE + auto const oldSponsorSle = view().peek(keylet::account(oldSponsorID)); + if (!oldSponsorSle) + return tefINTERNAL; // LCOV_EXCL_LINE + + // Decrement old sponsoring count + if (auto const ter = + decrementSponsorCount(view(), oldSponsorSle, sfSponsoringAccountCount, 1); + !isTesSuccess(ter)) + return ter; // LCOV_EXCL_LINE + } + + // Increment new sponsoring count + if (auto const ter = + incrementSponsorCount(view(), newSponsorSle, sfSponsoringAccountCount, 1); + !isTesSuccess(ter)) + return ter; // LCOV_EXCL_LINE + + // Account is now sponsored by new sponsor + sponseeSle->setAccountID(sfSponsor, newSponsorID); + view().update(sponseeSle); + } + else if (ctx_.tx.isFlag(tfSponsorshipEnd)) + { + // End account sponsor + auto const oldSponsorID = sponseeSle->getAccountID(sfSponsor); + if (!oldSponsorID) + return tefINTERNAL; // LCOV_EXCL_LINE + auto const oldSponsorSle = view().peek(keylet::account(oldSponsorID)); + if (!oldSponsorSle) + return tefINTERNAL; // LCOV_EXCL_LINE + + // The sponsee must be able to hold its own account reserve after + // the sponsorship is removed. + if (auto const ter = checkReserve( + ctx_.getApplyViewContext(), + sponseeSle, + balanceBeforeFee(sponseeSle), + SLE::pointer(), + {.accountCountDelta = 1}, + ctx_.journal); + !isTesSuccess(ter)) + return ter; + + sponseeSle->makeFieldAbsent(sfSponsor); + view().update(sponseeSle); + + // Decrement account sponsoring count + if (auto const ter = + decrementSponsorCount(view(), oldSponsorSle, sfSponsoringAccountCount, 1); + !isTesSuccess(ter)) + return ter; // LCOV_EXCL_LINE + } + } + + return tesSUCCESS; +} + +void +SponsorshipTransfer::visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref) +{ +} + +bool +SponsorshipTransfer::finalizeInvariants( + STTx const&, + TER, + XRPAmount, + ReadView const&, + beast::Journal const&) +{ + return true; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/system/Batch.cpp b/src/libxrpl/tx/transactors/system/Batch.cpp index ba54c14b1e..dee465cb88 100644 --- a/src/libxrpl/tx/transactors/system/Batch.cpp +++ b/src/libxrpl/tx/transactors/system/Batch.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -227,6 +228,16 @@ Batch::preflight(PreflightContext const& ctx) return temINVALID_FLAG; } + if (ctx.tx.isFieldPresent(sfSponsorFlags)) + { + if (isReserveSponsored(ctx.tx)) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]:" + << "spfSponsorReserve is not allowed on outer Batch."; + return temINVALID_FLAG; + } + } + auto const& rawTxns = ctx.tx.getFieldArray(sfRawTransactions); if (rawTxns.size() <= 1) { @@ -331,6 +342,14 @@ Batch::preflight(PreflightContext const& ctx) return ret; } } + if (stx.isFieldPresent(sfSponsorSignature)) + { + auto const sponsorSignature = stx.getFieldObject(sfSponsorSignature); + if (auto const ret = checkSignatureFields(sponsorSignature, hash, "sponsor signature ")) + { + return ret; + } + } // Check that the Fee is native asset (XRP) and zero if (auto const fee = stx.getFieldAmount(sfFee); !fee.native() || fee.xrp() != beast::kZero) @@ -341,6 +360,10 @@ Batch::preflight(PreflightContext const& ctx) return temBAD_FEE; } + // Disallow fee sponsorship on Batch inner txs + if (stx.isFieldPresent(sfSponsor) && isFeeSponsored(stx)) + return temINVALID_FLAG; + auto const innerAccount = stx.getAccountID(sfAccount); if (auto const preflightResult = xrpl::preflight(ctx.registry, ctx.rules, parentBatchId, stx, TapBatch, ctx.j); @@ -422,7 +445,7 @@ Batch::preflightSigValidated(PreflightContext const& ctx) { // A delegated inner is signed by the delegate, not the account holder, // so the delegate is the required signer when present. - AccountID const authorizer = rb.getFeePayer(); + AccountID const authorizer = rb.getInitiator(); // The outer account signs the batch itself, so it is never added to the // required signers. @@ -433,6 +456,10 @@ Batch::preflightSigValidated(PreflightContext const& ctx) if (auto const counterparty = rb[~sfCounterparty]; counterparty && counterparty != outerAccount) requiredSigners.push_back(*counterparty); + + if (auto const sponsor = rb.at(~sfSponsor); + sponsor && rb.isFieldPresent(sfSponsorSignature) && sponsor != outerAccount) + requiredSigners.push_back(*sponsor); } std::ranges::sort(requiredSigners); auto const dupes = std::ranges::unique(requiredSigners); diff --git a/src/libxrpl/tx/transactors/system/TicketCreate.cpp b/src/libxrpl/tx/transactors/system/TicketCreate.cpp index 73a72c217d..e19dc9fe96 100644 --- a/src/libxrpl/tx/transactors/system/TicketCreate.cpp +++ b/src/libxrpl/tx/transactors/system/TicketCreate.cpp @@ -75,13 +75,12 @@ TicketCreate::doApply() // check the starting balance because we want to allow dipping into the // reserve to pay fees. std::uint32_t const ticketCount = ctx_.tx[sfTicketCount]; - { - XRPAmount const reserve = - view().fees().accountReserve(sleAccountRoot->getFieldU32(sfOwnerCount) + ticketCount); - - if (preFeeBalance_ < reserve) - return tecINSUFFICIENT_RESERVE; - } + if (preFeeBalance_ < accountReserve( + view(), + sleAccountRoot, + j_, + {.ownerCountDelta = static_cast(ticketCount)})) + return tecINSUFFICIENT_RESERVE; beast::Journal const viewJ{ctx_.registry.get().getJournal("View")}; @@ -105,6 +104,7 @@ TicketCreate::doApply() sleTicket->setAccountID(sfAccount, accountID_); sleTicket->setFieldU32(sfTicketSequence, curTicketSeq); + view().insert(sleTicket); auto const page = view().dirInsert( @@ -125,7 +125,7 @@ TicketCreate::doApply() sleAccountRoot->setFieldU32(sfTicketCount, oldTicketCount + ticketCount); // Every added Ticket counts against the creator's reserve. - adjustOwnerCount(view(), sleAccountRoot, ticketCount, viewJ); + increaseOwnerCount(view(), sleAccountRoot, {}, 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/MPTokenAuthorize.cpp b/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp index 6db37515cb..0aeb6f33d1 100644 --- a/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp +++ b/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp @@ -164,7 +164,7 @@ MPTokenAuthorize::doApply() { auto const& tx = ctx_.tx; return authorizeMPToken( - ctx_.view(), + ctx_.getApplyViewContext(), preFeeBalance_, tx[sfMPTokenIssuanceID], accountID_, diff --git a/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp b/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp index e30127b688..aad1642f68 100644 --- a/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp +++ b/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -113,22 +114,35 @@ MPTokenIssuanceCreate::preflight(PreflightContext const& ctx) } std::expected -MPTokenIssuanceCreate::create(ApplyView& view, beast::Journal journal, MPTCreateArgs const& args) +MPTokenIssuanceCreate::create( + ApplyViewContext ctx, + beast::Journal journal, + MPTCreateArgs const& args) { - auto const acct = view.peek(keylet::account(args.account)); + auto const acct = ctx.view.peek(keylet::account(args.account)); if (!acct) return std::unexpected(tecINTERNAL); // LCOV_EXCL_LINE - if (args.priorBalance && - *(args.priorBalance) < view.fees().accountReserve((*acct)[sfOwnerCount] + 1)) - return std::unexpected(tecINSUFFICIENT_RESERVE); + // A reserve sponsor only covers tx.Account's own objects. + auto const sponsorExp = getEffectiveTxReserveSponsor(ctx, acct); + if (!sponsorExp) + return std::unexpected(sponsorExp.error()); // LCOV_EXCL_LINE + auto const sponsorSle = *sponsorExp; + + if (args.priorBalance) + { + if (auto const ret = checkReserve( + ctx, acct, *(args.priorBalance), sponsorSle, {.ownerCountDelta = 1}, journal); + !isTesSuccess(ret)) + return std::unexpected(ret); + } auto const mptId = makeMptID(args.sequence, args.account); auto const mptIssuanceKeylet = keylet::mptokenIssuance(mptId); // create the MPTokenIssuance { - auto const ownerNode = view.dirInsert( + auto const ownerNode = ctx.view.dirInsert( keylet::ownerDir(args.account), mptIssuanceKeylet, describeOwnerDir(args.account)); if (!ownerNode) @@ -166,7 +180,7 @@ MPTokenIssuanceCreate::create(ApplyView& view, beast::Journal journal, MPTCreate // populate this after the pseudo-account's MPToken / // RippleState has been installed. A missing holding here // would dangle the pointer and is a programmer error. - auto const sleHolding = view.read(keylet::unchecked(*args.referenceHolding)); + auto const sleHolding = ctx.view.read(keylet::unchecked(*args.referenceHolding)); if (!sleHolding) return std::unexpected(tecINTERNAL); // LCOV_EXCL_LINE auto const type = sleHolding->getType(); @@ -175,11 +189,13 @@ MPTokenIssuanceCreate::create(ApplyView& view, beast::Journal journal, MPTCreate (*mptIssuance)[sfReferenceHolding] = *args.referenceHolding; } - view.insert(mptIssuance); + addSponsorToLedgerEntry(mptIssuance, sponsorSle); + + ctx.view.insert(mptIssuance); } // Update owner count. - adjustOwnerCount(view, acct, 1, journal); + increaseOwnerCount(ctx.view, acct, sponsorSle, 1, journal); return mptId; } @@ -189,7 +205,7 @@ MPTokenIssuanceCreate::doApply() { auto const& tx = ctx_.tx; auto const result = create( - view(), + ctx_.getApplyViewContext(), j_, { .priorBalance = preFeeBalance_, diff --git a/src/libxrpl/tx/transactors/token/MPTokenIssuanceDestroy.cpp b/src/libxrpl/tx/transactors/token/MPTokenIssuanceDestroy.cpp index c56697767b..05d39c3596 100644 --- a/src/libxrpl/tx/transactors/token/MPTokenIssuanceDestroy.cpp +++ b/src/libxrpl/tx/transactors/token/MPTokenIssuanceDestroy.cpp @@ -49,10 +49,9 @@ MPTokenIssuanceDestroy::doApply() if (!view().dirRemove(keylet::ownerDir(accountID_), (*mpt)[sfOwnerNode], mpt->key(), false)) return tefBAD_LEDGER; // LCOV_EXCL_LINE + decreaseOwnerCountForObject(view(), accountID_, mpt, 1, j_); view().erase(mpt); - adjustOwnerCount(view(), view().peek(keylet::account(accountID_)), -1, j_); - return tesSUCCESS; } diff --git a/src/libxrpl/tx/transactors/token/TrustSet.cpp b/src/libxrpl/tx/transactors/token/TrustSet.cpp index e79e3cad5a..3d078ce825 100644 --- a/src/libxrpl/tx/transactors/token/TrustSet.cpp +++ b/src/libxrpl/tx/transactors/token/TrustSet.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -297,8 +298,6 @@ TrustSet::doApply() if (!sle) return tefINTERNAL; // LCOV_EXCL_LINE - std::uint32_t const uOwnerCount = sle->getFieldU32(sfOwnerCount); - // The reserve that is required to create the line. Note // that although the reserve increases with every item // an account owns, in the case of trust lines we only @@ -317,9 +316,23 @@ TrustSet::doApply() // well. A person with no intention of using the gateway // could use the extra XRP for their own purposes. + auto const sponsorExp = getTxReserveSponsor(ctx_.getApplyViewContext()); + if (!sponsorExp) + return sponsorExp.error(); // LCOV_EXCL_LINE + auto const sponsorSle = *sponsorExp; + + auto getSponsor = [&sponsorSle, this](AccountID const& account) { + return (sponsorSle && account == accountID_) ? sponsorSle : SLE::pointer(); + }; + + // The "free-tier" shortcut (ownerCount < 2) only applies when there is no sponsor. + // With any sponsor on the tx, the sponsor must cover the reserve (via balance or + // prefunded budget), so the reserve check always runs. + bool const freeTrustLine = !sponsorSle && (ownerCount(sle, j_) < 2); + std::uint32_t const uOwnerCount = ownerCount(sle, j_); XRPAmount const reserveCreate( (uOwnerCount < 2) ? XRPAmount(beast::kZero) - : view().fees().accountReserve(uOwnerCount + 1)); + : accountReserve(view(), sle, j_, {.ownerCountDelta = 1})); std::uint32_t const uQualityIn(bQualityIn ? ctx_.tx.getFieldU32(sfQualityIn) : 0); std::uint32_t uQualityOut(bQualityOut ? ctx_.tx.getFieldU32(sfQualityOut) : 0); @@ -505,6 +518,11 @@ TrustSet::doApply() bool bReserveIncrease = false; + auto const currentHighSponsor = + getLedgerEntryReserveSponsor(view(), sleRippleState, sfHighSponsor); + auto const currentLowSponsor = + getLedgerEntryReserveSponsor(view(), sleRippleState, sfLowSponsor); + if (bSetAuth) { uFlagsOut |= (bHigh ? lsfHighAuth : lsfLowAuth); @@ -512,10 +530,30 @@ TrustSet::doApply() if (bLowReserveSet && !bLowReserved) { + SLE::pointer const lowSponsor = getSponsor(uLowAccountID); + + if (view().rules().enabled(featureSponsor)) + { + if (auto const ret = checkReserve( + ctx_.getApplyViewContext(), + sleLowAccount, + preFeeBalance_, + lowSponsor, + {.ownerCountDelta = 1}, + j_, + tecINSUF_RESERVE_LINE); + lowSponsor && !isTesSuccess(ret)) + { + return ret; + } + } + // Set reserve for low account. - adjustOwnerCount(view(), sleLowAccount, 1, viewJ); + increaseOwnerCount(view(), sleLowAccount, lowSponsor, 1, viewJ); uFlagsOut |= lsfLowReserve; + addSponsorToLedgerEntry(sleRippleState, lowSponsor, sfLowSponsor); + if (!bHigh) bReserveIncrease = true; } @@ -523,16 +561,41 @@ TrustSet::doApply() if (bLowReserveClear && bLowReserved) { // Clear reserve for low account. - adjustOwnerCount(view(), sleLowAccount, -1, viewJ); + decreaseOwnerCount(view(), sleLowAccount, currentLowSponsor, 1, viewJ); uFlagsOut &= ~lsfLowReserve; + + removeSponsorFromLedgerEntry(sleRippleState, sfLowSponsor); } if (bHighReserveSet && !bHighReserved) { + SLE::pointer const highSponsor = getSponsor(uHighAccountID); + + // should be checked PreFunded Sponsor before increaseOwnerCount() + // For PreFunded sponsors, we need to check if there are sufficient reserves before + // calling increaseOwnerCount(). + if (view().rules().enabled(featureSponsor)) + { + if (auto const ret = checkReserve( + ctx_.getApplyViewContext(), + sleHighAccount, + preFeeBalance_, + highSponsor, + {.ownerCountDelta = 1}, + j_, + tecINSUF_RESERVE_LINE); + highSponsor && !isTesSuccess(ret)) + { + return ret; + } + } + // Set reserve for high account. - adjustOwnerCount(view(), sleHighAccount, 1, viewJ); + increaseOwnerCount(view(), sleHighAccount, highSponsor, 1, viewJ); uFlagsOut |= lsfHighReserve; + addSponsorToLedgerEntry(sleRippleState, highSponsor, sfHighSponsor); + if (bHigh) bReserveIncrease = true; } @@ -540,34 +603,75 @@ TrustSet::doApply() if (bHighReserveClear && bHighReserved) { // Clear reserve for high account. - adjustOwnerCount(view(), sleHighAccount, -1, viewJ); + decreaseOwnerCount(view(), sleHighAccount, currentHighSponsor, 1, viewJ); uFlagsOut &= ~lsfHighReserve; + + removeSponsorFromLedgerEntry(sleRippleState, sfHighSponsor); } if (uFlagsIn != uFlagsOut) sleRippleState->setFieldU32(sfFlags, uFlagsOut); - if (bDefault || badCurrency() == currency) + if (view().rules().enabled(featureSponsor)) { - // Delete. + if (bDefault || badCurrency() == currency) + { + // Delete. - terResult = trustDelete(view(), sleRippleState, uLowAccountID, uHighAccountID, viewJ); - } - // Reserve is not scaled by load. - else if (bReserveIncrease && preFeeBalance_ < reserveCreate) - { - JLOG(j_.trace()) << "Delay transaction: Insufficent reserve to " - "add trust line."; + terResult = + trustDelete(view(), sleRippleState, uLowAccountID, uHighAccountID, viewJ); + } + // Reserve is not scaled by load + else if ( + auto const ret = checkReserve( + ctx_.getApplyViewContext(), + sle, + preFeeBalance_, + sponsorSle, + {}, + j_, + tecINSUF_RESERVE_LINE); + !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; + // Another transaction could provide XRP to the account and then + // this transaction would succeed. + terResult = ret; + } + else + { + view().update(sleRippleState); + + JLOG(j_.trace()) << "Modify ripple line"; + } } else { - view().update(sleRippleState); + if (bDefault || badCurrency() == currency) + { + // Delete. - JLOG(j_.trace()) << "Modify ripple line"; + terResult = + trustDelete(view(), sleRippleState, uLowAccountID, uHighAccountID, viewJ); + } + // Reserve is not scaled by load. + else if (bReserveIncrease && preFeeBalance_ < reserveCreate) + { + 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; + } + else + { + view().update(sleRippleState); + + JLOG(j_.trace()) << "Modify ripple line"; + } } } // Line does not exist. @@ -582,8 +686,8 @@ TrustSet::doApply() JLOG(j_.trace()) << "Redundant: Setting non-existent ripple line to defaults."; return tecNO_LINE_REDUNDANT; } - else if (preFeeBalance_ < reserveCreate) // Reserve is not scaled by - // load. + // reserve is not scaled by load + else if (!view().rules().enabled(featureSponsor) && preFeeBalance_ < reserveCreate) { JLOG(j_.trace()) << "Delay transaction: Line does not exist. " "Insufficent reserve to create line."; @@ -592,6 +696,25 @@ TrustSet::doApply() // transaction would succeed. terResult = tecNO_LINE_INSUF_RESERVE; } + else if ( + auto const ret = checkReserve( + ctx_.getApplyViewContext(), + sle, + preFeeBalance_, + sponsorSle, + {.ownerCountDelta = 1}, + j_, + tecNO_LINE_INSUF_RESERVE); + view().rules().enabled(featureSponsor) && !freeTrustLine && + !isTesSuccess(ret)) // Reserve is not scaled by load. + { + 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 = ret; + } else { // Zero balance in currency. @@ -617,6 +740,7 @@ TrustSet::doApply() saLimitAllow, // Limit for who is being charged. uQualityIn, uQualityOut, + sponsorSle, viewJ); } diff --git a/src/libxrpl/tx/transactors/vault/VaultClawback.cpp b/src/libxrpl/tx/transactors/vault/VaultClawback.cpp index 541660c98f..d77286b667 100644 --- a/src/libxrpl/tx/transactors/vault/VaultClawback.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultClawback.cpp @@ -389,8 +389,8 @@ VaultClawback::doApply() auto const& vaultAccount = vault->at(sfAccount); // Transfer shares from holder to vault. - if (auto const ter = - accountSend(view(), holder, vaultAccount, sharesDestroyed, j_, WaiveTransferFee::Yes); + if (auto const ter = accountSend( + view(), holder, vaultAccount, sharesDestroyed, j_, {}, WaiveTransferFee::Yes); !isTesSuccess(ter)) return ter; @@ -399,7 +399,8 @@ VaultClawback::doApply() // Keep MPToken if holder is the vault owner. if (holder != vault->at(sfOwner)) { - if (auto const ter = removeEmptyHolding(view(), holder, sharesDestroyed.asset(), j_); + if (auto const ter = + removeEmptyHolding(ctx_.getApplyViewContext(), holder, sharesDestroyed.asset(), j_); isTesSuccess(ter)) { JLOG(j_.debug()) // @@ -425,7 +426,7 @@ VaultClawback::doApply() { // Transfer assets from vault to issuer. if (auto const ter = accountSend( - view(), vaultAccount, accountID_, assetsRecovered, j_, WaiveTransferFee::Yes); + view(), vaultAccount, accountID_, assetsRecovered, j_, {}, WaiveTransferFee::Yes); !isTesSuccess(ter)) return ter; diff --git a/src/libxrpl/tx/transactors/vault/VaultCreate.cpp b/src/libxrpl/tx/transactors/vault/VaultCreate.cpp index d262132b6f..e1f5873a89 100644 --- a/src/libxrpl/tx/transactors/vault/VaultCreate.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultCreate.cpp @@ -146,6 +146,7 @@ VaultCreate::doApply() // we can consider downgrading them to `tef` or `tem`. auto const& tx = ctx_.tx; + auto applyViewContext = ctx_.getApplyViewContext(); auto const sequence = tx.getSeqValue(); auto const owner = view().peek(keylet::account(accountID_)); if (owner == nullptr) @@ -156,9 +157,8 @@ VaultCreate::doApply() if (auto ter = dirLink(view(), accountID_, vault)) return ter; // We will create Vault and PseudoAccount, hence increase OwnerCount by 2 - adjustOwnerCount(view(), owner, 2, j_); - auto const ownerCount = owner->at(sfOwnerCount); - if (preFeeBalance_ < view().fees().accountReserve(ownerCount)) + increaseOwnerCount(view(), owner, {}, 2, j_); + if (preFeeBalance_ < accountReserve(view(), owner, j_)) return tecINSUFFICIENT_RESERVE; auto maybePseudo = createPseudoAccount(view(), vault->key(), sfVaultID); @@ -168,7 +168,8 @@ VaultCreate::doApply() AccountID const pseudoId = pseudo->at(sfAccount); auto const asset = tx[sfAsset]; - if (auto ter = addEmptyHolding(view(), pseudoId, preFeeBalance_, asset, j_); !isTesSuccess(ter)) + if (auto ter = addEmptyHolding(applyViewContext, pseudoId, preFeeBalance_, asset, j_); + !isTesSuccess(ter)) return ter; std::uint8_t const scale = (asset.holds() || asset.native()) @@ -197,7 +198,7 @@ VaultCreate::doApply() : keylet::trustLine(pseudoId, asset.get()).key; }(); auto const maybeShare = MPTokenIssuanceCreate::create( - view(), + applyViewContext, j_, { .priorBalance = std::nullopt, @@ -243,8 +244,8 @@ VaultCreate::doApply() view().insert(vault); // Explicitly create MPToken for the vault owner - if (auto const err = - authorizeMPToken(view(), preFeeBalance_, mptIssuanceID, accountID_, ctx_.journal); + if (auto const err = authorizeMPToken( + applyViewContext, preFeeBalance_, mptIssuanceID, accountID_, ctx_.journal); !isTesSuccess(err)) return err; @@ -252,7 +253,13 @@ VaultCreate::doApply() if (tx.isFlag(tfVaultPrivate)) { if (auto const err = authorizeMPToken( - view(), preFeeBalance_, mptIssuanceID, pseudoId, ctx_.journal, {}, accountID_); + applyViewContext, + preFeeBalance_, + mptIssuanceID, + pseudoId, + ctx_.journal, + {}, + accountID_); !isTesSuccess(err)) return err; } diff --git a/src/libxrpl/tx/transactors/vault/VaultDelete.cpp b/src/libxrpl/tx/transactors/vault/VaultDelete.cpp index fa38ae278b..497a2f2465 100644 --- a/src/libxrpl/tx/transactors/vault/VaultDelete.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultDelete.cpp @@ -96,13 +96,15 @@ TER VaultDelete::doApply() { auto const vault = view().peek(keylet::vault(ctx_.tx[sfVaultID])); + auto applyViewContext = ctx_.getApplyViewContext(); if (!vault) return tefINTERNAL; // LCOV_EXCL_LINE // Destroy the asset holding. auto asset = vault->at(sfAsset); - if (auto ter = removeEmptyHolding(view(), vault->at(sfAccount), asset, j_); !isTesSuccess(ter)) + if (auto ter = removeEmptyHolding(applyViewContext, vault->at(sfAccount), asset, j_); + !isTesSuccess(ter)) return ter; auto const& pseudoID = vault->at(sfAccount); @@ -130,7 +132,8 @@ VaultDelete::doApply() // Try to remove MPToken for vault shares for the vault owner if it exists. if (auto const mptoken = view().peek(keylet::mptoken(shareMPTID, accountID_))) { - if (auto const ter = removeEmptyHolding(view(), accountID_, MPTIssue(shareMPTID), j_); + if (auto const ter = + removeEmptyHolding(applyViewContext, accountID_, MPTIssue(shareMPTID), j_); !isTesSuccess(ter)) { // LCOV_EXCL_START @@ -151,7 +154,7 @@ VaultDelete::doApply() return tefBAD_LEDGER; // LCOV_EXCL_STOP } - adjustOwnerCount(view(), pseudoAcct, -1, j_); + decreaseOwnerCountForObject(view(), pseudoAcct, mpt, 1, j_); view().erase(mpt); @@ -210,7 +213,7 @@ VaultDelete::doApply() } // We are destroying Vault and PseudoAccount, hence decrease by 2 - adjustOwnerCount(view(), owner, -2, j_); + decreaseOwnerCountForObject(view(), owner, vault, 2, j_); // Destroy the vault. view().erase(vault); diff --git a/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp b/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp index d5eb80b84d..aa9cfc8537 100644 --- a/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp @@ -23,6 +23,7 @@ #include #include +#include #include namespace xrpl { @@ -196,6 +197,7 @@ VaultDeposit::doApply() { bool const fix320Enabled = view().rules().enabled(fixCleanup3_2_0); auto const vault = view().peek(keylet::vault(ctx_.tx[sfVaultID])); + auto applyViewContext = ctx_.getApplyViewContext(); if (!vault) return tefINTERNAL; // LCOV_EXCL_LINE auto const vaultAsset = vault->at(sfAsset); @@ -230,7 +232,7 @@ VaultDeposit::doApply() if (vault->isFlag(lsfVaultPrivate) && accountID_ != vault->at(sfOwner)) { if (auto const err = enforceMPTokenAuthorization( - ctx_.view(), mptIssuanceID, accountID_, preFeeBalance_, j_); + applyViewContext, mptIssuanceID, accountID_, preFeeBalance_, j_); !isTesSuccess(err)) return err; } @@ -240,7 +242,11 @@ VaultDeposit::doApply() if (!view().exists(keylet::mptoken(mptIssuanceID, accountID_))) { if (auto const err = authorizeMPToken( - view(), preFeeBalance_, mptIssuanceID->value(), accountID_, ctx_.journal); + applyViewContext, + preFeeBalance_, + mptIssuanceID->value(), + accountID_, + ctx_.journal); !isTesSuccess(err)) return err; } @@ -252,7 +258,7 @@ VaultDeposit::doApply() XRPL_ASSERT( accountID_ == vault->at(sfOwner), "xrpl::VaultDeposit::doApply : account is owner"); if (auto const err = authorizeMPToken( - view(), + applyViewContext, preFeeBalance_, // priorBalance mptIssuanceID->value(), // mptIssuanceID sleIssuance->at(sfIssuer), // account @@ -319,7 +325,7 @@ VaultDeposit::doApply() // Transfer assets from depositor to vault. if (auto const ter = accountSend( - view(), accountID_, vaultAccount, assetsDeposited, j_, WaiveTransferFee::Yes); + view(), accountID_, vaultAccount, assetsDeposited, j_, {}, WaiveTransferFee::Yes); !isTesSuccess(ter)) return ter; @@ -346,8 +352,8 @@ VaultDeposit::doApply() } // Transfer shares from vault to depositor. - if (auto const ter = - accountSend(view(), vaultAccount, accountID_, sharesCreated, j_, WaiveTransferFee::Yes); + if (auto const ter = accountSend( + view(), vaultAccount, accountID_, sharesCreated, j_, {}, 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 ad91c55723..353b72c30d 100644 --- a/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp @@ -194,6 +194,7 @@ TER VaultWithdraw::doApply() { auto const vault = view().peek(keylet::vault(ctx_.tx[sfVaultID])); + auto applyViewContext = ctx_.getApplyViewContext(); if (!vault) return tefINTERNAL; // LCOV_EXCL_LINE @@ -349,9 +350,10 @@ VaultWithdraw::doApply() view().update(vault); auto const& vaultAccount = vault->at(sfAccount); + // Transfer shares from depositor to vault. if (auto const ter = accountSend( - view(), accountID_, vaultAccount, sharesRedeemed, j_, WaiveTransferFee::Yes); + view(), accountID_, vaultAccount, sharesRedeemed, j_, {}, WaiveTransferFee::Yes); !isTesSuccess(ter)) return ter; @@ -360,7 +362,8 @@ VaultWithdraw::doApply() // Keep MPToken if holder is the vault owner. if (accountID_ != vault->at(sfOwner)) { - if (auto const ter = removeEmptyHolding(view(), accountID_, sharesRedeemed.asset(), j_); + if (auto const ter = + removeEmptyHolding(applyViewContext, accountID_, sharesRedeemed.asset(), j_); isTesSuccess(ter)) { JLOG(j_.debug()) // @@ -386,7 +389,7 @@ VaultWithdraw::doApply() auto const dstAcct = ctx_.tx[~sfDestination].value_or(accountID_); return doWithdraw( - view(), ctx_.tx, accountID_, dstAcct, vaultAccount, preFeeBalance_, assetsWithdrawn, j_); + applyViewContext, accountID_, dstAcct, vaultAccount, preFeeBalance_, assetsWithdrawn, j_); } void diff --git a/src/test/app/AMMExtendedMPT_test.cpp b/src/test/app/AMMExtendedMPT_test.cpp index e6dcc95733..63b7841fba 100644 --- a/src/test/app/AMMExtendedMPT_test.cpp +++ b/src/test/app/AMMExtendedMPT_test.cpp @@ -463,7 +463,7 @@ private: // Provide micro amounts to compensate for fees to make results round // nice. auto const startingXrp = - XRP(100) + env.current()->fees().accountReserve(2) + env.current()->fees().base * 3; + XRP(100) + env.current()->fees().accountReserve(2, 1) + env.current()->fees().base * 3; env.fund(startingXrp, gw_, alice_); env.fund(XRP(2'000), bob_); diff --git a/src/test/app/AMMExtended_test.cpp b/src/test/app/AMMExtended_test.cpp index a0a7d0fb15..563bb3ca33 100644 --- a/src/test/app/AMMExtended_test.cpp +++ b/src/test/app/AMMExtended_test.cpp @@ -546,7 +546,7 @@ private: // 1 for each trust limit == 3 (alice_ < mtgox/amazon/bitstamp) + // 1 for payment == 4 auto const startingXrp = - XRP(100) + env.current()->fees().accountReserve(3) + env.current()->fees().base * 4; + XRP(100) + env.current()->fees().accountReserve(3, 1) + env.current()->fees().base * 4; env.fund(startingXrp, gw1, gw2, gw3, localAlice); env.fund(XRP(2'000), localBob); diff --git a/src/test/app/AMMMPT_test.cpp b/src/test/app/AMMMPT_test.cpp index 4877406ca1..bf0bc5c7d7 100644 --- a/src/test/app/AMMMPT_test.cpp +++ b/src/test/app/AMMMPT_test.cpp @@ -1002,7 +1002,7 @@ private: // Insufficient reserve, XRP/MPT { - Env env(*this); + Env env(*this, features); auto const startingXrp = reserve(env, 4) + env.current()->fees().base * 4; env.fund(XRP(10'000), gw_); env.fund(XRP(10'000), alice_); diff --git a/src/test/app/AccountSet_test.cpp b/src/test/app/AccountSet_test.cpp index 362113aec2..d94fb66e50 100644 --- a/src/test/app/AccountSet_test.cpp +++ b/src/test/app/AccountSet_test.cpp @@ -1,4 +1,3 @@ - #include #include #include @@ -453,7 +452,7 @@ public: // We'll insert a replacement for the account root // with the higher (currently invalid) transfer rate. - auto replacement = std::make_shared(*sle, sle->key()); + auto replacement = std::make_shared(*sle); (*replacement)[sfTransferRate] = static_cast(transferRate * QUALITY_ONE); view.rawReplace(replacement); diff --git a/src/test/app/CheckMPT_test.cpp b/src/test/app/CheckMPT_test.cpp index 861a115fc9..93e752154c 100644 --- a/src/test/app/CheckMPT_test.cpp +++ b/src/test/app/CheckMPT_test.cpp @@ -409,7 +409,7 @@ class CheckMPT_test : public beast::unit_test::Suite // Insufficient reserve. Account const cheri{"cheri"}; - env.fund(env.current()->fees().accountReserve(1) - drops(1), cheri); + env.fund(env.current()->fees().accountReserve(1, 1) - drops(1), cheri); env(check::create(cheri, bob, usd(50)), Fee(drops(env.current()->fees().base)), diff --git a/src/test/app/Check_test.cpp b/src/test/app/Check_test.cpp index 3ad1e3fa4e..b19a9f6894 100644 --- a/src/test/app/Check_test.cpp +++ b/src/test/app/Check_test.cpp @@ -470,7 +470,7 @@ class Check_test : public beast::unit_test::Suite // Insufficient reserve. Account const cheri{"cheri"}; - env.fund(env.current()->fees().accountReserve(1) - drops(1), cheri); + env.fund(env.current()->fees().accountReserve(1, 1) - drops(1), cheri); env.close(); env(check::create(cheri, bob, usd(50)), diff --git a/src/test/app/Credentials_test.cpp b/src/test/app/Credentials_test.cpp index 456a53bc01..1f6ec012c8 100644 --- a/src/test/app/Credentials_test.cpp +++ b/src/test/app/Credentials_test.cpp @@ -638,8 +638,8 @@ struct Credentials_test : public beast::unit_test::Suite { Env env{*this, features}; - env.fund(drops(env.current()->fees().accountReserve(1)), issuer); - env.fund(drops(env.current()->fees().accountReserve(0)), subject); + env.fund(drops(env.current()->fees().accountReserve(1, 1)), issuer); + env.fund(drops(env.current()->fees().accountReserve(0, 1)), subject); env.close(); { diff --git a/src/test/app/Delegate_test.cpp b/src/test/app/Delegate_test.cpp index 6f1d2ce7de..257ed33619 100644 --- a/src/test/app/Delegate_test.cpp +++ b/src/test/app/Delegate_test.cpp @@ -275,7 +275,7 @@ class Delegate_test : public beast::unit_test::Suite Account const bob{"bob"}; auto const txFee = env.current()->fees().base; - env.fund(env.current()->fees().accountReserve(0) + txFee, alice); + env.fund(env.current()->fees().accountReserve(0, 1) + txFee, alice); env.fund(XRP(100000), bob); env.close(); @@ -292,7 +292,7 @@ class Delegate_test : public beast::unit_test::Suite auto const txFee = env.current()->fees().base; - env.fund(env.current()->fees().accountReserve(1) + (txFee * 4), alice); + env.fund(env.current()->fees().accountReserve(1, 1) + (txFee * 4), alice); env.fund(XRP(100000), bob, carol); env.close(); @@ -318,8 +318,8 @@ class Delegate_test : public beast::unit_test::Suite Account const alice{"alice"}; Account const bob{"bob"}; - env.fund(drops(env.current()->fees().accountReserve(1)), alice); - env.fund(drops(env.current()->fees().accountReserve(2)), bob); + env.fund(drops(env.current()->fees().accountReserve(1, 1)), alice); + env.fund(drops(env.current()->fees().accountReserve(2, 1)), bob); env.close(); // alice gives bob permission @@ -422,7 +422,7 @@ class Delegate_test : public beast::unit_test::Suite Account const carol{"carol"}; auto const baseFee = env.current()->fees().base; - auto const reserve = env.current()->fees().accountReserve(1); + auto const reserve = env.current()->fees().accountReserve(1, 1); auto const paymentAmount = XRP(1); auto const highFee = reserve + baseFee; BEAST_EXPECT(highFee > reserve); @@ -488,9 +488,9 @@ class Delegate_test : public beast::unit_test::Suite Account const carol{"carol"}; auto const baseFee = env.current()->fees().base; - auto const baseReserve = env.current()->fees().accountReserve(0); + auto const baseReserve = env.current()->fees().accountReserve(0, 1); - env.fund(env.current()->fees().accountReserve(1) + baseFee + XRP(1), alice); + env.fund(env.current()->fees().accountReserve(1, 1) + baseFee + XRP(1), alice); env.fund(baseReserve, bob); env.fund(XRP(1000), carol); env.close(); @@ -523,7 +523,7 @@ class Delegate_test : public beast::unit_test::Suite Account const carol{"carol"}; auto const baseFee = env.current()->fees().base; - auto const reserve = env.current()->fees().accountReserve(1); + auto const reserve = env.current()->fees().accountReserve(1, 1); // Alice is funded with (reserve + baseFee): after DelegateSet she has // exactly 'reserve', which is insufficient to send XRP(10) while keeping @@ -2612,7 +2612,9 @@ class Delegate_test : public beast::unit_test::Suite {"CredentialDelete", featureCredentials}, {"NFTokenModify", featureDynamicNFT}, {"PermissionedDomainSet", featurePermissionedDomains}, - {"PermissionedDomainDelete", featurePermissionedDomains}}; + {"PermissionedDomainDelete", featurePermissionedDomains}, + {"SponsorshipSet", featureSponsor}, + }; // Can not delegate tx if any required feature disabled. { @@ -2747,7 +2749,7 @@ class Delegate_test : public beast::unit_test::Suite // DO NOT modify expectedDelegableCount unless all scenarios, including // edge cases, have been fully tested and verified. // ==================================================================== - std::size_t const expectedDelegableCount = 56; + std::size_t const expectedDelegableCount = 57; BEAST_EXPECTS( delegableCount == expectedDelegableCount, diff --git a/src/test/app/DepositAuth_test.cpp b/src/test/app/DepositAuth_test.cpp index 3496e67b54..c75bdeaf3a 100644 --- a/src/test/app/DepositAuth_test.cpp +++ b/src/test/app/DepositAuth_test.cpp @@ -51,7 +51,7 @@ namespace xrpl::test { static XRPAmount reserve(jtx::Env& env, std::uint32_t count) { - return env.current()->fees().accountReserve(count); + return env.current()->fees().accountReserve(count, 1); } // Helper function that returns true if acct has the lsfDepositAuth flag set. @@ -1025,7 +1025,7 @@ struct DepositPreauth_test : public beast::unit_test::Suite { // not enough reserve Account const john{"john"}; - env.fund(env.current()->fees().accountReserve(0), john); + env.fund(env.current()->fees().accountReserve(0, 1), john); env.close(); auto jv = deposit::authCredentials(john, {{.issuer = issuer, .credType = credType}}); diff --git a/src/test/app/EscrowToken_test.cpp b/src/test/app/EscrowToken_test.cpp index 666ee498bd..4015f5ddc8 100644 --- a/src/test/app/EscrowToken_test.cpp +++ b/src/test/app/EscrowToken_test.cpp @@ -937,7 +937,7 @@ struct EscrowToken_test : public beast::unit_test::Suite auto const expectedResult = env.current()->rules().enabled(fixCleanup3_2_0) ? Ter(tesSUCCESS) - : Ter(tefEXCEPTION); + : Ter(tefINTERNAL); env(escrow::cancel(alice, alice, seq), Fee(baseFee), expectedResult); env.close(); diff --git a/src/test/app/FlowMPT_test.cpp b/src/test/app/FlowMPT_test.cpp index 39d722da65..302e55a2cc 100644 --- a/src/test/app/FlowMPT_test.cpp +++ b/src/test/app/FlowMPT_test.cpp @@ -726,7 +726,7 @@ struct FlowMPT_test : public beast::unit_test::Suite static XRPAmount reserve(jtx::Env& env, std::uint32_t count) { - return env.current()->fees().accountReserve(count); + return env.current()->fees().accountReserve(count, 1); } // Helper function that returns the Offers on an account. diff --git a/src/test/app/Flow_test.cpp b/src/test/app/Flow_test.cpp index 72e39ab890..8d5162394e 100644 --- a/src/test/app/Flow_test.cpp +++ b/src/test/app/Flow_test.cpp @@ -706,7 +706,7 @@ struct Flow_test : public beast::unit_test::Suite static XRPAmount reserve(jtx::Env& env, std::uint32_t count) { - return env.current()->fees().accountReserve(count); + return env.current()->fees().accountReserve(count, 1); } // Helper function that returns the Offers on an account. diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index 79fdbb54ed..b70d099ba4 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -337,7 +337,72 @@ class Invariants_test : public beast::unit_test::Suite // check. sleA1->at(sfBalance) = beast::kZero; BEAST_EXPECT(sleA1->at(sfOwnerCount) == 0); - adjustOwnerCount(ac.view(), sleA1, 1, ac.journal); + increaseOwnerCount(ac.view(), sleA1, {}, 1, ac.journal); + + ac.view().erase(sleA1); + + return true; + }, + XRPAmount{}, + STTx{ttACCOUNT_DELETE, [](STObject& tx) {}}); + + doInvariantCheck( + {{"account deletion left behind a sponsorship field"}}, + [&](Account const& a1, Account const& a2, ApplyContext& ac) { + auto const sleA1 = ac.view().peek(keylet::account(a1.id())); + if (!sleA1) + return false; + sleA1->at(sfBalance) = beast::kZero; + sleA1->setFieldU32(sfSponsoredOwnerCount, 1); + + ac.view().erase(sleA1); + + return true; + }, + XRPAmount{}, + STTx{ttACCOUNT_DELETE, [](STObject& tx) {}}); + + doInvariantCheck( + {{"account deletion left behind a sponsorship field"}}, + [&](Account const& a1, Account const& a2, ApplyContext& ac) { + auto const sleA1 = ac.view().peek(keylet::account(a1.id())); + if (!sleA1) + return false; + sleA1->at(sfBalance) = beast::kZero; + sleA1->setFieldU32(sfSponsoringOwnerCount, 1); + + ac.view().erase(sleA1); + + return true; + }, + XRPAmount{}, + STTx{ttACCOUNT_DELETE, [](STObject& tx) {}}); + + doInvariantCheck( + {{"account deletion left behind a sponsorship field"}}, + [&](Account const& a1, Account const& a2, ApplyContext& ac) { + auto const a1Id = a1.id(); + auto const sleA1 = ac.view().peek(keylet::account(a1Id)); + if (!sleA1) + return false; + sleA1->at(sfBalance) = beast::kZero; + sleA1->setFieldU32(sfSponsoringAccountCount, 1); + + ac.view().erase(sleA1); + + return true; + }, + XRPAmount{}, + STTx{ttACCOUNT_DELETE, [](STObject& tx) {}}); + + doInvariantCheck( + {{"account deletion left behind a sponsorship field"}}, + [&](Account const& a1, Account const& a2, ApplyContext& ac) { + auto const sleA1 = ac.view().peek(keylet::account(a1.id())); + if (!sleA1) + return false; + sleA1->at(sfBalance) = beast::kZero; + sleA1->setAccountID(sfSponsor, a2.id()); ac.view().erase(sleA1); @@ -1771,6 +1836,7 @@ class Invariants_test : public beast::unit_test::Suite "pseudo-account sequence changed" "pseudo-account flags are not set" "pseudo-account has a regular key" + "pseudo-account has a sponsorship field" */ struct Mod { @@ -1798,6 +1864,22 @@ class Invariants_test : public beast::unit_test::Suite .expectedFailure = "pseudo-account has a regular key", .func = [](SLE::pointer& sle) { sle->at(sfRegularKey) = Account("regular").id(); }, }, + { + .expectedFailure = "pseudo-account has a sponsorship field", + .func = [](SLE::pointer& sle) { sle->at(sfSponsoredOwnerCount) = 1; }, + }, + { + .expectedFailure = "pseudo-account has a sponsorship field", + .func = [](SLE::pointer& sle) { sle->at(sfSponsoringOwnerCount) = 1; }, + }, + { + .expectedFailure = "pseudo-account has a sponsorship field", + .func = [](SLE::pointer& sle) { sle->at(sfSponsoringAccountCount) = 1; }, + }, + { + .expectedFailure = "pseudo-account has a sponsorship field", + .func = [](SLE::pointer& sle) { sle->at(sfSponsor) = Account("sponsor").id(); }, + }, }); for (auto const& mod : mods) @@ -5065,6 +5147,111 @@ class Invariants_test : public beast::unit_test::Suite } } + void + testSponsorship() + { + using namespace test::jtx; + using namespace std::string_literals; + testcase("Sponsorship"); + { + auto const expectMessage = + "SponsoredOwnerCount does not equal SponsoringOwnerCount delta."; + + doInvariantCheck( + {{expectMessage}}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { + auto const sle = ac.view().peek(keylet::account(a1.id())); + if (!sle) + return false; + sle->setFieldU32(sfSponsoredOwnerCount, 1); + ac.view().update(sle); + return true; + }); + + doInvariantCheck( + {{expectMessage}}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { + auto const sle = ac.view().peek(keylet::account(a1.id())); + if (!sle) + return false; + sle->setFieldU32(sfSponsoringOwnerCount, 1); + ac.view().update(sle); + return true; + }); + } + + { + auto const expectMessage = + "OwnerCount must be greater than or equal to SponsoredOwnerCount."; + + doInvariantCheck( + {{expectMessage}}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { + auto const sle = ac.view().peek(keylet::account(a1.id())); + if (!sle) + return false; + sle->setFieldU32(sfOwnerCount, 0); + sle->setFieldU32(sfSponsoredOwnerCount, 1); + ac.view().update(sle); + + auto const sle2 = ac.view().peek(keylet::account(a2.id())); + if (!sle2) + return false; + sle2->setFieldU32(sfSponsoringOwnerCount, 1); + ac.view().update(sle2); + return true; + }); + } + + { + auto const expectMessage = + "SponsoredObjectOwnerCount does not equal SponsoredOwnerCount delta."; + uint256 checkID; + + doInvariantCheck( + {{expectMessage}}, + [&](Account const&, Account const& a2, ApplyContext& ac) { + auto const check = ac.view().peek(keylet::check(checkID)); + if (!check) + return false; + check->setAccountID(sfSponsor, a2.id()); + ac.view().update(check); + return true; + }, + XRPAmount{}, + STTx{ttACCOUNT_SET, [](STObject&) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + [&checkID](Account const& a1, Account const& a2, Env& env) { + checkID = keylet::check(a1.id(), env.seq(a1)).key; + env(check::create(a1, a2, XRP(1))); + return true; + }); + } + + { + auto const expectMessage = + "Invariant failed: Net delta of SponsoringAccountCount does " + "not match net delta of sfSponsor presence."; + + doInvariantCheck( + {{expectMessage}}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { + auto const sle = ac.view().peek(keylet::account(a1.id())); + if (!sle) + return false; + sle->setFieldU32(sfSponsoringAccountCount, 1); + ac.view().update(sle); + return true; + }); + + doInvariantCheck( + {{expectMessage}}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { + auto const sle = ac.view().peek(keylet::account(a1.id())); + if (!sle) + return false; + sle->setAccountID(sfSponsor, a2.id()); + ac.view().update(sle); + return true; + }); + } + } + void testObjectHasPseudoAccount() { @@ -5467,6 +5654,7 @@ public: testVaultComputeCoarsestScale(); testAMM(); testObjectHasPseudoAccount(); + testSponsorship(); } }; diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index 2a4de18a9c..b0b0924c6e 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -1078,7 +1079,7 @@ class LoanBroker_test : public beast::unit_test::Suite } auto const amt = - env.balance(alice) - env.current()->fees().accountReserve(env.ownerCount(alice)); + env.balance(alice) - accountReserve(*env.current(), alice.id(), env.journal); env(pay(alice, issuer, amt)); // preclaim:: tecINSUFFICIENT_RESERVE diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 3e62af48ff..569306e920 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -36,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -671,11 +673,11 @@ protected: case AssetType::MPT: { // Enough to cover initial fees if (!env.le(keylet::account(issuer))) - env.fund(env.current()->fees().accountReserve(10) * 10, issuer); + env.fund(env.current()->fees().accountReserve(10, 1) * 10, issuer); if (!env.le(keylet::account(lender))) - env.fund(env.current()->fees().accountReserve(10) * 10, noripple(lender)); + env.fund(env.current()->fees().accountReserve(10, 1) * 10, noripple(lender)); if (!env.le(keylet::account(borrower))) - env.fund(env.current()->fees().accountReserve(10) * 10, noripple(borrower)); + env.fund(env.current()->fees().accountReserve(10, 1) * 10, noripple(borrower)); MPTTester mptt{env, issuer, kMptInitNoFund}; mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); @@ -760,11 +762,11 @@ protected: using namespace jtx; // Enough to cover initial fees - env.fund(env.current()->fees().accountReserve(10) * 10, issuer); + env.fund(env.current()->fees().accountReserve(10, 1) * 10, issuer); if (lender != issuer) - env.fund(env.current()->fees().accountReserve(10) * 10, noripple(lender)); + env.fund(env.current()->fees().accountReserve(10, 1) * 10, noripple(lender)); if (borrower != issuer && borrower != lender) - env.fund(env.current()->fees().accountReserve(10) * 10, noripple(borrower)); + env.fund(env.current()->fees().accountReserve(10, 1) * 10, noripple(borrower)); describeLoan(env, brokerParams, loanParams, assetType, issuer, lender, borrower); @@ -841,11 +843,10 @@ protected: // Add extra for transaction fees and reserves, if appropriate, or a // tiny amount for the extra paid in each transaction auto const totalNeeded = state.totalValue + (serviceFee * state.paymentRemaining) + - (broker.asset.native() - ? Number( - baseFee * state.paymentRemaining + - env.current()->fees().accountReserve(env.ownerCount(borrower))) - : broker.asset(15).number()); + (broker.asset.native() ? Number( + baseFee * state.paymentRemaining + + accountReserve(*env.current(), borrower.id(), env.journal)) + : broker.asset(15).number()); auto const shortage = totalNeeded - borrowerBalance.number(); @@ -3071,7 +3072,7 @@ protected: auto const [acctReserve, incReserve] = [this]() -> std::pair { Env const env{*this, testableAmendments()}; return { - env.current()->fees().accountReserve(0).drops() / kDropsPerXrp.drops(), + env.current()->fees().accountReserve(0, 1).drops() / kDropsPerXrp.drops(), env.current()->fees().increment.drops() / kDropsPerXrp.drops()}; }(); @@ -4419,11 +4420,12 @@ protected: Account const lender{"lender"}; Account const issuer{"issuer"}; Account const borrower{"borrower"}; + Account const sponsor{"sponsor"}; auto const iou = issuer["IOU"]; auto testWrapper = [&](auto&& test) { Env env(*this); - env.fund(XRP(1'000), lender, issuer, borrower); + env.fund(XRP(1'000), lender, issuer, borrower, sponsor); env(trust(lender, iou(10'000'000))); env(pay(issuer, lender, iou(5'000'000))); BrokerInfo const brokerInfo{createVaultAndBroker(env, issuer["IOU"], lender)}; @@ -4438,6 +4440,15 @@ protected: BrokerInfo const& brokerInfo, jtx::Fee const& loanSetFee, Number const& debtMaximumRequest) { + for (auto const sponsorFlags : {spfSponsorReserve, spfSponsorReserve | spfSponsorFee}) + { + env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), + sponsor::As(sponsor, sponsorFlags), + Sig(sfCounterpartySignature, lender), + loanSetFee, + Ter(temINVALID_FLAG)); + } + // first temBAD_SIGNER: TODO // invalid grace period { @@ -4528,8 +4539,8 @@ protected: BrokerInfo const& brokerInfo, jtx::Fee const& loanSetFee, Number const& debtMaximumRequest) { - auto const amt = env.balance(borrower) - - env.current()->fees().accountReserve(env.ownerCount(borrower)); + auto const amt = + env.balance(borrower) - accountReserve(*env.current(), borrower.id(), env.journal); env(pay(borrower, issuer, amt)); // tecINSUFFICIENT_RESERVE diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index bbf1c54ab1..7543db7364 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -2108,6 +2108,15 @@ class MPToken_test : public beast::unit_test::Suite reward = STAmount{sfSignatureReward, usd(10)}; minAmount = STAmount{sfMinAccountCreateAmount, mpt}; } + // SponsorshipSet + { + json::Value jv; + jv[jss::TransactionType] = jss::SponsorshipSet; + jv[jss::Account] = alice.human(); + jv[sfSponsee.fieldName] = carol.human(); + jv[sfFeeAmount.fieldName] = mpt.getJson(JsonOptions::Values::None); + test(jv, sfFeeAmount.fieldName.c_str()); + } } BEAST_EXPECT(txWithAmounts.empty()); } @@ -7563,7 +7572,7 @@ class MPToken_test : public beast::unit_test::Suite void testFixDoubleOwnerCount(FeatureBitset all) { - testcase("Fix Double adjustOwnerCount in AMMWithdraw"); + testcase("Fix Double OwnerCount in AMMWithdraw"); using namespace jtx; diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index 844bb4ee87..acd54ae26a 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -452,7 +452,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // Just for sanity's sake we'll check that the current value // of sfMintedNFTokens matches what we expect. - auto replacement = std::make_shared(*sle, sle->key()); + auto replacement = std::make_shared(*sle); if (replacement->getFieldU32(sfMintedNFTokens) != 1) return false; // Unexpected test conditions. diff --git a/src/test/app/OfferMPT_test.cpp b/src/test/app/OfferMPT_test.cpp index 800b14877a..d03b1b8e93 100644 --- a/src/test/app/OfferMPT_test.cpp +++ b/src/test/app/OfferMPT_test.cpp @@ -59,7 +59,7 @@ class OfferMPT_test : public beast::unit_test::Suite static XRPAmount reserve(jtx::Env& env, std::uint32_t count) { - return env.current()->fees().accountReserve(count); + return env.current()->fees().accountReserve(count, 1); } static std::uint32_t @@ -1794,7 +1794,8 @@ public: // 1 for each trust limit == 3 (alice < mtgox/amazon/bitstamp) + // 1 for payment == 4 auto const base = env.current()->fees().base; - auto const startingXrp = XRP(100) + env.current()->fees().accountReserve(3) + base * 4; + auto const startingXrp = + XRP(100) + env.current()->fees().accountReserve(3, 1) + base * 4; env.fund(startingXrp, gw1, gw2, gw3, alice, bob); env.close(); @@ -1814,7 +1815,8 @@ public: env(offer(alice, usD1(200), XRP(200))); BEAST_EXPECT(env.balance(alice, usD1) == usD1(100)); - BEAST_EXPECT(env.balance(alice) == STAmount(env.current()->fees().accountReserve(3))); + BEAST_EXPECT( + env.balance(alice) == STAmount(env.current()->fees().accountReserve(3, 1))); BEAST_EXPECT(env.balance(bob, usD1) == usD1(400)); }; @@ -1865,7 +1867,7 @@ public: auto const bob = Account{"bob"}; auto const startingXrp = - XRP(100) + env.current()->fees().accountReserve(1) + env.current()->fees().base * 2; + XRP(100) + env.current()->fees().accountReserve(1, 1) + env.current()->fees().base * 2; env.fund(startingXrp, gw, alice, bob); @@ -1884,7 +1886,7 @@ public: jrr = ledgerEntryRoot(env, alice); BEAST_EXPECT( jrr[jss::node][sfBalance.fieldName] == - STAmount(env.current()->fees().accountReserve(1)).getText()); + STAmount(env.current()->fees().accountReserve(1, 1)).getText()); jrr = ledgerEntryMPT(env, bob, usd); BEAST_EXPECT(jrr[jss::node][sfMPTAmount.fieldName] == "400"); @@ -1904,7 +1906,7 @@ public: auto const bob = Account{"bob"}; auto const startingXrp = - XRP(100) + env.current()->fees().accountReserve(1) + env.current()->fees().base * 2; + XRP(100) + env.current()->fees().accountReserve(1, 1) + env.current()->fees().base * 2; env.fund(startingXrp, gw, alice, bob); @@ -1925,7 +1927,7 @@ public: jrr = ledgerEntryRoot(env, alice); BEAST_EXPECT( jrr[jss::node][sfBalance.fieldName] == - STAmount(env.current()->fees().accountReserve(1)).getText()); + STAmount(env.current()->fees().accountReserve(1, 1)).getText()); jrr = ledgerEntryMPT(env, bob, usd); BEAST_EXPECT(jrr[jss::node][sfMPTAmount.fieldName] == "300"); @@ -1946,7 +1948,7 @@ public: auto const base = env.current()->fees().base; auto const startingXrp = - XRP(100.1) + env.current()->fees().accountReserve(1) + base * 2; + XRP(100.1) + env.current()->fees().accountReserve(1, 1) + base * 2; env.fund(startingXrp, gw, alice, bob); env.close(); diff --git a/src/test/app/Offer_test.cpp b/src/test/app/Offer_test.cpp index 2724a6474b..7fc7161e36 100644 --- a/src/test/app/Offer_test.cpp +++ b/src/test/app/Offer_test.cpp @@ -60,7 +60,7 @@ class OfferBaseUtil_test : public beast::unit_test::Suite static XRPAmount reserve(jtx::Env& env, std::uint32_t count) { - return env.current()->fees().accountReserve(count); + return env.current()->fees().accountReserve(count, 1); } static std::uint32_t @@ -1962,7 +1962,7 @@ public: // 1 for each trust limit == 3 (alice < mtgox/amazon/bitstamp) + // 1 for payment == 4 auto const startingXrp = - XRP(100) + env.current()->fees().accountReserve(3) + env.current()->fees().base * 4; + XRP(100) + env.current()->fees().accountReserve(3, 1) + env.current()->fees().base * 4; env.fund(startingXrp, gw1, gw2, gw3, alice, bob); env.close(); @@ -1985,7 +1985,7 @@ public: jrr = ledgerEntryRoot(env, alice); BEAST_EXPECT( jrr[jss::node][sfBalance.fieldName] == - STAmount(env.current()->fees().accountReserve(3)).getText()); + STAmount(env.current()->fees().accountReserve(3, 1)).getText()); jrr = ledgerEntryState(env, bob, gw1, "USD"); BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-400"); @@ -2045,7 +2045,7 @@ public: auto const usd = gw["USD"]; auto const startingXrp = - XRP(100) + env.current()->fees().accountReserve(1) + env.current()->fees().base * 2; + XRP(100) + env.current()->fees().accountReserve(1, 1) + env.current()->fees().base * 2; env.fund(startingXrp, gw, alice, bob); env.close(); @@ -2066,7 +2066,7 @@ public: jrr = ledgerEntryRoot(env, alice); BEAST_EXPECT( jrr[jss::node][sfBalance.fieldName] == - STAmount(env.current()->fees().accountReserve(1)).getText()); + STAmount(env.current()->fees().accountReserve(1, 1)).getText()); jrr = ledgerEntryState(env, bob, gw, "USD"); BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-400"); @@ -2087,7 +2087,7 @@ public: auto const usd = gw["USD"]; auto const startingXrp = - XRP(100) + env.current()->fees().accountReserve(1) + env.current()->fees().base * 2; + XRP(100) + env.current()->fees().accountReserve(1, 1) + env.current()->fees().base * 2; env.fund(startingXrp, gw, alice, bob); env.close(); @@ -2110,7 +2110,7 @@ public: jrr = ledgerEntryRoot(env, alice); BEAST_EXPECT( jrr[jss::node][sfBalance.fieldName] == - STAmount(env.current()->fees().accountReserve(1)).getText()); + STAmount(env.current()->fees().accountReserve(1, 1)).getText()); jrr = ledgerEntryState(env, bob, gw, "USD"); BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-300"); @@ -2131,8 +2131,8 @@ public: auto const xts = gw["XTS"]; auto const xxx = gw["XXX"]; - auto const startingXrp = - XRP(100.1) + env.current()->fees().accountReserve(1) + env.current()->fees().base * 2; + auto const startingXrp = XRP(100.1) + env.current()->fees().accountReserve(1, 1) + + env.current()->fees().base * 2; env.fund(startingXrp, gw, alice, bob); env.close(); diff --git a/src/test/app/Oracle_test.cpp b/src/test/app/Oracle_test.cpp index 7e7f5e9bd0..b8d9fb32d1 100644 --- a/src/test/app/Oracle_test.cpp +++ b/src/test/app/Oracle_test.cpp @@ -60,7 +60,7 @@ private: // Insufficient reserve { Env env(*this); - env.fund(env.current()->fees().accountReserve(0), owner); + env.fund(env.current()->fees().accountReserve(0, 1), owner); Oracle const oracle( env, {.owner = owner, @@ -71,7 +71,7 @@ private: { Env env(*this); env.fund( - env.current()->fees().accountReserve(1) + env.current()->fees().base * 2, owner); + env.current()->fees().accountReserve(1, 1) + env.current()->fees().base * 2, owner); Oracle oracle( env, {.owner = owner, .fee = static_cast(env.current()->fees().base.drops())}); BEAST_EXPECT(oracle.exists()); @@ -640,7 +640,7 @@ private: Env env(*this); auto const baseFee = static_cast(env.current()->fees().base.drops()); env.fund( - env.current()->fees().accountReserve(1) + env.current()->fees().base * 2, owner); + env.current()->fees().accountReserve(1, 1) + env.current()->fees().base * 2, owner); Oracle oracle(env, {.owner = owner, .fee = baseFee}); oracle.set(UpdateArg{.series = {{"XRP", "USD", 742, 2}}, .fee = baseFee}); } diff --git a/src/test/app/Sponsor_test.cpp b/src/test/app/Sponsor_test.cpp new file mode 100644 index 0000000000..f20aac68f9 --- /dev/null +++ b/src/test/app/Sponsor_test.cpp @@ -0,0 +1,5511 @@ +#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 +#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 STAmount +reserve(jtx::Env& env, std::uint32_t count) +{ + return env.current()->fees().accountReserve(count, 1); +} + +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(); +} + +class Sponsor_test : public beast::unit_test::Suite +{ +public: + void + testDisabled() + { + testcase("Disabled"); + using namespace test::jtx; + Env env{*this, testableAmendments() - featureSponsor}; + Account const alice("alice"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, sponsor); + + // check Sponsor fields + auto const jt = noop(alice); + auto jt1 = jt; + jt1[sfSponsor.jsonName] = sponsor.human(); + env(jt1, Ter(temDISABLED)); + env(jt, Sig(sfSponsorSignature, sponsor), Ter(temDISABLED)); + + auto jt2 = jt; + jt2[sfSponsorFlags.jsonName] = spfSponsorFee | spfSponsorReserve; + env(jt2, Ter(temDISABLED)); + + // check Sponsor transactions + env(sponsor::transfer(alice, 0), Ter(temDISABLED)); + env(sponsor::set(sponsor, 0), Ter(temDISABLED)); + } + + void + testInvalidSponsorshipSet() + { + testcase("Invalid SponsorshipSet"); + using namespace test::jtx; + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + Account const noFunded("noFunded"); + Account const gw("gw"); + + auto const usd = gw["usd"]; + env.fund(XRP(10000), alice, sponsor, gw); + env.close(); + + // + // preflight + // + + // Invalid flags + { + env(sponsor::set(sponsor, ~tfSponsorshipSetMask - tfInnerBatchTxn), + sponsor::SponseeAcc(alice), + Ter(temINVALID_FLAG)); + + env(sponsor::set( + sponsor, + tfSponsorshipSetRequireSignForFee | tfSponsorshipClearRequireSignForFee), + sponsor::SponseeAcc(alice), + Ter(temINVALID_FLAG)); + + env(sponsor::set( + sponsor, + tfSponsorshipSetRequireSignForReserve | + tfSponsorshipClearRequireSignForReserve), + sponsor::SponseeAcc(alice), + Ter(temINVALID_FLAG)); + + for (auto flag : + {tfSponsorshipSetRequireSignForFee, + tfSponsorshipClearRequireSignForFee, + tfSponsorshipSetRequireSignForReserve, + tfSponsorshipClearRequireSignForReserve}) + { + env(sponsor::set(sponsor, tfDeleteObject | flag), + sponsor::SponseeAcc(alice), + Ter(temINVALID_FLAG)); + } + } + + // invalid SponsorAccount / Sponsee + // Account = Sponsor + env(sponsor::set(alice, tfDeleteObject), + sponsor::CounterpartySponsor(alice), + Ter(temMALFORMED)); + // Account = Sponsee + env(sponsor::set(alice, tfDeleteObject), sponsor::SponseeAcc(alice), Ter(temMALFORMED)); + // Both Sponsor and Sponsee are specified + env(sponsor::set(alice, 0), + sponsor::CounterpartySponsor(sponsor), + sponsor::SponseeAcc(alice), + Ter(temMALFORMED)); + + // Invalid feeAmount + for (auto const& amt : {XRP(-1), usd(1)}) + { + env(sponsor::set_fee(sponsor, 0, amt), sponsor::SponseeAcc(alice), Ter(temBAD_AMOUNT)); + } + // Invalid MaxFee + for (auto const& amt : {XRP(-1), usd(1)}) + { + env(sponsor::set_fee(sponsor, 0, XRP(1), amt), + sponsor::SponseeAcc(alice), + Ter(temBAD_AMOUNT)); + } + + // Invalid Delete operation + env(sponsor::set_reserve(sponsor, tfDeleteObject, 1), + sponsor::SponseeAcc(alice), + Ter(temMALFORMED)); + env(sponsor::set_fee(sponsor, tfDeleteObject, XRP(1)), + sponsor::SponseeAcc(alice), + Ter(temMALFORMED)); + env(sponsor::set_max_fee(sponsor, tfDeleteObject, XRP(1)), + sponsor::SponseeAcc(alice), + Ter(temMALFORMED)); + + // Invalid SponsorAccount with non-Delete operation + env(sponsor::set_reserve(sponsor, 0, 100), + sponsor::CounterpartySponsor(alice), + Ter(temMALFORMED)); + env(sponsor::set_fee(sponsor, 0, XRP(1), XRP(1)), + sponsor::CounterpartySponsor(alice), + Ter(temMALFORMED)); + + // + // preclaim + // + + // Invalid Sponsee + env(sponsor::set(sponsor, 0), sponsor::SponseeAcc(noFunded), Ter(tecNO_DST)); + env.close(); + + // Invalid Sponsor + env(sponsor::set(sponsor, tfDeleteObject), + sponsor::CounterpartySponsor(noFunded), + Ter(tecNO_DST)); + env.close(); + + // Invalid Delete operation (sponsorship not found) + env(sponsor::set(sponsor, tfDeleteObject), sponsor::SponseeAcc(alice), Ter(tecNO_ENTRY)); + env.close(); + + // insufficient balance to sponsor Fee + adjustAccountXRPBalance(env, sponsor, env.current()->fees().reserve); + env(sponsor::set_fee(sponsor, 0, XRP(4)), sponsor::SponseeAcc(alice), Ter(tecUNFUNDED)); + env.close(); + + // insufficient reserve to create sponsorship + adjustAccountXRPBalance(env, sponsor, XRP(100) + XRP(1) + reserve(env, 1) - drops(1)); + env(sponsor::set(sponsor, 0, 100, XRP(100)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tecUNFUNDED)); + env.close(); + + // FeeAmount + Fee > Balance + // Balance = 1000XRP, FeeAmount = 1001XRP + adjustAccountXRPBalance(env, sponsor, XRP(1000)); + env(sponsor::set_fee(sponsor, 0, XRP(1001)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tecUNFUNDED)); + env.close(); + // Balance = 1000XRP, FeeAmount = 999XRP, Fee=2XRP + adjustAccountXRPBalance(env, sponsor, XRP(1000)); + env(sponsor::set_fee(sponsor, 0, XRP(999)), + sponsor::SponseeAcc(alice), + Fee(XRP(2)), + Ter(tecUNFUNDED)); + env.close(); + + // create sponsor to use above tests + // need feeAmount(1000) + Fee(1) + reserve(~250) = ~1251 + adjustAccountXRPBalance(env, sponsor, XRP(1000) + XRP(1) + reserve(env, 1)); + env(sponsor::set(sponsor, 0, 100, XRP(1000)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tesSUCCESS)); + env.close(); + + // delta-based balance check + // After create: sponsor balance ~ 0, feeAmount = XRP(1000) + + // Decreasing feeAmount should succeed (refund, negative delta) + adjustAccountXRPBalance(env, sponsor, XRP(500)); + env(sponsor::set_fee(sponsor, 0, XRP(800)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tesSUCCESS)); + env.close(); + // balance was 500, delta = 800-1000 = -200 (refund), balance = 500+200-1 = 699 + + // Increasing feeAmount within delta budget should succeed + adjustAccountXRPBalance(env, sponsor, XRP(500)); + env(sponsor::set_fee(sponsor, 0, XRP(850)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tesSUCCESS)); + env.close(); + // balance was 500, delta = 850-800 = 50, balance = 500-50-1 = 449 + + // Increasing feeAmount where delta exceeds balance should fail + adjustAccountXRPBalance(env, sponsor, XRP(310)); + env(sponsor::set_fee(sponsor, 0, XRP(1200)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tecUNFUNDED)); + env.close(); + + // Increasing feeAmount to reach insufficient reserve + auto const currentFeeAmount = env.le(keylet::sponsorship(sponsor.id(), alice.id())) + ->getFieldAmount(sfFeeAmount) + .xrp(); + adjustAccountXRPBalance(env, sponsor, XRP(310)); + env(sponsor::set_fee(sponsor, 0, currentFeeAmount + XRP(309)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tecUNFUNDED)); + env.close(); + } + + void + testPseudoAccountSponsorship() + { + testcase("Pseudo account sponsorship"); + using namespace test::jtx; + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const gw("gw"); + Account const sp("sponsor"); + + Asset const asset = gw["IOU"].asset(); + + env.fund(XRP(1000000), alice, bob, gw, sp); + env.close(); + + // Create a vault to get a pseudo account + Vault const vault{env}; + auto [tx, keylet] = vault.create({.owner = alice, .asset = asset}); + env(tx); + env.close(); + + auto const vaultSle = env.le(keylet); + BEAST_EXPECT(vaultSle); + Account const pseudoAcc("vault", vaultSle->getAccountID(sfAccount)); + env.memoize(pseudoAcc); + + // Sponsee is a pseudo account -> tecNO_PERMISSION + env(sponsor::set(sp, 0, 100, XRP(100)), + sponsor::SponseeAcc(pseudoAcc), + Ter(tecNO_PERMISSION)); + env.close(); + + // Sponsor is a pseudo account -> tecNO_PERMISSION + // (submitted by bob with counterpartySponsor pointing to pseudo account) + env(sponsor::set(bob, tfDeleteObject), + sponsor::CounterpartySponsor(pseudoAcc), + Ter(tecNO_PERMISSION)); + env.close(); + } + + void + testSingleSigning() + { + testcase("Single signing"); + using namespace test::jtx; + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const sponsor("sponsor"); + Account const invalid("invalid"); + + env.fund(XRP(10000), alice, sponsor); + env.close(); + + // Signature doesn't exist + auto tx = noop(alice); + tx[sfSponsor.jsonName] = sponsor.human(); + tx[sfSponsorSignature.jsonName][sfSigningPubKey.jsonName] = strHex(sponsor.pk().slice()); + + env(tx, Fee(XRP(1)), sponsor::As(sponsor, spfSponsorReserve), Ter(telENV_RPC_FAILED)); + + // Invalid signature + tx[sfSponsorSignature.jsonName][sfTxnSignature.jsonName] = "DEADBEEF"; + env(tx, Fee(XRP(1)), sponsor::As(sponsor, spfSponsorReserve), Ter(telENV_RPC_FAILED)); + + // Signer account doesn't exist + env(noop(alice), + Fee(XRP(1)), + sponsor::As(invalid, spfSponsorReserve), + Sig(sfSponsorSignature, invalid), + Ter(terNO_ACCOUNT)); + + // Success + env(noop(alice), + Fee(XRP(1)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tesSUCCESS)); + } + + void + testMultiSigning() + { + testcase("Multi signing"); + using namespace test::jtx; + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + Account const signer1("signer1"); + Account const signer2("signer2"); + + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + env(signers(sponsor, 1, {{signer1, 1}, {signer2, 1}})); + env.close(); + + // Invalid signature + auto tx = noop(alice); + auto& signers1 = tx[sfSponsorSignature.jsonName][sfSigners.jsonName][0U][sfSigner.jsonName]; + signers1[sfAccount.jsonName] = signer1.human(); + signers1[sfSigningPubKey.jsonName] = strHex(signer1.pk().slice()); + signers1[sfTxnSignature.jsonName] = "DEADBEEF"; + env(tx, Fee(XRP(1)), sponsor::As(sponsor, spfSponsorReserve), Ter(telENV_RPC_FAILED)); + + // bob is not a multi-signing account. + env(noop(alice), + Fee(XRP(1)), + sponsor::As(bob, spfSponsorReserve), + Msig(sfSponsorSignature, {signer1}), + Ter(tefNOT_MULTI_SIGNING)); + + env(noop(alice), + Fee(XRP(1)), + sponsor::As(sponsor, spfSponsorReserve), + Msig(sfSponsorSignature, {signer1}), + Ter(tesSUCCESS)); + env.close(); + + env(signers(sponsor, 2, {{signer1, 1}, {signer2, 1}})); + env.close(); + + // test calculateBaseFee for multisigned sponsor + auto const baseFee = env.current()->fees().base; + env(noop(alice), + Fee(baseFee + 2 * baseFee - 1), + sponsor::As(sponsor, spfSponsorReserve), + Msig(sfSponsorSignature, {signer1, signer2}), + Ter(telINSUF_FEE_P)); + + env(noop(alice), + Fee(baseFee + 2 * baseFee), + sponsor::As(sponsor, spfSponsorReserve), + Msig(sfSponsorSignature, {signer1, signer2}), + Ter(tesSUCCESS)); + } + + void + testInvalidSponsorField() + { + testcase("Invalid Sponsor Field"); + using namespace test::jtx; + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const sponsor("sponsor"); + Account const noFunded("noFunded"); + env.fund(XRP(10000), alice, sponsor); + env.close(); + + // Invalid Sponsor Account (Account = Sponsor.Account) + env(noop(alice), sponsor::As(alice, spfSponsorFee), Ter(temMALFORMED)); + + // Invalid Sponsor Account + // (SponsorSignature is specified but Sponsor.Account is not specified) + env(noop(alice), Sig(sfSponsorSignature, sponsor), Ter(temMALFORMED)); + + // Invalid Sponsor Account (Sponsor.Account doesn't exist) + env(noop(alice), sponsor::As(noFunded, spfSponsorReserve), Ter(terNO_ACCOUNT)); + env(noop(alice), + sponsor::As(noFunded, spfSponsorReserve), + Sig(sfSponsorSignature, noFunded), + Ter(terNO_ACCOUNT)); + + // Invalid Flags + env(noop(alice), + sponsor::As(sponsor, (spfSponsorFee | spfSponsorReserve) + 1), + Ter(temINVALID_FLAG)); + + // SponsorFlags=0 with valid sponsor (no sponsorship purpose) + env(noop(alice), sponsor::As(sponsor, 0), Ter(temINVALID_FLAG)); + + // no SponsorFlag with valid sponsor + auto tx = noop(alice); + tx[sfSponsor.jsonName] = sponsor.human(); + env(tx, Ter(temINVALID_FLAG)); + + // Invalid Flags without sponsor + tx = noop(alice); + tx[sfSponsorFlags.jsonName] = spfSponsorFee | spfSponsorReserve; + env(tx, Ter(temINVALID_FLAG)); + } + + void + testSimpleSponsorshipSet() + { + testcase("Simple SponsorshipSet"); + using namespace test::jtx; + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, sponsor); + env.close(); + + { + // create sponsorship + env(sponsor::set( + sponsor, + tfSponsorshipSetRequireSignForFee | tfSponsorshipSetRequireSignForReserve, + 100, + XRP(100), + XRP(1)), + Fee(XRP(1)), + sponsor::SponseeAcc(alice), + Ter(tesSUCCESS)); + env.close(); + + auto sle = env.le(keylet::sponsorship(sponsor, alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 100); + BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(100)); + BEAST_EXPECT(sle->at(sfMaxFee) == XRP(1)); + BEAST_EXPECT(sle->isFlag(lsfSponsorshipRequireSignForFee)); + BEAST_EXPECT(sle->isFlag(lsfSponsorshipRequireSignForReserve)); + BEAST_EXPECT(env.balance(sponsor) == XRP(10000) - sle->at(sfFeeAmount) - XRP(1)); + + // update sponsorship (decrement) + env(sponsor::set(sponsor, 0, 50, XRP(50), XRP(0.5)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tesSUCCESS)); + env.close(); + + sle = env.le(keylet::sponsorship(sponsor, alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 50); + BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(50)); + BEAST_EXPECT(sle->at(sfMaxFee) == XRP(0.5)); + BEAST_EXPECT(env.balance(sponsor) == XRP(10000) - sle->at(sfFeeAmount) - XRP(2)); + + // update sponsorship (increment) + env(sponsor::set(sponsor, 0, 200, XRP(200), XRP(2)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tesSUCCESS)); + env.close(); + + sle = env.le(keylet::sponsorship(sponsor, alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 200); + BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(200)); + BEAST_EXPECT(sle->at(sfMaxFee) == XRP(2)); + BEAST_EXPECT(env.balance(sponsor) == XRP(10000) - sle->at(sfFeeAmount) - XRP(3)); + + // delete from sponsor + env(sponsor::del(sponsor), sponsor::SponseeAcc(alice), Fee(XRP(1)), Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(sponsor) == XRP(10000) - XRP(4)); + + env(sponsor::set( + sponsor, + tfSponsorshipSetRequireSignForFee | tfSponsorshipSetRequireSignForReserve, + 100, + XRP(100), + XRP(1)), + sponsor::SponseeAcc(alice), + Ter(tesSUCCESS)); + env.close(); + + // delete from sponsee + env(sponsor::del(alice), sponsor::CounterpartySponsor(sponsor), Ter(tesSUCCESS)); + env.close(); + BEAST_EXPECT(!env.le(keylet::sponsorship(sponsor, alice))); + + // Cannot create sponsorship with no fee or reserve budget. MaxFee + // and flags do not make a sponsorship object useful by themselves. + env(sponsor::set(sponsor, 0), sponsor::SponseeAcc(alice), Ter(tecNO_PERMISSION)); + env.close(); + BEAST_EXPECT(!env.le(keylet::sponsorship(sponsor, alice))); + + env(sponsor::set_max_fee(sponsor, 0, XRP(1)), + sponsor::SponseeAcc(alice), + Ter(tecNO_PERMISSION)); + env.close(); + BEAST_EXPECT(!env.le(keylet::sponsorship(sponsor, alice))); + + env(sponsor::set(sponsor, 0, 0, XRP(0), XRP(0)), + sponsor::SponseeAcc(alice), + Ter(tecNO_PERMISSION)); + env.close(); + BEAST_EXPECT(!env.le(keylet::sponsorship(sponsor, alice))); + + // update sponsorship with non-zero value + env(sponsor::set(sponsor, 0, 100, XRP(100), XRP(1)), + sponsor::SponseeAcc(alice), + Fee(XRP(1))); + env.close(); + + sle = env.le(keylet::sponsorship(sponsor, alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 100); + BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(100)); + BEAST_EXPECT(sle->at(sfMaxFee) == XRP(1)); + + // update sponsorship flags + auto testFlagUpdate = [&](auto setFlag, auto clearFlag, auto ledgerFlag) { + env(sponsor::set(sponsor, setFlag), sponsor::SponseeAcc(alice), Fee(XRP(1))); + env.close(); + + sle = env.le(keylet::sponsorship(sponsor, alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->isFlag(ledgerFlag)); + + env(sponsor::set(sponsor, clearFlag), sponsor::SponseeAcc(alice), Fee(XRP(1))); + env.close(); + + sle = env.le(keylet::sponsorship(sponsor, alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(!sle->isFlag(ledgerFlag)); + }; + + testFlagUpdate( + tfSponsorshipSetRequireSignForFee, + tfSponsorshipClearRequireSignForFee, + lsfSponsorshipRequireSignForFee); + testFlagUpdate( + tfSponsorshipSetRequireSignForReserve, + tfSponsorshipClearRequireSignForReserve, + lsfSponsorshipRequireSignForReserve); + + // Cannot update sponsorship so both fee and reserve budgets are absent. + env(sponsor::set(sponsor, 0, 0, XRP(0), XRP(0)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tecNO_PERMISSION)); + env.close(); + + sle = env.le(keylet::sponsorship(sponsor, alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 100); + BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(100)); + BEAST_EXPECT(sle->at(sfMaxFee) == XRP(1)); + } + + { + // Removing one budget field while the other remains keeps the + // Sponsorship valid. Starting state (from above): + // RemainingOwnerCount = 100, FeeAmount = XRP(100). + + // Remove only FeeAmount (set to 0); RemainingOwnerCount remains. + env(sponsor::set_fee(sponsor, 0, XRP(0)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tesSUCCESS)); + env.close(); + + auto sle = env.le(keylet::sponsorship(sponsor, alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(!sle->isFieldPresent(sfFeeAmount)); + BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 100); + + // Re-add FeeAmount, then remove only RemainingOwnerCount; + // FeeAmount remains. + env(sponsor::set_fee(sponsor, 0, XRP(100)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tesSUCCESS)); + env.close(); + + env(sponsor::set_reserve(sponsor, 0, 0), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tesSUCCESS)); + env.close(); + + sle = env.le(keylet::sponsorship(sponsor, alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(!sle->isFieldPresent(sfRemainingOwnerCount)); + BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(100)); + } + + { + // Update Sponsorship (FeeAmount) + // set empty FeeAmount + env(sponsor::set_reserve(sponsor, 0, 100), sponsor::SponseeAcc(alice), Ter(tesSUCCESS)); + env.close(); + + // add FeeAmount + env(sponsor::set_fee(sponsor, 0, XRP(100)), + sponsor::SponseeAcc(alice), + Ter(tesSUCCESS)); + env.close(); + + env(sponsor::del(alice), sponsor::CounterpartySponsor(sponsor), Ter(tesSUCCESS)); + env.close(); + } + { + // Update Sponsorship (ReserveCount) + // set empty ReserveCount + env(sponsor::set_fee(sponsor, 0, XRP(100)), + sponsor::SponseeAcc(alice), + Ter(tesSUCCESS)); + env.close(); + + // add ReserveCount + env(sponsor::set_reserve(sponsor, 0, 100), sponsor::SponseeAcc(alice), Ter(tesSUCCESS)); + env.close(); + + env(sponsor::del(alice), sponsor::CounterpartySponsor(sponsor), Ter(tesSUCCESS)); + env.close(); + } + { + // delete Sponsorship (only with FeeAmount) + env(sponsor::set_fee(sponsor, 0, XRP(100)), + sponsor::SponseeAcc(alice), + Ter(tesSUCCESS)); + env.close(); + + env(sponsor::del(alice), sponsor::CounterpartySponsor(sponsor), Ter(tesSUCCESS)); + env.close(); + } + { + // delete Sponsorship (only with ReserveCount) + env(sponsor::set_reserve(sponsor, 0, 100), sponsor::SponseeAcc(alice), Ter(tesSUCCESS)); + env.close(); + + env(sponsor::del(alice), sponsor::CounterpartySponsor(sponsor), Ter(tesSUCCESS)); + env.close(); + } + } + + void + testPreFundAndCosign() + { + testcase("PreFund and Cosign"); + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const charlie("charlie"); + Account const sponsor("sponsor"); + + { + // Both pre-funded and co-signed; the pre-funded value is used. + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + env(sponsor::set(sponsor, 0, 100, XRP(100), XRP(1)), + sponsor::SponseeAcc(alice), + Ter(tesSUCCESS)); + env.close(); + + auto const checkSeq = env.seq(alice); + env(check::create(alice, bob, XRP(1)), + sponsor::As(sponsor, spfSponsorReserve | spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Fee(XRP(1)), + Ter(tesSUCCESS)); + env.close(); + + auto sle = env.le(keylet::sponsorship(sponsor, alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 99); + BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(99)); + + env(check::cancel(alice, keylet::check(alice, checkSeq).key), Ter(tesSUCCESS)); + env.close(); + + sle = env.le(keylet::sponsorship(sponsor, alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 99); // not restored + BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(99)); + } + + { + // if pre-funded value is not enough, error + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, bob, charlie, sponsor); + env.close(); + + env(sponsor::set(sponsor, 0, 1, XRP(10), XRP(100)), + sponsor::SponseeAcc(alice), + Ter(tesSUCCESS)); + env.close(); + + // Fee insufficient + env(check::create(alice, bob, XRP(1)), + sponsor::As(sponsor, spfSponsorReserve | spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Fee(XRP(11)), + Ter(terINSUF_FEE_B)); + env.close(); + + env(sponsor::set_reserve(sponsor, 0, 0), sponsor::SponseeAcc(alice), Ter(tesSUCCESS)); + env.close(); + + // reserve insufficient + env(check::create(alice, bob, XRP(1)), + sponsor::As(sponsor, spfSponsorReserve | spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Fee(XRP(1)), + Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + } + } + + void + testSponsoredFreeTierReserve() + { + testcase("Sponsored Free-Tier Reserve"); + using namespace test::jtx; + Account const alice("alice"); + Account const issuer("issuer"); + Account const sponsor("sponsor"); + + // Trust lines and MPTokens normally skip the reserve check when the + // holder's ownerCount < 2 (the "free-tier" / first-two-items shortcut). When the + // tx is sponsored, that shortcut must not apply — the sponsor must + // still cover the reserve. + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, issuer); + // Sponsor is funded just below the reserve required to cover a single + // sponsored item. + env.fund(reserve(env, 1) - drops(1), sponsor); + env.close(); + BEAST_EXPECT(ownerCount(env, alice) == 0); + + MPTTester mptt(env, issuer, {.fund = false}); + mptt.create(); + + // Free-tier trust line cosigned by an undercapitalized sponsor must + // fail — the holder's free-first-two-items shortcut does not let the + // sponsor skip the reserve check. + env(trust(alice, issuer["USD"](100)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tecNO_LINE_INSUF_RESERVE)); + env.close(); + + // Free-tier MPTokenAuthorize must also fail for the same reason. + env(MPTTester::authorizeJV({.account = alice, .id = mptt.issuanceID()}), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + } + + void + testTransferSponsor() + { + testcase("Transfer Sponsor"); + using namespace test::jtx; + + // Verify preflight checks + { + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + // Test invalid flags for SponsorshipTransfer + { + // Invalid flag is provided + env(sponsor::transfer( + alice, + (tfSponsorshipCreate | tfSponsorshipReassign | tfSponsorshipEnd) + 1), + Ter(temINVALID_FLAG)); + + // No SponsorshipTransfer flag is provided + env(sponsor::transfer(alice, 0), Ter(temINVALID_FLAG)); + + // Only one of the three valid flags can be set. + // Setting more than one flag is invalid + for (auto flag : { + tfSponsorshipCreate | tfSponsorshipReassign, + tfSponsorshipCreate | tfSponsorshipEnd, + tfSponsorshipReassign | tfSponsorshipEnd, + tfSponsorshipCreate | tfSponsorshipReassign | tfSponsorshipEnd, + }) + env(sponsor::transfer(alice, flag), Ter(temINVALID_FLAG)); + } + + // Malformed tests for tfSponsorshipCreate + { + // No sponsor field present + env(sponsor::transfer(alice, tfSponsorshipCreate), Ter(temMALFORMED)); + + // Sponsor field present without spfSponsorReserve + env(sponsor::transfer(alice, tfSponsorshipCreate), + sponsor::As(sponsor, spfSponsorFee), + Ter(temINVALID_FLAG)); + + // Sponsee field present + env(sponsor::transfer(alice, tfSponsorshipCreate), + sponsor::SponseeAcc(bob), + sponsor::As(sponsor, spfSponsorReserve), + Ter(temMALFORMED)); + } + + // Malformed tests for tfSponsorshipReassign + { + // No sponsor field present + env(sponsor::transfer(alice, tfSponsorshipReassign), Ter(temMALFORMED)); + + // Sponsor field present without spfSponsorReserve + env(sponsor::transfer(alice, tfSponsorshipReassign), + sponsor::As(sponsor, spfSponsorFee), + Ter(temINVALID_FLAG)); + + // Sponsee field present + env(sponsor::transfer(alice, tfSponsorshipReassign), + sponsor::SponseeAcc(bob), + sponsor::As(sponsor, spfSponsorReserve), + Ter(temMALFORMED)); + } + + // Malformed tests for tfSponsorshipEnd + { + // Sponsor field present + env(sponsor::transfer(alice, tfSponsorshipEnd), + sponsor::As(sponsor, spfSponsorReserve), + Ter(temMALFORMED)); + + // SponsorFlags field present + auto tx = sponsor::transfer(alice, tfSponsorshipEnd); + tx[sfSponsorFlags.jsonName] = spfSponsorFee; + env(tx, Ter(temINVALID_FLAG)); + + // Account = Sponsee + env(sponsor::transfer(alice, tfSponsorshipEnd), + sponsor::SponseeAcc(alice), + Ter(temMALFORMED)); + } + } + + { + // Invalid SponsorshipEnd permission (sponsor object/sponsor account) + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const charlie("charlie"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + { + // sponsor object + env.fund(XRP(1000), charlie); + env.close(); + env(deposit::auth(alice, charlie), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + auto const keylet = keylet::depositPreauth(alice, charlie); + env(sponsor::transfer(bob, tfSponsorshipEnd, keylet.key), + sponsor::SponseeAcc(alice), + Ter(tecNO_PERMISSION)); + } + { + // sponsor object + env(sponsor::transfer(alice, tfSponsorshipCreate), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + env(sponsor::transfer(bob, tfSponsorshipEnd), + sponsor::SponseeAcc(alice), + Ter(tecNO_PERMISSION)); + } + { + // The provided sfSponsee account does not exist + // when ending sponsorship. + Account const ghost("ghost"); // never funded, absent from ledger + env(sponsor::transfer(sponsor, tfSponsorshipEnd), + sponsor::SponseeAcc(ghost), + Ter(terNO_ACCOUNT)); + } + } + + { + // sponsor account + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor1("sponsor1"); + Account const sponsor2("sponsor2"); + env.fund(XRP(10000), alice, bob, sponsor1, sponsor2); + + // sfSponsor provided but sfSponsorSignature not provided + env(sponsor::transfer(alice, tfSponsorshipCreate), + sponsor::As(sponsor1, spfSponsorReserve), + Ter(temMALFORMED)); + env.close(); + + adjustAccountXRPBalance(env, sponsor1, accountReserve(env, 2) - drops(1)); + + env(sponsor::transfer(alice, tfSponsorshipCreate), + sponsor::As(sponsor1, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor1), + Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + + adjustAccountXRPBalance(env, sponsor1, accountReserve(env, 2)); + + env(sponsor::transfer(alice, tfSponsorshipCreate), + sponsor::As(sponsor1, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor1)); + env.close(); + + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, alice) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor1) == 1); + auto const sle1 = env.le(keylet::account(alice)); + BEAST_EXPECT(sle1->isFieldPresent(sfSponsor)); + BEAST_EXPECT(sle1->getAccountID(sfSponsor) == sponsor1.id()); + + // transfer sponsor + adjustAccountXRPBalance(env, sponsor2, accountReserve(env, 2) - drops(1)); + + env(sponsor::transfer(alice, tfSponsorshipReassign), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2), + Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + + adjustAccountXRPBalance(env, sponsor2, accountReserve(env, 2)); + + env(sponsor::transfer(alice, tfSponsorshipReassign), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor2) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, alice) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor2) == 1); + BEAST_EXPECT( + !env.le(keylet::account(sponsor1))->isFieldPresent(sfSponsoringAccountCount)); + auto const sle2 = env.le(keylet::account(alice)); + BEAST_EXPECT(sle2->isFieldPresent(sfSponsor)); + BEAST_EXPECT(sle2->getAccountID(sfSponsor) == sponsor2.id()); + + // Reassign to the current sponsor is a no-op and is rejected + env(sponsor::transfer(alice, tfSponsorshipReassign), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2), + Ter(tecNO_PERMISSION)); + env.close(); + + BEAST_EXPECT(sponsoringAccountCount(env, sponsor2) == 1); + + // sponsor 2 accounts + adjustAccountXRPBalance(env, sponsor2, accountReserve(env, 3)); + env(sponsor::transfer(bob, tfSponsorshipCreate), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + + // dissolve sponsors + adjustAccountXRPBalance(env, alice, accountReserve(env, 1) - drops(1)); + + env(sponsor::transfer(alice, tfSponsorshipEnd), Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + + adjustAccountXRPBalance(env, alice, accountReserve(env, 1)); + + env(sponsor::transfer(alice, tfSponsorshipEnd)); + env.close(); + + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor2) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, alice) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor2) == 1); + auto const sle3 = env.le(keylet::account(alice)); + BEAST_EXPECT(!sle3->isFieldPresent(sfSponsor)); + + env(sponsor::transfer(bob, tfSponsorshipEnd)); + env.close(); + + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor2) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, bob) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, bob) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor2) == 0); + BEAST_EXPECT( + !env.le(keylet::account(sponsor2))->isFieldPresent(sfSponsoringAccountCount)); + auto const sle4 = env.le(keylet::account(bob)); + BEAST_EXPECT(!sle4->isFieldPresent(sfSponsor)); + + // not sponsored + env(sponsor::transfer(bob, tfSponsorshipEnd), Ter(tecNO_PERMISSION)); + env.close(); + } + { + // dissolve account sponsorship from sponsor + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + env(sponsor::transfer(alice, tfSponsorshipCreate), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + BEAST_EXPECT(env.le(alice)->getAccountID(sfSponsor) == sponsor.id()); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor) == 1); + + env(sponsor::transfer(sponsor, tfSponsorshipEnd), sponsor::SponseeAcc(alice)); + env.close(); + + BEAST_EXPECT(!env.le(alice)->isFieldPresent(sfSponsor)); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor) == 0); + } + + { + // sponsor object (co-signing) + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor1("sponsor1"); + Account const sponsor2("sponsor2"); + env.fund(XRP(10000), alice, bob, sponsor1, sponsor2); + env.close(); + + adjustAccountXRPBalance(env, sponsor1, reserve(env, 1) - drops(1)); + adjustAccountXRPBalance(env, sponsor2, reserve(env, 1) - drops(1)); + + auto const seq = env.seq(alice); + env(check::create(alice, bob, XRP(1))); + env.close(); + + auto const checkId = keylet::check(alice, seq).key; + BEAST_EXPECT(env.le(keylet::unchecked(checkId)) != nullptr); + + env(sponsor::transfer(alice, tfSponsorshipCreate, checkId), + sponsor::As(sponsor1, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor1), + Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + + env(pay(alice, sponsor1, drops(1))); + env.close(); + + // Invalid ObjectID (not found) + env(sponsor::transfer(alice, tfSponsorshipCreate, keylet::check(alice, 0).key), + sponsor::As(sponsor1, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor1), + Ter(tecNO_ENTRY)); + env.close(); + + // Invalid Owner + env(sponsor::transfer(bob, tfSponsorshipCreate, checkId), + sponsor::As(sponsor1, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor1), + Ter(tecNO_PERMISSION)); + env.close(); + + // Reassign an object that is not sponsored yet + env(sponsor::transfer(alice, tfSponsorshipReassign, checkId), + sponsor::As(sponsor1, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor1), + Ter(tecNO_PERMISSION)); + env.close(); + + // Valid Owner + env(sponsor::transfer(alice, tfSponsorshipCreate, checkId), + sponsor::As(sponsor1, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor1)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 1); + BEAST_EXPECT(sponsoringAccountCount(env, alice) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor1) == 0); + auto const sle1 = env.le(keylet::unchecked(checkId)); + BEAST_EXPECT(sle1->isFieldPresent(sfSponsor)); + BEAST_EXPECT(sle1->getAccountID(sfSponsor) == sponsor1.id()); + + // Create on an object that is already sponsored + env(sponsor::transfer(alice, tfSponsorshipCreate, checkId), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2), + Ter(tecNO_PERMISSION)); + env.close(); + + // Reassign to the current sponsor is a no-op and is rejected + env(sponsor::transfer(alice, tfSponsorshipReassign, checkId), + sponsor::As(sponsor1, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor1), + Ter(tecNO_PERMISSION)); + env.close(); + + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 1); + + // transfer sponsor + env(sponsor::transfer(alice, tfSponsorshipReassign, checkId), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2), + Ter(tecINSUFFICIENT_RESERVE)); + + env(pay(alice, sponsor2, drops(1))); + env.close(); + + env(sponsor::transfer(alice, tfSponsorshipReassign, checkId), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor2) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + BEAST_EXPECT(sponsoringAccountCount(env, alice) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor2) == 0); + auto const sle2 = env.le(keylet::unchecked(checkId)); + BEAST_EXPECT(sle2->isFieldPresent(sfSponsor)); + BEAST_EXPECT(sle2->getAccountID(sfSponsor) == sponsor2.id()); + + // dissolve sponsor: ending an object sponsorship succeeds even + // when the sponsee lacks sufficient reserve to reclaim the object. + adjustAccountXRPBalance(env, alice, reserve(env, 1) - drops(1)); + + env(sponsor::transfer(alice, tfSponsorshipEnd, checkId)); + env.close(); + + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor2) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, alice) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor2) == 0); + BEAST_EXPECT( + !env.le(keylet::account(sponsor2))->isFieldPresent(sfSponsoringOwnerCount)); + auto const sle3 = env.le(keylet::unchecked(checkId)); + BEAST_EXPECT(!sle3->isFieldPresent(sfSponsor)); + + // Ending sponsorship on an object that is not sponsored (a ticket, + // which cannot be sponsored) is rejected. + adjustAccountXRPBalance(env, alice, reserve(env, 2)); + auto const ticketSeq = env.seq(alice); + env(ticket::create(alice, 1)); + env.close(); + auto ticketId = keylet::ticket(alice, ticketSeq + 1).key; + BEAST_EXPECT(env.le(keylet::unchecked(ticketId))); + env(sponsor::transfer(alice, tfSponsorshipEnd, ticketId), Ter(tecNO_PERMISSION)); + env.close(); + env(noop(alice), ticket::Use(ticketSeq + 1)); + env.close(); + } + { + // sponsor object (pre-funded + no ltSponsorship entry) + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor1("sponsor1"); + Account const sponsor2("sponsor2"); + env.fund(XRP(10000), alice, bob, sponsor1, sponsor2); + env.close(); + + auto const seq = env.seq(alice); + env(check::create(alice, bob, XRP(1))); + env.close(); + + auto const checkId = keylet::check(alice, seq).key; + BEAST_EXPECT(env.le(keylet::unchecked(checkId)) != nullptr); + + env(sponsor::transfer(alice, tfSponsorshipCreate, checkId), + sponsor::As(sponsor1, spfSponsorReserve), + Ter(terNO_PERMISSION)); + env.close(); + + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); + env.close(); + + env(sponsor::transfer(alice, tfSponsorshipCreate, checkId), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + + env(sponsor::transfer(alice, tfSponsorshipReassign, checkId), + sponsor::As(sponsor1, spfSponsorReserve), + Ter(terNO_PERMISSION)); + env.close(); + } + { + // sponsor object (pre-funded) + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor1("sponsor1"); + Account const sponsor2("sponsor2"); + env.fund(XRP(10000), alice, bob, sponsor1, sponsor2); + env.close(); + + auto const seq = env.seq(alice); + env(check::create(alice, bob, XRP(1))); + env.close(); + + auto const checkId = keylet::check(alice, seq).key; + BEAST_EXPECT(env.le(keylet::unchecked(checkId)) != nullptr); + + // insufficient reserve count + env(sponsor::set_fee(sponsor1, 0, XRP(100)), sponsor::SponseeAcc(alice)); + env.close(); + env(sponsor::transfer(alice, tfSponsorshipCreate, checkId), + sponsor::As(sponsor1, spfSponsorReserve), + Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + + env(sponsor::set_reserve(sponsor1, 0, 100), sponsor::SponseeAcc(alice)); + env.close(); + + env(sponsor::transfer(alice, tfSponsorshipCreate, checkId), + sponsor::As(sponsor1, spfSponsorReserve)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 1); + BEAST_EXPECT(sponsoringAccountCount(env, alice) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor1) == 0); + auto checkSle = env.le(keylet::unchecked(checkId)); + BEAST_EXPECT(checkSle->isFieldPresent(sfSponsor)); + BEAST_EXPECT(checkSle->getAccountID(sfSponsor) == sponsor1.id()); + auto sponsor1Sle = env.le(keylet::sponsorship(sponsor1, alice)); + BEAST_EXPECT(sponsor1Sle->getFieldU32(sfRemainingOwnerCount) == 99); + + // Reassign to the current sponsor is rejected and must not draw + // down the pre-funded reserve budget + env(sponsor::transfer(alice, tfSponsorshipReassign, checkId), + sponsor::As(sponsor1, spfSponsorReserve), + Ter(tecNO_PERMISSION)); + env.close(); + + sponsor1Sle = env.le(keylet::sponsorship(sponsor1, alice)); + BEAST_EXPECT(sponsor1Sle->getFieldU32(sfRemainingOwnerCount) == 99); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 1); + + // transfer sponsor + env(sponsor::set_reserve(sponsor2, 0, 100), sponsor::SponseeAcc(alice)); + env.close(); + + env(sponsor::transfer(alice, tfSponsorshipReassign, checkId), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor2) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + BEAST_EXPECT(sponsoringAccountCount(env, alice) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor2) == 0); + checkSle = env.le(keylet::unchecked(checkId)); + BEAST_EXPECT(checkSle->isFieldPresent(sfSponsor)); + BEAST_EXPECT(checkSle->getAccountID(sfSponsor) == sponsor2.id()); + sponsor1Sle = env.le(keylet::sponsorship(sponsor1, alice)); + BEAST_EXPECT(sponsor1Sle->getFieldU32(sfRemainingOwnerCount) == 99); + auto sponsor2Sle = env.le(keylet::sponsorship(sponsor2, alice)); + BEAST_EXPECT(sponsor2Sle->getFieldU32(sfRemainingOwnerCount) == 99); + + // dissolve sponsor + adjustAccountXRPBalance(env, alice, reserve(env, 1)); + env(sponsor::transfer(alice, tfSponsorshipEnd, checkId)); + env.close(); + + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor2) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, alice) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor2) == 0); + BEAST_EXPECT( + !env.le(keylet::account(sponsor2))->isFieldPresent(sfSponsoringOwnerCount)); + checkSle = env.le(keylet::unchecked(checkId)); + BEAST_EXPECT(!checkSle->isFieldPresent(sfSponsor)); + sponsor2Sle = env.le(keylet::sponsorship(sponsor2, alice)); + BEAST_EXPECT(sponsor2Sle->getFieldU32(sfRemainingOwnerCount) == 99); + } + + { + // Dissolve object sponsorship from sponsor(no-ltSponsorship) + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + auto const seq = env.seq(alice); + env(check::create(alice, bob, XRP(1))); + env.close(); + + auto const checkId = keylet::check(alice, seq).key; + BEAST_EXPECT(env.le(keylet::unchecked(checkId)) != nullptr); + + env(sponsor::transfer(alice, tfSponsorshipCreate, checkId), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + BEAST_EXPECT( + env.le(keylet::unchecked(checkId))->getAccountID(sfSponsor) == sponsor.id()); + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + + // not the owner of the object + env(sponsor::transfer(sponsor, tfSponsorshipEnd, checkId), Ter(tecNO_PERMISSION)); + env.close(); + + env(sponsor::transfer(sponsor, tfSponsorshipEnd, checkId), sponsor::SponseeAcc(alice)); + env.close(); + + BEAST_EXPECT(!env.le(keylet::unchecked(checkId))->isFieldPresent(sfSponsor)); + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + } + + { + // Dissolve object sponsorship from sponsor (with ltSponsorship) + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + auto const seq = env.seq(alice); + env(check::create(alice, bob, XRP(1))); + env.close(); + + auto const checkId = keylet::check(alice, seq).key; + BEAST_EXPECT(env.le(keylet::unchecked(checkId)) != nullptr); + + env(sponsor::transfer(alice, tfSponsorshipCreate, checkId), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + env(sponsor::set_reserve(sponsor, 0, 100), sponsor::SponseeAcc(alice)); + env.close(); + + BEAST_EXPECT( + env.le(keylet::unchecked(checkId))->getAccountID(sfSponsor) == sponsor.id()); + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + BEAST_EXPECT( + env.le(keylet::sponsorship(sponsor, alice))->getFieldU32(sfRemainingOwnerCount) == + 100); + + // not the owner of the object + env(sponsor::transfer(sponsor, tfSponsorshipEnd, checkId), Ter(tecNO_PERMISSION)); + env.close(); + + env(sponsor::transfer(sponsor, tfSponsorshipEnd, checkId), sponsor::SponseeAcc(alice)); + env.close(); + + BEAST_EXPECT(!env.le(keylet::unchecked(checkId))->isFieldPresent(sfSponsor)); + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT( + env.le(keylet::sponsorship(sponsor, alice))->getFieldU32(sfRemainingOwnerCount) == + 100); + } + + { + // sponsor trustline + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + + auto const& highAcc = alice > bob ? alice : bob; + auto const& lowAcc = alice > bob ? bob : alice; + + for (bool const isIssuerHigh : {false, true}) + { + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + auto const& issuer = isIssuerHigh ? highAcc : lowAcc; + auto const& user = isIssuerHigh ? lowAcc : highAcc; + + auto const usd = issuer["usd"]; + auto const currency = usd.currency; + + env(trust(user, issuer["usd"](100))); + env.close(); + + auto const trustId = keylet::trustLine(user, issuer, currency); + BEAST_EXPECT(env.le(trustId)); + + // transfer sponsor + env(sponsor::transfer(user, tfSponsorshipCreate, trustId.key), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + BEAST_EXPECT(env.le(trustId)); + + BEAST_EXPECT( + env.le(trustId)->getAccountID(isIssuerHigh ? sfLowSponsor : sfHighSponsor) == + sponsor.id()); + BEAST_EXPECT( + !env.le(trustId)->isFieldPresent(isIssuerHigh ? sfHighSponsor : sfLowSponsor)); + + // dissolve sponsor + env(sponsor::transfer(user, tfSponsorshipEnd, trustId.key)); + env.close(); + + BEAST_EXPECT(env.le(trustId)); + BEAST_EXPECT( + !env.le(trustId)->isFieldPresent(isIssuerHigh ? sfLowSponsor : sfHighSponsor)); + BEAST_EXPECT( + !env.le(trustId)->isFieldPresent(isIssuerHigh ? sfHighSponsor : sfLowSponsor)); + } + } + + { + // invalid transfer + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + // create owner dir + env(ticket::create(alice, 1)); + env.close(); + + // AccountRoot + // Amendments + // LedgerHashes + // FeeSettings + // NegativeUNL + // DirNode + auto const keylets = { + keylet::account(alice), + // keylet::amendments(), + keylet::skip(), + keylet::feeSettings(), + // keylet::negativeUNL(), + keylet::ownerDir(alice), + }; + for (auto const& keylet : keylets) + { + env(sponsor::transfer(alice, tfSponsorshipCreate, keylet.key), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tecNO_PERMISSION)); + } + } + + { + // existing owner objects that are outside the v1 SponsorshipTransfer + // object allow-list + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, sponsor); + env.close(); + + auto const checkBlocked = [&](Account const& account, uint256 const& objectID) { + env(sponsor::transfer(account, tfSponsorshipCreate, objectID), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tecNO_PERMISSION)); + env.close(); + }; + + auto const ticketSeq = env.seq(alice); + env(ticket::create(alice, 1)); + env.close(); + auto const ticketID = keylet::ticket(alice, ticketSeq + 1).key; + BEAST_EXPECT(env.le(keylet::unchecked(ticketID))); + checkBlocked(alice, ticketID); + + env(did::setValid(alice)); + env.close(); + auto const didKeylet = keylet::did(alice.id()); + BEAST_EXPECT(env.le(didKeylet)); + checkBlocked(alice, didKeylet.key); + + env(token::mint(alice, 0u)); + env.close(); + auto const nftPageKeylet = keylet::nftokenPageMax(alice); + BEAST_EXPECT(env.le(nftPageKeylet)); + checkBlocked(alice, nftPageKeylet.key); + + Account const borrower("borrower"); + env.fund(XRP(1000000), borrower); + env.close(); + + PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; + Vault const vault{env}; + auto [vaultTx, vaultKeylet] = vault.create({.owner = alice, .asset = xrpAsset}); + env(vaultTx); + env.close(); + + env(vault.deposit( + {.depositor = alice, .id = vaultKeylet.key, .amount = xrpAsset(1000)})); + env.close(); + + auto const brokerKeylet = keylet::loanBroker(alice.id(), env.seq(alice)); + env(loanBroker::set(alice, vaultKeylet.key), + loanBroker::kDebtMaximum(xrpAsset(1000).value()), + loanBroker::kManagementFeeRate(TenthBips16{0}), + loanBroker::kCoverRateMinimum(TenthBips32{0}), + loanBroker::kCoverRateLiquidation(TenthBips32{0})); + env.close(); + + auto const loanKeylet = keylet::loan(brokerKeylet.key, 1); + env(loan::set(borrower, brokerKeylet.key, xrpAsset(100).value()), + Sig(sfCounterpartySignature, alice), + Fee(env.current()->fees().base * 2)); + env.close(); + BEAST_EXPECT(env.le(loanKeylet)); + checkBlocked(borrower, loanKeylet.key); + } + } + + void + testSponsorFee() + { + using namespace test::jtx; + + testcase("Sponsor Fee"); + + { + // co-signing + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, bob); + env.close(); + + { + // Fee should be checked before sponsor permission, otherwise a tec + // result from a later check could cause context reset to pay Fee. + auto aliceBalance = env.balance(alice); + auto bobBalance = env.balance(bob); + auto sponsorBalance = env.balance(sponsor); + + env(pay(alice, bob, XRP(100)), + Fee(XRP(2000)), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Ter(terNO_ACCOUNT)); + env.close(); + BEAST_EXPECT(env.balance(alice) == aliceBalance); + BEAST_EXPECT(env.balance(bob) == bobBalance); + BEAST_EXPECT(env.balance(sponsor) == sponsorBalance); + } + + env.fund(XRP(1000), sponsor); + env.close(); + + { + // Sponsor pays the Fee + auto aliceBalance = env.balance(alice); + auto bobBalance = env.balance(bob); + auto sponsorBalance = env.balance(sponsor); + + auto const sendAmt = XRP(100); + auto const feeAmt = XRP(10); + env(pay(alice, bob, sendAmt), + Fee(feeAmt), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor)); + env.close(); + BEAST_EXPECT(env.balance(alice) == aliceBalance - sendAmt); + BEAST_EXPECT(env.balance(bob) == bobBalance + sendAmt); + BEAST_EXPECT(env.balance(sponsor) == sponsorBalance - feeAmt); + } + + { + // insufficient balance to pay Fee + auto aliceBalance = env.balance(alice); + auto bobBalance = env.balance(bob); + auto sponsorBalance = env.balance(sponsor); + + env(pay(alice, bob, XRP(100)), + Fee(XRP(2000)), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Ter(terINSUF_FEE_B)); + env.close(); + BEAST_EXPECT(env.balance(alice) == aliceBalance); + BEAST_EXPECT(env.balance(bob) == bobBalance); + BEAST_EXPECT(env.balance(sponsor) == sponsorBalance); + } + + { + // Fee is paid by Sponsor + // on context reset (tec error) + auto aliceBalance = env.balance(alice); + auto bobBalance = env.balance(bob); + auto sponsorBalance = env.balance(sponsor); + auto const feeAmt = XRP(10); + + env(pay(alice, bob, XRP(20000)), + Fee(feeAmt), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Ter(tecUNFUNDED_PAYMENT)); + env.close(); + + BEAST_EXPECT(env.balance(alice) == aliceBalance); + BEAST_EXPECT(env.balance(bob) == bobBalance); + BEAST_EXPECT(env.balance(sponsor) == sponsorBalance - feeAmt); + } + + { + // A co-signed sponsor pays the fee from its own balance, but + // must never be charged into its own account reserve. With a + // balance of exactly reserve + fee the fee is still payable, + // charging the sponsor down to precisely its reserve. + auto const feeAmt = XRP(10); + adjustAccountXRPBalance(env, sponsor, reserve(env, 0) + feeAmt); + auto const sponsorBalance = env.balance(sponsor); + + env(noop(alice), + Fee(feeAmt), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(sponsor) == sponsorBalance - feeAmt); + BEAST_EXPECT(env.balance(sponsor) == reserve(env, 0)); + } + + { + // below reserve + adjustAccountXRPBalance(env, sponsor, env.current()->fees().reserve); + + env(noop(alice), + Fee(env.current()->fees().base), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Ter(terINSUF_FEE_B)); + env.close(); + + env(noop(alice), + Fee(XRP(10)), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Ter(terINSUF_FEE_B)); + env.close(); + } + } + + { + // pre funded + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + { + // Fee should be checked before sponsor permission, otherwise a tec + // result from a later check could cause context reset to pay Fee. + auto aliceBalance = env.balance(alice); + auto bobBalance = env.balance(bob); + auto sponsorBalance = env.balance(sponsor); + + env(pay(alice, bob, XRP(100)), + Fee(XRP(2000)), + sponsor::As(sponsor, spfSponsorFee), + Ter(terNO_PERMISSION)); + env.close(); + BEAST_EXPECT(env.balance(alice) == aliceBalance); + BEAST_EXPECT(env.balance(bob) == bobBalance); + BEAST_EXPECT(env.balance(sponsor) == sponsorBalance); + } + + env(sponsor::set_fee(sponsor, 0, XRP(100)), sponsor::SponseeAcc(alice)); + env.close(); + + { + // Sponsor pays the Fee + auto aliceBalance = env.balance(alice); + auto bobBalance = env.balance(bob); + auto sponsorBalance = env.balance(sponsor); + auto sponsorFee = sponsor::sponsorshipFeeBalance(env, sponsor, alice); + + auto const sendAmt = XRP(100); + auto const feeAmt = XRP(10); + env(pay(alice, bob, sendAmt), Fee(feeAmt), sponsor::As(sponsor, spfSponsorFee)); + env.close(); + + BEAST_EXPECT(env.balance(alice) == aliceBalance - sendAmt); + BEAST_EXPECT(env.balance(bob) == bobBalance + sendAmt); + BEAST_EXPECT(env.balance(sponsor) == sponsorBalance); + BEAST_EXPECT( + sponsor::sponsorshipFeeBalance(env, sponsor, alice) == sponsorFee - feeAmt); + } + + { + // insufficient balance to pay Fee + { + // > FeeAmount + auto aliceBalance = env.balance(alice); + auto bobBalance = env.balance(bob); + auto sponsorBalance = env.balance(sponsor); + auto sponsorFee = sponsor::sponsorshipFeeBalance(env, sponsor, alice); + + env(pay(alice, bob, XRP(100)), + Fee(XRP(90) + drops(1)), + sponsor::As(sponsor, spfSponsorFee), + Ter(terINSUF_FEE_B)); + env.close(); + + BEAST_EXPECT(env.balance(alice) == aliceBalance); + BEAST_EXPECT(env.balance(bob) == bobBalance); + BEAST_EXPECT(env.balance(sponsor) == sponsorBalance); + BEAST_EXPECT(sponsor::sponsorshipFeeBalance(env, sponsor, alice) == sponsorFee); + } + // use all FeeAmount + { + // = FeeAmount + auto aliceBalance = env.balance(alice); + auto bobBalance = env.balance(bob); + auto sponsorBalance = env.balance(sponsor); + + env(pay(alice, bob, XRP(100)), + Fee(XRP(90)), + sponsor::As(sponsor, spfSponsorFee), + Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice) == aliceBalance - XRP(100)); + BEAST_EXPECT(env.balance(bob) == bobBalance + XRP(100)); + BEAST_EXPECT(env.balance(sponsor) == sponsorBalance); + BEAST_EXPECT( + !env.le(keylet::sponsorship(sponsor, alice))->isFieldPresent(sfFeeAmount)); + } + + // reset FeeAmount and MaxFee + env(sponsor::del(sponsor), sponsor::SponseeAcc(alice)); + env.close(); + env(sponsor::set_fee(sponsor, 0, XRP(10), XRP(1)), sponsor::SponseeAcc(alice)); + env.close(); + + { + // > MaxFee + auto aliceBalance = env.balance(alice); + auto bobBalance = env.balance(bob); + auto sponsorBalance = env.balance(sponsor); + auto sponsorFee = sponsor::sponsorshipFeeBalance(env, sponsor, alice); + + env(pay(alice, bob, XRP(100)), + Fee(XRP(1) + drops(1)), + sponsor::As(sponsor, spfSponsorFee), + Ter(terINSUF_FEE_B)); + env.close(); + + BEAST_EXPECT(env.balance(alice) == aliceBalance); + BEAST_EXPECT(env.balance(bob) == bobBalance); + BEAST_EXPECT(env.balance(sponsor) == sponsorBalance); + BEAST_EXPECT(sponsor::sponsorshipFeeBalance(env, sponsor, alice) == sponsorFee); + } + } + + { + // Fee is paid by Sponsor + // on context reset (tec error) + auto aliceBalance = env.balance(alice); + auto bobBalance = env.balance(bob); + auto sponsorBalance = env.balance(sponsor); + auto sponsorFee = sponsor::sponsorshipFeeBalance(env, sponsor, alice); + auto const feeAmt = XRP(1); + + env(pay(alice, bob, XRP(20000)), + Fee(feeAmt), + sponsor::As(sponsor, spfSponsorFee), + Ter(tecUNFUNDED_PAYMENT)); + env.close(); + + BEAST_EXPECT(env.balance(alice) == aliceBalance); + BEAST_EXPECT(env.balance(bob) == bobBalance); + BEAST_EXPECT(env.balance(sponsor) == sponsorBalance); + BEAST_EXPECT( + sponsor::sponsorshipFeeBalance(env, sponsor, alice) == sponsorFee - feeAmt); + } + + // make sfFeeAmount absent if tec error and all Fee is paid + { + // reset FeeAmount and MaxFee + env(sponsor::del(sponsor), sponsor::SponseeAcc(alice)); + env(sponsor::set_fee(sponsor, 0, XRP(10)), sponsor::SponseeAcc(alice)); + env.close(); + + BEAST_EXPECT( + env.le(keylet::sponsorship(sponsor, alice))->isFieldPresent(sfFeeAmount)); + auto sponsorAvailableFee = sponsor::sponsorshipFeeBalance(env, sponsor, alice); + env(check::cancel(alice, uint256(1)), + Fee(sponsorAvailableFee), + sponsor::As(sponsor, spfSponsorFee), + Ter(tecNO_ENTRY)); + env.close(); + BEAST_EXPECT( + !env.le(keylet::sponsorship(sponsor, alice))->isFieldPresent(sfFeeAmount)); + } + } + + // MaxFee cap is enforced in reset() for tec-failing transactions. + // On a closed ledger view (!view.open()), checkFee returns tecINSUFF_FEE when + // Fee > MaxFee (not terINSUF_FEE_B), triggering reset() + { + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const carol("sponsor"); + + env.fund(XRP(10000), alice, carol); + env.close(); + + // FeeAmount=1000 drops, MaxFee=10 drops + env(sponsor::set_fee(carol, 0, drops(1000), drops(10)), sponsor::SponseeAcc(alice)); + env.close(); + + // Apply directly against the closed ledger view (open_ = false) so that + // checkFee returns tecINSUFF_FEE and reset() is invoked. + OpenView overlay(&*env.closed()); + + auto jt = env.jt( + noop(alice), + Fee(drops(1000)), + Seq(env.seq(alice)), + sponsor::As(carol, spfSponsorFee)); + + auto const result = xrpl::apply(env.app(), overlay, *jt.stx, TapNone, env.journal); + BEAST_EXPECT(result.ter == tecINSUFF_FEE); + BEAST_EXPECT(result.applied); + + // Only MaxFee (10 drops) must be deducted, not the full 1000 drops. + auto const sle = overlay.read(keylet::sponsorship(carol.id(), alice.id())); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->isFieldPresent(sfFeeAmount)); + BEAST_EXPECT(sle->getFieldAmount(sfFeeAmount) == drops(990)); // 1000 - MaxFee(10) + } + + // LedgerStateFix charges an owner-reserve fee and can claim that fee + // while returning tecFAILED_PROCESSING. That path must be safe when the + // fee is pre-funded by a sponsorship object. + { + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const sponsor("sponsor"); + env.fund(XRP(1000), alice, sponsor); + env.close(); + + auto const fixFee = drops(env.current()->fees().increment); + env(sponsor::set_fee(sponsor, 0, fixFee), sponsor::SponseeAcc(alice)); + env.close(); + + env(ledgerStateFix::nftPageLinks(alice, alice), + Fee(fixFee), + sponsor::As(sponsor, spfSponsorFee), + Ter(tecFAILED_PROCESSING)); + + if (auto const sle = env.le(keylet::sponsorship(sponsor, alice)); BEAST_EXPECT(sle)) + BEAST_EXPECT(!sle->isFieldPresent(sfFeeAmount)); + } + + // If preclaim saw spendable sponsored FeeAmount but the apply view no + // longer has it, the fee path should fail cleanly instead of throwing. + { + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const sponsor("sponsor"); + env.fund(XRP(1000), alice, sponsor); + env.close(); + + auto const fixFee = drops(env.current()->fees().increment); + env(sponsor::set_fee(sponsor, 0, fixFee), sponsor::SponseeAcc(alice)); + env.close(); + + OpenView overlay(&*env.closed()); + auto jt = env.jt( + ledgerStateFix::nftPageLinks(alice, alice), + Fee(fixFee), + sponsor::As(sponsor, spfSponsorFee)); + + auto const pf = preflight(env.app(), overlay.rules(), *jt.stx, TapNone, env.journal); + BEAST_EXPECT(isTesSuccess(pf.ter)); + auto const pc = preclaim(pf, env.app(), overlay); + BEAST_EXPECT(isTesSuccess(pc.ter)); + + auto const original = overlay.read(keylet::sponsorship(sponsor, alice)); + if (BEAST_EXPECT(original)) + { + auto sle = std::make_shared(*original); + sle->makeFieldAbsent(sfFeeAmount); + overlay.rawReplace(sle); + } + + auto const result = doApply(pc, env.app(), overlay); + BEAST_EXPECT(result.ter == terINSUF_FEE_B); + BEAST_EXPECT(!result.applied); + } + + // test lsfSponsorshipRequireSignForFee + { + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + // set flag + env(sponsor::set_fee(sponsor, tfSponsorshipSetRequireSignForFee, XRP(10)), + sponsor::SponseeAcc(alice)); + env.close(); + + env(pay(alice, bob, XRP(100)), + Fee(XRP(10)), + sponsor::As(sponsor, spfSponsorFee), + Ter(terNO_PERMISSION)); + env.close(); + + BEAST_EXPECT( + env.le(keylet::sponsorship(sponsor, alice))->getFieldAmount(sfFeeAmount) == + XRP(10)); + + // clear flag + env(sponsor::set_fee(sponsor, tfSponsorshipClearRequireSignForFee, XRP(10)), + sponsor::SponseeAcc(alice)); + env.close(); + + // Payment is re-applied + BEAST_EXPECT(!env.le(keylet::sponsorship(sponsor, alice))->isFieldPresent(sfFeeAmount)); + } + + // RequireSignForFee: co-signing should succeed + { + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + // set flag + env(sponsor::set_fee(sponsor, tfSponsorshipSetRequireSignForFee, XRP(10)), + sponsor::SponseeAcc(alice)); + env.close(); + + // pre-funded (no sig) should fail + env(pay(alice, bob, XRP(100)), + Fee(XRP(1)), + sponsor::As(sponsor, spfSponsorFee), + Ter(terNO_PERMISSION)); + env.close(); + + // co-signing (with sig) should succeed + env(pay(alice, bob, XRP(100)), + Fee(XRP(1)), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT( + env.le(keylet::sponsorship(sponsor, alice))->getFieldAmount(sfFeeAmount) == XRP(9)); + } + } + + void + testSponsorAccount() + { + testcase("Sponsor Account"); + using namespace test::jtx; + + Account const alice("alice"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); + Account const sponsor3("sponsor3"); + Account const bob("bob"); + Account const charlie("charlie"); + Account const dave("dave"); + Account const gw("gw"); + auto const usd = gw["usd"]; + + { + // Disabled + Env env{*this, testableAmendments() - featureSponsor}; + env.fund(XRP(10000), alice, sponsor); + env.close(); + env(pay(alice, bob, XRP(100)), Txflags(tfSponsorCreatedAccount), Ter(temDISABLED)); + env.close(); + } + + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, sponsor, sponsor2, sponsor3); + env.close(); + + // Invalid flags + for (auto flag : { + tfNoRippleDirect, + tfPartialPayment, + tfLimitQuality, + }) + { + env(pay(alice, bob, XRP(100)), + Txflags(tfSponsorCreatedAccount | flag), + Ter(temINVALID_FLAG)); + env.close(); + } + + // Invalid amount(iou) + env(pay(alice, bob, usd(100)), Txflags(tfSponsorCreatedAccount), Ter(temBAD_AMOUNT)); + env.close(); + + // Sponsored account creation is reserve sponsorship and is only supported for direct XRP + // payments. + env(pay(alice, bob, drops(1)), + Txflags(tfSponsorCreatedAccount), + Sendmax(usd(2)), + Ter(temINVALID)); + env.close(); + + env(pay(alice, bob, drops(1)), + Txflags(tfSponsorCreatedAccount), + Path(~XRP), + Ter(temINVALID)); + env.close(); + + // Account is not sponsored by normal Sponsor specification + { + env(pay(alice, bob, drops(env.current()->fees().accountReserve(0, 1))), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + auto const bobSle = env.le(keylet::account(bob)); + BEAST_EXPECT(!bobSle->isFieldPresent(sfSponsor)); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor) == 0); + } + + // Use tfSponsorCreatedAccount to sponsor an account + { + // to funded account + env(pay(sponsor2, bob, drops(1)), + Txflags(tfSponsorCreatedAccount), + Fee(XRP(1)), + Ter(tecNO_SPONSOR_PERMISSION)); + env.close(); + + BEAST_EXPECT(env.balance(sponsor2) == XRP(9999)); + + // to non-funded account / insufficient balance for reserve + env(pay(sponsor2, charlie, XRP(9999) - env.current()->fees().reserve + drops(1)), + Txflags(tfSponsorCreatedAccount), + Ter(tecUNFUNDED_PAYMENT)); + env.close(); + + // to non-funded account + auto const sponsor2BalanceBefore = env.balance(sponsor2); + env(pay(sponsor2, charlie, drops(1)), Txflags(tfSponsorCreatedAccount), Fee(XRP(1))); + env.close(); + + auto const charlieSle = env.le(keylet::account(charlie)); + BEAST_EXPECT(charlieSle->isFieldPresent(sfSponsor)); + BEAST_EXPECT(charlieSle->getAccountID(sfSponsor) == sponsor2.id()); + BEAST_EXPECT(sponsoredOwnerCount(env, charlie) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor2) == 1); + // verify sponsor balance decreased by payment + Fee + BEAST_EXPECT(env.balance(sponsor2) == sponsor2BalanceBefore - drops(1) - XRP(1)); + } + { + // insufficient reserve to sponsor acount + + auto const sendAmount = drops(1); + // 2 account reserve + send amount + auto const requireBalance = accountReserve(env, 2) + sendAmount; + adjustAccountXRPBalance(env, sponsor3, requireBalance - drops(1)); + env(pay(sponsor3, dave, sendAmount), + Txflags(tfSponsorCreatedAccount), + Fee(XRP(1)), + Ter(tecUNFUNDED_PAYMENT)); + env.close(); + + adjustAccountXRPBalance(env, sponsor3, requireBalance); + env(pay(sponsor3, dave, sendAmount), + Txflags(tfSponsorCreatedAccount), + Fee(XRP(1)), + Ter(tesSUCCESS)); + env.close(); + } + } + + void + testRequireFlag() + { + using namespace test::jtx; + { + testcase("SponsorshipRequireSignForReserve"); + + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + // set flag + env(sponsor::set_reserve(sponsor, tfSponsorshipSetRequireSignForReserve, 10), + sponsor::SponseeAcc(alice)); + env.close(); + + env(check::create(alice, bob, XRP(100)), + Fee(XRP(10)), + sponsor::As(sponsor, spfSponsorReserve), + Ter(terNO_PERMISSION)); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + + // clear flag + env(sponsor::set_reserve(sponsor, tfSponsorshipClearRequireSignForReserve, 1), + sponsor::SponseeAcc(alice)); + env.close(); + + // CheckCreate is re-applied + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + } + + { + testcase("SponsorshipRequireSignForFee"); + + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + // set flag + env(sponsor::set_fee(sponsor, tfSponsorshipSetRequireSignForFee, XRP(10)), + sponsor::SponseeAcc(alice)); + env.close(); + + env(check::create(alice, bob, XRP(100)), + Fee(XRP(10)), + sponsor::As(sponsor, spfSponsorFee), + Ter(terNO_PERMISSION)); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT( + env.le(keylet::sponsorship(sponsor, alice))->getFieldAmount(sfFeeAmount) == + XRP(10)); + + // clear flag + env(sponsor::set_fee(sponsor, tfSponsorshipClearRequireSignForFee, XRP(10)), + sponsor::SponseeAcc(alice)); + env.close(); + + // CheckCreate is re-applied + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(!env.le(keylet::sponsorship(sponsor, alice))->isFieldPresent(sfFeeAmount)); + } + } + + void + testSponsorReserveSimple(bool cosigning) + { + testcase("SponsorReserveSimple"); + using namespace test::jtx; + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + if (cosigning) + { + adjustAccountXRPBalance(env, sponsor, reserve(env, 1) - drops(1)); + + env(check::create(alice, bob, XRP(100)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + + adjustAccountXRPBalance(env, sponsor, reserve(env, 1)); + + env(check::create(alice, bob, XRP(100)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tesSUCCESS)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor, 0, 250), sponsor::SponseeAcc(alice)); + env.close(); + + adjustAccountXRPBalance(env, sponsor, reserve(env, 2) - drops(1)); + + env(check::create(alice, bob, XRP(100)), + sponsor::As(sponsor, spfSponsorReserve), + Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + + adjustAccountXRPBalance(env, sponsor, reserve(env, 2)); + + env(check::create(alice, bob, XRP(100)), + sponsor::As(sponsor, spfSponsorReserve), + Ter(tesSUCCESS)); + env.close(); + } + } + + // test helper for both cosigning and pre-funded sponsorship + template + void + testEachSponsorship( + test::jtx::Env& env, + bool cosigning, + jtx::Account const& sponsor, + jtx::Account const& sponsee, + uint32_t reserveCount, + uint32_t sponsorReserveCount, + TER insufficientReserveResult, + SubmitCallback callback, + std::optional> expected = std::nullopt) + { + using namespace test::jtx; + auto const sponseeOwnerCountBefore = ownerCount(env, sponsee); + auto const sponseeSponsoredOwnerCountBefore = sponsoredOwnerCount(env, sponsee); + auto const sponseeSponsoringOwnerCountBefore = sponsoringOwnerCount(env, sponsee); + auto const sponsorOwnerCountBefore = ownerCount(env, sponsor); + auto const sponsorSponsoredOwnerCountBefore = sponsoredOwnerCount(env, sponsor); + auto const sponsorSponsoringOwnerCountBefore = sponsoringOwnerCount(env, sponsor); + + std::optional sponsorSig = + cosigning ? std::optional(Sig(sfSponsorSignature, sponsor)) : std::nullopt; + + auto const sponsorCurrentOwnerCount = ownerCount(env, sponsor) - + sponsoredOwnerCount(env, sponsor) + sponsoringOwnerCount(env, sponsor); + + auto submit = [&](TER ter) { + return [&, ter](json::Value const& jv, auto const&... fN) { + if (sponsorSig) + { + env(jv, fN..., sponsor::As(sponsor, spfSponsorReserve), *sponsorSig, Ter(ter)); + } + else + { + env(jv, fN..., sponsor::As(sponsor, spfSponsorReserve), Ter(ter)); + } + }; + }; + + // Insufficient Reserve + { + if (cosigning) + { + adjustAccountXRPBalance( + env, + sponsor, + reserve(env, sponsorCurrentOwnerCount + sponsorReserveCount) - drops(1)); + } + else + { + // cleanup previous sponsorship + if (env.le(keylet::sponsorship(sponsor, sponsee))) + { + env(sponsor::del(sponsor), sponsor::SponseeAcc(sponsee)); + env.close(); + } + + if (sponsorReserveCount > 1) + { + env(sponsor::set(sponsor, 0, sponsorReserveCount - 1, XRP(1)), + sponsor::SponseeAcc(sponsee)); + } + else + { + // just create sponsor object + env(sponsor::set(sponsor, 0, std::nullopt, XRP(1)), + sponsor::SponseeAcc(sponsee)); + } + env.close(); + } + + // A failed sponsored create must not consume prefunded reserve or mutate owner counts. + auto const sponseeOwnerCountBeforeAttempt = ownerCount(env, sponsee); + auto const sponseeSponsoredOwnerCountBeforeAttempt = sponsoredOwnerCount(env, sponsee); + auto const sponseeSponsoringOwnerCountBeforeAttempt = + sponsoringOwnerCount(env, sponsee); + auto const sponsorOwnerCountBeforeAttempt = ownerCount(env, sponsor); + auto const sponsorSponsoredOwnerCountBeforeAttempt = sponsoredOwnerCount(env, sponsor); + auto const sponsorSponsoringOwnerCountBeforeAttempt = + sponsoringOwnerCount(env, sponsor); + auto const sponsorshipSleBeforeAttempt = env.le(keylet::sponsorship(sponsor, sponsee)); + bool const reserveCountPresentBeforeAttempt = sponsorshipSleBeforeAttempt && + sponsorshipSleBeforeAttempt->isFieldPresent(sfRemainingOwnerCount); + std::uint32_t const reserveCountBeforeAttempt = reserveCountPresentBeforeAttempt + ? sponsorshipSleBeforeAttempt->getFieldU32(sfRemainingOwnerCount) + : 0; + + callback(env, submit(insufficientReserveResult)); + env.close(); + + BEAST_EXPECT(ownerCount(env, sponsee) == sponseeOwnerCountBeforeAttempt); + BEAST_EXPECT( + sponsoredOwnerCount(env, sponsee) == sponseeSponsoredOwnerCountBeforeAttempt); + BEAST_EXPECT( + sponsoringOwnerCount(env, sponsee) == sponseeSponsoringOwnerCountBeforeAttempt); + BEAST_EXPECT(ownerCount(env, sponsor) == sponsorOwnerCountBeforeAttempt); + BEAST_EXPECT( + sponsoredOwnerCount(env, sponsor) == sponsorSponsoredOwnerCountBeforeAttempt); + BEAST_EXPECT( + sponsoringOwnerCount(env, sponsor) == sponsorSponsoringOwnerCountBeforeAttempt); + + auto const sponsorshipSleAfterAttempt = env.le(keylet::sponsorship(sponsor, sponsee)); + BEAST_EXPECT( + static_cast(sponsorshipSleAfterAttempt) == + static_cast(sponsorshipSleBeforeAttempt)); + if (sponsorshipSleAfterAttempt) + { + BEAST_EXPECT( + sponsorshipSleAfterAttempt->isFieldPresent(sfRemainingOwnerCount) == + reserveCountPresentBeforeAttempt); + if (reserveCountPresentBeforeAttempt) + { + BEAST_EXPECT( + sponsorshipSleAfterAttempt->getFieldU32(sfRemainingOwnerCount) == + reserveCountBeforeAttempt); + } + } + } + + // Success + { + if (cosigning) + { + adjustAccountXRPBalance( + env, sponsor, reserve(env, sponsorCurrentOwnerCount + sponsorReserveCount)); + } + else + { + // reset sponsorship + env(sponsor::del(sponsor), sponsor::SponseeAcc(sponsee)); + env(sponsor::set(sponsor, 0, sponsorReserveCount, XRP(1)), + sponsor::SponseeAcc(sponsee)); + env.close(); + } + callback(env, submit(tesSUCCESS)); + env.close(); + + if (!cosigning) + { + // Prefunded success consumes the reserved owner slot before cleanup. + auto const sponsorshipSle = env.le(keylet::sponsorship(sponsor, sponsee)); + BEAST_EXPECT(sponsorshipSle); + BEAST_EXPECT( + !sponsorshipSle->isFieldPresent(sfRemainingOwnerCount) || + sponsorshipSle->getFieldU32(sfRemainingOwnerCount) == 0); + + // cleanup sponsorship + env(sponsor::del(sponsor), sponsor::SponseeAcc(sponsee)); + env.close(); + } + } + + if (expected) + { + (*expected)(); + } + else + { + BEAST_EXPECT(ownerCount(env, sponsee) - sponseeOwnerCountBefore == reserveCount); + BEAST_EXPECT( + sponsoredOwnerCount(env, sponsee) - sponseeSponsoredOwnerCountBefore == + sponsorReserveCount); + BEAST_EXPECT( + sponsoringOwnerCount(env, sponsee) - sponseeSponsoringOwnerCountBefore == 0); + BEAST_EXPECT(ownerCount(env, sponsor) == sponsorOwnerCountBefore); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor) == sponsorSponsoredOwnerCountBefore); + BEAST_EXPECT( + sponsoringOwnerCount(env, sponsor) - sponsorSponsoringOwnerCountBefore == + sponsorReserveCount); + } + }; + + void + testCheck(bool cosigning) + { + testcase("Check"); + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const gw("gw"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); + + auto const usd = gw["usd"]; + + { + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, bob, sponsor, sponsor2); + env.close(); + + // CheckCreate -> Check -> CheckCancel + + uint32_t seq = 0; + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + seq = env.seq(alice); + submit(check::create(alice, bob, XRP(1))); + }); + + BEAST_EXPECT(ownerCount(env, alice) == 1); // Check + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + + auto const keylet = keylet::check(alice, seq); + BEAST_EXPECT(env.le(keylet)->getAccountID(sfSponsor) == sponsor.id()); + + if (cosigning) + { + // transfer sponsor + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); + env.close(); + + // transfer sponsor + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + } + + BEAST_EXPECT(ownerCount(env, alice) == 1); // Check + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + + BEAST_EXPECT(env.le(keylet)->getAccountID(sfSponsor) == sponsor2.id()); + + // CheckCancel + env(check::cancel(alice, keylet.key)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + } + + { + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + // CheckCreate -> CheckCash + uint32_t seq2 = 0; + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + seq2 = env.seq(alice); + submit(check::create(alice, bob, XRP(1))); + }); + + BEAST_EXPECT(ownerCount(env, bob) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0); + + // CheckCash + auto const checkId2 = keylet::check(alice, seq2).key; + env(check::cash(bob, checkId2, XRP(1))); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(ownerCount(env, bob) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + } + + // RippleState sponsor (CheckCashMakesTrustLine) + { + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, bob, gw, sponsor, sponsor2); + env.close(); + + env.trust(usd(100), alice); + env.close(); + env(pay(gw, alice, usd(100))); + env.close(); + + // CheckCreate -> CheckCash + uint32_t seq2 = 0; + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + seq2 = env.seq(alice); + submit(check::create(alice, bob, usd(1))); + }); + + BEAST_EXPECT(ownerCount(env, bob) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0); + + auto const keylet = keylet::check(alice, seq2); + BEAST_EXPECT(env.le(keylet)->getAccountID(sfSponsor) == sponsor.id()); + + // CheckCash + testEachSponsorship( + env, + cosigning, + sponsor, + bob, + 1, + 1, + tecNO_LINE_INSUF_RESERVE, + [&](Env& env, auto const& submit) { submit(check::cash(bob, keylet.key, usd(1))); }, + [&]() { + BEAST_EXPECT(ownerCount(env, alice) == 1); // RippleState + BEAST_EXPECT(ownerCount(env, bob) == 1); // RippleState + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + + // the newly-created trust line carries the sponsor on + // bob's side, and no sponsor on the counterparty side + bool const bobLow = bob.id() < gw.id(); + auto const lineSle = env.le(keylet::trustLine(bob, gw, usd.currency)); + BEAST_EXPECT(lineSle); + if (lineSle) + { + BEAST_EXPECT( + lineSle->getAccountID(bobLow ? sfLowSponsor : sfHighSponsor) == + sponsor.id()); + BEAST_EXPECT( + !lineSle->isFieldPresent(bobLow ? sfHighSponsor : sfLowSponsor)); + } + }); + } + + // MPT sponsor: cashing an MPT check creates a sponsored MPToken for + // the casher. Unlike the trust-line path, this returns + // tecINSUFFICIENT_RESERVE (not tecNO_LINE_INSUF_RESERVE) on shortfall. + { + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), bob, sponsor, sponsor2); + env.close(); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, alice, mpt(10'000))); + env.close(); + + // CheckCreate (alice -> bob) paying the MPT + uint32_t seq2 = 0; + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + seq2 = env.seq(alice); + submit(check::create(alice, bob, mpt(1))); + }); + + auto const checkKeylet = keylet::check(alice, seq2); + BEAST_EXPECT(env.le(checkKeylet)->getAccountID(sfSponsor) == sponsor.id()); + BEAST_EXPECT(ownerCount(env, bob) == 0); + + // CheckCash by bob (no MPToken yet) creates a sponsored MPToken + testEachSponsorship( + env, + cosigning, + sponsor, + bob, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + submit(check::cash(bob, checkKeylet.key, mpt(1))); + }, + [&]() { + BEAST_EXPECT(ownerCount(env, bob) == 1); // MPToken + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + BEAST_EXPECT( + env.le(keylet::mptoken(mptGw.issuanceID(), bob))->getAccountID(sfSponsor) == + sponsor.id()); + }); + } + } + + void + testDelegate(bool cosigning) + { + testcase("Delegate"); + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); + + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, sponsor, sponsor2); + env.close(); + + // DelegateSet + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + submit(delegate::set(alice, bob, {"Payment"})); + }); + + // the created Delegate object carries the sponsor + auto const keylet = keylet::delegate(alice, bob); + BEAST_EXPECT(env.le(keylet)->getAccountID(sfSponsor) == sponsor.id()); + + // transfer sponsor + if (cosigning) + { + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); + env.close(); + + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + } + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + BEAST_EXPECT(env.le(keylet)->getAccountID(sfSponsor) == sponsor2.id()); + + // delete + env(delegate::set(alice, bob, {})); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + } + } + + void + testDepositPreauth(bool cosigning) + { + testcase("DepositPreauth"); + using namespace test::jtx; + Account const alice("alice"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); + auto const credType = std::string("credType"); + + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, sponsor, sponsor2); + env.close(); + + // DepositPreauthSet + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { submit(deposit::auth(alice, sponsor)); }); + + // the created DepositPreauth object carries the sponsor + auto const keylet = keylet::depositPreauth(alice, sponsor); + BEAST_EXPECT(env.le(keylet)->getAccountID(sfSponsor) == sponsor.id()); + + // transfer sponsor + if (cosigning) + { + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); + env.close(); + // No sponsor signature here: this exercises the prefunded reassign path. + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + + auto const sponsor2Sle = env.le(keylet::sponsorship(sponsor2, alice)); + BEAST_EXPECT(sponsor2Sle); + if (sponsor2Sle) + { + BEAST_EXPECT( + !sponsor2Sle->isFieldPresent(sfRemainingOwnerCount) || + sponsor2Sle->getFieldU32(sfRemainingOwnerCount) == 0); + } + } + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + BEAST_EXPECT(env.le(keylet)->getAccountID(sfSponsor) == sponsor2.id()); + + // DepositPreauthDelete + env(deposit::unauth(alice, sponsor)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + } + + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, sponsor); + env.close(); + auto const authCreds = std::vector{ + {.issuer = sponsor, .credType = credType}}; + auto const preauthKeylet = keylet::depositPreauth( + alice.id(), + std::set>{ + {sponsor.id(), Slice(credType.data(), credType.size())}}); + + // Cover DepositPreauth's sfAuthorizeCredentials sponsor-reserve branch. + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env&, auto const& submit) { + submit(deposit::authCredentials(alice, authCreds)); + }); + + // Cover sfUnauthorizeCredentials cleanup for a sponsored preauth object. + BEAST_EXPECT(env.le(preauthKeylet)); + BEAST_EXPECT(env.le(preauthKeylet)->getAccountID(sfSponsor) == sponsor.id()); + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + + env(deposit::unauthCredentials(alice, authCreds)); + env.close(); + + BEAST_EXPECT(!env.le(preauthKeylet)); + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + } + } + + void + testCredential(bool cosigning) + { + testcase("Credential"); + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); + auto const credType = std::string("test"); + + // Self-issued credential: alice creates for herself, sponsor covers reserve + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, sponsor, sponsor2); + env.close(); + + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + submit(credentials::create(alice, alice, credType)); + }); + + auto const credKeylet = credentials::keylet(alice, alice, credType); + BEAST_EXPECT(env.le(credKeylet)->getAccountID(sfSponsor) == sponsor.id()); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + + // Transfer sponsor + if (cosigning) + { + env(sponsor::transfer(alice, tfSponsorshipReassign, credKeylet.key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); + env.close(); + env(sponsor::transfer(alice, tfSponsorshipReassign, credKeylet.key), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + } + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + BEAST_EXPECT(env.le(credKeylet)->getAccountID(sfSponsor) == sponsor2.id()); + + // CredentialDelete + env(credentials::deleteCred(alice, alice, alice, credType)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + } + + // Issuer creates for subject; sponsorship transfers to subject on accept + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, sponsor); + env.close(); + + // alice (issuer) creates credential for bob (subject), sponsor covers alice's reserve + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + submit(credentials::create(bob, alice, credType)); + }); + + // Before accept: alice (issuer) owns the credential + auto const credKeylet = credentials::keylet(bob, alice, credType); + BEAST_EXPECT(env.le(credKeylet)->getAccountID(sfSponsor) == sponsor.id()); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + BEAST_EXPECT(ownerCount(env, bob) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0); + + // Bob accepts: issuer's sponsorship ends, bob becomes unsponsored owner + env(credentials::accept(bob, alice, credType)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); // original sponsorship ended + BEAST_EXPECT(ownerCount(env, bob) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0); // bob owns it without a sponsor + + // CredentialDelete by subject + env(credentials::deleteCred(bob, bob, alice, credType)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(ownerCount(env, bob) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + } + + // Issuer creates for subject; accept tx itself is reserve-sponsored + // This exercises the checkReserve() call in CredentialAccept::doApply() + // that guards the sponsor's reserve when featureSponsor is enabled. + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, sponsor); + env.close(); + + // alice (issuer) creates credential for bob (subject) without sponsor + env(credentials::create(bob, alice, credType)); + env.close(); + + auto const credKeylet = credentials::keylet(bob, alice, credType); + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(ownerCount(env, bob) == 0); + + // Bob accepts with a sponsored reserve; the first attempt uses an + // undercapitalized sponsor and must fail with tecINSUFFICIENT_RESERVE; + // the second attempt uses a properly funded sponsor and must succeed. + testEachSponsorship( + env, + cosigning, + sponsor, + bob, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + submit(credentials::accept(bob, alice, credType)); + }); + + // After successful accept: alice (issuer) no longer owns the credential, + // bob owns it and sponsor covers his reserve. + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(ownerCount(env, bob) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + BEAST_EXPECT(env.le(credKeylet)->getAccountID(sfSponsor) == sponsor.id()); + + // Cleanup + env(credentials::deleteCred(bob, bob, alice, credType)); + env.close(); + } + } + + void + testEscrow(bool cosigning) + { + testcase("Escrow"); + using namespace test::jtx; + using namespace std::chrono_literals; + + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); + { + // Native Escrow + Env env{*this, testableAmendments()}; + auto const baseFee = env.current()->fees().base; + + env.fund(XRP(1000000), alice, bob, sponsor, sponsor2); + env.close(); + + // EscrowCreate + uint32_t seq = 0; + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + seq = env.seq(alice); + submit( + escrow::create(alice, bob, XRP(100)), + escrow::kCondition(escrow::kCb1), + escrow::kCancelTime(env.now() + 100s)); + }); + BEAST_EXPECT( + env.le(keylet::escrow(alice, seq))->getAccountID(sfSponsor) == sponsor.id()); + + // transfer sponsor + if (cosigning) + { + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet::escrow(alice, seq).key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); + env.close(); + + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet::escrow(alice, seq).key), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + } + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + + BEAST_EXPECT( + env.le(keylet::escrow(alice, seq))->getAccountID(sfSponsor) == sponsor2.id()); + + // EscrowFinish + env(escrow::finish(bob, alice, seq), + escrow::kCondition(escrow::kCb1), + escrow::kFulfillment(escrow::kFb1), + Fee(baseFee * 150)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + } + + Account const gw("gw"); + auto const usd = gw["usd"]; + { + // IOU Escrow + Env env{*this, testableAmendments()}; + auto const baseFee = env.current()->fees().base; + + env.fund(XRP(1000000), alice, bob, gw, sponsor, sponsor2); + env.close(); + + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + + env.trust(usd(1000000), alice); + env.close(); + env(pay(gw, alice, usd(10000))); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + + // EscrowCreate + uint32_t seq = 0; + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + seq = env.seq(alice); + submit( + escrow::create(alice, bob, usd(100)), + escrow::kCondition(escrow::kCb1), + escrow::kCancelTime(env.now() + 100s)); + }); + + BEAST_EXPECT( + env.le(keylet::escrow(alice, seq))->getAccountID(sfSponsor) == sponsor.id()); + + // EscrowFinish + testEachSponsorship( + env, + cosigning, + sponsor2, + bob, + 1, + 1, + tecNO_LINE_INSUF_RESERVE, + [&](Env& env, auto const& submit) { + submit( + escrow::finish(bob, alice, seq), + escrow::kCondition(escrow::kCb1), + escrow::kFulfillment(escrow::kFb1), + Fee(baseFee * 150)); + }); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + + BEAST_EXPECT( + env.le(keylet::trustLine(bob, gw, usd.currency))->getAccountID(sfHighSponsor) == + sponsor2.id()); + } + { + // IOU EscrowFinish recycles reserve when the same sponsor backs + // the escrow being removed and the destination line being created. + Env env{*this, testableAmendments()}; + auto const baseFee = env.current()->fees().base; + + env.fund(XRP(1000000), alice, bob, gw, sponsor); + env.close(); + + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + + env.trust(usd(1000000), alice); + env.close(); + env(pay(gw, alice, usd(10000))); + env.close(); + + uint32_t seq = 0; + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + seq = env.seq(alice); + submit( + escrow::create(alice, bob, usd(100)), + escrow::kCondition(escrow::kCb1), + escrow::kCancelTime(env.now() + 100s)); + }); + + BEAST_EXPECT( + env.le(keylet::escrow(alice, seq))->getAccountID(sfSponsor) == sponsor.id()); + + if (cosigning) + { + adjustAccountXRPBalance(env, sponsor, reserve(env, 1)); + } + else + { + env(sponsor::set_reserve(sponsor, 0, 1), sponsor::SponseeAcc(bob)); + env.close(); + } + + if (cosigning) + { + env(escrow::finish(bob, alice, seq), + escrow::kCondition(escrow::kCb1), + escrow::kFulfillment(escrow::kFb1), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Fee(baseFee * 150), + Ter(tesSUCCESS)); + } + else + { + env(escrow::finish(bob, alice, seq), + escrow::kCondition(escrow::kCb1), + escrow::kFulfillment(escrow::kFb1), + sponsor::As(sponsor, spfSponsorReserve), + Fee(baseFee * 150), + Ter(tesSUCCESS)); + } + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(ownerCount(env, bob) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + + BEAST_EXPECT( + env.le(keylet::trustLine(bob, gw, usd.currency))->getAccountID(sfHighSponsor) == + sponsor.id()); + } + { + // IOU Escrow cancel re-creates the owner's trust line, and the + // cancel transaction's sponsor can cover that new line's reserve. + // Creating an IOU escrow moves the sender's balance to the issuer, + // so the sender can delete their now-zero trust line while the + // escrow is pending. Cancelling returns the funds, re-creating the + // line. + Env env{*this, testableAmendments()}; + + env.fund(XRP(1000000), alice, bob, gw, sponsor, sponsor2); + env.close(); + + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + + env.trust(usd(1000000), alice); + env.close(); + env(pay(gw, alice, usd(10000))); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + + // alice escrows her entire balance so the trust line can be removed + auto const cancelAfter = env.now() + 100s; + auto const seq = env.seq(alice); + env(escrow::create(alice, bob, usd(10000)), + escrow::kCondition(escrow::kCb1), + escrow::kCancelTime(cancelAfter)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 2); // trust line + escrow + + // alice deletes her now-zero trust line while the escrow is pending + env(trust(alice, usd(0))); + env.close(); + BEAST_EXPECT(!env.le(keylet::trustLine(alice, gw, usd.currency))); + BEAST_EXPECT(ownerCount(env, alice) == 1); // just the escrow + + // advance the ledger past the cancel time + for (; env.now() < cancelAfter; env.close()) + { + } + + // EscrowCancel by alice re-creates her trust line; the sponsor + // covers the new line's reserve. On the insufficient-reserve + // attempt the whole transaction rolls back, leaving the escrow + // intact to be cancelled on the success attempt. + bool const aliceLow = alice.id() < gw.id(); + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecNO_LINE_INSUF_RESERVE, + [&](Env& env, auto const& submit) { submit(escrow::cancel(alice, alice, seq)); }, + [&]() { + BEAST_EXPECT(!env.le(keylet::escrow(alice, seq))); + auto const trustSle = env.le(keylet::trustLine(alice, gw, usd.currency)); + BEAST_EXPECT(trustSle); + if (trustSle) + { + BEAST_EXPECT( + trustSle->getAccountID(aliceLow ? sfLowSponsor : sfHighSponsor) == + sponsor.id()); + BEAST_EXPECT( + !trustSle->isFieldPresent(aliceLow ? sfHighSponsor : sfLowSponsor)); + } + BEAST_EXPECT(ownerCount(env, alice) == 1); // re-created trust line + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + }); + } + { + // MPT Escrow + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), bob, sponsor); + env.close(); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, alice, mpt(10'000))); + env.close(); + + // create Escrow from alice to bob + auto const seq = env.seq(alice); + env(escrow::create(alice, bob, mpt(100)), + escrow::kCondition(escrow::kCb1), + escrow::kCancelTime(env.now() + 100s)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 2); + BEAST_EXPECT(ownerCount(env, bob) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + + // finish Escrow: bob has no MPToken, so finishing creates one and + // the finish transaction's sponsor covers its reserve. The MPT + // create-token path returns tecINSUFFICIENT_RESERVE (not + // tecNO_LINE_INSUF_RESERVE) when the sponsor is underfunded. + testEachSponsorship( + env, + cosigning, + sponsor, + bob, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + submit( + escrow::finish(bob, alice, seq), + escrow::kCondition(escrow::kCb1), + escrow::kFulfillment(escrow::kFb1), + Fee(XRP(1))); + }); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(ownerCount(env, bob) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + BEAST_EXPECT( + env.le(keylet::mptoken(mptGw.issuanceID(), bob))->getAccountID(sfSponsor) == + sponsor.id()); + } + { + // MPT EscrowFinish has the same reserve recycling behavior as IOU + // when it creates the destination MPToken. + Env env{*this, testableAmendments()}; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(1000000), bob, sponsor); + env.close(); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, alice, mpt(10'000))); + env.close(); + + uint32_t seq = 0; + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + seq = env.seq(alice); + submit( + escrow::create(alice, bob, mpt(100)), + escrow::kCondition(escrow::kCb1), + escrow::kCancelTime(env.now() + 100s)); + }); + + BEAST_EXPECT( + env.le(keylet::escrow(alice, seq))->getAccountID(sfSponsor) == sponsor.id()); + + if (cosigning) + { + adjustAccountXRPBalance(env, sponsor, reserve(env, 1)); + } + else + { + env(sponsor::set_reserve(sponsor, 0, 1), sponsor::SponseeAcc(bob)); + env.close(); + } + + if (cosigning) + { + env(escrow::finish(bob, alice, seq), + escrow::kCondition(escrow::kCb1), + escrow::kFulfillment(escrow::kFb1), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Fee(baseFee * 150), + Ter(tesSUCCESS)); + } + else + { + env(escrow::finish(bob, alice, seq), + escrow::kCondition(escrow::kCb1), + escrow::kFulfillment(escrow::kFb1), + sponsor::As(sponsor, spfSponsorReserve), + Fee(baseFee * 150), + Ter(tesSUCCESS)); + } + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(ownerCount(env, bob) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + + BEAST_EXPECT( + env.le(keylet::mptoken(mptGw.issuanceID(), bob))->getAccountID(sfSponsor) == + sponsor.id()); + } + + // A sponsored EscrowCreate must still verify that the source + // can fund the escrow amount and stay above its own base + // reserve. The sponsor covers the new object's owner + // increment, but cannot cover the source's base reserve. + { + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + // alice's balance is just above the base reserve. After + // locking escrowAmount she would dip below it. + adjustAccountXRPBalance(env, alice, accountReserve(env, 1) + XRP(1)); + + auto const escrowAmount = XRP(2); + auto const seq = env.seq(alice); + + if (cosigning) + { + env(escrow::create(alice, bob, escrowAmount), + escrow::kCondition(escrow::kCb1), + escrow::kCancelTime(env.now() + 100s), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tecUNFUNDED)); + } + else + { + env(sponsor::set(sponsor, 0, 1, XRP(1)), sponsor::SponseeAcc(alice)); + env.close(); + + env(escrow::create(alice, bob, escrowAmount), + escrow::kCondition(escrow::kCb1), + escrow::kCancelTime(env.now() + 100s), + sponsor::As(sponsor, spfSponsorReserve), + Ter(tecUNFUNDED)); + } + env.close(); + + BEAST_EXPECT(!env.le(keylet::escrow(alice, seq))); + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + } + } + + void + testMPToken(bool cosigning) + { + testcase("MPToken"); + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); + + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, sponsor, sponsor2); + env.close(); + + // MPTokenIssuanceCreate + json::Value jv = {}; + jv[sfAccount] = alice.human(); + jv[sfTransactionType] = jss::MPTokenIssuanceCreate; + MPTID mptid; + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + mptid = makeMptID(env.seq(alice), alice.id()); + submit(jv); + }); + + // transfer sponsor + auto const mptIssuanceKeylet = keylet::mptokenIssuance(mptid); + + if (cosigning) + { + env(sponsor::transfer(alice, tfSponsorshipReassign, mptIssuanceKeylet.key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); + env.close(); + + env(sponsor::transfer(alice, tfSponsorshipReassign, mptIssuanceKeylet.key), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + } + + // MPTokenAuthorize + jv = {}; + jv[sfTransactionType] = jss::MPTokenAuthorize; + jv[sfAccount] = bob.human(); + jv[sfMPTokenIssuanceID] = to_string(mptid); + + if (cosigning) + { + adjustAccountXRPBalance(env, sponsor, reserve(env, 2)); + env(ticket::create(sponsor, 2)); // adjust for free mptoken + env.close(); + } + + testEachSponsorship( + env, + cosigning, + sponsor, + bob, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { submit(jv); }); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + + // transfer sponsor + auto const mptTokenKeylet = keylet::mptoken(mptid, bob); + if (cosigning) + { + env(sponsor::transfer(bob, tfSponsorshipReassign, mptTokenKeylet.key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(bob)); + env.close(); + + env(sponsor::transfer(bob, tfSponsorshipReassign, mptTokenKeylet.key), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + } + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(ownerCount(env, bob) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 2); + + // MPTokenAuthorize Unauthorize + jv = {}; + jv[sfTransactionType] = jss::MPTokenAuthorize; + jv[sfAccount] = bob.human(); + jv[sfMPTokenIssuanceID] = to_string(mptid); + jv[sfFlags] = tfMPTUnauthorize; + env(jv); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(ownerCount(env, bob) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + + // MPTokenIssuanceDestroy + jv = {}; + jv[sfTransactionType] = jss::MPTokenIssuanceDestroy; + jv[sfAccount] = alice.human(); + jv[sfMPTokenIssuanceID] = to_string(mptid); + env(jv); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + } + { + // check INSUFFICIENT_RESERVE for MPToken + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, sponsor); + env.close(); + + // MPTokenAuthorize + json::Value jv = {}; + jv[sfAccount] = alice.human(); + jv[sfTransactionType] = jss::MPTokenIssuanceCreate; + auto const mptid = makeMptID(env.seq(alice), alice.id()); + env(jv); + env.close(); + + // Create tickets so the sponsor is past free-tier reserve behavior. + std::uint32_t const ticketSeq{env.seq(sponsor) + 1}; + env(ticket::create(sponsor, 2)); + env.close(); + + jv = {}; + jv[sfTransactionType] = jss::MPTokenAuthorize; + jv[sfAccount] = bob.human(); + jv[sfMPTokenIssuanceID] = to_string(mptid); + // error (non-free mptoken) + if (cosigning) + { + adjustAccountXRPBalance(env, sponsor, reserve(env, 3) - drops(1)); + env(jv, + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + } + else + { + env(sponsor::set(sponsor, 0, std::nullopt, XRP(1)), sponsor::SponseeAcc(bob)); + env.close(); + + env(jv, sponsor::As(sponsor, spfSponsorReserve), Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + } + + env(noop(sponsor), ticket::Use(ticketSeq)); + env.close(); + + // pass (free-tier mptoken for the holder, but the sponsor is still + // charged a reserve increment regardless of the ownerCount < 2 shortcut). + if (cosigning) + { + adjustAccountXRPBalance(env, sponsor, reserve(env, 2)); + env(jv, + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tesSUCCESS)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor, 0, 1), sponsor::SponseeAcc(bob)); + env.close(); + env(jv, sponsor::As(sponsor, spfSponsorReserve), Ter(tesSUCCESS)); + env.close(); + } + } + } + + void + testPayChan(bool cosigning) + { + testcase("PayChan"); + using namespace test::jtx; + using namespace std::literals::chrono_literals; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); + + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, sponsor, sponsor2); + env.close(); + + // PayChanCreate + auto const pk = alice.pk(); + auto const settleDelay = 10s; + uint256 chan; + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + chan = paychan::channel(alice, bob, env.seq(alice)); + submit(paychan::create(alice, bob, XRP(100), settleDelay, pk)); + }); + + // the created PayChannel object carries the sponsor + BEAST_EXPECT(env.le(Keylet(ltPAYCHAN, chan))->getAccountID(sfSponsor) == sponsor.id()); + + // transfer sponsor + if (cosigning) + { + env(sponsor::transfer(alice, tfSponsorshipReassign, chan), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); + env.close(); + + env(sponsor::transfer(alice, tfSponsorshipReassign, chan), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + } + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + BEAST_EXPECT(env.le(Keylet(ltPAYCHAN, chan))->getAccountID(sfSponsor) == sponsor2.id()); + + env.close(env.now() + settleDelay); + // PayChanClaim (delete PayChan) + env(paychan::claim(bob, chan), Txflags(tfClose)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + } + + // A sponsored PaymentChannelCreate must still verify that the + // source can fund the channel amount and stay above its own + // base reserve. The sponsor covers the new object's owner + // increment, but cannot cover the source's base reserve. + { + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + // alice's balance is just above the base reserve. After + // locking channelAmount she would dip below it. + adjustAccountXRPBalance(env, alice, accountReserve(env, 1) + XRP(1)); + + auto const pk = alice.pk(); + auto const settleDelay = 10s; + auto const channelAmount = XRP(2); + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + + if (cosigning) + { + env(paychan::create(alice, bob, channelAmount, settleDelay, pk), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tecUNFUNDED)); + } + else + { + env(sponsor::set(sponsor, 0, 1, XRP(1)), sponsor::SponseeAcc(alice)); + env.close(); + + env(paychan::create(alice, bob, channelAmount, settleDelay, pk), + sponsor::As(sponsor, spfSponsorReserve), + Ter(tecUNFUNDED)); + } + env.close(); + + BEAST_EXPECT(!paychan::channelExists(*env.current(), chan)); + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + } + } + + void + testSignerList(bool cosigning) + { + testcase("SignerList"); + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); + + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, sponsor, sponsor2); + env.close(); + + // SignerListSet + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { submit(signers(alice, 1, {{bob, 1}})); }); + + // the created SignerList object carries the sponsor + BEAST_EXPECT(env.le(keylet::signerList(alice))->getAccountID(sfSponsor) == sponsor.id()); + + // transfer sponsor + if (cosigning) + { + // invalid signer list owner 1 + // account doesn't have signer list but specified signer list exists + env(sponsor::transfer(bob, tfSponsorshipReassign, keylet::signerList(alice).key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2), + Ter(tecNO_PERMISSION)); + // invalid signer list owner 2 + // account has signer list and specified signer list exists + env(signers(bob, 1, {{alice, 1}})); + env.close(); + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet::signerList(bob).key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2), + Ter(tecNO_PERMISSION)); + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet::signerList(alice).key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); + env.close(); + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet::signerList(alice).key), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + } + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + BEAST_EXPECT(env.le(keylet::signerList(alice))->getAccountID(sfSponsor) == sponsor2.id()); + + // Delete + env(signers(alice, NoneT())); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + } + + // Legacy (pre-MultiSignReserve) SignerLists lack lsfOneOwnerCount and cost + // 2 + signer_count owner units, whereas modern lists cost 1. + void + testLegacySignerListReserve() + { + testcase("Legacy SignerList sponsorship reserve"); + using namespace test::jtx; + + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const dave("dave"); + Account const sponsor("sponsor"); + + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, carol, dave, sponsor); + env.close(); + + // Modern 3-signer list: weight 1, lsfOneOwnerCount set. + env(signers(alice, 1, {{bob, 1}, {carol, 1}, {dave, 1}})); + env.close(); + + auto const signerListKeylet = keylet::signerList(alice.id()); + auto const sponsorKeylet = keylet::sponsorship(sponsor.id(), alice.id()); + std::uint32_t const legacyWeight = 5; // 2 + 3 signers + BEAST_EXPECT(ownerCount(env, alice) == 1); + + // Pre-fund exactly the legacy weight + env(sponsor::set_reserve(sponsor, 0, legacyWeight), sponsor::SponseeAcc(alice)); + env.close(); + if (auto const sle = env.le(sponsorKeylet); BEAST_EXPECT(sle)) + BEAST_EXPECT(sle->getFieldU32(sfRemainingOwnerCount) == legacyWeight); + + // Synthesize a pre-MultiSignReserve list: clear lsfOneOwnerCount and + // restore the owner's OwnerCount to the legacy weight. + env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal) -> bool { + auto signerList = std::make_shared(*view.read(signerListKeylet)); + auto account = std::make_shared(*view.read(keylet::account(alice.id()))); + signerList->clearFlag(lsfOneOwnerCount); + account->setFieldU32(sfOwnerCount, legacyWeight); + view.rawReplace(signerList); + view.rawReplace(account); + return true; + }); + if (auto const sle = env.le(signerListKeylet); BEAST_EXPECT(sle)) + BEAST_EXPECT((sle->getFlags() & lsfOneOwnerCount) == 0); + BEAST_EXPECT(ownerCount(env, alice) == legacyWeight); + + // Create must charge the full legacy weight (5), not 1: the bug bumped + // the counters by 1 and left 4 pre-funded units unspent. + env(sponsor::transfer(alice, tfSponsorshipCreate, signerListKeylet.key), + sponsor::As(sponsor, spfSponsorReserve)); + + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == legacyWeight); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == legacyWeight); + // All pre-funded units consumed (drained to absent). + if (auto const sle = env.le(sponsorKeylet); BEAST_EXPECT(sle)) + BEAST_EXPECT(!sle->isFieldPresent(sfRemainingOwnerCount)); + + // Delete unwinds the legacy weight; create bumped by the same amount, so + // the counters return to 0 (the bug bumped by 1 -> underflow on delete). + env(signers(alice, NoneT())); + + BEAST_EXPECT(!env.le(signerListKeylet)); + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + if (auto const sle = env.le(sponsorKeylet); BEAST_EXPECT(sle)) + BEAST_EXPECT(!sle->isFieldPresent(sfRemainingOwnerCount)); + } + + void + testSponsoredTrustLineNoFreeReserve() + { + // An account with ownerCount < 2 may create its first trust lines even + // without meeting the reserve. In any case, the sponsor pays the full + // reserve in all cases, even for the sponsee's very first trust line. + testcase("Sponsored trust line gets no free-reserve exception"); + using namespace test::jtx; + + Account const issuer("issuer"); + Account const alice("alice"); + Account const sponsor("sponsor"); + + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), issuer, alice, sponsor); + env.close(); + + auto const usd = issuer["usd"]; + auto const lineKeylet = keylet::trustLine(alice, issuer, usd.currency); + + // Sponsor funded for exactly its base reserve + adjustAccountXRPBalance(env, sponsor, reserve(env, 0)); + + // alice's ownerCount is 0, so an unsponsored first trust line would be + // free; but because it is sponsored, the reserve check is enforced + // against the sponsor, which is one increment short. + env(trust(alice, usd(100)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tecNO_LINE_INSUF_RESERVE)); + env.close(); + + BEAST_EXPECT(!env.le(lineKeylet)); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + + // Give the sponsor has exactly one owner-reserve increment; the same + // sponsored first trust line now succeeds and the sponsor pays for it. + adjustAccountXRPBalance(env, sponsor, reserve(env, 1)); + + env(trust(alice, usd(100)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + BEAST_EXPECT(env.le(lineKeylet)); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(ownerCount(env, alice) == 1); + } + + void + testCoSignReserveBoundedBySponsorshipBudget() + { + // sponsor co-signs, so a fee-only object (ReserveCount == 0) makes a co-signed + // reserve sponsorship fail -- with no fallback to the sponsor's balance. + testcase("Co-signed reserve sponsorship is bounded by Sponsorship budget"); + using namespace test::jtx; + + Env env{*this, testableAmendments()}; + Account const sponsor("sponsor"); + Account const sponsee("sponsee"); + env.fund(XRP(10000), sponsor, sponsee); + env.close(); + + // Prefund a FEE-only Sponsorship for the sponsee; ReserveCount + // defaults to 0. + env(sponsor::set_fee(sponsor, 0, XRP(100)), sponsor::SponseeAcc(sponsee)); + env.close(); + BEAST_EXPECT(env.le(keylet::sponsorship(sponsor, sponsee))); + + // Sponsee creates a Check with the sponsor co-signing the reserve. The + // fee-only Sponsorship's has ReserveCount (0), so this fails + // with tecINSUFFICIENT_RESERVE + env(check::create(sponsee, sponsor, XRP(1)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + + BEAST_EXPECT(ownerCount(env, sponsee) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsee) == 0); + + // Bumping the Sponsorship's ReserveCount budget makes the same + // co-signed reserve sponsorship succeed, the budget is what gates it. + env(sponsor::set_reserve(sponsor, 0, 1), sponsor::SponseeAcc(sponsee)); + env.close(); + + env(check::create(sponsee, sponsor, XRP(1)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(ownerCount(env, sponsee) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsee) == 1); + } + + void + testTrustSet(bool cosigning) + { + testcase("TrustSet"); + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const charlie("charlie"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); + + auto const validateSponsoredTrustline = + [&](std::shared_ptr const& sle, bool isIssuerHigh, Account const& sponsor) { + BEAST_EXPECT( + sle->getAccountID(isIssuerHigh ? sfLowSponsor : sfHighSponsor) == sponsor.id()); + BEAST_EXPECT(!sle->isFieldPresent(isIssuerHigh ? sfHighSponsor : sfLowSponsor)); + }; + + auto const& highAcc = alice > bob ? alice : bob; + auto const& lowAcc = alice > bob ? bob : alice; + + // create and delete + for (bool const isIssuerHigh : {false, true}) + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, charlie, sponsor, sponsor2); + env.close(); + + auto const& issuer = isIssuerHigh ? highAcc : lowAcc; + auto const& user = isIssuerHigh ? lowAcc : highAcc; + + auto const usd = issuer["usd"]; + auto const currency = usd.currency; + + // create TrustLine + if (cosigning) + { + adjustAccountXRPBalance(env, sponsor, reserve(env, 2)); + env(ticket::create(sponsor, 2)); // adjust for free trustline + env.close(); + } + + testEachSponsorship( + env, + cosigning, + sponsor, + user, + 1, + 1, + tecNO_LINE_INSUF_RESERVE, + [&](Env& env, auto const& submit) { submit(trust(user, usd(100))); }); + + auto const keylet = keylet::trustLine(user, issuer, currency); + + if (cosigning) + { + // invalid owner + env(sponsor::transfer(charlie, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2), + Ter(tecNO_PERMISSION)); + // invalid reserve owner + env(sponsor::transfer(issuer, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2), + Ter(tecNO_PERMISSION)); + env(sponsor::transfer(user, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(user)); + env.close(); + env(sponsor::transfer(user, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + } + + // delete TrustLine + env(trust(user, usd(0))); + env.close(); + + BEAST_EXPECT(ownerCount(env, user) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, user) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + + BEAST_EXPECT(!env.le(keylet)); + } + + // update + for (bool const isIssuerHigh : {false, true}) + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, sponsor, sponsor2); + env.close(); + + auto const& issuer = isIssuerHigh ? highAcc : lowAcc; + auto const& user = isIssuerHigh ? lowAcc : highAcc; + + auto const usd = issuer["usd"]; + auto const currency = usd.currency; + + // create TrustLine from issuer + env(trust(issuer, user["usd"](100))); + env.close(); + + BEAST_EXPECT(env.le(keylet::trustLine(user, issuer, currency))); + + if (cosigning) + { + adjustAccountXRPBalance(env, sponsor, reserve(env, 2)); + env(ticket::create(sponsor, 2)); // adjust for free trustline + env.close(); + } + + testEachSponsorship( + env, + cosigning, + sponsor, + user, + 1, + 1, + tecINSUF_RESERVE_LINE, + [&](Env& env, auto const& submit) { submit(trust(user, usd(100))); }); + + auto const line = env.le(keylet::trustLine(user, issuer, currency)); + validateSponsoredTrustline(line, isIssuerHigh, sponsor); + + // update TrustLine from user to clear reserve + env(trust(user, usd(0))); + env.close(); + + BEAST_EXPECT(ownerCount(env, user) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, user) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(env.le(keylet::trustLine(user, issuer, currency))); + + // remove TrustLine from issuer + env(trust(issuer, user["usd"](0))); + env.close(); + BEAST_EXPECT(!env.le(keylet::trustLine(user, issuer, currency))); + } + + // both High and Low sponsored + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, sponsor); + env.close(); + + // create TrustLines + env(trust(alice, bob["usd"](100)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + env(trust(bob, alice["usd"](100)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + auto sle = env.le(keylet::trustLine(alice, bob, alice["usd"].currency)); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->isFlag(lsfHighReserve)); + BEAST_EXPECT(sle->isFlag(lsfLowReserve)); + BEAST_EXPECT(sle->getAccountID(sfHighSponsor) == sponsor.id()); + BEAST_EXPECT(sle->getAccountID(sfLowSponsor) == sponsor.id()); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(ownerCount(env, bob) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 2); + + // clear TrustLines + env(trust(alice, bob["usd"](0))); + env.close(); + env(trust(bob, alice["usd"](0))); + env.close(); + + sle = env.le(keylet::trustLine(alice, bob, alice["usd"].currency)); + BEAST_EXPECT(!sle); + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(ownerCount(env, bob) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + } + } + + void + testAccountDelete() + { + testcase("AccountDelete"); + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + + { + // Delete Sponsor/Sponsee Account with ltSponsorship (tecHAS_OBLIGATIONS) + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, sponsor); + env.close(); + + // set sponsor + env(sponsor::set(sponsor, 0, 100, XRP(100)), + sponsor::SponseeAcc(alice), + Ter(tesSUCCESS)); + env.close(); + + incLgrSeqForAccDel(env, sponsor); + + auto const keylet = keylet::sponsorship(sponsor, alice); + auto const sponsorObj = env.le(keylet); + BEAST_EXPECT(sponsorObj); + + // AccountDelete + auto const requiredFee = drops(env.current()->fees().increment); + env(acctdelete(alice, bob), Fee(requiredFee), Ter(tecHAS_OBLIGATIONS)); + env(acctdelete(sponsor, bob), Fee(requiredFee), Ter(tecHAS_OBLIGATIONS)); + } + + { + // Delete SponsoredAccount + Env env{*this, testableAmendments()}; + env.memoize(alice); + env.fund(XRP(1000000), bob, sponsor); + env.close(); + + // create SponsoredAccount + env(pay(sponsor, alice, XRP(10000)), Txflags(tfSponsorCreatedAccount)); + env.close(); + + incLgrSeqForAccDel(env, alice); + + // AccountDelete: destination = non-sponsor + auto const requiredFee = drops(env.current()->fees().increment); + env(acctdelete(alice, bob), Fee(requiredFee), Ter(tecNO_SPONSOR_PERMISSION)); + + // The failed delete must leave the account sponsored by the original sponsor. + auto const aliceSle = env.le(keylet::account(alice)); + BEAST_EXPECT(aliceSle); + if (aliceSle) + BEAST_EXPECT(aliceSle->getAccountID(sfSponsor) == sponsor.id()); + + auto const sponsorSle = env.le(keylet::account(sponsor)); + BEAST_EXPECT(sponsorSle->getFieldU32(sfSponsoringAccountCount) == 1); + + incLgrSeqForAccDel(env, alice); + + // AccountDelete: destination = sponsor + env(acctdelete(alice, sponsor), Fee(requiredFee), Ter(tesSUCCESS)); + + auto const sponsorSle2 = env.le(keylet::account(sponsor)); + BEAST_EXPECT(!sponsorSle2->isFieldPresent(sfSponsoringAccountCount)); + } + + { + // Sponsor with sfSponsoringOwnerCount cannot delete (tecHAS_OBLIGATIONS) + Env env{*this, testableAmendments()}; + Account const gw("gw"); + env.fund(XRP(1000000), alice, bob, sponsor, gw); + env.close(); + + auto const usd = gw["usd"]; + + // Create a trust line for alice + env(trust(alice, usd(1000))); + env.close(); + + // Transfer reserve sponsorship of trust line to sponsor + auto const trustId = keylet::trustLine(alice, gw, usd.currency); + BEAST_EXPECT(env.le(trustId)); + + env(sponsor::transfer(alice, tfSponsorshipCreate, trustId.key), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + // Verify sfSponsoringOwnerCount is set on sponsor + auto const sponsorSle = env.le(keylet::account(sponsor)); + BEAST_EXPECT(sponsorSle->isFieldPresent(sfSponsoringOwnerCount)); + auto const sponsoringOwnerCount = sponsorSle->getFieldU32(sfSponsoringOwnerCount); + BEAST_EXPECT(sponsoringOwnerCount >= 1); + + incLgrSeqForAccDel(env, sponsor); + + // AccountDelete should fail + auto const requiredFee = drops(env.current()->fees().increment); + env(acctdelete(sponsor, bob), Fee(requiredFee), Ter(tecHAS_OBLIGATIONS)); + // The failed delete must not decrement the outstanding sponsored-object count. + auto const sponsorSleAfter = env.le(keylet::account(sponsor)); + BEAST_EXPECT(sponsorSleAfter->isFieldPresent(sfSponsoringOwnerCount)); + BEAST_EXPECT( + sponsorSleAfter->getFieldU32(sfSponsoringOwnerCount) == sponsoringOwnerCount); + } + + { + // Sponsor with sfSponsoringAccountCount cannot delete (tecHAS_OBLIGATIONS) + Env env{*this, testableAmendments()}; + env.memoize(alice); + env.fund(XRP(1000000), bob, sponsor); + env.close(); + + // Create SponsoredAccount (sets sfSponsoringAccountCount on sponsor) + env(pay(sponsor, alice, XRP(10000)), Txflags(tfSponsorCreatedAccount)); + env.close(); + + // Verify sfSponsoringAccountCount is set on sponsor + auto const sponsorSle = env.le(keylet::account(sponsor)); + BEAST_EXPECT(sponsorSle->isFieldPresent(sfSponsoringAccountCount)); + auto const sponsoringAccountCount = sponsorSle->getFieldU32(sfSponsoringAccountCount); + BEAST_EXPECT(sponsoringAccountCount == 1); + + incLgrSeqForAccDel(env, sponsor); + + // AccountDelete should fail + auto const requiredFee = drops(env.current()->fees().increment); + env(acctdelete(sponsor, bob), Fee(requiredFee), Ter(tecHAS_OBLIGATIONS)); + // The failed delete must not decrement the outstanding sponsored-account count. + auto const sponsorSleAfter = env.le(keylet::account(sponsor)); + BEAST_EXPECT(sponsorSleAfter->isFieldPresent(sfSponsoringAccountCount)); + BEAST_EXPECT( + sponsorSleAfter->getFieldU32(sfSponsoringAccountCount) == sponsoringAccountCount); + } + + { + // Account with sponsored objects should be deletable + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, sponsor); + env.close(); + + // Create sponsored delegate (a non-deletion-blocker) + env(deposit::auth(alice, bob), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + + { + auto const sponsorSle = env.le(keylet::account(sponsor)); + auto const aliceSle = env.le(keylet::account(alice)); + BEAST_EXPECT(sponsorSle->at(sfSponsoringOwnerCount) == 1); + BEAST_EXPECT(aliceSle->at(sfOwnerCount) == 1); + BEAST_EXPECT(aliceSle->at(sfSponsoredOwnerCount) == 1); + } + + incLgrSeqForAccDel(env, alice); + + // AccountDelete should succeed + { + auto const requiredFee = drops(env.current()->fees().increment); + env(acctdelete(alice, bob), Fee(requiredFee), Ter(tesSUCCESS)); + BEAST_EXPECT(!env.le(keylet::account(alice))); + auto const sponsorSle = env.le(keylet::account(sponsor)); + BEAST_EXPECT(sponsorSle->at(sfSponsoringOwnerCount) == 0); + } + } + } + + void + testDelegatePermission() + { + testcase("DelegatePermission"); + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + + // + // SponsorshipTransfer is not delegable. + // + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, carol); + env.close(); + + auto const seq = env.seq(alice); + env(check::create(alice, bob, XRP(1))); + env.close(); + + auto const keylet = keylet::check(alice, seq); + + env(sponsor::transfer(alice, tfSponsorshipCreate, keylet.key), + sponsor::As(bob, spfSponsorReserve), + Sig(sfSponsorSignature, bob), + delegate::As(carol), + Ter(temINVALID)); + + env(delegate::set(alice, carol, {"SponsorshipTransfer"}), Ter(temMALFORMED)); + } + // + // test send SponsorshipSet on other's behalf + // + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, carol); + env.close(); + + env(sponsor::set(alice, 0, 100, XRP(100)), + sponsor::SponseeAcc(bob), + delegate::As(carol), + Ter(terNO_DELEGATE_PERMISSION)); + + env(delegate::set(alice, carol, {"SponsorshipSet"})); + env.close(); + + env(sponsor::set(alice, 0, 100, XRP(100)), + sponsor::SponseeAcc(bob), + delegate::As(carol), + Ter(tesSUCCESS)); + env.close(); + } + } + + void + testDelegateBlockReserveSponsor() + { + testcase("Delegate Block Reserve Sponsor"); + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const sponsor("sponsor"); + + // Co-signed reserve sponsorship is blocked for delegated transactions. + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, carol, sponsor); + env.close(); + + env(delegate::set(alice, bob, {"CheckCreate"})); + env.close(); + + env(check::create(alice, carol, XRP(1)), + delegate::As(bob), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(temINVALID)); + } + + // Pre-funded reserve sponsorship is blocked for delegated transactions. + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, carol, sponsor); + env.close(); + + env(delegate::set(alice, bob, {"CheckCreate"})); + env(sponsor::set_reserve(sponsor, 0, 1), sponsor::SponseeAcc(bob)); + env.close(); + + env(check::create(alice, carol, XRP(1)), + delegate::As(bob), + sponsor::As(sponsor, spfSponsorReserve), + Ter(temINVALID)); + } + } + + void + testDelegateSponsorFeePayer() + { + testcase("Delegate Sponsor Fee Payer"); + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const sponsor("sponsor"); + + // Co-signing: the sponsor account pays the delegated transaction fee. + { + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, bob, carol, sponsor); + env.close(); + + env(delegate::set(alice, bob, {"Payment"})); + env.close(); + + auto const aliceBalance = env.balance(alice); + auto const bobBalance = env.balance(bob); + auto const carolBalance = env.balance(carol); + auto const sponsorBalance = env.balance(sponsor); + auto const sendAmt = XRP(100); + auto const feeAmt = XRP(10); + + env(pay(alice, carol, sendAmt), + delegate::As(bob), + Fee(feeAmt), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + BEAST_EXPECT(env.balance(alice) == aliceBalance - sendAmt); + BEAST_EXPECT(env.balance(bob) == bobBalance); + BEAST_EXPECT(env.balance(carol) == carolBalance + sendAmt); + // sponsor pays the fee + BEAST_EXPECT(env.balance(sponsor) == sponsorBalance - feeAmt); + } + + // Pre-funded: the Sponsorship object for sponsorship(sponsor, delegate) + // pays the fee. + { + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, bob, carol, sponsor); + env.close(); + + env(delegate::set(alice, bob, {"Payment"})); + env(sponsor::set_fee(sponsor, 0, XRP(100)), sponsor::SponseeAcc(bob)); + env.close(); + + auto const aliceBalance = env.balance(alice); + auto const bobBalance = env.balance(bob); + auto const carolBalance = env.balance(carol); + auto const sponsorBalance = env.balance(sponsor); + auto const sponsorFee = sponsor::sponsorshipFeeBalance(env, sponsor, bob); + auto const sendAmt = XRP(100); + auto const feeAmt = XRP(10); + + // verify sponsorship(sponsor, alice) is not present, because we are testing + // sponsorship(sponsor, bob) will pay the fee. bob is sfDelegate. + BEAST_EXPECT(!env.le(keylet::sponsorship(sponsor, alice))); + + env(pay(alice, carol, sendAmt), + delegate::As(bob), + Fee(feeAmt), + sponsor::As(sponsor, spfSponsorFee)); + env.close(); + + BEAST_EXPECT(env.balance(alice) == aliceBalance - sendAmt); + BEAST_EXPECT(env.balance(bob) == bobBalance); + BEAST_EXPECT(env.balance(carol) == carolBalance + sendAmt); + BEAST_EXPECT(env.balance(sponsor) == sponsorBalance); + // sponsorship(sponsor, bob) pays the fee, bob is sfDelegate + BEAST_EXPECT(sponsor::sponsorshipFeeBalance(env, sponsor, bob) == sponsorFee - feeAmt); + } + } + + void + testBatch() + { + testcase("Batch"); + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + + // + // outer transaction + // + { + // test outer transaction with co-signing sponsor + Env env{*this, testableAmendments()}; + env.fund(XRP(1000), alice, bob, sponsor); + env.close(); + + auto const seq = env.seq(alice); + env(batch::outer(alice, seq, XRP(1), tfAllOrNothing), + batch::Inner(noop(alice), seq + 1), + batch::Inner(ticket::create(alice, 1), seq + 2), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Ter(tesSUCCESS)); + env.close(); + + // does not affect reserve + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + + // Fee is paid by sponsor + BEAST_EXPECT(env.balance(alice) == XRP(1000)); + BEAST_EXPECT(env.balance(sponsor) == XRP(1000 - 1)); + } + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000), alice, bob, sponsor); + env.close(); + + // spfSponsorReserve on outer Batch is rejected + for (auto const flags : {spfSponsorReserve | spfSponsorFee, spfSponsorReserve}) + { + auto const seq = env.seq(alice); + env(batch::outer(alice, seq, XRP(1), tfAllOrNothing), + batch::Inner(noop(alice), seq + 1), + batch::Inner(noop(alice), seq + 2), + sponsor::As(sponsor, flags), + Sig(sfSponsorSignature, sponsor), + Ter(temINVALID_FLAG)); + env.close(); + } + } + { + // test outer transaction with prefunded sponsor + Env env{*this, testableAmendments()}; + env.fund(XRP(1000), alice, bob); + env.fund(XRP(1001), sponsor); + env.close(); + + env(sponsor::set(sponsor, 0, 100, XRP(100)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tesSUCCESS)); + env.close(); + + auto const seq = env.seq(alice); + env(batch::outer(alice, seq, XRP(1), tfAllOrNothing), + batch::Inner(noop(alice), seq + 1), + batch::Inner(ticket::create(alice, 1), seq + 2), + sponsor::As(sponsor, spfSponsorFee), + Ter(tesSUCCESS)); + env.close(); + + // does not affect reserve + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + + // Fee is paid by sponsor object + BEAST_EXPECT(env.balance(alice) == XRP(1000)); + BEAST_EXPECT(env.balance(sponsor) == XRP(900)); + + auto const sponsorshipSle = env.le(keylet::sponsorship(sponsor, alice)); + BEAST_EXPECT(sponsorshipSle); + BEAST_EXPECT(sponsorshipSle->at(sfFeeAmount) == XRP(100 - 1)); + BEAST_EXPECT(sponsorshipSle->at(sfRemainingOwnerCount) == 100); + } + // + // Inner transaction + // + { + // test invalid Inner transaction with co-signing sponsor + Account const signerAccount("signer"); + Env env{*this, testableAmendments()}; + env.fund(XRP(1000), alice, bob, sponsor, signerAccount); + env.close(); + + env(signers(sponsor, 1, {Signer(signerAccount, 1)})); + env.close(); + + { + auto jt = env.jtnofill( + noop(alice), + sponsor::As(sponsor, spfSponsorReserve | spfSponsorFee), + Sig(sfSponsorSignature, sponsor)); + jt.jv.removeMember(sfTxnSignature.jsonName); + + auto const seq = env.seq(alice); + // should fail because Inner transaction cannot include SponsorSignature with + // TxnSignature + BEAST_EXPECT(jt.jv[sfSponsorSignature.jsonName].isMember(sfTxnSignature.jsonName)); + env(batch::outer(alice, seq, XRP(1), tfAllOrNothing), + batch::Inner(jt.jv, seq + 1), + batch::Inner(ticket::create(alice, 1), seq + 2), + Ter(temBAD_SIGNATURE)); + } + + { + auto jt = env.jtnofill( + noop(alice), + sponsor::As(sponsor, spfSponsorReserve | spfSponsorFee), + Msig(sfSponsorSignature, sponsor, signerAccount)); + jt.jv.removeMember(sfTxnSignature.jsonName); + + auto const seq = env.seq(alice); + // should fail because Inner transaction cannot include SponsorSignature with + // Signers + BEAST_EXPECT(jt.jv[sfSponsorSignature.jsonName].isMember(sfSigners.jsonName)); + env(batch::outer(alice, seq, XRP(1), tfAllOrNothing), + batch::Inner(jt.jv, seq + 1), + batch::Inner(ticket::create(alice, 1), seq + 2), + Ter(temBAD_SIGNER)); + } + + { + auto jt = env.jtnofill( + noop(alice), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + jt.jv.removeMember(sfTxnSignature.jsonName); + jt.jv[sfSponsorSignature.jsonName].removeMember(sfTxnSignature.jsonName); + jt.jv[sfSponsorSignature.jsonName][sfSigningPubKey.jsonName] = ""; + + auto const seq = env.seq(alice); + // should fail because BatchSigners does not have signer for SponsorSignature + env(batch::outer(alice, seq, XRP(1), tfAllOrNothing), + batch::Inner(jt.jv, seq + 1), + batch::Inner(ticket::create(alice, 1), seq + 2), + Ter(temBAD_SIGNER)); + } + } + + { + // test Inner transaction with prefunded sponsor + Env env{*this, testableAmendments()}; + env.fund(XRP(1000), alice, bob); + env.fund(XRP(1001), sponsor); + env.close(); + + env(sponsor::set(sponsor, 0, 100, XRP(100)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tesSUCCESS)); + env.close(); + BEAST_EXPECT(env.balance(sponsor) == XRP(900)); + + auto jt = env.jtnofill( + check::create(alice, bob, XRP(1)), sponsor::As(sponsor, spfSponsorReserve)); + // remove txn signature since it is filled by env.jtnofill() + jt.jv.removeMember(jss::TxnSignature); + + auto const seq = env.seq(alice); + env(batch::outer(alice, seq, XRP(1), tfAllOrNothing), + batch::Inner(noop(alice), seq + 1), + batch::Inner(jt.jv, seq + 2), + Ter(tesSUCCESS)); + env.close(); + + // affect sponsor reserve + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + + // Fee is paid by outer transaction originator (alice) + BEAST_EXPECT(env.balance(alice) == XRP(999)); + BEAST_EXPECT(env.balance(sponsor) == XRP(900)); + + // reserve count is decreased + auto const sponsorshipSle = env.le(keylet::sponsorship(sponsor, alice)); + BEAST_EXPECT(sponsorshipSle); + BEAST_EXPECT(sponsorshipSle->at(sfFeeAmount) == XRP(100)); + BEAST_EXPECT(sponsorshipSle->at(sfRemainingOwnerCount) == 99); + } + + { + // test Inner transaction with co-signing sponsor + Env env{*this, testableAmendments()}; + env.fund(XRP(1000), alice, bob, sponsor); + env.close(); + + auto jt = env.jtnofill( + check::create(alice, bob, XRP(1)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + // remove txn signature since it is filled by env.jtnofill() + jt.jv.removeMember(sfTxnSignature.jsonName); + jt.jv[sfSponsorSignature.jsonName].removeMember(sfTxnSignature.jsonName); + jt.jv[sfSponsorSignature.jsonName][sfSigningPubKey.jsonName] = ""; + + auto const seq = env.seq(alice); + env(batch::outer(alice, seq, XRP(1), tfAllOrNothing), + batch::Inner(noop(alice), seq + 1), + batch::Inner(jt.jv, seq + 2), + batch::Sig(sponsor), + Ter(tesSUCCESS)); + env.close(); + + // affect sponsor reserve + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + + // Fee is paid by outer transaction originator (alice) + BEAST_EXPECT(env.balance(alice) == XRP(999)); + BEAST_EXPECT(env.balance(sponsor) == XRP(1000)); + } + + // Inner tx with sfSponsor + spfSponsorFee (pre-funded) is rejected + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000), alice, bob, sponsor); + env.close(); + + // Create pre-funded sponsorship + env(sponsor::set(sponsor, 0, 0, XRP(1)), sponsor::SponseeAcc(alice), Fee(XRP(1))); + env.close(); + + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + + // Create inner transaction with sponsor fee + auto innerPay = pay(alice, bob, XRP(1)); + innerPay[sfSponsor.jsonName] = sponsor.human(); + innerPay[sfSponsorFlags.jsonName] = static_cast(spfSponsorFee); + + // Should be rejected with temBAD_FEE + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::Inner(innerPay, seq + 1), + batch::Inner(noop(alice), seq + 2), + Ter(temINVALID_FLAG)); + env.close(); + } + + // Inner tx with sfSponsor + spfSponsorFee (co-signed) is rejected + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000), alice, bob, sponsor); + env.close(); + + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 1, 2); + + auto innerPay = pay(alice, bob, XRP(1)); + innerPay[sfSponsor.jsonName] = sponsor.human(); + innerPay[sfSponsorFlags.jsonName] = static_cast(spfSponsorFee); + + // Should be rejected with temBAD_FEE + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::Inner(innerPay, seq + 1), + batch::Inner(noop(alice), seq + 2), + batch::Sig(sponsor), + Ter(temINVALID_FLAG)); + env.close(); + } + + // Inner tx with spfSponsorFee + spfSponsorReserve is rejected + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000), alice, bob, sponsor); + env.close(); + + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + + auto innerTx = check::create(alice, bob, XRP(1)); + innerTx[sfSponsor.jsonName] = sponsor.human(); + innerTx[sfSponsorFlags.jsonName] = + static_cast(spfSponsorFee | spfSponsorReserve); + + // Should be rejected with temBAD_FEE (fee sponsorship check comes first) + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::Inner(innerTx, seq + 1), + batch::Inner(noop(alice), seq + 2), + Ter(temINVALID_FLAG)); + env.close(); + } + + // Outer batch tx with sponsor fee is allowed + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000), alice, bob, sponsor); + env.close(); + + auto const seq = env.seq(bob); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + + // Outer batch with sponsor fee should work fine + env(batch::outer(bob, seq, batchFee, tfAllOrNothing), + batch::Inner(noop(bob), seq + 1), + batch::Inner(noop(bob), seq + 2), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Ter(tesSUCCESS)); + env.close(); + + // Sponsor paid the fee + BEAST_EXPECT(env.balance(bob) == XRP(1000)); + } + } + + // Verify that the central allow-list in preflight1Sponsor rejects + // spfSponsorReserve for transaction types that v1 does not permit. + void + testReserveSponsorGate() + { + testcase("Reserve sponsor allow-list gate"); + using namespace test::jtx; + + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + env(sponsor::set(sponsor, 0, 10, XRP(10)), sponsor::SponseeAcc(alice)); + env.close(); + + auto checkBlocked = [&](json::Value const& jv) { + env(jv, + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(temINVALID_FLAG)); + }; + + checkBlocked(ticket::create(alice, 1)); + checkBlocked(offer(alice, XRP(100), bob["USD"](100))); + checkBlocked(did::setValid(alice)); + checkBlocked(token::mint(alice, 0u)); + checkBlocked(sponsor::set(alice, 0, 10, XRP(10))); + checkBlocked(acctdelete(alice, bob)); + checkBlocked(loan::set(alice, uint256(1), Number{1})); + } + + void + testSponsorReserve(bool cosigning) + { + testRequireFlag(); + testSponsorReserveSimple(cosigning); + testCheck(cosigning); + testCredential(cosigning); + testDelegate(cosigning); + testDepositPreauth(cosigning); + testEscrow(cosigning); + testMPToken(cosigning); + testPayChan(cosigning); + testSignerList(cosigning); + testTrustSet(cosigning); + } + + void + testZeroBalanceSponsoredPaymentFeePayerCheck() + { + // Zero-balance sponsored Payment: getFeePayer() consistency check + testcase("Sponsored Payment: minimal-balance account with sponsor-pays-fee"); + + using namespace jtx; + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const sponsor("sponsor"); + Account const dest("dest"); + + auto const baseFee = env.current()->fees().base; + auto const baseReserve = env.current()->fees().reserve; + + // Fund sponsor and dest generously, alice with base reserve + 1 XRP for payment + env.fund(XRP(10000), sponsor, dest); + env.fund(baseReserve + XRP(1), alice); + env.close(); + + // Precondition: alice has base reserve + 1 XRP (enough for payment but not fee) + BEAST_EXPECT(env.balance(alice) == baseReserve + XRP(1)); + + // Alice tries to send a Payment to dest where sponsor pays the fee via spfSponsorFee. + // Passed even alice balance doesn't have enough to pay fee. + auto const preDest = env.balance(dest); + auto const preSponsor = env.balance(sponsor); + + // Alice sends 1 XRP to dest, sponsor pays the fee + env(pay(alice, dest, XRP(1)), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Fee(baseFee)); + env.close(); + + // Payment succeeded + // Alice's balance decreased by 1 XRP (the payment amount, NOT the fee) + BEAST_EXPECT(env.balance(alice) == baseReserve); + + // Dest received 1 XRP + BEAST_EXPECT(env.balance(dest) == preDest + XRP(1)); + + // Sponsor paid the fee (NOT alice) + BEAST_EXPECT(env.balance(sponsor) == preSponsor - baseFee); + } + + void + testTrustSetCounterpartySponsorMisroute() + { + // TrustSet's modify path applies the tx-level reserve sponsor to whichever + // side has its reserve gate trip on this update, regardless of whether that + // side belongs to the tx submitter. trustCreate only sets the submitter's + // reserve flag and snapshots the counterparty's asfDefaultRipple state into + // the line's NoRipple bit; if the counterparty later toggles asfDefaultRipple + // (the canonical issuer flow), the line and account flags disagree and on + // the submitter's next TrustSet the counterparty-side gate fires. Sponsor still will be + // checked if it can be applied to that end of the trustLine. + + testcase("TrustSet modify with sponsor does not misroute onto counterparty side"); + + using namespace test::jtx; + + Env env{*this, testableAmendments()}; + Account const alice{"alice_t2178"}; + Account const bob{"bob_t2178"}; + Account const carol{"carol_t2178"}; + + // Fund without auto-setting asfDefaultRipple + env.fund(XRP(100'000), alice, bob, carol); + env.close(); + + // Determine account ordering + bool const aliceIsHigh = alice.id() > bob.id(); + + // To trigger the bug, we need the COUNTERPARTY's reserve gate to trip + // We use issuer/holder terminology where: + // - holder creates the trust line (their reserve is set first) + // - issuer enables DefaultRipple after (creates flag mismatch) + // - holder's second TrustSet triggers issuer's reserve gate + + auto const issuer = aliceIsHigh ? bob : alice; + auto const holder = aliceIsHigh ? alice : bob; + auto const usd = issuer["USD"]; + + // Issuer must NOT have DefaultRipple set initially + // Clear it explicitly (env.fund may have set it) + env(fclear(issuer, asfDefaultRipple)); + env.close(); + + // Holder creates the trust line first (holder's reserve flag is set) + // At this point, issuer does NOT have DefaultRipple set, so + // the NoRipple bit on issuer's side is set according to issuer's current flag + env(trust(holder, usd(1'000))); + env.close(); + + // Issuer now enables asfDefaultRipple (canonical issuer flow) + // This creates a mismatch: issuer's account flag says DefaultRipple=true + // but the trust line's NoRipple bit on issuer's side is still set + env(fset(issuer, asfDefaultRipple)); + env.close(); + + SF_ACCOUNT const& issuerSponsorField = aliceIsHigh ? sfLowSponsor : sfHighSponsor; + SF_ACCOUNT const& holderSponsorField = aliceIsHigh ? sfHighSponsor : sfLowSponsor; + + auto const lineKey = keylet::trustLine(alice, bob, usd.currency); + auto const sleLineBefore = env.le(lineKey); + if (!BEAST_EXPECT(sleLineBefore)) + return; + BEAST_EXPECT(!sleLineBefore->isFieldPresent(sfLowSponsor)); + BEAST_EXPECT(!sleLineBefore->isFieldPresent(sfHighSponsor)); + + auto const carolBefore = sponsoringOwnerCount(env, carol); + BEAST_EXPECT(carolBefore == 0); + auto const issuerSponsoredBefore = sponsoredOwnerCount(env, issuer); + BEAST_EXPECT(issuerSponsoredBefore == 0); + + // Holder modifies the trust line with Carol as sponsor + // This should trigger the issuer's reserve gate because of the DefaultRipple mismatch + // Carol (sponsor) should NOT be applied to issuer's side (issuer != tx submitter) + env(trust(holder, usd(2'000)), + sponsor::As(carol, spfSponsorReserve), + Sig(sfSponsorSignature, carol), + Ter(tesSUCCESS)); + env.close(); + + auto const sleLineAfter = env.le(lineKey); + if (!BEAST_EXPECT(sleLineAfter)) + return; + + // Carol only agreed to back the holder, not the issuer + BEAST_EXPECT(!sleLineAfter->isFieldPresent(issuerSponsorField)); + + // Holder's side also has no sponsor because holder's reserve flag was + // already set on the FIRST TrustSet (no sponsor in scope then) + BEAST_EXPECT(!sleLineAfter->isFieldPresent(holderSponsorField)); + + // Carol's sponsoring count should remain unchanged (no misroute) + auto const carolAfter = sponsoringOwnerCount(env, carol); + BEAST_EXPECT(carolAfter == carolBefore); + + // Issuer's sponsored count should remain unchanged (no misroute) + auto const issuerSponsoredAfter = sponsoredOwnerCount(env, issuer); + BEAST_EXPECT(issuerSponsoredAfter == issuerSponsoredBefore); + } + + void + testSelfEscrowFinishReserveGate() + { + testcase("Self-escrow finish reserve order gated by amendment"); + using namespace test::jtx; + using namespace std::chrono_literals; + + // Finishing a self-escrow (source == destination) whose trust line + // was deleted while the escrow was outstanding auto-creates the line, + // and the outcome of that reserve check depends on whether the escrow + // reserve is released before delivery (Sponsor) or after (legacy). + // With the source's balance in the one-increment window + // [reserve(1), reserve(2)), the legacy order requires reserve(2) and + // fails, while the Sponsor order requires reserve(1) and succeeds. + auto runTest = [&](FeatureBitset features, TER expected) { + Account const alice("alice"); + Account const gw("gw"); + auto const usd = gw["usd"]; + + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + + env.fund(XRP(10000), alice, gw); + env.close(); + + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + + env.trust(usd(1000), alice); + env.close(); + env(pay(gw, alice, usd(100))); + env.close(); + + // Escrow alice's entire USD balance to herself. The escrowed + // IOUs return to the issuer, zeroing the line balance. + auto const seq = env.seq(alice); + env(escrow::create(alice, alice, usd(100)), + escrow::kCondition(escrow::kCb1), + escrow::kCancelTime(env.now() + 100s)); + env.close(); + + // Delete the now-empty trust line. Both accounts have + // DefaultRipple set (jtx fund does that), so a plain limit-0 + // TrustSet returns the line to its default state. + env(trust(alice, usd(0))); + env.close(); + BEAST_EXPECT(!env.le(keylet::trustLine(alice, gw, usd.currency))); + BEAST_EXPECT(ownerCount(env, alice) == 1); // just the escrow + + // Put alice's balance in the window. Pay the excess away + // directly: adjustAccountXRPBalance needs the Sponsor amendment. + STAmount const target = reserve(env, 1) + XRP(1); + env(pay(alice, env.master, env.balance(alice) - target - baseFee), Fee(baseFee)); + env.close(); + BEAST_EXPECT(env.balance(alice) == target); + + env(escrow::finish(alice, alice, seq), + escrow::kCondition(escrow::kCb1), + escrow::kFulfillment(escrow::kFb1), + Fee(baseFee * 150), + Ter(expected)); + env.close(); + + if (expected == tesSUCCESS) + { + BEAST_EXPECT(!env.le(keylet::escrow(alice, seq))); + BEAST_EXPECT(env.le(keylet::trustLine(alice, gw, usd.currency))); + BEAST_EXPECT(env.balance(alice, usd) == usd(100)); + BEAST_EXPECT(ownerCount(env, alice) == 1); // the new line + } + else + { + BEAST_EXPECT(env.le(keylet::escrow(alice, seq))); + BEAST_EXPECT(!env.le(keylet::trustLine(alice, gw, usd.currency))); + BEAST_EXPECT(ownerCount(env, alice) == 1); // still the escrow + } + }; + + // Pre-amendment: legacy order — the escrow still counts against the + // reserve while the auto-created line is checked. + runTest(testableAmendments() - featureSponsor, tecNO_LINE_INSUF_RESERVE); + + // Post-amendment: the escrow reserve is recycled into the new line. + runTest(testableAmendments(), tesSUCCESS); + } + + void + testFeeSponsoredVaultInvariant() + { + // The ValidVault invariant checks that the vault's balance and the + // depositor's balance change by equal amounts. For XRP vaults it adds the + // fee back into the depositor's balance change: normally the depositor + // pays the fee, so their balance drops by (deposit amount + fee), and + // adding the fee back leaves just the deposit amount to compare against + // the vault. But when a fee sponsor pays, the depositor's balance drops + // by only the deposit amount, so the fee must NOT be added back or the + // equal-amount check fails. + testcase("Fee-sponsored VaultDeposit/VaultWithdraw pass ValidVault invariant"); + using namespace test::jtx; + + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, sponsor); + env.close(); + + PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; + Vault const vault{env}; + auto [vaultTx, vaultKeylet] = vault.create({.owner = alice, .asset = xrpAsset}); + env(vaultTx); + env.close(); + + // Control: the same deposit shape, unsponsored, succeeds. + env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = xrpAsset(100)}), + Ter(tesSUCCESS)); + env.close(); + + // Fee-sponsored (co-signed) deposit succeeds. + env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = xrpAsset(100)}), + Fee(XRP(1)), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Ter(tesSUCCESS)); + env.close(); + + // The same helper (deltaAssetsTxAccount) drives the withdraw path, so a + // fee-sponsored withdrawal back to the depositor's own account also + // passes on the destination side. + env(vault.withdraw({.depositor = alice, .id = vaultKeylet.key, .amount = xrpAsset(50)}), + Fee(XRP(1)), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Ter(tesSUCCESS)); + env.close(); + } + + void + testSponsoredObjectDeletionRefund() + { + // Deleting a co-signed reserve-sponsored object must + // refund the sponsor's SponsoringOwnerCount back to zero. + testcase("Sponsored object deletion refunds sponsor owner count"); + using namespace test::jtx; + + Env env{*this, testableAmendments()}; + Account const sponsor("sponsor"); + env.fund(XRP(100000), sponsor); + env.close(); + + // Sponsored Check deletion - verify decreaseOwnerCountForObject + // (used by CheckCancel) reads the object's sfSponsor field and refunds + // the sponsor's owner count. + { + testcase(" — Check deletion"); + + Account const checkOwner("check_owner"); + Account const dest("check_dest"); + env.fund(XRP(100000), checkOwner, dest); + env.close(); + + // Create a check with co-signed reserve sponsorship. This bumps the + // sponsor's sfSponsoringOwnerCount rather than the check owner's. + auto const checkSeq = env.seq(checkOwner); + env(check::create(checkOwner, dest, XRP(1)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + auto sponsorCountBefore = sponsoringOwnerCount(env, sponsor); + BEAST_EXPECT(sponsorCountBefore == 1); // check costs 1 owner count + + // Cancel (delete) the check. + env(check::cancel(checkOwner, keylet::check(checkOwner, checkSeq).key)); + env.close(); + + auto sponsorCountAfter = sponsoringOwnerCount(env, sponsor); + BEAST_EXPECT(sponsorCountAfter == 0); // fully refunded + } + + // Sponsored TrustSet (trust line) deletion - verify the sponsor + // refund works when a sponsored trust line is deleted + { + testcase(" — TrustLine deletion"); + + Account const issuer("issuer"); + Account const holder("holder"); + env.fund(XRP(100000), issuer, holder); + env.close(); + + auto const usd = issuer["USD"]; + + // Create trust line with sponsorship on the holder side + env(trust(holder, usd(1000)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + auto sponsorCountBefore = sponsoringOwnerCount(env, sponsor); + BEAST_EXPECT(sponsorCountBefore == 1); // trust line costs 1 + + // Delete the trust line by clearing it to default + env(trust(holder, usd(0))); + env.close(); + + auto sponsorCountAfter = sponsoringOwnerCount(env, sponsor); + BEAST_EXPECT(sponsorCountAfter == 0); // fully refunded + } + } + +protected: + void + testSponsor() + { + testDisabled(); + testInvalidSponsorshipSet(); + testPseudoAccountSponsorship(); + + testSingleSigning(); + testMultiSigning(); + + testInvalidSponsorField(); + + testSimpleSponsorshipSet(); + + testPreFundAndCosign(); + testSponsoredFreeTierReserve(); + + testTransferSponsor(); + testLegacySignerListReserve(); + testSponsorFee(); + testSponsorAccount(); + + testAccountDelete(); + + testDelegatePermission(); + testDelegateBlockReserveSponsor(); + testDelegateSponsorFeePayer(); + + testBatch(); + + testSponsoredTrustLineNoFreeReserve(); + testCoSignReserveBoundedBySponsorshipBudget(); + testReserveSponsorGate(); + + testZeroBalanceSponsoredPaymentFeePayerCheck(); + testTrustSetCounterpartySponsorMisroute(); + testSelfEscrowFinishReserveGate(); + + testFeeSponsoredVaultInvariant(); + testSponsoredObjectDeletionRefund(); + } + + void + testTxSponsor(bool cosigning) + { + testSponsorReserve(cosigning); + } + +public: + void + run() override + { + testSponsor(); + } +}; + +class SponsorTxCosigning_test : public Sponsor_test +{ + void + run() override + { + testTxSponsor(true); + } +}; + +class SponsorTxPrefunded_test : public Sponsor_test +{ + void + run() override + { + testTxSponsor(false); + } +}; + +BEAST_DEFINE_TESTSUITE(Sponsor, app, xrpl); +BEAST_DEFINE_TESTSUITE(SponsorTxCosigning, app, xrpl); +BEAST_DEFINE_TESTSUITE(SponsorTxPrefunded, app, xrpl); + +} // namespace xrpl::test diff --git a/src/test/app/Ticket_test.cpp b/src/test/app/Ticket_test.cpp index 626cdbe44d..5700503830 100644 --- a/src/test/app/Ticket_test.cpp +++ b/src/test/app/Ticket_test.cpp @@ -503,7 +503,7 @@ class Ticket_test : public beast::unit_test::Suite Account const alice{"alice"}; // Fund alice not quite enough to make the reserve for a Ticket. - env.fund(env.current()->fees().accountReserve(1) - drops(1), alice); + env.fund(env.current()->fees().accountReserve(1, 1) - drops(1), alice); env.close(); env(ticket::create(alice, 1), Ter(tecINSUFFICIENT_RESERVE)); @@ -511,7 +511,8 @@ class Ticket_test : public beast::unit_test::Suite env.require(Owners(alice, 0), tickets(alice, 0)); // Give alice enough to exactly meet the reserve for one Ticket. - env(pay(env.master, alice, env.current()->fees().accountReserve(1) - env.balance(alice))); + env(pay( + env.master, alice, env.current()->fees().accountReserve(1, 1) - env.balance(alice))); env.close(); env(ticket::create(alice, 1)); @@ -524,7 +525,7 @@ class Ticket_test : public beast::unit_test::Suite env( pay(env.master, alice, - env.current()->fees().accountReserve(250) - drops(1) - env.balance(alice))); + env.current()->fees().accountReserve(250, 1) - drops(1) - env.balance(alice))); env.close(); // alice doesn't quite have the reserve for a total of 250 @@ -535,7 +536,8 @@ class Ticket_test : public beast::unit_test::Suite // Give alice enough so she can make the reserve for all 250 // Tickets. - env(pay(env.master, alice, env.current()->fees().accountReserve(250) - env.balance(alice))); + env(pay( + env.master, alice, env.current()->fees().accountReserve(250, 1) - env.balance(alice))); env.close(); std::uint32_t const ticketSeq{env.seq(alice) + 1}; diff --git a/src/test/app/TrustSet_test.cpp b/src/test/app/TrustSet_test.cpp index 15abc3bd06..e24373fd21 100644 --- a/src/test/app/TrustSet_test.cpp +++ b/src/test/app/TrustSet_test.cpp @@ -191,7 +191,7 @@ public: auto const txFee = env.current()->fees().base; auto const baseReserve = env.current()->fees().reserve; - auto const threelineReserve = env.current()->fees().accountReserve(3); + auto const threelineReserve = env.current()->fees().accountReserve(3, 1); env.fund(XRP(10000), gwA, gwB, assistor); diff --git a/src/test/app/TxQ_test.cpp b/src/test/app/TxQ_test.cpp index 91eb26da7e..3175e742d9 100644 --- a/src/test/app/TxQ_test.cpp +++ b/src/test/app/TxQ_test.cpp @@ -20,6 +20,8 @@ #include #include #include +#include +#include #include #include #include @@ -2335,6 +2337,43 @@ public: BEAST_EXPECT(env.balance(alice) == drops(5)); } + void + testSponsorTxCannotQueue() + { + using namespace jtx; + testcase("disallow sponsored transaction from being queued"); + + Env env(*this, makeConfig({{Keys::kMinimumTxnInLedgerStandalone, "3"}})); + + auto sponsor = Account("sponsor"); + auto sponsee = Account("sponsee"); + auto filler = Account("filler"); + + env.fund(XRP(50000), noripple(sponsor, sponsee)); + env.close(); + env.fund(XRP(50000), noripple(filler)); + env.close(); + + fillQueue(env, filler); + checkMetrics(*this, env, 0, 6, 4, 3); + + // Sponsored transactions are not allowed to be queued. + env(noop(sponsee), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Ter(telCAN_NOT_QUEUE)); + checkMetrics(*this, env, 0, 6, 4, 3); + + // Sponsored transactions may still apply directly if they pay the + // open ledger fee. They just cannot be held in the queue. + env(noop(sponsee), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Fee(openLedgerCost(env)), + Ter(tesSUCCESS)); + checkMetrics(*this, env, 0, 6, 5, 3); + } + void testDelegateTxCannotQueue() { @@ -4698,6 +4737,7 @@ public: testBlockersSeq(); testBlockersTicket(); testInFlightBalance(); + testSponsorTxCannotQueue(); testDelegateTxCannotQueue(); testConsequences(); } diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 3877e08f7e..4de8c9c616 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -1804,7 +1805,7 @@ class Vault_test : public beast::unit_test::Suite auto const [acctReserve, incReserve] = [this]() -> std::pair { Env const env{*this, testableAmendments()}; return { - env.current()->fees().accountReserve(0).drops() / kDropsPerXrp.drops(), + env.current()->fees().accountReserve(0, 1).drops() / kDropsPerXrp.drops(), env.current()->fees().increment.drops() / kDropsPerXrp.drops()}; }(); @@ -2686,7 +2687,7 @@ class Vault_test : public beast::unit_test::Suite auto const [acctReserve, incReserve] = [this]() -> std::pair { Env const env{*this, testableAmendments()}; return { - env.current()->fees().accountReserve(0).drops() / kDropsPerXrp.drops(), + env.current()->fees().accountReserve(0, 1).drops() / kDropsPerXrp.drops(), env.current()->fees().increment.drops() / kDropsPerXrp.drops()}; }(); @@ -5927,8 +5928,9 @@ class Vault_test : public beast::unit_test::Suite token->setFieldVL(*field, gMakeZeroBuffer(kEcGamalEncryptedTotalLength)); sb.update(token); + auto const dummyTx = *env.jt(noop(holder)).stx; BEAST_EXPECT( - removeEmptyHolding(sb, holder.id(), MPTIssue(mpt.issuanceID()), j) == + removeEmptyHolding({sb, dummyTx}, holder.id(), MPTIssue(mpt.issuanceID()), j) == tecHAS_OBLIGATIONS); BEAST_EXPECT(sb.peek(tokenKeylet) != nullptr); } diff --git a/src/test/app/XChain_test.cpp b/src/test/app/XChain_test.cpp index 6c8377d8e3..4b007eea13 100644 --- a/src/test/app/XChain_test.cpp +++ b/src/test/app/XChain_test.cpp @@ -141,7 +141,7 @@ struct SEnv XRPAmount reserve(std::uint32_t count) { - return env.current()->fees().accountReserve(count); + return env.current()->fees().accountReserve(count, 1); } XRPAmount @@ -370,7 +370,7 @@ struct XChain_test : public beast::unit_test::Suite, public jtx::XChainBridgeObj XRPAmount reserve(std::uint32_t count) { - return XEnv(*this).env.current()->fees().accountReserve(count); + return XEnv(*this).env.current()->fees().accountReserve(count, 1); } XRPAmount diff --git a/src/test/jtx/Env.h b/src/test/jtx/Env.h index ba28d19738..04cbce953a 100644 --- a/src/test/jtx/Env.h +++ b/src/test/jtx/Env.h @@ -561,6 +561,24 @@ public: [[nodiscard]] std::uint32_t ownerCount(Account const& account) const; + /** Return the number of sponsored objects owned by an account. + * Throws if the account does not exist. + */ + [[nodiscard]] std::uint32_t + sponsoredOwnerCount(Account const& account) const; + + /** Return the number of sponsoring objects owned by an account. + * Throws if the account does not exist. + */ + [[nodiscard]] std::uint32_t + sponsoringOwnerCount(Account const& account) const; + + /** Return the number of sponsoring accounts owned by an account. + * Throws if the account does not exist. + */ + [[nodiscard]] std::uint32_t + sponsoringAccountCount(Account const& account) const; + /** Return an account root. @return empty if the account does not exist. */ diff --git a/src/test/jtx/JTx.h b/src/test/jtx/JTx.h index da105e01aa..ed585ebb6e 100644 --- a/src/test/jtx/JTx.h +++ b/src/test/jtx/JTx.h @@ -37,7 +37,7 @@ struct JTx // Functions that sign the transaction from the Account std::vector> mainSigners; // Functions that sign something else after the mainSigners, such as - // sfCounterpartySignature + // sfCounterpartySignature and sfSponsorSignature std::vector> postSigners; JTx() = default; diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index cb2193db99..4c18d7343c 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -379,6 +379,18 @@ checkArraySize(json::Value const& val, unsigned int size); std::uint32_t ownerCount(test::jtx::Env const& env, test::jtx::Account const& account); +// Helper function that returns the sponsored owner count on an account. +std::uint32_t +sponsoredOwnerCount(test::jtx::Env const& env, test::jtx::Account const& account); + +// Helper function that returns the sponsoring owner count on an account. +std::uint32_t +sponsoringOwnerCount(test::jtx::Env const& env, test::jtx::Account const& account); + +// Helper function that returns the sponsoring account count on an account. +std::uint32_t +sponsoringAccountCount(test::jtx::Env const& env, test::jtx::Account const& account); + [[nodiscard]] inline bool checkVL(Slice const& result, std::string const& expected) diff --git a/src/test/jtx/impl/AMMTest.cpp b/src/test/jtx/impl/AMMTest.cpp index 46c5e44bd3..3b4aae20e2 100644 --- a/src/test/jtx/impl/AMMTest.cpp +++ b/src/test/jtx/impl/AMMTest.cpp @@ -197,7 +197,7 @@ AMMTestBase::testAMM(std::function const& cb, TestAM XRPAmount AMMTest::reserve(jtx::Env& env, std::uint32_t count) { - return env.current()->fees().accountReserve(count); + return env.current()->fees().accountReserve(count, 1); } XRPAmount diff --git a/src/test/jtx/impl/Env.cpp b/src/test/jtx/impl/Env.cpp index db45deb6de..3f6aca9fcb 100644 --- a/src/test/jtx/impl/Env.cpp +++ b/src/test/jtx/impl/Env.cpp @@ -271,6 +271,33 @@ Env::ownerCount(Account const& account) const return sle->getFieldU32(sfOwnerCount); } +std::uint32_t +Env::sponsoredOwnerCount(Account const& account) const +{ + auto const sle = le(account); + if (!sle) + Throw("missing account root"); + return sle->getFieldU32(sfSponsoredOwnerCount); +} + +std::uint32_t +Env::sponsoringOwnerCount(Account const& account) const +{ + auto const sle = le(account); + if (!sle) + Throw("missing account root"); + return sle->getFieldU32(sfSponsoringOwnerCount); +} + +std::uint32_t +Env::sponsoringAccountCount(Account const& account) const +{ + auto const sle = le(account); + if (!sle) + Throw("missing account root"); + return sle->getFieldU32(sfSponsoringAccountCount); +} + std::uint32_t Env::seq(Account const& account) const { diff --git a/src/test/jtx/impl/TestHelpers.cpp b/src/test/jtx/impl/TestHelpers.cpp index 95c8a68436..4d3869b4f9 100644 --- a/src/test/jtx/impl/TestHelpers.cpp +++ b/src/test/jtx/impl/TestHelpers.cpp @@ -94,6 +94,24 @@ ownerCount(Env const& env, Account const& account) return env.ownerCount(account); } +std::uint32_t +sponsoredOwnerCount(Env const& env, Account const& account) +{ + return env.sponsoredOwnerCount(account); +} + +std::uint32_t +sponsoringOwnerCount(Env const& env, Account const& account) +{ + return env.sponsoringOwnerCount(account); +} + +std::uint32_t +sponsoringAccountCount(Env const& env, Account const& account) +{ + return env.sponsoringAccountCount(account); +} + /* Path finding */ /******************************************************************************/ void diff --git a/src/test/jtx/impl/multisign.cpp b/src/test/jtx/impl/multisign.cpp index 66c5a4f27d..d948042bda 100644 --- a/src/test/jtx/impl/multisign.cpp +++ b/src/test/jtx/impl/multisign.cpp @@ -64,7 +64,8 @@ Msig::operator()(Env& env, JTx& jt) const { auto const mySigners = signers; auto callback = [subField = subField, mySigners, &env](Env&, JTx& jtx) { - // Where to put the signature. Supports sfCounterPartySignature. + // Where to put the signature. Supports sfCounterPartySignature and + // sfSponsorSignature. auto& sigObject = subField ? jtx[*subField] : jtx.jv; // The signing pub key is only required at the top level. diff --git a/src/test/jtx/impl/owners.cpp b/src/test/jtx/impl/owners.cpp index 2ff93757f0..ed10ca6930 100644 --- a/src/test/jtx/impl/owners.cpp +++ b/src/test/jtx/impl/owners.cpp @@ -44,6 +44,24 @@ Owners::operator()(Env& env) const env.test.expect(env.le(account_)->getFieldU32(sfOwnerCount) == value_); } +void +SponsoredOwners::operator()(Env& env) const +{ + env.test.expect(env.le(account_)->getFieldU32(sfSponsoredOwnerCount) == value_); +} + +void +SponsoringOwners::operator()(Env& env) const +{ + env.test.expect(env.le(account_)->getFieldU32(sfSponsoringOwnerCount) == value_); +} + +void +SponsoringAccountCount::operator()(Env& env) const +{ + env.test.expect(env.le(account_)->getFieldU32(sfSponsoringAccountCount) == value_); +} + } // namespace test::jtx } // namespace xrpl diff --git a/src/test/jtx/impl/sig.cpp b/src/test/jtx/impl/sig.cpp index 7140fad3de..e0123073b1 100644 --- a/src/test/jtx/impl/sig.cpp +++ b/src/test/jtx/impl/sig.cpp @@ -18,7 +18,7 @@ Sig::operator()(Env&, JTx& jt) const // VFALCO Inefficient pre-C++14 auto const account = *account_; auto callback = [subField = subField_, account](Env&, JTx& jtx) { - // Where to put the signature. Supports sfCounterPartySignature. + // Where to put the signature. Supports sfCounterPartySignature and sfSponsorSignature. auto& sigObject = subField ? jtx[*subField] : jtx.jv; jtx::sign(jtx.jv, account, sigObject); diff --git a/src/test/jtx/impl/sponsor.cpp b/src/test/jtx/impl/sponsor.cpp new file mode 100644 index 0000000000..cdf68800f5 --- /dev/null +++ b/src/test/jtx/impl/sponsor.cpp @@ -0,0 +1,98 @@ +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::test::jtx::sponsor { + +json::Value +set(jtx::Account const& account, + uint32_t flags, + std::optional const reserveCount, + std::optional const feeAmount, + std::optional const maxFee) +{ + json::Value jv; + jv[jss::TransactionType] = jss::SponsorshipSet; + jv[jss::Account] = account.human(); + jv[sfFlags.jsonName] = flags; + if (reserveCount) + jv[sfRemainingOwnerCount.jsonName] = *reserveCount; + if (feeAmount) + jv[sfFeeAmount.jsonName] = feeAmount->getJson(JsonOptions::Values::None); + if (maxFee) + jv[sfMaxFee.jsonName] = maxFee->getJson(JsonOptions::Values::None); + return jv; +} + +json::Value +del(jtx::Account const& account) +{ + json::Value jv; + jv[jss::TransactionType] = jss::SponsorshipSet; + jv[jss::Account] = account.human(); + jv[sfFlags.jsonName] = tfDeleteObject; + return jv; +} + +json::Value +transfer(jtx::Account const& account, uint32_t flags, std::optional const& index) +{ + json::Value jv; + jv[jss::TransactionType] = jss::SponsorshipTransfer; + jv[jss::Account] = account.human(); + jv[sfFlags.jsonName] = flags; + if (index) + jv[sfObjectID.jsonName] = to_string(*index); + return jv; +} + +void +CounterpartySponsor::operator()(Env& env, JTx& jt) const +{ + jt.jv[sfCounterpartySponsor.jsonName] = sponsor_.human(); +} + +void +SponseeAcc::operator()(Env& env, JTx& jt) const +{ + jt.jv[sfSponsee.jsonName] = sponsee_.human(); +} + +void +As::operator()(Env& env, JTx& jt) const +{ + jt.jv[sfSponsor.jsonName] = sponsor_.human(); + jt.jv[sfSponsorFlags.jsonName] = flags_; +} + +json::Value +ledgerEntry(jtx::Env& env, jtx::Account const& sponsor, jtx::Account const& sponsee) +{ + json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::sponsorship][jss::sponsor] = sponsor.human(); + jvParams[jss::sponsorship][jss::sponsee] = sponsee.human(); + return env.rpc("json", "ledger_entry", to_string(jvParams)); +} + +STAmount +sponsorshipFeeBalance(jtx::Env& env, jtx::Account const& sponsor, jtx::Account const& sponsee) +{ + return env.le(keylet::sponsorship(sponsor, sponsee))->getFieldAmount(sfFeeAmount).xrp(); +} + +} // namespace xrpl::test::jtx::sponsor diff --git a/src/test/jtx/owners.h b/src/test/jtx/owners.h index 0fb38b407c..3f64601244 100644 --- a/src/test/jtx/owners.h +++ b/src/test/jtx/owners.h @@ -64,6 +64,60 @@ public: operator()(Env& env) const; }; +/** Match the account's SponsoredOwnerCount field: the number of owned + objects whose reserve is sponsored by another account. */ +class SponsoredOwners +{ +private: + Account account_; + std::uint32_t value_; + +public: + SponsoredOwners(Account account, std::uint32_t value) + : account_(std::move(account)), value_(value) + { + } + + void + operator()(Env& env) const; +}; + +/** Match the account's SponsoringOwnerCount field: the number of objects + (owned by other accounts) whose reserve this account sponsors. */ +class SponsoringOwners +{ +private: + Account account_; + std::uint32_t value_; + +public: + SponsoringOwners(Account account, std::uint32_t value) + : account_(std::move(account)), value_(value) + { + } + + void + operator()(Env& env) const; +}; + +/** Match the account's SponsoringAccountCount field: the number of accounts + whose base reserve this account sponsors. */ +class SponsoringAccountCount +{ +private: + Account account_; + std::uint32_t value_; + +public: + SponsoringAccountCount(Account account, std::uint32_t value) + : account_(std::move(account)), value_(value) + { + } + + void + operator()(Env& env) const; +}; + /** Match the number of trust lines in the account's owner directory */ using lines = OwnerCount; diff --git a/src/test/jtx/sponsor.h b/src/test/jtx/sponsor.h new file mode 100644 index 0000000000..43d55d7246 --- /dev/null +++ b/src/test/jtx/sponsor.h @@ -0,0 +1,105 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +namespace xrpl::test::jtx::sponsor { + +json::Value +set(jtx::Account const& account, + std::uint32_t flags, + std::optional const reserveCount = std::nullopt, + std::optional const feeAmount = std::nullopt, + std::optional const maxFee = std::nullopt); + +inline json::Value +set_fee( + jtx::Account const& account, + std::uint32_t flags, + STAmount feeAmount, + std::optional maxFee = std::nullopt) +{ + return set(account, flags, std::nullopt, std::move(feeAmount), std::move(maxFee)); +} + +inline json::Value +set_reserve(jtx::Account const& account, std::uint32_t flags, std::uint32_t reserveCount) +{ + return set(account, flags, reserveCount); +} + +inline json::Value +set_max_fee(jtx::Account const& account, std::uint32_t flags, STAmount maxFee) +{ + return set(account, flags, std::nullopt, std::nullopt, std::move(maxFee)); +} + +json::Value +del(jtx::Account const& account); + +json::Value +transfer( + jtx::Account const& account, + uint32_t flags, + std::optional const& index = std::nullopt); + +struct CounterpartySponsor +{ +private: + jtx::Account sponsor_; + +public: + CounterpartySponsor(jtx::Account account) : sponsor_(std::move(account)) + { + } + + void + operator()(jtx::Env&, jtx::JTx& jtx) const; +}; + +struct SponseeAcc +{ +private: + jtx::Account sponsee_; + +public: + SponseeAcc(jtx::Account account) : sponsee_(std::move(account)) + { + } + + void + operator()(jtx::Env&, jtx::JTx& jtx) const; +}; + +struct As +{ +private: + jtx::Account sponsor_; + std::uint32_t flags_; + +public: + As(jtx::Account account, std::uint32_t flags = 0) : sponsor_(std::move(account)), flags_(flags) + { + } + + void + operator()(jtx::Env&, jtx::JTx& jtx) const; +}; + +json::Value +ledgerEntry(jtx::Env& env, jtx::Account const& sponsor, jtx::Account const& sponsee); + +STAmount +sponsorshipFeeBalance(jtx::Env& env, jtx::Account const& sponsor, jtx::Account const& sponsee); + +} // namespace xrpl::test::jtx::sponsor diff --git a/src/test/ledger/PaymentSandbox_test.cpp b/src/test/ledger/PaymentSandbox_test.cpp index f59b75091a..75f7410f94 100644 --- a/src/test/ledger/PaymentSandbox_test.cpp +++ b/src/test/ledger/PaymentSandbox_test.cpp @@ -1,17 +1,23 @@ #include #include #include +#include #include #include #include #include #include +#include +#include +#include +#include #include #include #include #include #include +#include #include #include #include @@ -19,7 +25,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -236,7 +244,7 @@ class PaymentSandbox_test : public beast::unit_test::Suite auto const startingAmount = accountHolds(pv, alice, iss.currency, iss.account, FreezeHandling::IgnoreFreeze, j); - BEAST_EXPECT(issueIOU(pv, alice, toCredit, iss, j) == tesSUCCESS); + BEAST_EXPECT(issueIOU(pv, alice, toCredit, iss, {}, j) == tesSUCCESS); BEAST_EXPECT( accountHolds( pv, alice, iss.currency, iss.account, FreezeHandling::IgnoreFreeze, j) == @@ -330,7 +338,7 @@ class PaymentSandbox_test : public beast::unit_test::Suite }; auto reserve = [](jtx::Env& env, std::uint32_t count) -> XRPAmount { - return env.current()->fees().accountReserve(count); + return env.current()->fees().accountReserve(count, 1); }; Env env(*this, features); @@ -389,6 +397,217 @@ class PaymentSandbox_test : public beast::unit_test::Suite BEAST_EXPECT(balance.getIssuer() == usd.account.id()); } + void + testOwnerCountHook(FeatureBitset features) + { + // Test that PaymentSandbox::adjustOwnerCountHook and ownerCountHook + // correctly track and return the maximum owner counts during a payment. + testcase("ownerCountHook"); + + using namespace jtx; + Env env(*this, features); + Account const alice("alice"); + Account const sponsor("sponsor"); + + env.fund(XRP(10000), alice, sponsor); + env.close(); + + ApplyViewImpl av(&*env.current(), TapNone); + PaymentSandbox sb(&av); + + // Test basic owner count hook without sponsor + { + auto const aliceSle = sb.peek(keylet::account(alice)); + BEAST_EXPECT(aliceSle); + + OwnerCounts const initial(aliceSle); + OwnerCounts updated = initial; + updated.owner = initial.owner + 2; + + // Simulate adjusting owner count + sb.adjustOwnerCountHook(alice, initial, updated); + + // ownerCountHook should return the max value + OwnerCounts const retrieved = sb.ownerCountHook(alice, initial); + BEAST_EXPECT(retrieved.owner == updated.owner); + BEAST_EXPECT(retrieved.sponsored == updated.sponsored); + BEAST_EXPECT(retrieved.sponsoring == updated.sponsoring); + } + + // Test owner count hook with sponsor-related counts + { + auto const sponsorSle = sb.peek(keylet::account(sponsor)); + BEAST_EXPECT(sponsorSle); + + OwnerCounts const sponsorInitial(sponsorSle); + OwnerCounts sponsorUpdated = sponsorInitial; + sponsorUpdated.owner = sponsorInitial.owner + 1; + sponsorUpdated.sponsoring = sponsorInitial.sponsoring + 1; + + sb.adjustOwnerCountHook(sponsor, sponsorInitial, sponsorUpdated); + + OwnerCounts const sponsorRetrieved = sb.ownerCountHook(sponsor, sponsorInitial); + BEAST_EXPECT(sponsorRetrieved.owner == sponsorUpdated.owner); + BEAST_EXPECT(sponsorRetrieved.sponsoring == sponsorUpdated.sponsoring); + } + + // Test with stacked PaymentSandboxes + { + PaymentSandbox sb2(&sb); + + auto const aliceSle = sb2.peek(keylet::account(alice)); + OwnerCounts const current(aliceSle); + OwnerCounts further = current; + further.owner = current.owner + 3; + + sb2.adjustOwnerCountHook(alice, current, further); + + // The nested sandbox should see the max from both levels + OwnerCounts const retrieved = sb2.ownerCountHook(alice, OwnerCounts()); + BEAST_EXPECT(retrieved.owner >= further.owner); + } + + // Test that max logic works correctly + { + auto const aliceSle = sb.peek(keylet::account(alice)); + OwnerCounts const current(aliceSle); + OwnerCounts lower = current; + lower.owner = (current.owner > 0) ? current.owner - 1 : 0; + + // Adjusting to a lower value + sb.adjustOwnerCountHook(alice, current, lower); + + // Should still return the higher value seen previously + OwnerCounts const retrieved = sb.ownerCountHook(alice, OwnerCounts()); + BEAST_EXPECT(retrieved.owner >= lower.owner); + } + } + + void + testOwnerCountWithTransaction(FeatureBitset features) + { + // Test that owner count hooks work correctly during actual transactions. + // This verifies that when transactions modify owner counts (by creating + // or deleting ledger objects), the hooks properly track these changes. + testcase( + std::string("ownerCountWithTransaction") + + (features[featureSponsor] ? " with sponsor" : " without sponsor")); + + using namespace jtx; + + auto reserve = [](jtx::Env& env, std::uint32_t count) -> XRPAmount { + return env.current()->fees().accountReserve(count, 1); + }; + + Env env(*this, features); + Account const gw("gw"); + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + + auto const usd = gw["USD"]; + + // Fund accounts. Alice starts with exactly enough for base reserve + 2 objects + env.fund(XRP(10000), gw, bob, sponsor); + env.fund(reserve(env, 3) + XRP(100), alice); // Base + 2 objects + extra for fees + env.close(); + + // Verify initial state - no owner count + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(ownerCount(env, bob) == 0); + + // Create a trust line - this increases owner count + env(trust(alice, usd(1000))); + env.close(); + + // alice now has 1 object (owner count = 1) + BEAST_EXPECT(ownerCount(env, alice) == 1); + + // Create an offer - this further increases owner count + env(trust(bob, usd(1000))); + env(pay(gw, alice, usd(100))); + env.close(); + + auto const aliceOfferSeq = env.seq(alice); // Capture the sequence before creating offer + env(offer(alice, usd(50), XRP(50))); + env.close(); + + // alice now has 2 objects (trust line + offer) + BEAST_EXPECT(ownerCount(env, alice) == 2); + + // If sponsor feature is enabled, test sponsorship transfer + if (features[featureSponsor]) + { + auto const trustId = keylet::trustLine(alice, gw, usd.currency); + BEAST_EXPECT(env.le(trustId)); + + // Transfer sponsorship - sponsor now sponsors alice's trust line + env(sponsor::transfer(alice, tfSponsorshipCreate, trustId.key), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + // alice still has 2 objects but 1 is sponsored + BEAST_EXPECT(ownerCount(env, alice) == 2); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + // sponsor's sponsoring count should increase + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + } + + // Verify alice's available balance respects the reserve + auto const aliceSle = env.le(keylet::account(alice)); + BEAST_EXPECT(aliceSle); + + auto const aliceBalance = aliceSle->getFieldAmount(sfBalance); + // With sponsor, 1 object is sponsored so only 1 counts for reserve + auto const aliceReserve = reserve(env, features[featureSponsor] ? 1 : 2); + + // alice should have limited available balance after accounting for reserve + auto const available = aliceBalance.xrp() - aliceReserve; + if (features[featureSponsor]) + { + // With sponsor, alice has more available (1 sponsored object = less reserve) + BEAST_EXPECT(available > XRP(150)); + } + else + { + BEAST_EXPECT(available < XRP(150)); // Most of the balance is in reserve + } + + // Try to send nearly all balance - should fail due to reserve in both cases + auto const tooMuch = aliceBalance.xrp() - XRP(1); + env(pay(alice, bob, tooMuch), Ter(tecUNFUNDED_PAYMENT)); + env.close(); + + // Verify owner count hasn't changed + BEAST_EXPECT(ownerCount(env, alice) == 2); + + // Cancel the offer - this decreases owner count + env(offerCancel(alice, aliceOfferSeq)); + env.close(); + + // alice now has 1 object (just the trust line) + BEAST_EXPECT(ownerCount(env, alice) == 1); + + if (features[featureSponsor]) + { + // Verify sponsored count stayed the same (trust line is still sponsored) + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + } + + // Now alice should have more available balance (less reserve needed) + auto const aliceSle2 = env.le(keylet::account(alice)); + auto const aliceBalance2 = aliceSle2->getFieldAmount(sfBalance); + // With sponsor, trust line is still sponsored so 0 objects for reserve + // Without sponsor, 1 object for reserve + auto const aliceReserve2 = reserve(env, features[featureSponsor] ? 0 : 1); + auto const available2 = aliceBalance2.xrp() - aliceReserve2; + + // available2 should be greater than available (less reserve needed) + BEAST_EXPECT(available2 > available); + } + public: void run() override @@ -399,11 +618,16 @@ public: testTinyBalance(features); testReserve(features); testBalanceHook(features); + testOwnerCountHook(features); }; using namespace jtx; auto const sa = testableAmendments(); testAll(sa - featurePermissionedDEX); testAll(sa); + + // Test owner count with transactions + testOwnerCountWithTransaction(sa - featureSponsor); + testOwnerCountWithTransaction(sa); } }; diff --git a/src/test/rpc/AccountObjects_test.cpp b/src/test/rpc/AccountObjects_test.cpp index 31b20b37d4..6ba6e7de9f 100644 --- a/src/test/rpc/AccountObjects_test.cpp +++ b/src/test/rpc/AccountObjects_test.cpp @@ -9,8 +9,11 @@ #include // IWYU pragma: keep #include #include +#include +#include #include #include +#include #include #include @@ -22,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -200,6 +204,15 @@ public: resp[jss::result][jss::error_message] == "Invalid field 'limit', not unsigned integer."); } + // test error on sponsored param not a boolean + { + json::Value params; + params[jss::account] = bob.human(); + params[jss::sponsored] = "true"; + auto resp = env.rpc("json", "account_objects", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == "Invalid field 'sponsored', not boolean."); + } // test errors on marker { Account const gw{"G"}; @@ -605,6 +618,7 @@ public: BEAST_EXPECT(acctObjsIsSize(acctObjs(gw, jss::amm), 0)); BEAST_EXPECT(acctObjsIsSize(acctObjs(gw, jss::did), 0)); BEAST_EXPECT(acctObjsIsSize(acctObjs(gw, jss::permissioned_domain), 0)); + BEAST_EXPECT(acctObjsIsSize(acctObjs(gw, jss::sponsorship), 0)); // we expect invalid field type reported for the following types BEAST_EXPECT(acctObjsTypeIsInvalid(acctObjs(gw, jss::amendments))); @@ -926,6 +940,30 @@ public: BEAST_EXPECT(ticket[sfTicketSequence.jsonName].asUInt() == seq + 1); } + { + // Create a sponsorship + env(sponsor::set(alice, tfSponsorshipSetRequireSignForFee, 200, XRP(100), drops(10)), + sponsor::SponseeAcc(gw)); + env.close(); + + // Find the sponsorship. + for (auto const& acct : {alice, gw}) + { + json::Value const resp = acctObjs(acct, jss::sponsorship); + BEAST_EXPECT(acctObjsIsSize(resp, 1)); + + auto const& sponsorship = resp[jss::result][jss::account_objects][0u]; + + BEAST_EXPECT(sponsorship[sfOwner.jsonName] == alice.human()); + BEAST_EXPECT(sponsorship[sfSponsee.jsonName] == gw.human()); + BEAST_EXPECT( + sponsorship[sfFlags.jsonName].asUInt() == tfSponsorshipSetRequireSignForFee); + BEAST_EXPECT(sponsorship[sfRemainingOwnerCount.jsonName].asUInt() == 200); + BEAST_EXPECT(sponsorship[sfFeeAmount.jsonName].asUInt() == 100000000); + BEAST_EXPECT(sponsorship[sfMaxFee.jsonName].asUInt() == 10); + } + } + { // See how "deletion_blockers_only" handles gw's directory. json::Value params; @@ -940,7 +978,8 @@ public: jss::NFTokenPage.cStr(), jss::RippleState.cStr(), jss::PayChannel.cStr(), - jss::PermissionedDomain.cStr()}; + jss::PermissionedDomain.cStr(), + jss::Sponsorship.cStr()}; std::ranges::sort(v); return v; }(); @@ -1350,6 +1389,233 @@ public: } } + void + testSponsoredFilter() + { + testcase("SponsoredFilter"); + using namespace jtx; + + Env env(*this, testableAmendments()); + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor1("sponsor1"); + Account const gw("gw"); + auto const usd = gw["USD"]; + + env.fund(XRP(10000), alice, bob, sponsor1, gw); + env.close(); + + // Helper to call account_objects with sponsored filter + auto acctObjsSponsored = [](Env& testEnv, + AccountID const& acct, + bool sponsored, + std::optional const& type = std::nullopt) { + json::Value params; + params[jss::account] = to_string(acct); + params[jss::sponsored] = sponsored; + if (type) + params[jss::type] = *type; + params[jss::ledger_index] = "validated"; + return testEnv.rpc("json", "account_objects", to_string(params)); + }; + + // Create a trust line for bob (not sponsored) + env(trust(bob, usd(1000))); + env.close(); + + // sponsored=true should not find any objects for bob (doesn't have any sponsored objects) + { + auto const resp = acctObjsSponsored(env, bob.id(), true); + auto const& objs = resp[jss::result][jss::account_objects]; + BEAST_EXPECT(objs.size() == 0); + } + + // Now sponsor bob's trust line + auto const trustId = keylet::trustLine(bob, gw, usd.currency); + if (!BEAST_EXPECT(env.le(trustId))) + return; + + env(sponsor::transfer(bob, tfSponsorshipCreate, trustId.key), + sponsor::As(sponsor1, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor1)); + env.close(); + + // Verify trust line has sponsor field + { + auto const sle = env.le(trustId); + if (!BEAST_EXPECT(sle)) + return; + BEAST_EXPECT(sle->isFieldPresent(sfHighSponsor) || sle->isFieldPresent(sfLowSponsor)); + } + + // sponsored=true on bob should include the sponsored trust line + { + auto const resp = acctObjsSponsored(env, bob.id(), true); + auto const& objs = resp[jss::result][jss::account_objects]; + if (!BEAST_EXPECT(objs.size() == 1)) + return; + + auto const& obj = objs[0u]; + BEAST_EXPECT(obj[sfLedgerEntryType.jsonName] == jss::RippleState); + BEAST_EXPECT( + obj.isMember(sfHighSponsor.jsonName) || obj.isMember(sfLowSponsor.jsonName)); + } + + // sponsored=false on bob should NOT include the sponsored trust line + { + auto const resp = acctObjsSponsored(env, bob.id(), false); + BEAST_EXPECT(resp[jss::result][jss::account_objects].size() == 0); + } + + // A trust line sponsored on either side is classified as sponsored + // for both parties. + { + Env env(*this, testableAmendments()); + Account const issuer("issuer"); + Account const user("user"); + Account const sponsor("sponsor"); + auto const usd = issuer["USD"]; + + env.fund(XRP(10000), issuer, user, sponsor); + env.close(); + + env(trust(issuer, user["USD"](100))); + env.close(); + + env(trust(user, usd(100))); + env.close(); + + auto const trustId = keylet::trustLine(user, issuer, usd.currency); + if (!BEAST_EXPECT(env.le(trustId))) + return; + + env(sponsor::transfer(user, tfSponsorshipCreate, trustId.key), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + auto const line = env.le(trustId); + if (!BEAST_EXPECT(line)) + return; + + auto const userIsHigh = line->getFieldAmount(sfHighLimit).getIssuer() == user.id(); + auto const& userSponsorField = userIsHigh ? sfHighSponsor : sfLowSponsor; + auto const& issuerSponsorField = userIsHigh ? sfLowSponsor : sfHighSponsor; + + BEAST_EXPECT(line->isFieldPresent(userSponsorField)); + BEAST_EXPECT(!line->isFieldPresent(issuerSponsorField)); + + { + auto const resp = acctObjsSponsored(env, user.id(), true, jss::state); + auto const& objs = resp[jss::result][jss::account_objects]; + if (BEAST_EXPECT(objs.size() == 1)) + BEAST_EXPECT(objs[0u][sfLedgerEntryType.jsonName] == jss::RippleState); + } + { + auto const resp = acctObjsSponsored(env, user.id(), false, jss::state); + auto const& objs = resp[jss::result][jss::account_objects]; + BEAST_EXPECT(objs.size() == 0); + } + { + auto const resp = acctObjsSponsored(env, issuer.id(), true, jss::state); + auto const& objs = resp[jss::result][jss::account_objects]; + if (BEAST_EXPECT(objs.size() == 1)) + BEAST_EXPECT(objs[0u][sfLedgerEntryType.jsonName] == jss::RippleState); + } + { + auto const resp = acctObjsSponsored(env, issuer.id(), false, jss::state); + auto const& objs = resp[jss::result][jss::account_objects]; + BEAST_EXPECT(objs.size() == 0); + } + } + + // A sponsored Check is classified as sponsored in both the writer's + // and the destination's results. + { + Env env(*this, testableAmendments()); + Account const owner("owner"); + Account const dest("dest"); + Account const sponsor("sponsor"); + + env.fund(XRP(10000), owner, dest, sponsor); + env.close(); + + auto const checkSeq = env.seq(owner); + env(check::create(owner, dest, XRP(1))); + env.close(); + + auto const checkId = keylet::check(owner, checkSeq); + if (!BEAST_EXPECT(env.le(checkId))) + return; + + env(sponsor::transfer(owner, tfSponsorshipCreate, checkId.key), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + { + auto const sle = env.le(checkId); + if (!BEAST_EXPECT(sle)) + return; + BEAST_EXPECT(sle->isFieldPresent(sfSponsor)); + } + + for (auto const& acct : {owner.id(), dest.id()}) + { + { + auto const resp = acctObjsSponsored(env, acct, true, jss::check); + auto const& objs = resp[jss::result][jss::account_objects]; + if (BEAST_EXPECT(objs.size() == 1)) + BEAST_EXPECT(objs[0u][sfLedgerEntryType.jsonName] == jss::Check); + } + { + auto const resp = acctObjsSponsored(env, acct, false, jss::check); + BEAST_EXPECT(resp[jss::result][jss::account_objects].size() == 0); + } + } + } + + // A Sponsorship object is visible to both sides. + { + Env env(*this, testableAmendments()); + Account const owner("owner"); + Account const sponsee("sponsee"); + + env.fund(XRP(10000), owner, sponsee); + env.close(); + + env(sponsor::set(owner, 0, 100, XRP(100)), sponsor::SponseeAcc(sponsee)); + env.close(); + + auto const sponsorshipKeylet = keylet::sponsorship(owner, sponsee); + if (!BEAST_EXPECT(env.le(sponsorshipKeylet))) + return; + + { + auto const resp = acctObjsSponsored(env, owner.id(), false, jss::sponsorship); + auto const& objs = resp[jss::result][jss::account_objects]; + if (BEAST_EXPECT(objs.size() == 1)) + BEAST_EXPECT(objs[0u][sfLedgerEntryType.jsonName] == jss::Sponsorship); + } + { + auto const resp = acctObjsSponsored(env, sponsee.id(), false, jss::sponsorship); + auto const& objs = resp[jss::result][jss::account_objects]; + if (BEAST_EXPECT(objs.size() == 1)) + BEAST_EXPECT(objs[0u][sfLedgerEntryType.jsonName] == jss::Sponsorship); + } + { + auto const resp = acctObjsSponsored(env, owner.id(), true, jss::sponsorship); + auto const& objs = resp[jss::result][jss::account_objects]; + BEAST_EXPECT(objs.size() == 0); + } + { + auto const resp = acctObjsSponsored(env, sponsee.id(), true, jss::sponsorship); + auto const& objs = resp[jss::result][jss::account_objects]; + BEAST_EXPECT(objs.size() == 0); + } + } + } + void run() override { @@ -1360,6 +1626,7 @@ public: testNFTsMarker(); testAccountNFTs(); testAccountObjectMarker(); + testSponsoredFilter(); } }; diff --git a/src/test/rpc/AccountTx_test.cpp b/src/test/rpc/AccountTx_test.cpp index ace8743e1e..a7f37d39b9 100644 --- a/src/test/rpc/AccountTx_test.cpp +++ b/src/test/rpc/AccountTx_test.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -773,7 +774,7 @@ class AccountTx_test : public beast::unit_test::Suite // All it takes is a large enough XRP payment to resurrect // becky's account. Try too small a payment. - env(pay(alice, becky, drops(env.current()->fees().accountReserve(0)) - drops(1)), + env(pay(alice, becky, drops(env.current()->fees().accountReserve(0, 1)) - drops(1)), Ter(tecNO_DST_INSUF_XRP)); env.close(); @@ -888,6 +889,82 @@ class AccountTx_test : public beast::unit_test::Suite checkAliceAcctTx(9, jss::Payment); } + void + testSponsorship() + { + // test all sponsored transactions are in sponsor and sponsee's account + // tx list + testcase("Sponsorship"); + + using namespace test::jtx; + Env env(*this); + Account const alice("alice"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); + env.fund(XRP(10000), alice, sponsor, sponsor2); + env.close(); + + // check the latest sponsorship-related txn is in account tx list + auto const checkTx = [&](Account const& account, json::StaticString txType) { + json::Value params; + params[jss::account] = account.human(); + params[jss::limit] = 100; + auto const jv = env.rpc("json", "account_tx", to_string(params))[jss::result]; + + auto const& tx0(jv[jss::transactions][0u][jss::tx]); + BEAST_EXPECT(tx0[jss::TransactionType] == txType); + + std::string const txHash{ + env.tx()->getJson(JsonOptions::Values::None)[jss::hash].asString()}; + BEAST_EXPECT(tx0[jss::hash] == txHash); + }; + + // fee sponsorship + env(noop(alice), sponsor::As(sponsor, spfSponsorFee), Sig(sfSponsorSignature, sponsor)); + env.close(); + checkTx(alice, jss::AccountSet); + checkTx(sponsor, jss::AccountSet); + + // set sponsor + env(sponsor::set(sponsor, 0, 100, XRP(100)), sponsor::SponseeAcc(alice), Ter(tesSUCCESS)); + env.close(); + checkTx(alice, jss::SponsorshipSet); + checkTx(sponsor, jss::SponsorshipSet); + + // create an object with sponsor + auto const checkId = keylet::check(alice, env.seq(alice)).key; + env(check::create(alice, sponsor, XRP(1)), sponsor::As(sponsor, spfSponsorReserve)); + env.close(); + checkTx(alice, jss::CheckCreate); + checkTx(sponsor, jss::CheckCreate); + + // transfer object sponsorship + env(sponsor::transfer(alice, tfSponsorshipReassign, checkId), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + checkTx(alice, jss::SponsorshipTransfer); + checkTx(sponsor, jss::SponsorshipTransfer); + checkTx(sponsor2, jss::SponsorshipTransfer); + + // delete the sponsored object + env(check::cancel(alice, checkId), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor)); + env.close(); + checkTx(alice, jss::CheckCancel); + checkTx(sponsor, jss::CheckCancel); + checkTx(sponsor2, jss::CheckCancel); + + // account sponsorship + env(sponsor::transfer(alice, tfSponsorshipCreate), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + checkTx(alice, jss::SponsorshipTransfer); + checkTx(sponsor, jss::SponsorshipTransfer); + } + public: void run() override @@ -896,6 +973,7 @@ public: testContents(); testAccountDelete(); testMPT(); + testSponsorship(); } }; BEAST_DEFINE_TESTSUITE(AccountTx, rpc, xrpl); diff --git a/src/test/rpc/LedgerEntry_test.cpp b/src/test/rpc/LedgerEntry_test.cpp index c7ef5cef2d..d321ab39aa 100644 --- a/src/test/rpc/LedgerEntry_test.cpp +++ b/src/test/rpc/LedgerEntry_test.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -93,6 +94,8 @@ std::vector> gMappings{ {jss::oracle_document_id, FieldType::UInt32Field}, {jss::owner, FieldType::AccountField}, {jss::seq, FieldType::UInt32Field}, + {jss::sponsor, FieldType::AccountField}, + {jss::sponsee, FieldType::AccountField}, {jss::subject, FieldType::AccountField}, {jss::ticket_seq, FieldType::UInt32Field}, }; @@ -107,7 +110,7 @@ getFieldType(json::StaticString fieldName) return it->second; } - Throw("`mappings` is missing field " + std::string(fieldName.cStr())); + Throw("`gMappings` is missing field " + std::string(fieldName.cStr())); } std::string @@ -1884,6 +1887,59 @@ class LedgerEntry_test : public beast::unit_test::Suite runLedgerEntryTest(env, jss::signer_list); } + void + testSponsorship() + { + testcase("Sponsorship"); + + using namespace test::jtx; + + Env env{*this}; + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(10000), alice, bob); + env.close(); + env(sponsor::set(alice, 0, 100), sponsor::SponseeAcc(bob)); + env.close(); + std::string const ledgerHash{to_string(env.closed()->header().hash)}; + auto const sponsorshipIndex = to_string(keylet::sponsorship(alice.id(), bob.id()).key); + + { + // Request by sponsor and sponsee. + json::Value jvParams; + jvParams[jss::sponsorship][jss::sponsor] = alice.human(); + jvParams[jss::sponsorship][jss::sponsee] = bob.human(); + jvParams[jss::ledger_hash] = ledgerHash; + auto const jrr = env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result]; + BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::Sponsorship); + BEAST_EXPECT(jrr[jss::node][sfOwner.jsonName] == alice.human()); + BEAST_EXPECT(jrr[jss::node][sfSponsee.jsonName] == bob.human()); + BEAST_EXPECT(sponsorshipIndex == jrr[jss::node][jss::index].asString()); + } + { + // Request by index. + json::Value jvParams; + jvParams[jss::sponsorship] = sponsorshipIndex; + jvParams[jss::ledger_hash] = ledgerHash; + json::Value const jrr = + env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result]; + BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::Sponsorship); + BEAST_EXPECT(jrr[jss::node][sfOwner.jsonName] == alice.human()); + BEAST_EXPECT(jrr[jss::node][sfSponsee.jsonName] == bob.human()); + BEAST_EXPECT(sponsorshipIndex == jrr[jss::node][jss::index].asString()); + } + { + // Check all malformed cases. + runLedgerEntryTest( + env, + jss::sponsorship, + { + {.fieldName = jss::sponsor, .malformedErrorMsg = "malformedSponsor"}, + {.fieldName = jss::sponsee, .malformedErrorMsg = "malformedSponsee"}, + }); + } + } + void testTicket() { @@ -2676,6 +2732,7 @@ public: testPayChan(); testRippleState(); testSignerList(); + testSponsorship(); testTicket(); testDID(); testInvalidOracleLedgerEntry(); diff --git a/src/test/rpc/Simulate_test.cpp b/src/test/rpc/Simulate_test.cpp index 0d00360a58..16df733a66 100644 --- a/src/test/rpc/Simulate_test.cpp +++ b/src/test/rpc/Simulate_test.cpp @@ -364,6 +364,37 @@ class Simulate_test : public beast::unit_test::Suite auto const resp = env.rpc("json", "simulate", to_string(params)); BEAST_EXPECT(resp[jss::result][jss::error_message] == "Invalid field 'tx.Signers[0]'."); } + { + // Non-object SponsorSignature field + json::Value params; + json::Value txJson = json::ValueType::Object; + txJson[jss::TransactionType] = jss::AccountSet; + txJson[jss::Account] = env.master.human(); + txJson[sfSponsorSignature] = ""; + params[jss::tx_json] = txJson; + + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Invalid field 'SponsorSignature', not object."); + } + { + // Invalid SponsorSignature.Signers field + json::Value params; + json::Value txJson = json::ValueType::Object; + txJson[jss::TransactionType] = jss::AccountSet; + txJson[jss::Account] = env.master.human(); + json::Value sponsorSignature = json::ValueType::Object; + sponsorSignature[sfSigners] = "1"; + txJson[sfSponsorSignature] = sponsorSignature; + params[jss::tx_json] = txJson; + + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECTS( + resp[jss::result][jss::error_message] == + "Invalid field 'tx.SponsorSignature.Signers'.", + resp.toStyledString()); + } { // Invalid transaction json::Value params; @@ -561,6 +592,75 @@ class Simulate_test : public beast::unit_test::Suite // test without autofill testTx(env, tx, validateOutput); } + + { + // autofill sponsor signature + + auto validateOutput = [&](json::Value const& resp, json::Value const& tx) { + auto result = resp[jss::result]; + checkBasicReturnValidity( + result, tx, env.seq(env.master), env.current()->fees().base); + + BEAST_EXPECT(result[jss::engine_result] == "tesSUCCESS"); + BEAST_EXPECT(result[jss::engine_result_code] == 0); + BEAST_EXPECT( + result[jss::engine_result_message] == + "The simulated transaction would have been applied."); + + if (BEAST_EXPECT(result.isMember(jss::meta) || result.isMember(jss::meta_blob))) + { + json::Value const metadata = getJsonMetadata(result); + + if (BEAST_EXPECT(metadata.isMember(sfAffectedNodes.jsonName))) + { + BEAST_EXPECT(metadata[sfAffectedNodes.jsonName].size() == 2); + + auto node = metadata[sfAffectedNodes.jsonName][0u]; + if (BEAST_EXPECT(node.isMember(sfModifiedNode.jsonName))) + { + auto modifiedNode = node[sfModifiedNode]; + BEAST_EXPECT(modifiedNode[sfLedgerEntryType] == "AccountRoot"); + auto previousFields = modifiedNode[sfPreviousFields]; + BEAST_EXPECT(!previousFields.isMember(sfBalance.jsonName)); + } + + auto node2 = metadata[sfAffectedNodes.jsonName][1u]; + if (BEAST_EXPECT(node2.isMember(sfModifiedNode.jsonName))) + { + auto modifiedNode = node2[sfModifiedNode]; + BEAST_EXPECT(modifiedNode[sfLedgerEntryType] == "AccountRoot"); + + auto previousFields = modifiedNode[sfPreviousFields]; + BEAST_EXPECT(previousFields.isMember(sfBalance.jsonName)); + } + } + BEAST_EXPECT(metadata[sfTransactionIndex.jsonName] == 0); + BEAST_EXPECT(metadata[sfTransactionResult.jsonName] == "tesSUCCESS"); + } + }; + + Account const sponsor("sponsor"); + env.fund(XRP(10000), sponsor); + env.close(); + + json::Value tx; + + tx[jss::Account] = env.master.human(); + tx[jss::TransactionType] = jss::AccountSet; + tx[sfDomain.jsonName] = kNewDomain; + tx[sfSponsor.jsonName] = sponsor.human(); + tx[sfSponsorFlags.jsonName] = spfSponsorFee; + tx[sfSponsorSignature.jsonName] = json::ValueType::Object; + + // test with autofill + testTx(env, tx, validateOutput); + + tx[sfSponsorSignature.jsonName][sfTxnSignature.jsonName] = ""; + tx[sfSponsorSignature.jsonName][sfSigningPubKey.jsonName] = ""; + + // test without autofill + testTx(env, tx, validateOutput); + } } void @@ -763,6 +863,58 @@ class Simulate_test : public beast::unit_test::Suite } } + void + testSuccessfulSponsoredTransactionMultisigned() + { + testcase("Successful sponsored multi-signed transaction"); + + using namespace jtx; + Env env(*this); + Account const sponsor("sponsor"); + Account const signer("signer"); + env.fund(XRP(10000), sponsor, signer); + env.close(); + + env(signers(sponsor, 1, {{signer, 1}})); + env.close(); + + auto validateOutput = [&](json::Value const& resp, json::Value const& tx) { + auto const result = resp[jss::result]; + // Verifies Fee autofill counts nested sponsor-signature signers. + auto const expectedFee = env.current()->fees().base * 2; + checkBasicReturnValidity(result, tx, env.seq(env.master), expectedFee); + + BEAST_EXPECT(result[jss::engine_result] == "tesSUCCESS"); + BEAST_EXPECT(result[jss::engine_result_code] == 0); + BEAST_EXPECT( + result[jss::engine_result_message] == + "The simulated transaction would have been applied."); + + if (BEAST_EXPECT(result.isMember(jss::meta) || result.isMember(jss::meta_blob))) + { + json::Value const metadata = getJsonMetadata(result); + BEAST_EXPECT(metadata[sfTransactionResult.jsonName] == "tesSUCCESS"); + } + }; + + json::Value tx; + tx[jss::Account] = env.master.human(); + tx[jss::TransactionType] = jss::AccountSet; + tx[sfDomain] = "123ABC"; + tx[sfSponsor.jsonName] = sponsor.human(); + tx[sfSponsorFlags.jsonName] = spfSponsorFee; + tx[sfSponsorSignature.jsonName] = json::ValueType::Object; + tx[sfSponsorSignature.jsonName][sfSigners.jsonName] = json::ValueType::Array; + + json::Value signerObj; + signerObj[sfSigner][jss::Account] = signer.human(); + tx[sfSponsorSignature.jsonName][sfSigners.jsonName].append(signerObj); + + // Leave Fee unset so simulate must autofill it after sponsor signer normalization. + BEAST_EXPECT(!tx.isMember(jss::Fee)); + testTx(env, tx, validateOutput, false); + } + void testTransactionSigningFailure() { @@ -1195,6 +1347,7 @@ public: testTransactionNonTecFailure(); testTransactionTecFailure(); testSuccessfulTransactionMultisigned(); + testSuccessfulSponsoredTransactionMultisigned(); testTransactionSigningFailure(); testInvalidSingleAndMultiSigningTransaction(); testMultisignedBadPubKey(); diff --git a/src/tests/libxrpl/protocol_autogen/ledger_entries/AccountRootTests.cpp b/src/tests/libxrpl/protocol_autogen/ledger_entries/AccountRootTests.cpp index e967b614b7..17c80899f9 100644 --- a/src/tests/libxrpl/protocol_autogen/ledger_entries/AccountRootTests.cpp +++ b/src/tests/libxrpl/protocol_autogen/ledger_entries/AccountRootTests.cpp @@ -40,6 +40,9 @@ TEST(AccountRootTests, BuilderSettersRoundTrip) auto const mintedNFTokensValue = canonical_UINT32(); auto const burnedNFTokensValue = canonical_UINT32(); auto const firstNFTokenSequenceValue = canonical_UINT32(); + auto const sponsoredOwnerCountValue = canonical_UINT32(); + auto const sponsoringOwnerCountValue = canonical_UINT32(); + auto const sponsoringAccountCountValue = canonical_UINT32(); auto const aMMIDValue = canonical_UINT256(); auto const vaultIDValue = canonical_UINT256(); auto const loanBrokerIDValue = canonical_UINT256(); @@ -67,6 +70,9 @@ TEST(AccountRootTests, BuilderSettersRoundTrip) builder.setMintedNFTokens(mintedNFTokensValue); builder.setBurnedNFTokens(burnedNFTokensValue); builder.setFirstNFTokenSequence(firstNFTokenSequenceValue); + builder.setSponsoredOwnerCount(sponsoredOwnerCountValue); + builder.setSponsoringOwnerCount(sponsoringOwnerCountValue); + builder.setSponsoringAccountCount(sponsoringAccountCountValue); builder.setAMMID(aMMIDValue); builder.setVaultID(vaultIDValue); builder.setLoanBrokerID(loanBrokerIDValue); @@ -228,6 +234,30 @@ TEST(AccountRootTests, BuilderSettersRoundTrip) EXPECT_TRUE(entry.hasFirstNFTokenSequence()); } + { + auto const& expected = sponsoredOwnerCountValue; + auto const actualOpt = entry.getSponsoredOwnerCount(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfSponsoredOwnerCount"); + EXPECT_TRUE(entry.hasSponsoredOwnerCount()); + } + + { + auto const& expected = sponsoringOwnerCountValue; + auto const actualOpt = entry.getSponsoringOwnerCount(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfSponsoringOwnerCount"); + EXPECT_TRUE(entry.hasSponsoringOwnerCount()); + } + + { + auto const& expected = sponsoringAccountCountValue; + auto const actualOpt = entry.getSponsoringAccountCount(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfSponsoringAccountCount"); + EXPECT_TRUE(entry.hasSponsoringAccountCount()); + } + { auto const& expected = aMMIDValue; auto const actualOpt = entry.getAMMID(); @@ -285,6 +315,9 @@ TEST(AccountRootTests, BuilderFromSleRoundTrip) auto const mintedNFTokensValue = canonical_UINT32(); auto const burnedNFTokensValue = canonical_UINT32(); auto const firstNFTokenSequenceValue = canonical_UINT32(); + auto const sponsoredOwnerCountValue = canonical_UINT32(); + auto const sponsoringOwnerCountValue = canonical_UINT32(); + auto const sponsoringAccountCountValue = canonical_UINT32(); auto const aMMIDValue = canonical_UINT256(); auto const vaultIDValue = canonical_UINT256(); auto const loanBrokerIDValue = canonical_UINT256(); @@ -311,6 +344,9 @@ TEST(AccountRootTests, BuilderFromSleRoundTrip) sle->at(sfMintedNFTokens) = mintedNFTokensValue; sle->at(sfBurnedNFTokens) = burnedNFTokensValue; sle->at(sfFirstNFTokenSequence) = firstNFTokenSequenceValue; + sle->at(sfSponsoredOwnerCount) = sponsoredOwnerCountValue; + sle->at(sfSponsoringOwnerCount) = sponsoringOwnerCountValue; + sle->at(sfSponsoringAccountCount) = sponsoringAccountCountValue; sle->at(sfAMMID) = aMMIDValue; sle->at(sfVaultID) = vaultIDValue; sle->at(sfLoanBrokerID) = loanBrokerIDValue; @@ -566,6 +602,45 @@ TEST(AccountRootTests, BuilderFromSleRoundTrip) expectEqualField(expected, *fromBuilderOpt, "sfFirstNFTokenSequence"); } + { + auto const& expected = sponsoredOwnerCountValue; + + auto const fromSleOpt = entryFromSle.getSponsoredOwnerCount(); + auto const fromBuilderOpt = entryFromBuilder.getSponsoredOwnerCount(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfSponsoredOwnerCount"); + expectEqualField(expected, *fromBuilderOpt, "sfSponsoredOwnerCount"); + } + + { + auto const& expected = sponsoringOwnerCountValue; + + auto const fromSleOpt = entryFromSle.getSponsoringOwnerCount(); + auto const fromBuilderOpt = entryFromBuilder.getSponsoringOwnerCount(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfSponsoringOwnerCount"); + expectEqualField(expected, *fromBuilderOpt, "sfSponsoringOwnerCount"); + } + + { + auto const& expected = sponsoringAccountCountValue; + + auto const fromSleOpt = entryFromSle.getSponsoringAccountCount(); + auto const fromBuilderOpt = entryFromBuilder.getSponsoringAccountCount(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfSponsoringAccountCount"); + expectEqualField(expected, *fromBuilderOpt, "sfSponsoringAccountCount"); + } + { auto const& expected = aMMIDValue; @@ -697,6 +772,12 @@ TEST(AccountRootTests, OptionalFieldsReturnNullopt) EXPECT_FALSE(entry.getBurnedNFTokens().has_value()); EXPECT_FALSE(entry.hasFirstNFTokenSequence()); EXPECT_FALSE(entry.getFirstNFTokenSequence().has_value()); + EXPECT_FALSE(entry.hasSponsoredOwnerCount()); + EXPECT_FALSE(entry.getSponsoredOwnerCount().has_value()); + EXPECT_FALSE(entry.hasSponsoringOwnerCount()); + EXPECT_FALSE(entry.getSponsoringOwnerCount().has_value()); + EXPECT_FALSE(entry.hasSponsoringAccountCount()); + EXPECT_FALSE(entry.getSponsoringAccountCount().has_value()); EXPECT_FALSE(entry.hasAMMID()); EXPECT_FALSE(entry.getAMMID().has_value()); EXPECT_FALSE(entry.hasVaultID()); diff --git a/src/tests/libxrpl/protocol_autogen/ledger_entries/RippleStateTests.cpp b/src/tests/libxrpl/protocol_autogen/ledger_entries/RippleStateTests.cpp index a51ce55f6f..de0769793b 100644 --- a/src/tests/libxrpl/protocol_autogen/ledger_entries/RippleStateTests.cpp +++ b/src/tests/libxrpl/protocol_autogen/ledger_entries/RippleStateTests.cpp @@ -31,6 +31,8 @@ TEST(RippleStateTests, BuilderSettersRoundTrip) auto const highNodeValue = canonical_UINT64(); auto const highQualityInValue = canonical_UINT32(); auto const highQualityOutValue = canonical_UINT32(); + auto const highSponsorValue = canonical_ACCOUNT(); + auto const lowSponsorValue = canonical_ACCOUNT(); RippleStateBuilder builder{ balanceValue, @@ -46,6 +48,8 @@ TEST(RippleStateTests, BuilderSettersRoundTrip) builder.setHighNode(highNodeValue); builder.setHighQualityIn(highQualityInValue); builder.setHighQualityOut(highQualityOutValue); + builder.setHighSponsor(highSponsorValue); + builder.setLowSponsor(lowSponsorValue); builder.setLedgerIndex(index); builder.setFlags(0x1u); @@ -134,6 +138,22 @@ TEST(RippleStateTests, BuilderSettersRoundTrip) EXPECT_TRUE(entry.hasHighQualityOut()); } + { + auto const& expected = highSponsorValue; + auto const actualOpt = entry.getHighSponsor(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfHighSponsor"); + EXPECT_TRUE(entry.hasHighSponsor()); + } + + { + auto const& expected = lowSponsorValue; + auto const actualOpt = entry.getLowSponsor(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfLowSponsor"); + EXPECT_TRUE(entry.hasLowSponsor()); + } + EXPECT_TRUE(entry.hasLedgerIndex()); auto const ledgerIndex = entry.getLedgerIndex(); ASSERT_TRUE(ledgerIndex.has_value()); @@ -158,6 +178,8 @@ TEST(RippleStateTests, BuilderFromSleRoundTrip) auto const highNodeValue = canonical_UINT64(); auto const highQualityInValue = canonical_UINT32(); auto const highQualityOutValue = canonical_UINT32(); + auto const highSponsorValue = canonical_ACCOUNT(); + auto const lowSponsorValue = canonical_ACCOUNT(); auto sle = std::make_shared(RippleState::entryType, index); @@ -172,6 +194,8 @@ TEST(RippleStateTests, BuilderFromSleRoundTrip) sle->at(sfHighNode) = highNodeValue; sle->at(sfHighQualityIn) = highQualityInValue; sle->at(sfHighQualityOut) = highQualityOutValue; + sle->at(sfHighSponsor) = highSponsorValue; + sle->at(sfLowSponsor) = lowSponsorValue; RippleStateBuilder builderFromSle{sle}; EXPECT_TRUE(builderFromSle.validate()); @@ -310,6 +334,32 @@ TEST(RippleStateTests, BuilderFromSleRoundTrip) expectEqualField(expected, *fromBuilderOpt, "sfHighQualityOut"); } + { + auto const& expected = highSponsorValue; + + auto const fromSleOpt = entryFromSle.getHighSponsor(); + auto const fromBuilderOpt = entryFromBuilder.getHighSponsor(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfHighSponsor"); + expectEqualField(expected, *fromBuilderOpt, "sfHighSponsor"); + } + + { + auto const& expected = lowSponsorValue; + + auto const fromSleOpt = entryFromSle.getLowSponsor(); + auto const fromBuilderOpt = entryFromBuilder.getLowSponsor(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfLowSponsor"); + expectEqualField(expected, *fromBuilderOpt, "sfLowSponsor"); + } + EXPECT_EQ(entryFromSle.getKey(), index); EXPECT_EQ(entryFromBuilder.getKey(), index); } @@ -384,5 +434,9 @@ TEST(RippleStateTests, OptionalFieldsReturnNullopt) EXPECT_FALSE(entry.getHighQualityIn().has_value()); EXPECT_FALSE(entry.hasHighQualityOut()); EXPECT_FALSE(entry.getHighQualityOut().has_value()); + EXPECT_FALSE(entry.hasHighSponsor()); + EXPECT_FALSE(entry.getHighSponsor().has_value()); + EXPECT_FALSE(entry.hasLowSponsor()); + EXPECT_FALSE(entry.getLowSponsor().has_value()); } } diff --git a/src/tests/libxrpl/protocol_autogen/ledger_entries/SponsorshipTests.cpp b/src/tests/libxrpl/protocol_autogen/ledger_entries/SponsorshipTests.cpp new file mode 100644 index 0000000000..e1d9ff15b9 --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/ledger_entries/SponsorshipTests.cpp @@ -0,0 +1,329 @@ +// Auto-generated unit tests for ledger entry Sponsorship + + +#include + +#include + +#include +#include +#include + +#include + +namespace xrpl::ledger_entries { + +// 1 & 4) Set fields via builder setters, build, then read them back via +// wrapper getters. After build(), validate() should succeed for both the +// builder's STObject and the wrapper's SLE. +TEST(SponsorshipTests, BuilderSettersRoundTrip) +{ + uint256 const index{1u}; + + auto const previousTxnIDValue = canonical_UINT256(); + auto const previousTxnLgrSeqValue = canonical_UINT32(); + auto const ownerValue = canonical_ACCOUNT(); + auto const sponseeValue = canonical_ACCOUNT(); + auto const feeAmountValue = canonical_AMOUNT(); + auto const maxFeeValue = canonical_AMOUNT(); + auto const remainingOwnerCountValue = canonical_UINT32(); + auto const ownerNodeValue = canonical_UINT64(); + auto const sponseeNodeValue = canonical_UINT64(); + + SponsorshipBuilder builder{ + previousTxnIDValue, + previousTxnLgrSeqValue, + ownerValue, + sponseeValue, + ownerNodeValue, + sponseeNodeValue + }; + + builder.setFeeAmount(feeAmountValue); + builder.setMaxFee(maxFeeValue); + builder.setRemainingOwnerCount(remainingOwnerCountValue); + + builder.setLedgerIndex(index); + builder.setFlags(0x1u); + + EXPECT_TRUE(builder.validate()); + + auto const entry = builder.build(index); + + EXPECT_TRUE(entry.validate()); + + { + auto const& expected = previousTxnIDValue; + auto const actual = entry.getPreviousTxnID(); + expectEqualField(expected, actual, "sfPreviousTxnID"); + } + + { + auto const& expected = previousTxnLgrSeqValue; + auto const actual = entry.getPreviousTxnLgrSeq(); + expectEqualField(expected, actual, "sfPreviousTxnLgrSeq"); + } + + { + auto const& expected = ownerValue; + auto const actual = entry.getOwner(); + expectEqualField(expected, actual, "sfOwner"); + } + + { + auto const& expected = sponseeValue; + auto const actual = entry.getSponsee(); + expectEqualField(expected, actual, "sfSponsee"); + } + + { + auto const& expected = ownerNodeValue; + auto const actual = entry.getOwnerNode(); + expectEqualField(expected, actual, "sfOwnerNode"); + } + + { + auto const& expected = sponseeNodeValue; + auto const actual = entry.getSponseeNode(); + expectEqualField(expected, actual, "sfSponseeNode"); + } + + { + auto const& expected = feeAmountValue; + auto const actualOpt = entry.getFeeAmount(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfFeeAmount"); + EXPECT_TRUE(entry.hasFeeAmount()); + } + + { + auto const& expected = maxFeeValue; + auto const actualOpt = entry.getMaxFee(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfMaxFee"); + EXPECT_TRUE(entry.hasMaxFee()); + } + + { + auto const& expected = remainingOwnerCountValue; + auto const actualOpt = entry.getRemainingOwnerCount(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfRemainingOwnerCount"); + EXPECT_TRUE(entry.hasRemainingOwnerCount()); + } + + EXPECT_TRUE(entry.hasLedgerIndex()); + auto const ledgerIndex = entry.getLedgerIndex(); + ASSERT_TRUE(ledgerIndex.has_value()); + EXPECT_EQ(*ledgerIndex, index); + EXPECT_EQ(entry.getKey(), index); +} + +// 2 & 4) Start from an SLE, set fields directly on it, construct a builder +// from that SLE, build a new wrapper, and verify all fields (and validate()). +TEST(SponsorshipTests, BuilderFromSleRoundTrip) +{ + uint256 const index{2u}; + + auto const previousTxnIDValue = canonical_UINT256(); + auto const previousTxnLgrSeqValue = canonical_UINT32(); + auto const ownerValue = canonical_ACCOUNT(); + auto const sponseeValue = canonical_ACCOUNT(); + auto const feeAmountValue = canonical_AMOUNT(); + auto const maxFeeValue = canonical_AMOUNT(); + auto const remainingOwnerCountValue = canonical_UINT32(); + auto const ownerNodeValue = canonical_UINT64(); + auto const sponseeNodeValue = canonical_UINT64(); + + auto sle = std::make_shared(Sponsorship::entryType, index); + + sle->at(sfPreviousTxnID) = previousTxnIDValue; + sle->at(sfPreviousTxnLgrSeq) = previousTxnLgrSeqValue; + sle->at(sfOwner) = ownerValue; + sle->at(sfSponsee) = sponseeValue; + sle->at(sfFeeAmount) = feeAmountValue; + sle->at(sfMaxFee) = maxFeeValue; + sle->at(sfRemainingOwnerCount) = remainingOwnerCountValue; + sle->at(sfOwnerNode) = ownerNodeValue; + sle->at(sfSponseeNode) = sponseeNodeValue; + + SponsorshipBuilder builderFromSle{sle}; + EXPECT_TRUE(builderFromSle.validate()); + + auto const entryFromBuilder = builderFromSle.build(index); + + Sponsorship entryFromSle{sle}; + EXPECT_TRUE(entryFromBuilder.validate()); + EXPECT_TRUE(entryFromSle.validate()); + + { + auto const& expected = previousTxnIDValue; + + auto const fromSle = entryFromSle.getPreviousTxnID(); + auto const fromBuilder = entryFromBuilder.getPreviousTxnID(); + + expectEqualField(expected, fromSle, "sfPreviousTxnID"); + expectEqualField(expected, fromBuilder, "sfPreviousTxnID"); + } + + { + auto const& expected = previousTxnLgrSeqValue; + + auto const fromSle = entryFromSle.getPreviousTxnLgrSeq(); + auto const fromBuilder = entryFromBuilder.getPreviousTxnLgrSeq(); + + expectEqualField(expected, fromSle, "sfPreviousTxnLgrSeq"); + expectEqualField(expected, fromBuilder, "sfPreviousTxnLgrSeq"); + } + + { + auto const& expected = ownerValue; + + auto const fromSle = entryFromSle.getOwner(); + auto const fromBuilder = entryFromBuilder.getOwner(); + + expectEqualField(expected, fromSle, "sfOwner"); + expectEqualField(expected, fromBuilder, "sfOwner"); + } + + { + auto const& expected = sponseeValue; + + auto const fromSle = entryFromSle.getSponsee(); + auto const fromBuilder = entryFromBuilder.getSponsee(); + + expectEqualField(expected, fromSle, "sfSponsee"); + expectEqualField(expected, fromBuilder, "sfSponsee"); + } + + { + auto const& expected = ownerNodeValue; + + auto const fromSle = entryFromSle.getOwnerNode(); + auto const fromBuilder = entryFromBuilder.getOwnerNode(); + + expectEqualField(expected, fromSle, "sfOwnerNode"); + expectEqualField(expected, fromBuilder, "sfOwnerNode"); + } + + { + auto const& expected = sponseeNodeValue; + + auto const fromSle = entryFromSle.getSponseeNode(); + auto const fromBuilder = entryFromBuilder.getSponseeNode(); + + expectEqualField(expected, fromSle, "sfSponseeNode"); + expectEqualField(expected, fromBuilder, "sfSponseeNode"); + } + + { + auto const& expected = feeAmountValue; + + auto const fromSleOpt = entryFromSle.getFeeAmount(); + auto const fromBuilderOpt = entryFromBuilder.getFeeAmount(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfFeeAmount"); + expectEqualField(expected, *fromBuilderOpt, "sfFeeAmount"); + } + + { + auto const& expected = maxFeeValue; + + auto const fromSleOpt = entryFromSle.getMaxFee(); + auto const fromBuilderOpt = entryFromBuilder.getMaxFee(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfMaxFee"); + expectEqualField(expected, *fromBuilderOpt, "sfMaxFee"); + } + + { + auto const& expected = remainingOwnerCountValue; + + auto const fromSleOpt = entryFromSle.getRemainingOwnerCount(); + auto const fromBuilderOpt = entryFromBuilder.getRemainingOwnerCount(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfRemainingOwnerCount"); + expectEqualField(expected, *fromBuilderOpt, "sfRemainingOwnerCount"); + } + + EXPECT_EQ(entryFromSle.getKey(), index); + EXPECT_EQ(entryFromBuilder.getKey(), index); +} + +// 3) Verify wrapper throws when constructed from wrong ledger entry type. +TEST(SponsorshipTests, WrapperThrowsOnWrongEntryType) +{ + uint256 const index{3u}; + + // Build a valid ledger entry of a different type + // Ticket requires: Account, OwnerNode, TicketSequence, PreviousTxnID, PreviousTxnLgrSeq + // Check requires: Account, Destination, SendMax, Sequence, OwnerNode, DestinationNode, PreviousTxnID, PreviousTxnLgrSeq + TicketBuilder wrongBuilder{ + canonical_ACCOUNT(), + canonical_UINT64(), + canonical_UINT32(), + canonical_UINT256(), + canonical_UINT32()}; + auto wrongEntry = wrongBuilder.build(index); + + EXPECT_THROW(Sponsorship{wrongEntry.getSle()}, std::runtime_error); +} + +// 4) Verify builder throws when constructed from wrong ledger entry type. +TEST(SponsorshipTests, BuilderThrowsOnWrongEntryType) +{ + uint256 const index{4u}; + + // Build a valid ledger entry of a different type + TicketBuilder wrongBuilder{ + canonical_ACCOUNT(), + canonical_UINT64(), + canonical_UINT32(), + canonical_UINT256(), + canonical_UINT32()}; + auto wrongEntry = wrongBuilder.build(index); + + EXPECT_THROW(SponsorshipBuilder{wrongEntry.getSle()}, std::runtime_error); +} + +// 5) Build with only required fields and verify optional fields return nullopt. +TEST(SponsorshipTests, OptionalFieldsReturnNullopt) +{ + uint256 const index{3u}; + + auto const previousTxnIDValue = canonical_UINT256(); + auto const previousTxnLgrSeqValue = canonical_UINT32(); + auto const ownerValue = canonical_ACCOUNT(); + auto const sponseeValue = canonical_ACCOUNT(); + auto const ownerNodeValue = canonical_UINT64(); + auto const sponseeNodeValue = canonical_UINT64(); + + SponsorshipBuilder builder{ + previousTxnIDValue, + previousTxnLgrSeqValue, + ownerValue, + sponseeValue, + ownerNodeValue, + sponseeNodeValue + }; + + auto const entry = builder.build(index); + + // Verify optional fields are not present + EXPECT_FALSE(entry.hasFeeAmount()); + EXPECT_FALSE(entry.getFeeAmount().has_value()); + EXPECT_FALSE(entry.hasMaxFee()); + EXPECT_FALSE(entry.getMaxFee().has_value()); + EXPECT_FALSE(entry.hasRemainingOwnerCount()); + EXPECT_FALSE(entry.getRemainingOwnerCount().has_value()); +} +} diff --git a/src/tests/libxrpl/protocol_autogen/transactions/SponsorshipSetTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/SponsorshipSetTests.cpp new file mode 100644 index 0000000000..dce8cfca3f --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/transactions/SponsorshipSetTests.cpp @@ -0,0 +1,261 @@ +// Auto-generated unit tests for transaction SponsorshipSet + + +#include + +#include + +#include +#include +#include +#include +#include + +#include + +namespace xrpl::transactions { + +// 1 & 4) Set fields via builder setters, build, then read them back via +// wrapper getters. After build(), validate() should succeed. +TEST(TransactionsSponsorshipSetTests, BuilderSettersRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testSponsorshipSet")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 1; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific field values + auto const counterpartySponsorValue = canonical_ACCOUNT(); + auto const sponseeValue = canonical_ACCOUNT(); + auto const feeAmountValue = canonical_AMOUNT(); + auto const maxFeeValue = canonical_AMOUNT(); + auto const remainingOwnerCountValue = canonical_UINT32(); + + SponsorshipSetBuilder builder{ + accountValue, + sequenceValue, + feeValue + }; + + // Set optional fields + builder.setCounterpartySponsor(counterpartySponsorValue); + builder.setSponsee(sponseeValue); + builder.setFeeAmount(feeAmountValue); + builder.setMaxFee(maxFeeValue); + builder.setRemainingOwnerCount(remainingOwnerCountValue); + + auto tx = builder.build(publicKey, secretKey); + + std::string reason; + EXPECT_TRUE(tx.validate(reason)) << reason; + + // Verify signing was applied + EXPECT_FALSE(tx.getSigningPubKey().empty()); + EXPECT_TRUE(tx.hasTxnSignature()); + + // Verify common fields + EXPECT_EQ(tx.getAccount(), accountValue); + EXPECT_EQ(tx.getSequence(), sequenceValue); + EXPECT_EQ(tx.getFee(), feeValue); + + // Verify required fields + // Verify optional fields + { + auto const& expected = counterpartySponsorValue; + auto const actualOpt = tx.getCounterpartySponsor(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfCounterpartySponsor should be present"; + expectEqualField(expected, *actualOpt, "sfCounterpartySponsor"); + EXPECT_TRUE(tx.hasCounterpartySponsor()); + } + + { + auto const& expected = sponseeValue; + auto const actualOpt = tx.getSponsee(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfSponsee should be present"; + expectEqualField(expected, *actualOpt, "sfSponsee"); + EXPECT_TRUE(tx.hasSponsee()); + } + + { + auto const& expected = feeAmountValue; + auto const actualOpt = tx.getFeeAmount(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfFeeAmount should be present"; + expectEqualField(expected, *actualOpt, "sfFeeAmount"); + EXPECT_TRUE(tx.hasFeeAmount()); + } + + { + auto const& expected = maxFeeValue; + auto const actualOpt = tx.getMaxFee(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfMaxFee should be present"; + expectEqualField(expected, *actualOpt, "sfMaxFee"); + EXPECT_TRUE(tx.hasMaxFee()); + } + + { + auto const& expected = remainingOwnerCountValue; + auto const actualOpt = tx.getRemainingOwnerCount(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfRemainingOwnerCount should be present"; + expectEqualField(expected, *actualOpt, "sfRemainingOwnerCount"); + EXPECT_TRUE(tx.hasRemainingOwnerCount()); + } + +} + +// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper, +// and verify all fields match. +TEST(TransactionsSponsorshipSetTests, BuilderFromStTxRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testSponsorshipSetFromTx")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 2; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific field values + auto const counterpartySponsorValue = canonical_ACCOUNT(); + auto const sponseeValue = canonical_ACCOUNT(); + auto const feeAmountValue = canonical_AMOUNT(); + auto const maxFeeValue = canonical_AMOUNT(); + auto const remainingOwnerCountValue = canonical_UINT32(); + + // Build an initial transaction + SponsorshipSetBuilder initialBuilder{ + accountValue, + sequenceValue, + feeValue + }; + + initialBuilder.setCounterpartySponsor(counterpartySponsorValue); + initialBuilder.setSponsee(sponseeValue); + initialBuilder.setFeeAmount(feeAmountValue); + initialBuilder.setMaxFee(maxFeeValue); + initialBuilder.setRemainingOwnerCount(remainingOwnerCountValue); + + auto initialTx = initialBuilder.build(publicKey, secretKey); + + // Create builder from existing STTx + SponsorshipSetBuilder builderFromTx{initialTx.getSTTx()}; + + auto rebuiltTx = builderFromTx.build(publicKey, secretKey); + + std::string reason; + EXPECT_TRUE(rebuiltTx.validate(reason)) << reason; + + // Verify common fields + EXPECT_EQ(rebuiltTx.getAccount(), accountValue); + EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue); + EXPECT_EQ(rebuiltTx.getFee(), feeValue); + + // Verify required fields + // Verify optional fields + { + auto const& expected = counterpartySponsorValue; + auto const actualOpt = rebuiltTx.getCounterpartySponsor(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfCounterpartySponsor should be present"; + expectEqualField(expected, *actualOpt, "sfCounterpartySponsor"); + } + + { + auto const& expected = sponseeValue; + auto const actualOpt = rebuiltTx.getSponsee(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfSponsee should be present"; + expectEqualField(expected, *actualOpt, "sfSponsee"); + } + + { + auto const& expected = feeAmountValue; + auto const actualOpt = rebuiltTx.getFeeAmount(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfFeeAmount should be present"; + expectEqualField(expected, *actualOpt, "sfFeeAmount"); + } + + { + auto const& expected = maxFeeValue; + auto const actualOpt = rebuiltTx.getMaxFee(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfMaxFee should be present"; + expectEqualField(expected, *actualOpt, "sfMaxFee"); + } + + { + auto const& expected = remainingOwnerCountValue; + auto const actualOpt = rebuiltTx.getRemainingOwnerCount(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfRemainingOwnerCount should be present"; + expectEqualField(expected, *actualOpt, "sfRemainingOwnerCount"); + } + +} + +// 3) Verify wrapper throws when constructed from wrong transaction type. +TEST(TransactionsSponsorshipSetTests, WrapperThrowsOnWrongTxType) +{ + // Build a valid transaction of a different type + auto const [pk, sk] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongType")); + auto const account = calcAccountID(pk); + + AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()}; + auto wrongTx = wrongBuilder.build(pk, sk); + + EXPECT_THROW(SponsorshipSet{wrongTx.getSTTx()}, std::runtime_error); +} + +// 4) Verify builder throws when constructed from wrong transaction type. +TEST(TransactionsSponsorshipSetTests, BuilderThrowsOnWrongTxType) +{ + // Build a valid transaction of a different type + auto const [pk, sk] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongTypeBuilder")); + auto const account = calcAccountID(pk); + + AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()}; + auto wrongTx = wrongBuilder.build(pk, sk); + + EXPECT_THROW(SponsorshipSetBuilder{wrongTx.getSTTx()}, std::runtime_error); +} + +// 5) Build with only required fields and verify optional fields return nullopt. +TEST(TransactionsSponsorshipSetTests, OptionalFieldsReturnNullopt) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testSponsorshipSetNullopt")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 3; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific required field values + + SponsorshipSetBuilder builder{ + accountValue, + sequenceValue, + feeValue + }; + + // Do NOT set optional fields + + auto tx = builder.build(publicKey, secretKey); + + // Verify optional fields are not present + EXPECT_FALSE(tx.hasCounterpartySponsor()); + EXPECT_FALSE(tx.getCounterpartySponsor().has_value()); + EXPECT_FALSE(tx.hasSponsee()); + EXPECT_FALSE(tx.getSponsee().has_value()); + EXPECT_FALSE(tx.hasFeeAmount()); + EXPECT_FALSE(tx.getFeeAmount().has_value()); + EXPECT_FALSE(tx.hasMaxFee()); + EXPECT_FALSE(tx.getMaxFee().has_value()); + EXPECT_FALSE(tx.hasRemainingOwnerCount()); + EXPECT_FALSE(tx.getRemainingOwnerCount().has_value()); +} + +} diff --git a/src/tests/libxrpl/protocol_autogen/transactions/SponsorshipTransferTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/SponsorshipTransferTests.cpp new file mode 100644 index 0000000000..c8521b7b20 --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/transactions/SponsorshipTransferTests.cpp @@ -0,0 +1,198 @@ +// Auto-generated unit tests for transaction SponsorshipTransfer + + +#include + +#include + +#include +#include +#include +#include +#include + +#include + +namespace xrpl::transactions { + +// 1 & 4) Set fields via builder setters, build, then read them back via +// wrapper getters. After build(), validate() should succeed. +TEST(TransactionsSponsorshipTransferTests, BuilderSettersRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testSponsorshipTransfer")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 1; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific field values + auto const objectIDValue = canonical_UINT256(); + auto const sponseeValue = canonical_ACCOUNT(); + + SponsorshipTransferBuilder builder{ + accountValue, + sequenceValue, + feeValue + }; + + // Set optional fields + builder.setObjectID(objectIDValue); + builder.setSponsee(sponseeValue); + + auto tx = builder.build(publicKey, secretKey); + + std::string reason; + EXPECT_TRUE(tx.validate(reason)) << reason; + + // Verify signing was applied + EXPECT_FALSE(tx.getSigningPubKey().empty()); + EXPECT_TRUE(tx.hasTxnSignature()); + + // Verify common fields + EXPECT_EQ(tx.getAccount(), accountValue); + EXPECT_EQ(tx.getSequence(), sequenceValue); + EXPECT_EQ(tx.getFee(), feeValue); + + // Verify required fields + // Verify optional fields + { + auto const& expected = objectIDValue; + auto const actualOpt = tx.getObjectID(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfObjectID should be present"; + expectEqualField(expected, *actualOpt, "sfObjectID"); + EXPECT_TRUE(tx.hasObjectID()); + } + + { + auto const& expected = sponseeValue; + auto const actualOpt = tx.getSponsee(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfSponsee should be present"; + expectEqualField(expected, *actualOpt, "sfSponsee"); + EXPECT_TRUE(tx.hasSponsee()); + } + +} + +// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper, +// and verify all fields match. +TEST(TransactionsSponsorshipTransferTests, BuilderFromStTxRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testSponsorshipTransferFromTx")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 2; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific field values + auto const objectIDValue = canonical_UINT256(); + auto const sponseeValue = canonical_ACCOUNT(); + + // Build an initial transaction + SponsorshipTransferBuilder initialBuilder{ + accountValue, + sequenceValue, + feeValue + }; + + initialBuilder.setObjectID(objectIDValue); + initialBuilder.setSponsee(sponseeValue); + + auto initialTx = initialBuilder.build(publicKey, secretKey); + + // Create builder from existing STTx + SponsorshipTransferBuilder builderFromTx{initialTx.getSTTx()}; + + auto rebuiltTx = builderFromTx.build(publicKey, secretKey); + + std::string reason; + EXPECT_TRUE(rebuiltTx.validate(reason)) << reason; + + // Verify common fields + EXPECT_EQ(rebuiltTx.getAccount(), accountValue); + EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue); + EXPECT_EQ(rebuiltTx.getFee(), feeValue); + + // Verify required fields + // Verify optional fields + { + auto const& expected = objectIDValue; + auto const actualOpt = rebuiltTx.getObjectID(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfObjectID should be present"; + expectEqualField(expected, *actualOpt, "sfObjectID"); + } + + { + auto const& expected = sponseeValue; + auto const actualOpt = rebuiltTx.getSponsee(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfSponsee should be present"; + expectEqualField(expected, *actualOpt, "sfSponsee"); + } + +} + +// 3) Verify wrapper throws when constructed from wrong transaction type. +TEST(TransactionsSponsorshipTransferTests, WrapperThrowsOnWrongTxType) +{ + // Build a valid transaction of a different type + auto const [pk, sk] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongType")); + auto const account = calcAccountID(pk); + + AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()}; + auto wrongTx = wrongBuilder.build(pk, sk); + + EXPECT_THROW(SponsorshipTransfer{wrongTx.getSTTx()}, std::runtime_error); +} + +// 4) Verify builder throws when constructed from wrong transaction type. +TEST(TransactionsSponsorshipTransferTests, BuilderThrowsOnWrongTxType) +{ + // Build a valid transaction of a different type + auto const [pk, sk] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongTypeBuilder")); + auto const account = calcAccountID(pk); + + AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()}; + auto wrongTx = wrongBuilder.build(pk, sk); + + EXPECT_THROW(SponsorshipTransferBuilder{wrongTx.getSTTx()}, std::runtime_error); +} + +// 5) Build with only required fields and verify optional fields return nullopt. +TEST(TransactionsSponsorshipTransferTests, OptionalFieldsReturnNullopt) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testSponsorshipTransferNullopt")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 3; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific required field values + + SponsorshipTransferBuilder builder{ + accountValue, + sequenceValue, + feeValue + }; + + // Do NOT set optional fields + + auto tx = builder.build(publicKey, secretKey); + + // Verify optional fields are not present + EXPECT_FALSE(tx.hasObjectID()); + EXPECT_FALSE(tx.getObjectID().has_value()); + EXPECT_FALSE(tx.hasSponsee()); + EXPECT_FALSE(tx.getSponsee().has_value()); +} + +} diff --git a/src/xrpld/app/misc/detail/TxQ.cpp b/src/xrpld/app/misc/detail/TxQ.cpp index f6d00974b4..041d2ade1e 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 @@ -408,6 +409,9 @@ TxQ::canBeHeld( // Disallow delegated transactions from being queued. if (tx.isFieldPresent(sfDelegate)) return telCAN_NOT_QUEUE; + // Disallow fee-sponsored transactions from being queued. + if (isFeeSponsored(tx)) + return telCAN_NOT_QUEUE; { // To be queued and relayed, the transaction needs to diff --git a/src/xrpld/rpc/detail/TransactionSign.cpp b/src/xrpld/rpc/detail/TransactionSign.cpp index 8c3a5ea245..b8731a2289 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; diff --git a/src/xrpld/rpc/handlers/account/AccountObjects.cpp b/src/xrpld/rpc/handlers/account/AccountObjects.cpp index 8375feb4e7..8967509262 100644 --- a/src/xrpld/rpc/handlers/account/AccountObjects.cpp +++ b/src/xrpld/rpc/handlers/account/AccountObjects.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -33,6 +34,7 @@ namespace xrpl { @param dirIndex Begin gathering account objects from this directory. @param entryIndex Begin gathering objects from this directory node. @param limit Maximum number of objects to find. + @param sponsoredFilter If set, only return objects whose sponsored state matches the value. @param jvResult A JSON result that holds the request objects. */ bool @@ -43,6 +45,7 @@ getAccountObjects( uint256 dirIndex, uint256 entryIndex, std::uint32_t const limit, + std::optional const sponsoredFilter, json::Value& jvResult) { // check if dirIndex is valid @@ -55,6 +58,12 @@ getAccountObjects( return it != typeFilter.end(); }; + auto sponsoredMatchesFilter = [&sponsoredFilter](std::optional const& sponsor) { + if (!sponsoredFilter.has_value()) + return true; + return sponsor.has_value() == *sponsoredFilter; + }; + // if dirIndex != 0, then all NFTs have already been returned. only // iterate NFT pages if the filter says so AND dirIndex == 0 bool iterateNFTPages = @@ -93,7 +102,10 @@ getAccountObjects( while (currentPage) { - jvObjects.append(currentPage->getJson(JsonOptions::Values::None)); + std::optional const nftSponsor = currentPage->at(~sfSponsor); + bool const canAppendNFT = sponsoredMatchesFilter(nftSponsor); + if (canAppendNFT) + jvObjects.append(currentPage->getJson()); auto const npm = (*currentPage)[~sfNextPageMin]; if (npm) { @@ -179,11 +191,32 @@ getAccountObjects( { auto const sleNode = ledger.read(keylet::child(*entryIter)); - if (!typeFilter.has_value() || - typeMatchesFilter(typeFilter.value(), sleNode->getType())) + bool canAppend = true; + + if (typeFilter.has_value() && + !typeMatchesFilter(typeFilter.value(), sleNode->getType())) + canAppend = false; + + // An object counts as sponsored no matter which party's directory + // it was found through; the sponsorship need not belong to + // `account`'s side. + std::optional sponsor; + if (sleNode->getType() == ltRIPPLE_STATE) { - jvObjects.append(sleNode->getJson(JsonOptions::Values::None)); + sponsor = getLedgerEntryReserveSponsorID(sleNode, sfHighSponsor); + if (!sponsor) + sponsor = getLedgerEntryReserveSponsorID(sleNode, sfLowSponsor); } + else if (isLedgerEntrySupportedBySponsorship(*sleNode)) + { + sponsor = getLedgerEntryReserveSponsorID(sleNode); + } + + if (!sponsoredMatchesFilter(sponsor)) + canAppend = false; + + if (canAppend) + jvObjects.append(sleNode->getJson(JsonOptions::Values::None)); if (++itemsAdded == limitLeft) { @@ -271,6 +304,7 @@ doAccountObjects(RPC::JsonContext& context) {.name = jss::mptoken, .type = ltMPTOKEN}, {.name = jss::permissioned_domain, .type = ltPERMISSIONED_DOMAIN}, {.name = jss::vault, .type = ltVAULT}, + {.name = jss::sponsorship, .type = ltSPONSORSHIP}, }; typeFilter.emplace(); @@ -329,7 +363,18 @@ doAccountObjects(RPC::JsonContext& context) return RPC::invalidFieldError(jss::marker); } - if (!getAccountObjects(*ledger, accountID, typeFilter, dirIndex, entryIndex, limit, result)) + std::optional sponsoredFilter; + if (params.isMember(jss::sponsored)) + { + auto const& sponsoredJv = params[jss::sponsored]; + if (!sponsoredJv.isBool()) + return RPC::expectedFieldError(jss::sponsored, "boolean"); + + sponsoredFilter = sponsoredJv.asBool(); + } + + if (!getAccountObjects( + *ledger, accountID, typeFilter, dirIndex, entryIndex, limit, sponsoredFilter, result)) return RPC::invalidFieldError(jss::marker); result[jss::account] = toBase58(accountID); diff --git a/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp b/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp index 2dd7ae34d4..784be779bb 100644 --- a/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp +++ b/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp @@ -719,6 +719,28 @@ parseSignerList( return parseObjectID(params, fieldName, "hex string"); } +static std::expected +parseSponsorship( + json::Value const& params, + json::StaticString const fieldName, + [[maybe_unused]] unsigned const apiVersion) +{ + if (!params.isObject()) + return parseObjectID(params, fieldName); + + auto const sponsorID = + LedgerEntryHelpers::requiredAccountID(params, jss::sponsor, "malformedSponsor"); + if (!sponsorID) + return std::unexpected(sponsorID.error()); + + auto const sponseeID = + LedgerEntryHelpers::requiredAccountID(params, jss::sponsee, "malformedSponsee"); + if (!sponseeID) + return std::unexpected(sponseeID.error()); + + return keylet::sponsorship(*sponsorID, *sponseeID).key; +} + static std::expected parseTicket( json::Value const& params, diff --git a/src/xrpld/rpc/handlers/transaction/Simulate.cpp b/src/xrpld/rpc/handlers/transaction/Simulate.cpp index 2a6c2280e3..0f163c7356 100644 --- a/src/xrpld/rpc/handlers/transaction/Simulate.cpp +++ b/src/xrpld/rpc/handlers/transaction/Simulate.cpp @@ -77,7 +77,7 @@ getAutofillSequence(json::Value const& txJson, RPC::JsonContext& context) } static std::optional -autofillSignature(json::Value& sigObject) +autofillSignature(json::Value& sigObject, std::string const& fieldPrefix = "tx") { if (!sigObject.isMember(jss::SigningPubKey)) { @@ -88,14 +88,17 @@ autofillSignature(json::Value& sigObject) if (sigObject.isMember(jss::Signers)) { if (!sigObject[jss::Signers].isArray()) - return RPC::invalidFieldError("tx.Signers"); + return RPC::invalidFieldError(fieldPrefix + ".Signers"); // check multisigned signers for (unsigned index = 0; index < sigObject[jss::Signers].size(); index++) { auto& signer = sigObject[jss::Signers][index]; if (!signer.isObject() || !signer.isMember(jss::Signer) || !signer[jss::Signer].isObject()) - return RPC::invalidFieldError("tx.Signers[" + std::to_string(index) + "]"); + { + return RPC::invalidFieldError( + fieldPrefix + ".Signers[" + std::to_string(index) + "]"); + } if (!signer[jss::Signer].isMember(jss::SigningPubKey)) { @@ -132,26 +135,19 @@ autofillSignature(json::Value& sigObject) static std::optional autofillTx(json::Value& txJson, RPC::JsonContext& context) { - if (!txJson.isMember(jss::Fee)) - { - // autofill Fee - // Must happen after all the other autofills happen - // Error handling/messaging works better that way - auto feeOrError = RPC::getCurrentNetworkFee( - context.role, - context.app.config(), - context.app.getFeeTrack(), - context.app.getTxQ(), - context.app, - txJson); - if (feeOrError.isMember(jss::error)) - return feeOrError; - txJson[jss::Fee] = feeOrError; - } - if (auto error = autofillSignature(txJson)) return error; + if (txJson.isMember(sfSponsorSignature.jsonName)) + { + auto& sponsorSignature = txJson[sfSponsorSignature.jsonName]; + if (!sponsorSignature.isObject()) + return RPC::objectFieldError(sfSponsorSignature.jsonName); + + if (auto const error = autofillSignature(sponsorSignature, "tx.SponsorSignature")) + return error; + } + if (!txJson.isMember(jss::Sequence)) { auto const seq = getAutofillSequence(txJson, context); @@ -167,6 +163,22 @@ autofillTx(json::Value& txJson, RPC::JsonContext& context) txJson[jss::NetworkID] = to_string(networkId); } + if (!txJson.isMember(jss::Fee)) + { + // Autofill Fee after normalizing nested signer fields so the fee + // estimator sees the full transaction shape. + auto feeOrError = RPC::getCurrentNetworkFee( + context.role, + context.app.config(), + context.app.getFeeTrack(), + context.app.getTxQ(), + context.app, + txJson); + if (feeOrError.isMember(jss::error)) + return feeOrError; + txJson[jss::Fee] = feeOrError; + } + return std::nullopt; }