diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 73e5173876b..ce491b4586f 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -1084,7 +1084,7 @@ TRANSACTION(ttLOAN_PAY, 84, LoanPay, TRANSACTION(ttSPONSORSHIP_TRANSFER, 85, SponsorshipTransfer, Delegation::Delegable, featureSponsor, - NoPriv, + MayModifyVault, ({ {sfObjectID, SoeOptional}, {sfSponsee, SoeOptional}, diff --git a/include/xrpl/protocol_autogen/transactions/SponsorshipTransfer.h b/include/xrpl/protocol_autogen/transactions/SponsorshipTransfer.h index 8ac43071ee8..ff11a957a7b 100644 --- a/include/xrpl/protocol_autogen/transactions/SponsorshipTransfer.h +++ b/include/xrpl/protocol_autogen/transactions/SponsorshipTransfer.h @@ -21,7 +21,7 @@ class SponsorshipTransferBuilder; * Type: ttSPONSORSHIP_TRANSFER (85) * Delegable: Delegation::Delegable * Amendment: featureSponsor - * Privileges: NoPriv + * Privileges: MayModifyVault * * Immutable wrapper around STTx providing type-safe field access. * Use SponsorshipTransferBuilder to construct new transactions. diff --git a/src/libxrpl/tx/invariants/VaultInvariant.cpp b/src/libxrpl/tx/invariants/VaultInvariant.cpp index 80b8f36bd92..ad29617f6b0 100644 --- a/src/libxrpl/tx/invariants/VaultInvariant.cpp +++ b/src/libxrpl/tx/invariants/VaultInvariant.cpp @@ -1048,6 +1048,10 @@ ValidVault::finalize( // TBD return true; } + case ttSPONSORSHIP_TRANSFER: { + // SponsorshipTransfer may update a vault's sfSponsor + return true; + } default: // LCOV_EXCL_START diff --git a/src/libxrpl/tx/transactors/Sponsor/SponsorshipTransfer.cpp b/src/libxrpl/tx/transactors/Sponsor/SponsorshipTransfer.cpp index 24eb54e632a..fd41ee892e7 100644 --- a/src/libxrpl/tx/transactors/Sponsor/SponsorshipTransfer.cpp +++ b/src/libxrpl/tx/transactors/Sponsor/SponsorshipTransfer.cpp @@ -205,6 +205,9 @@ getLedgerEntryOwnerCount(T const& sle) 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; } diff --git a/src/libxrpl/tx/transactors/payment/Payment.cpp b/src/libxrpl/tx/transactors/payment/Payment.cpp index 08632158ac2..db176611e85 100644 --- a/src/libxrpl/tx/transactors/payment/Payment.cpp +++ b/src/libxrpl/tx/transactors/payment/Payment.cpp @@ -132,6 +132,9 @@ Payment::preflight(PreflightContext const& ctx) 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; } diff --git a/src/test/app/Sponsor_test.cpp b/src/test/app/Sponsor_test.cpp index d3f94c2ccb6..50bf9f4c92d 100644 --- a/src/test/app/Sponsor_test.cpp +++ b/src/test/app/Sponsor_test.cpp @@ -20,8 +20,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -1848,6 +1850,20 @@ class Sponsor_test : public beast::unit_test::Suite 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(baseAccountReserve(*env.current(), 0))), @@ -4985,6 +5001,61 @@ class Sponsor_test : public beast::unit_test::Suite BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); } + // SponsorshipTransfer + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, gw, sponsor); + env.close(); + + Vault const vault{env}; + auto const [tx, vaultKeylet] = vault.create({.owner = alice, .asset = asset}); + env(tx); + env.close(); + + // Alice owns the vault, pseudo account and MPToken + BEAST_EXPECT(ownerCount(env, alice) == 3); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + + if (cosigning) + { + // Alice lets sponsor to sponsor her Vault + env(sponsor::transfer(alice, tfSponsorshipCreate, vaultKeylet.key), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + } + else + { + // Create sponsorship with reserve count being 2 (for vault and pseudo account) + env(sponsor::set_reserve(sponsor, 0, 2), sponsor::SponseeAcc(alice)); + env.close(); + env(sponsor::transfer(alice, tfSponsorshipCreate, vaultKeylet.key), + sponsor::As(sponsor, spfSponsorReserve)); + env.close(); + + auto const sponsorshipSle = env.le(keylet::sponsor(sponsor, alice)); + if (!BEAST_EXPECT(sponsorshipSle)) + return; + BEAST_EXPECT(sponsorshipSle->getFieldU32(sfReserveCount) == 0); + } + + BEAST_EXPECT(env.le(vaultKeylet)->getAccountID(sfSponsor) == sponsor.id()); + BEAST_EXPECT(ownerCount(env, alice) == 3); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 2); + // Vault counts for 2 reserves, vault and the pseudo account + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 2); + + // End sponsorship + env(sponsor::transfer(alice, tfSponsorshipEnd, vaultKeylet.key)); + env.close(); + + BEAST_EXPECT(!env.le(vaultKeylet)->isFieldPresent(sfSponsor)); + BEAST_EXPECT(ownerCount(env, alice) == 3); + // Sponsorship ended and the sponsored owner count should be 0. + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + } } void