diff --git a/cspell.config.yaml b/cspell.config.yaml index b13e79779b..386e1367c3 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -319,6 +319,7 @@ words: - unserviced - unshareable - unshares + - unsponsored - unsquelch - unsquelched - unsquelching diff --git a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp index a85804c4fc..e2ec5374a4 100644 --- a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp @@ -34,6 +34,7 @@ #include #include #include +#include namespace xrpl { @@ -143,8 +144,7 @@ addEmptyHolding( if (accountID == mptIssue.getIssuer()) return tesSUCCESS; - return authorizeMPToken( - {.view = ctx.view, .tx = ctx.tx}, priorBalance, mptID, accountID, journal); + return authorizeMPToken(ctx, priorBalance, mptID, accountID, journal, 0, std::nullopt); } [[nodiscard]] TER @@ -193,9 +193,14 @@ authorizeMPToken( // - add the new mptokenKey to the owner directory // - create the MPToken object for the holder - auto const sponsorSle = getTxReserveSponsor({.view = ctx.view, .tx = ctx.tx}); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE + SLE::pointer sponsorSle; + if (account == tx[sfAccount]) + { + auto sle = getTxReserveSponsor(ctx); + if (!sle) + return sle.error(); // LCOV_EXCL_LINE + sponsorSle = std::move(*sle); + } // The reserve that is required to create the MPToken. Note // that although the reserve increases with every item @@ -205,10 +210,10 @@ authorizeMPToken( // 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 (sponsorSle || ownerCount(sleAcct, journal) >= 2) { if (auto const ret = checkInsufficientReserve( - view, ctx.tx, sleAcct, priorBalance, *sponsorSle, 1, 0, journal); + view, ctx.tx, sleAcct, priorBalance, sponsorSle, 1, 0, journal); !isTesSuccess(ret)) return ret; } @@ -235,8 +240,8 @@ authorizeMPToken( view.insert(mptoken); // Update owner count. - adjustOwnerCount(view, sleAcct, *sponsorSle, 1, journal); - addSponsorToLedgerEntry(mptoken, *sponsorSle); + adjustOwnerCount(view, sleAcct, sponsorSle, 1, journal); + addSponsorToLedgerEntry(mptoken, sponsorSle); return tesSUCCESS; } diff --git a/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp b/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp index 78e4585720..9a8bb02647 100644 --- a/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp +++ b/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp @@ -664,7 +664,9 @@ addEmptyHolding( return tecDUPLICATE; SLE::pointer sponsorSle; - if (!isPseudoAccount(sleDst)) + + // A reserve sponsor only covers tx.Account's own objects. + if (!isPseudoAccount(sleDst) && accountID == tx[sfAccount]) { auto sle = getTxReserveSponsor({.view = ctx.view, .tx = ctx.tx}); if (!sle) 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/libxrpl/tx/transactors/lending/LoanSet.cpp b/src/libxrpl/tx/transactors/lending/LoanSet.cpp index f879c12d63..4273fc27ea 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(featureBatch) && !tx.isFieldPresent(sfCounterparty)) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 47ef458518..423fe7b6cc 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -4436,11 +4437,12 @@ protected: Account const lender{"lender"}; Account const issuer{"issuer"}; Account const borrower{"borrower"}; + Account const sponsor{"sponsor"}; auto const iou = issuer["IOU"]; auto testWrapper = [&](auto&& test) { Env env(*this); - env.fund(XRP(1'000), lender, issuer, borrower); + env.fund(XRP(1'000), lender, issuer, borrower, sponsor); env(trust(lender, iou(10'000'000))); env(pay(issuer, lender, iou(5'000'000))); BrokerInfo const brokerInfo{createVaultAndBroker(env, issuer["IOU"], lender)}; @@ -4455,6 +4457,15 @@ protected: BrokerInfo const& brokerInfo, jtx::Fee const& loanSetFee, Number const& debtMaximumRequest) { + for (auto const sponsorFlags : {spfSponsorReserve, spfSponsorReserve | spfSponsorFee}) + { + env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), + sponsor::As(sponsor, sponsorFlags), + Sig(sfCounterpartySignature, lender), + loanSetFee, + Ter(temINVALID_FLAG)); + } + // first temBAD_SIGNER: TODO // invalid grace period { diff --git a/src/test/app/Sponsor_test.cpp b/src/test/app/Sponsor_test.cpp index 04d448e2bc..6b974c28a1 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 @@ -404,8 +407,6 @@ public: Account const alice("alice"); Account const bob("bob"); Account const sponsor("sponsor"); - Account const invalid("invalid"); - Account const signer1("signer1"); Account const signer2("signer2"); @@ -679,10 +680,11 @@ public: using namespace test::jtx; Account const alice("alice"); Account const bob("bob"); + Account const charlie("charlie"); Account const sponsor("sponsor"); { - // both pre-funded and co-signed,pre-funded value is used + // Both pre-funded and co-signed; the pre-funded value is used. Env env{*this, testableAmendments()}; env.fund(XRP(10000), alice, bob, sponsor); env.close(); @@ -710,17 +712,17 @@ public: sle = env.le(keylet::sponsorship(sponsor, alice)); BEAST_EXPECT(sle); - BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 99); // not paybacked + BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 99); // not restored BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(99)); } { // if pre-funded value is not enough, error Env env{*this, testableAmendments()}; - env.fund(XRP(10000), alice, bob, sponsor); + env.fund(XRP(10000), alice, bob, charlie, sponsor); env.close(); - env(sponsor::set(sponsor, 0, 10, XRP(10), XRP(100)), + env(sponsor::set(sponsor, 0, 1, XRP(10), XRP(100)), sponsor::SponseeAcc(alice), Ter(tesSUCCESS)); env.close(); @@ -799,8 +801,7 @@ public: Account const alice("alice"); Account const bob("bob"); Account const sponsor1("sponsor1"); - Account const sponsor2("sponsor2"); - env.fund(XRP(10000), alice, bob, sponsor1, sponsor2); + env.fund(XRP(10000), alice, bob, sponsor1); env.close(); env(sponsor::transfer( @@ -850,19 +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)); @@ -1454,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 @@ -1473,10 +1543,8 @@ public: env.close(); { - // Fee should be checked before permission check, - // otherwise tecNO_SPONSOR_PERMISSION returned when permission - // check fails could cause context reset to pay Fee because it - // is tec error + // Fee should be checked before sponsor permission, otherwise a tec + // result from a later check could cause context reset to pay Fee. auto aliceBalance = env.balance(alice); auto bobBalance = env.balance(bob); auto sponsorBalance = env.balance(sponsor); @@ -1553,8 +1621,6 @@ public: { // below reserve adjustAccountXRPBalance(env, sponsor, env.current()->fees().reserve); - env.close(); - auto const feeAmt = XRP(4); env(noop(alice), Fee(env.current()->fees().base), @@ -1588,10 +1654,8 @@ public: }; { - // Fee should be checked before permission check, - // otherwise tecNO_SPONSOR_PERMISSION returned when permission - // check fails could cause context reset to pay Fee because it - // is tec error + // Fee should be checked before sponsor permission, otherwise a tec + // result from a later check could cause context reset to pay Fee. auto aliceBalance = env.balance(alice); auto bobBalance = env.balance(bob); auto sponsorBalance = env.balance(sponsor); @@ -2050,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)); @@ -2227,7 +2290,7 @@ public: env.fund(XRP(10000), alice, bob, sponsor, sponsor2); env.close(); - // CheckCreate -> Check = 0Cancel + // CheckCreate -> Check -> CheckCancel uint32_t seq = 0; testEachSponsorship( @@ -2293,7 +2356,7 @@ public: env.fund(XRP(10000), alice, bob, sponsor); env.close(); - // CheckCreate -> = 0 CheckCash + // CheckCreate -> CheckCash uint32_t seq2 = 0; testEachSponsorship( env, @@ -2334,7 +2397,7 @@ public: env(pay(gw, alice, usd(100))); env.close(); - // CheckCreat = 0e -> CheckCash + // CheckCreate -> CheckCash uint32_t seq2 = 0; testEachSponsorship( env, @@ -2476,8 +2539,7 @@ public: env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); env.close(); env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), - sponsor::As(sponsor2, spfSponsorReserve), - Sig(sfSponsorSignature, sponsor2)); + sponsor::As(sponsor2, spfSponsorReserve)); env.close(); } @@ -2485,6 +2547,13 @@ public: BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + if (!cosigning) + { + auto const sponsorshipSle = env.le(keylet::sponsorship(sponsor2, alice)); + BEAST_EXPECT(sponsorshipSle); + if (sponsorshipSle) + BEAST_EXPECT(sponsorshipSle->getFieldU32(sfRemainingOwnerCount) == 0); + } // DepositPreauthDelete env(deposit::unauth(alice, sponsor)); @@ -2828,14 +2897,11 @@ public: env(jv); env.close(); - // for free mptoken checks - // adjustAccountXRPBalance(env, sponsor, reserve(env, 2)); + // Create tickets so the sponsor is past free-tier reserve behavior. std::uint32_t const ticketSeq{env.seq(sponsor) + 1}; env(ticket::create(sponsor, 2)); env.close(); - // adjustAccountXRPBalance(env, sponsor, reserve(env, 3) - - // drops(1)); jv = {}; jv[sfTransactionType] = jss::MPTokenAuthorize; jv[sfAccount] = bob.human(); @@ -3022,6 +3088,105 @@ public: BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); } + void + testSponsoredTrustLineNoFreeReserve() + { + // An account with ownerCount < 2 may create its first trust lines even + // without meeting the reserve. In any case, the sponsor pays the full + // reserve in all cases, even for the sponsee's very first trust line. + testcase("Sponsored trust line gets no free-reserve exception"); + using namespace test::jtx; + + Account const issuer("issuer"); + Account const alice("alice"); + Account const sponsor("sponsor"); + + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), issuer, alice, sponsor); + env.close(); + + auto const usd = issuer["usd"]; + auto const lineKeylet = keylet::line(alice, issuer, usd.currency); + + // Sponsor funded for exactly its base reserve + adjustAccountXRPBalance(env, sponsor, reserve(env, 0)); + + // alice's ownerCount is 0, so an unsponsored first trust line would be + // free; but because it is sponsored, the reserve check is enforced + // against the sponsor, which is one increment short. + env(trust(alice, usd(100)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tecNO_LINE_INSUF_RESERVE)); + env.close(); + + BEAST_EXPECT(!env.le(lineKeylet)); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + + // Give the sponsor has exactly one owner-reserve increment; the same + // sponsored first trust line now succeeds and the sponsor pays for it. + adjustAccountXRPBalance(env, sponsor, reserve(env, 1)); + + env(trust(alice, usd(100)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + BEAST_EXPECT(env.le(lineKeylet)); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(ownerCount(env, alice) == 1); + } + + void + testCoSignReserveBoundedBySponsorshipBudget() + { + // sponsor co-signs, so a fee-only object (ReserveCount == 0) makes a co-signed + // reserve sponsorship fail -- with no fallback to the sponsor's balance. + testcase("Co-signed reserve sponsorship is bounded by Sponsorship budget"); + using namespace test::jtx; + + Env env{*this, testableAmendments()}; + Account const sponsor("sponsor"); + Account const sponsee("sponsee"); + env.fund(XRP(10000), sponsor, sponsee); + env.close(); + + // Prefund a FEE-only Sponsorship for the sponsee; ReserveCount + // defaults to 0. + env(sponsor::set_fee(sponsor, 0, XRP(100)), sponsor::SponseeAcc(sponsee)); + env.close(); + BEAST_EXPECT(env.le(keylet::sponsorship(sponsor, sponsee))); + + // Sponsee creates a Check with the sponsor co-signing the reserve. The + // fee-only Sponsorship's has ReserveCount (0), so this fails + // with tecINSUFFICIENT_RESERVE + env(check::create(sponsee, sponsor, XRP(1)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + + BEAST_EXPECT(ownerCount(env, sponsee) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsee) == 0); + + // Bumping the Sponsorship's ReserveCount budget makes the same + // co-signed reserve sponsorship succeed, the budget is what gates it. + env(sponsor::set_reserve(sponsor, 0, 1), sponsor::SponseeAcc(sponsee)); + env.close(); + + env(check::create(sponsee, sponsor, XRP(1)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(ownerCount(env, sponsee) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsee) == 1); + } + void testTrustSet(bool cosigning) { @@ -3649,7 +3814,7 @@ public: jt.jv[sfSponsorSignature.jsonName][sfSigningPubKey.jsonName] = ""; auto const seq = env.seq(alice); - // should fail BatchSigners does have signer for SponsorSignature + // should fail because BatchSigners does not have signer for SponsorSignature env(batch::outer(alice, seq, XRP(1), tfAllOrNothing), batch::Inner(jt.jv, seq + 1), batch::Inner(ticket::create(alice, 1), seq + 2), @@ -3734,6 +3899,40 @@ public: } } + // Verify that the central allow-list in preflight1Sponsor rejects + // spfSponsorReserve for transaction types that v1 does not permit. + void + testReserveSponsorGate() + { + testcase("Reserve sponsor allow-list gate"); + using namespace test::jtx; + + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + env(sponsor::set(sponsor, 0, 10, XRP(10)), sponsor::SponseeAcc(alice)); + env.close(); + + auto checkBlocked = [&](json::Value const& jv) { + env(jv, + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(temINVALID_FLAG)); + }; + + checkBlocked(ticket::create(alice, 1)); + checkBlocked(offer(alice, XRP(100), bob["USD"](100))); + checkBlocked(did::setValid(alice)); + checkBlocked(token::mint(alice, 0u)); + checkBlocked(sponsor::set(alice, 0, 10, XRP(10))); + checkBlocked(acctdelete(alice, bob)); + checkBlocked(loan::set(alice, uint256(1), Number{1})); + } + void testSponsorReserve(bool cosigning) { @@ -3775,6 +3974,10 @@ protected: testDelegatePermission(); testBatch(); + + testSponsoredTrustLineNoFreeReserve(); + testCoSignReserveBoundedBySponsorshipBudget(); + 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); } } }