From 018e736a3b5b5b59cb82e02abe3173c0c7d5fdb8 Mon Sep 17 00:00:00 2001 From: yinyiqian1 Date: Mon, 22 Jun 2026 15:18:22 -0400 Subject: [PATCH] Fix sponsor reserve accounting edge cases (#7575) --- .../xrpl/protocol/detail/transactions.macro | 2 +- .../transactions/SponsorshipTransfer.h | 2 +- src/libxrpl/tx/invariants/VaultInvariant.cpp | 4 ++ .../Sponsor/SponsorshipTransfer.cpp | 3 + .../tx/transactors/payment/Payment.cpp | 3 + src/test/app/Sponsor_test.cpp | 71 +++++++++++++++++++ 6 files changed, 83 insertions(+), 2 deletions(-) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 8eabc99f57..c901a8e551 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 8ac43071ee..ff11a957a7 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 80b8f36bd9..ad29617f6b 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 424f1a6fb6..7b0942fbff 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 08632158ac..db176611e8 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 da473f5c3f..8c90464c5d 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 @@ -1856,6 +1858,20 @@ public: 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))), @@ -4994,6 +5010,61 @@ public: 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