From 70c16e8e1e32499a13912ed90efa82c2025e5073 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Mon, 6 Jul 2026 16:48:59 -0400 Subject: [PATCH 1/2] fix: Remove SponsorshipTransfer from VaultInvariant/PermissionedDomainInvariant (#7739) --- include/xrpl/protocol/detail/transactions.macro | 2 +- .../transactions/SponsorshipTransfer.h | 2 +- .../tx/invariants/PermissionedDomainInvariant.cpp | 12 ------------ src/libxrpl/tx/invariants/VaultInvariant.cpp | 2 -- 4 files changed, 2 insertions(+), 16 deletions(-) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 97ff0f191e..951fe4a172 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -1170,7 +1170,7 @@ TRANSACTION(ttCONFIDENTIAL_MPT_CLAWBACK, 89, ConfidentialMPTClawback, TRANSACTION(ttSPONSORSHIP_TRANSFER, 90, SponsorshipTransfer, Delegation::NotDelegable, featureSponsor, - MayModifyVault, + NoPriv, ({ {sfObjectID, SoeOptional}, {sfSponsee, SoeOptional}, diff --git a/include/xrpl/protocol_autogen/transactions/SponsorshipTransfer.h b/include/xrpl/protocol_autogen/transactions/SponsorshipTransfer.h index 03bc4d4e38..bc27f1603b 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 (90) * Delegable: Delegation::NotDelegable * Amendment: featureSponsor - * Privileges: MayModifyVault + * Privileges: NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use SponsorshipTransferBuilder to construct new transactions. diff --git a/src/libxrpl/tx/invariants/PermissionedDomainInvariant.cpp b/src/libxrpl/tx/invariants/PermissionedDomainInvariant.cpp index ba4725560c..fcb53e7c8f 100644 --- a/src/libxrpl/tx/invariants/PermissionedDomainInvariant.cpp +++ b/src/libxrpl/tx/invariants/PermissionedDomainInvariant.cpp @@ -151,18 +151,6 @@ ValidPermissionedDomain::finalize( } return true; } - case ttSPONSORSHIP_TRANSFER: { - if (sleStatus_.empty()) - return true; - - if (sleStatus_[0].isDelete) - { - JLOG(j.fatal()) << "Invariant failed: domain object " - "deleted by SponsorshipTransfer"; - return false; - } - return check(sleStatus_[0], j); - } default: { if (!sleStatus_.empty()) { diff --git a/src/libxrpl/tx/invariants/VaultInvariant.cpp b/src/libxrpl/tx/invariants/VaultInvariant.cpp index 31d7de58fc..691a92966a 100644 --- a/src/libxrpl/tx/invariants/VaultInvariant.cpp +++ b/src/libxrpl/tx/invariants/VaultInvariant.cpp @@ -1045,8 +1045,6 @@ ValidVault::finalize( case ttLOAN_SET: case ttLOAN_MANAGE: case ttLOAN_PAY: - case ttSPONSORSHIP_TRANSFER: - // TBD for loans; SponsorshipTransfer may update a vault's sfSponsor return true; default: From 7868d6ed7994db9f71b8a8abbae499a291d8ed3b Mon Sep 17 00:00:00 2001 From: Peter Chen <34582813+PeterChen13579@users.noreply.github.com> Date: Mon, 6 Jul 2026 14:09:53 -0700 Subject: [PATCH 2/2] fix sponsor fee (#7738) --- src/libxrpl/tx/invariants/VaultInvariant.cpp | 5 +- src/test/app/Sponsor_test.cpp | 52 ++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/libxrpl/tx/invariants/VaultInvariant.cpp b/src/libxrpl/tx/invariants/VaultInvariant.cpp index 691a92966a..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(); diff --git a/src/test/app/Sponsor_test.cpp b/src/test/app/Sponsor_test.cpp index 21320c034a..9f0d7e0398 100644 --- a/src/test/app/Sponsor_test.cpp +++ b/src/test/app/Sponsor_test.cpp @@ -4565,6 +4565,56 @@ public: BEAST_EXPECT(issuerSponsoredAfter == issuerSponsoredBefore); } + 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(); + } + protected: void testSponsor() @@ -4602,6 +4652,8 @@ protected: testZeroBalanceSponsoredPaymentFeePayerCheck(); testTrustSetCounterpartySponsorMisroute(); + + testFeeSponsoredVaultInvariant(); } void