Merge branch 'xrplf/sponsor' into mvadari/sponsor/accdelete

This commit is contained in:
Mayukha Vadari
2026-07-06 17:22:41 -04:00
committed by GitHub
5 changed files with 58 additions and 17 deletions

View File

@@ -1170,7 +1170,7 @@ TRANSACTION(ttCONFIDENTIAL_MPT_CLAWBACK, 89, ConfidentialMPTClawback,
TRANSACTION(ttSPONSORSHIP_TRANSFER, 90, SponsorshipTransfer,
Delegation::NotDelegable,
featureSponsor,
MayModifyVault,
NoPriv,
({
{sfObjectID, SoeOptional},
{sfSponsee, SoeOptional},

View File

@@ -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.

View File

@@ -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())
{

View File

@@ -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();
@@ -1045,8 +1048,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:

View File

@@ -4583,6 +4583,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()
@@ -4620,6 +4670,8 @@ protected:
testZeroBalanceSponsoredPaymentFeePayerCheck();
testTrustSetCounterpartySponsorMisroute();
testFeeSponsoredVaultInvariant();
}
void