diff --git a/src/libxrpl/tx/Transactor.cpp b/src/libxrpl/tx/Transactor.cpp index cd92bc4eae..c38109bece 100644 --- a/src/libxrpl/tx/Transactor.cpp +++ b/src/libxrpl/tx/Transactor.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -202,6 +203,46 @@ preflight1Sponsor(PreflightContext const& ctx, AccountID const& id) JLOG(ctx.j.debug()) << "preflight1: invalid sponsor flags"; return temINVALID_FLAG; } + + // Reserve sponsorship is only permitted for an explicit allow-list of + // transaction types, for v1. All other tx types reject spfSponsorReserve here. + if ((sponsorFlags & spfSponsorReserve) != 0u) + { + static std::unordered_set const kReserveSponsorAllowed = { + // Explicitly allow-listed for v1. + ttDELEGATE_SET, + ttDEPOSIT_PREAUTH, + ttPAYMENT, + ttSIGNER_LIST_SET, + ttCHECK_CANCEL, + ttCHECK_CASH, + ttCHECK_CREATE, + ttESCROW_CANCEL, + ttESCROW_CREATE, + ttESCROW_FINISH, + ttPAYCHAN_CLAIM, + ttPAYCHAN_CREATE, + ttPAYCHAN_FUND, + ttCLAWBACK, + ttMPTOKEN_AUTHORIZE, + ttMPTOKEN_ISSUANCE_CREATE, + ttMPTOKEN_ISSUANCE_DESTROY, + ttMPTOKEN_ISSUANCE_SET, + ttTRUST_SET, + ttCREDENTIAL_ACCEPT, + ttCREDENTIAL_CREATE, + ttCREDENTIAL_DELETE, + ttACCOUNT_SET, + ttREGULAR_KEY_SET, + ttSPONSORSHIP_TRANSFER, + }; + if (!kReserveSponsorAllowed.contains(ctx.tx.getTxnType())) + { + JLOG(ctx.j.debug()) + << "preflight1: spfSponsorReserve not allowed for this transaction type"; + return temINVALID_FLAG; + } + } } else { diff --git a/src/libxrpl/tx/transactors/Sponsor/SponsorshipTransfer.cpp b/src/libxrpl/tx/transactors/Sponsor/SponsorshipTransfer.cpp index c4e9ac817f..0c98511a5c 100644 --- a/src/libxrpl/tx/transactors/Sponsor/SponsorshipTransfer.cpp +++ b/src/libxrpl/tx/transactors/Sponsor/SponsorshipTransfer.cpp @@ -16,7 +16,6 @@ #include #include #include -#include #include #include @@ -123,29 +122,15 @@ getLedgerEntryOwner(ReadView const& view, T const& sle, AccountID const& account { switch (sle->getType()) { - case ltNFTOKEN_OFFER: - case ltORACLE: - case ltPERMISSIONED_DOMAIN: - case ltVAULT: - case ltLOAN_BROKER: - return sle->getAccountID(sfOwner); case ltCHECK: - case ltDID: - case ltTICKET: - case ltOFFER: - case ltXCHAIN_OWNED_CLAIM_ID: - case ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID: case ltESCROW: case ltPAYCHAN: case ltMPTOKEN: case ltDELEGATE: - case ltBRIDGE: case ltDEPOSIT_PREAUTH: return sle->getAccountID(sfAccount); case ltMPTOKEN_ISSUANCE: return sle->getAccountID(sfIssuer); - case ltLOAN: - return sle->getAccountID(sfBorrower); case ltSIGNER_LIST: { auto const signerList = view.read(keylet::signers(account)); if (!signerList) @@ -159,12 +144,6 @@ getLedgerEntryOwner(ReadView const& view, T const& sle, AccountID const& account return sle->getAccountID(sfSubject); return sle->getAccountID(sfIssuer); } - case ltNFTOKEN_PAGE: { - // the upper 20 bytes of the index of ltNFTokenPage are the Owner's - // AccountID - uint256 const& key = sle->key(); - return AccountID::fromVoid(key.data()); - } case ltRIPPLE_STATE: { if (sle->isFlag(lsfHighReserve)) { @@ -180,39 +159,12 @@ getLedgerEntryOwner(ReadView const& view, T const& sle, AccountID const& account } return std::nullopt; } - case ltACCOUNT_ROOT: { - // AccountRoot is not supported for object sponsorship - return std::nullopt; - } - case ltNEGATIVE_UNL: - case ltDIR_NODE: - case ltAMENDMENTS: - case ltLEDGER_HASHES: - case ltFEE_SETTINGS: - case ltAMM: - return std::nullopt; default: + UNREACHABLE("Object is not supported by sponsorship."); return std::nullopt; }; } -template -inline std::uint32_t -getLedgerEntryOwnerCount(T const& sle) -{ - switch (sle->getType()) - { - case ltORACLE: { - return OracleSet::calculateOracleReserve(sle->getFieldArray(sfPriceDataSeries).size()); - } - // Vaults require 2 owner counts (the vault and a pseudo-account) - case ltVAULT: - return 2; - default: - return 1; - } -}; - template inline SF_ACCOUNT const& getLedgerEntrySponsorField(T const& sle, AccountID const& owner) @@ -265,7 +217,30 @@ SponsorshipTransfer::preclaim(PreclaimContext const& ctx) if (!sle) return tecNO_ENTRY; - auto const ownerCountDelta = getLedgerEntryOwnerCount(sle); + // v1 scope: an object is only sponsorable via SponsorshipTransfer if + // its creating transaction type is itself permitted to set + // spfSponsorReserve (the allow-list in preflight1Sponsor). Otherwise + // an Oracle / Ticket / DID / etc. could be retroactively sponsored + // even though its creating tx cannot be, leaving downstream + // transactors with no path to maintain the sponsorship invariants. + switch (sle->getType()) + { + case ltDELEGATE: + case ltDEPOSIT_PREAUTH: + case ltMPTOKEN: + case ltMPTOKEN_ISSUANCE: + case ltCREDENTIAL: + case ltRIPPLE_STATE: + case ltSIGNER_LIST: + case ltCHECK: + case ltESCROW: + case ltPAYCHAN: + break; + default: + return tecNO_PERMISSION; + } + + std::uint32_t const ownerCountDelta = 1; auto const owner = getLedgerEntryOwner(ctx.view, sle, sponseeID); if (!owner || owner != sponseeID) @@ -450,7 +425,7 @@ SponsorshipTransfer::doApply() if (!ownerSle) return tefINTERNAL; // LCOV_EXCL_LINE - std::int64_t const ownerCountDelta = getLedgerEntryOwnerCount(objSle); + std::int64_t const ownerCountDelta = 1; auto const& sponsorField = getLedgerEntrySponsorField(objSle, *ownerID); diff --git a/src/test/app/Sponsor_test.cpp b/src/test/app/Sponsor_test.cpp index 2432bbad8e..51fe35cd77 100644 --- a/src/test/app/Sponsor_test.cpp +++ b/src/test/app/Sponsor_test.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -22,10 +23,12 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -677,6 +680,7 @@ public: using namespace test::jtx; Account const alice("alice"); Account const bob("bob"); + Account const charlie("charlie"); Account const sponsor("sponsor"); { @@ -715,10 +719,10 @@ public: { // if pre-funded value is not enough, error Env env{*this, testableAmendments()}; - env.fund(XRP(10000), alice, bob, sponsor); + env.fund(XRP(10000), alice, bob, charlie, sponsor); env.close(); - env(sponsor::set(sponsor, 0, 10, XRP(10), XRP(100)), + env(sponsor::set(sponsor, 0, 1, XRP(10), XRP(100)), sponsor::SponseeAcc(alice), Ter(tesSUCCESS)); env.close(); @@ -847,19 +851,21 @@ public: Env env{*this, testableAmendments()}; Account const alice("alice"); Account const bob("bob"); + Account const charlie("charlie"); Account const sponsor("sponsor"); env.fund(XRP(10000), alice, bob, sponsor); env.close(); { // sponsor object - env(did::set(alice), - did::Uri("uri"), + env.fund(XRP(1000), charlie); + env.close(); + env(deposit::auth(alice, charlie), sponsor::As(sponsor, spfSponsorReserve), Sig(sfSponsorSignature, sponsor)); env.close(); - auto const keylet = keylet::did(alice); + auto const keylet = keylet::depositPreauth(alice, charlie); env(sponsor::transfer(bob, tfSponsorshipEnd, keylet.key), sponsor::SponseeAcc(alice), Ter(tecNO_PERMISSION)); @@ -1451,6 +1457,73 @@ public: Ter(tecNO_PERMISSION)); } } + + { + // existing owner objects that are outside the v1 SponsorshipTransfer + // object allow-list + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, sponsor); + env.close(); + + auto const checkBlocked = [&](Account const& account, uint256 const& objectID) { + env(sponsor::transfer(account, tfSponsorshipCreate, objectID), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tecNO_PERMISSION)); + env.close(); + }; + + auto const ticketSeq = env.seq(alice); + env(ticket::create(alice, 1)); + env.close(); + auto const ticketID = keylet::TicketT()(alice, ticketSeq + 1).key; + BEAST_EXPECT(env.le(keylet::unchecked(ticketID))); + checkBlocked(alice, ticketID); + + env(did::setValid(alice)); + env.close(); + auto const didKeylet = keylet::did(alice.id()); + BEAST_EXPECT(env.le(didKeylet)); + checkBlocked(alice, didKeylet.key); + + env(token::mint(alice, 0u)); + env.close(); + auto const nftPageKeylet = keylet::nftpageMax(alice); + BEAST_EXPECT(env.le(nftPageKeylet)); + checkBlocked(alice, nftPageKeylet.key); + + Account const borrower("borrower"); + env.fund(XRP(1000000), borrower); + env.close(); + + PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; + Vault const vault{env}; + auto [vaultTx, vaultKeylet] = vault.create({.owner = alice, .asset = xrpAsset}); + env(vaultTx); + env.close(); + + env(vault.deposit( + {.depositor = alice, .id = vaultKeylet.key, .amount = xrpAsset(1000)})); + env.close(); + + auto const brokerKeylet = keylet::loanbroker(alice.id(), env.seq(alice)); + env(loanBroker::set(alice, vaultKeylet.key), + loanBroker::kDebtMaximum(xrpAsset(1000).value()), + loanBroker::kManagementFeeRate(TenthBips16{0}), + loanBroker::kCoverRateMinimum(TenthBips32{0}), + loanBroker::kCoverRateLiquidation(TenthBips32{0})); + env.close(); + + auto const loanKeylet = keylet::loan(brokerKeylet.key, 1); + env(loan::set(borrower, brokerKeylet.key, xrpAsset(100).value()), + Sig(sfCounterpartySignature, alice), + Fee(env.current()->fees().base * 2)); + env.close(); + BEAST_EXPECT(env.le(loanKeylet)); + checkBlocked(borrower, loanKeylet.key); + } } void @@ -2041,7 +2114,6 @@ public: env.fund(XRP(10000), alice, bob, sponsor); env.close(); - // test Sufficient sponsor balance if (cosigning) { adjustAccountXRPBalance(env, sponsor, reserve(env, 1) - drops(1)); @@ -3728,6 +3800,40 @@ public: } } + // Verify that the central allow-list in preflight1Sponsor rejects + // spfSponsorReserve for transaction types that v1 does not permit. + void + testReserveSponsorGate() + { + testcase("Reserve sponsor allow-list gate"); + using namespace test::jtx; + + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + env(sponsor::set(sponsor, 0, 10, XRP(10)), sponsor::SponseeAcc(alice)); + env.close(); + + auto checkBlocked = [&](json::Value const& jv) { + env(jv, + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(temINVALID_FLAG)); + }; + + checkBlocked(ticket::create(alice, 1)); + checkBlocked(offer(alice, XRP(100), bob["USD"](100))); + checkBlocked(did::setValid(alice)); + checkBlocked(token::mint(alice, 0u)); + checkBlocked(sponsor::set(alice, 0, 10, XRP(10))); + checkBlocked(acctdelete(alice, bob)); + checkBlocked(loan::set(alice, uint256(1), Number{1})); + } + void testSponsorReserve(bool cosigning) { @@ -3769,6 +3875,8 @@ protected: testDelegatePermission(); testBatch(); + + testReserveSponsorGate(); } void diff --git a/src/test/rpc/AccountObjects_test.cpp b/src/test/rpc/AccountObjects_test.cpp index d319b92950..c040c0a5f5 100644 --- a/src/test/rpc/AccountObjects_test.cpp +++ b/src/test/rpc/AccountObjects_test.cpp @@ -1535,86 +1535,43 @@ public: } } - // A Sponsorship object is visible to both sides, but its reserve side - // belongs only to sfOwner. + // A Sponsorship object is visible to both sides. { Env env(*this, testableAmendments()); Account const owner("owner"); Account const sponsee("sponsee"); - Account const sponsor("sponsor"); - env.fund(XRP(10000), owner, sponsee, sponsor); + env.fund(XRP(10000), owner, sponsee); env.close(); - env(sponsor::set_reserve(sponsor, 0, 100), sponsor::SponseeAcc(owner)); + env(sponsor::set(owner, 0, 100, XRP(100)), sponsor::SponseeAcc(sponsee)); env.close(); - env(sponsor::set(owner, 0, 100, XRP(100)), - sponsor::SponseeAcc(sponsee), - sponsor::As(sponsor, spfSponsorReserve), - Sig(sfSponsorSignature, sponsor)); - env.close(); - - auto const sponsorship = env.le(keylet::sponsorship(owner, sponsee)); - if (!BEAST_EXPECT(sponsorship)) + auto const sponsorshipKeylet = keylet::sponsorship(owner, sponsee); + if (!BEAST_EXPECT(env.le(sponsorshipKeylet))) return; - BEAST_EXPECT(sponsorship->isFieldPresent(sfSponsor)); { - auto const resp = acctObjsSponsored(env, owner.id(), true, jss::sponsorship); + auto const resp = acctObjsSponsored(env, owner.id(), false, jss::sponsorship); auto const& objs = resp[jss::result][jss::account_objects]; if (BEAST_EXPECT(objs.size() == 1)) BEAST_EXPECT(objs[0u][sfLedgerEntryType.jsonName] == jss::Sponsorship); } - { - auto const resp = acctObjsSponsored(env, sponsee.id(), true, jss::sponsorship); - auto const& objs = resp[jss::result][jss::account_objects]; - BEAST_EXPECT(objs.size() == 0); - } { auto const resp = acctObjsSponsored(env, sponsee.id(), false, jss::sponsorship); auto const& objs = resp[jss::result][jss::account_objects]; if (BEAST_EXPECT(objs.size() == 1)) BEAST_EXPECT(objs[0u][sfLedgerEntryType.jsonName] == jss::Sponsorship); } - } - - // NFT page sponsored filter - { - // Mint an NFT for bob (creates NFT page) - env(token::mint(bob, 0)); - env.close(); - - auto const nftPageKeylet = keylet::nftpageMax(bob); - if (!BEAST_EXPECT(env.le(nftPageKeylet))) - return; - - // Sponsor the NFT page - env(sponsor::transfer(bob, tfSponsorshipCreate, nftPageKeylet.key), - sponsor::As(sponsor1, spfSponsorReserve), - Sig(sfSponsorSignature, sponsor1)); - env.close(); - - // Verify NFT page has sponsor field - auto const nftPage = env.le(nftPageKeylet); - if (!BEAST_EXPECT(nftPage)) - return; - BEAST_EXPECT(nftPage->isFieldPresent(sfSponsor)); - - // sponsored=true should include the sponsored NFT page - // sponsored=false should NOT include the sponsored NFT page - for (auto const sponsored : {true, false}) { - auto const resp = acctObjsSponsored(env, bob.id(), sponsored); + auto const resp = acctObjsSponsored(env, owner.id(), true, jss::sponsorship); auto const& objs = resp[jss::result][jss::account_objects]; - bool foundNFTPage = false; - for (auto const& obj : objs) - { - if (obj[sfLedgerEntryType.jsonName] == jss::NFTokenPage && - obj.isMember(sfSponsor.jsonName)) - foundNFTPage = true; - } - BEAST_EXPECT(foundNFTPage == sponsored); + BEAST_EXPECT(objs.size() == 0); + } + { + auto const resp = acctObjsSponsored(env, sponsee.id(), true, jss::sponsorship); + auto const& objs = resp[jss::result][jss::account_objects]; + BEAST_EXPECT(objs.size() == 0); } } }