diff --git a/include/xrpl/ledger/View.h b/include/xrpl/ledger/View.h index cd23cf4978..5940785ad9 100644 --- a/include/xrpl/ledger/View.h +++ b/include/xrpl/ledger/View.h @@ -1103,6 +1103,9 @@ sharesToAssetsWithdraw( std::shared_ptr const& issuance, STAmount const& shares); +[[nodiscard]] bool +isVaultDonate(Rules const& rules, STTx const& tx); + /** Has the specified time passed? @param now the current time diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index e55a3fdc15..088a0d150c 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -186,6 +186,10 @@ inline constexpr FlagValue tfUniversalMask = ~tfUniversal; TF_FLAG(tfVaultShareNonTransferable, 0x00020000), \ MASK_ADJ(0)) \ \ + TRANSACTION(VaultDeposit, \ + TF_FLAG(tfVaultDonate, 0x00010000), \ + MASK_ADJ(0)) \ + \ TRANSACTION(Batch, \ TF_FLAG(tfAllOrNothing, 0x00010000) \ TF_FLAG(tfOnlyOne, 0x00020000) \ @@ -214,8 +218,6 @@ inline constexpr FlagValue tfUniversalMask = ~tfUniversal; TF_FLAG(tfLoanUnimpair, 0x00040000), \ MASK_ADJ(0)) -constexpr std::uint32_t const tfVaultDonate = 0x00010000; -constexpr std::uint32_t const tfVaultDepositMask = ~(tfUniversal | tfVaultDonate); // clang-format on // Create all the flag values. diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index d5c94a9981..348d171d0a 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -3723,6 +3723,12 @@ rippleUnlockEscrowMPT( return tesSUCCESS; } +[[nodiscard]] bool +isVaultDonate(Rules const& rules, STTx const& tx) +{ + return rules.enabled(featureLendingProtocolV1_1) && tx.isFlag(tfVaultDonate); +} + bool after(NetClock::time_point now, std::uint32_t mark) { diff --git a/src/libxrpl/tx/invariants/VaultInvariant.cpp b/src/libxrpl/tx/invariants/VaultInvariant.cpp index a609d2782a..051ea69c78 100644 --- a/src/libxrpl/tx/invariants/VaultInvariant.cpp +++ b/src/libxrpl/tx/invariants/VaultInvariant.cpp @@ -419,8 +419,7 @@ ValidVault::finalize( return std::nullopt; }(); - bool const isDonate = - view.rules().enabled(featureLendingProtocolV1_1) && tx.isFlag(tfVaultDonate); + bool const isDonate = isVaultDonate(view.rules(), tx); bool const shouldUpdateShares = // Vault Asset donation is the only operation that can succeed without updating shares ((tx.getTxnType() == ttVAULT_DEPOSIT && !isDonate) || // @@ -711,7 +710,7 @@ ValidVault::finalize( } // If assets are donated, check share invariants - if (view.rules().enabled(featureLendingProtocolV1_1) && tx.isFlag(tfVaultDonate)) + if (isDonate) { auto const accountDeltaShares = deltaShares(tx[sfAccount]); if (accountDeltaShares) diff --git a/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp b/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp index 79a6e58093..1cd3b2adc6 100644 --- a/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp @@ -77,7 +77,7 @@ VaultDeposit::preclaim(PreclaimContext const& ctx) // LCOV_EXCL_STOP } - if (ctx.view.rules().enabled(featureLendingProtocolV1_1) && ctx.tx.isFlag(tfVaultDonate)) + if (isVaultDonate(ctx.view.rules(), ctx.tx)) { if (account != vault->at(sfOwner)) { @@ -168,8 +168,7 @@ VaultDeposit::doApply() // LCOV_EXCL_STOP } - auto const isDonate = - ctx_.view().rules().enabled(featureLendingProtocolV1_1) && ctx_.tx.isFlag(tfVaultDonate); + auto const isDonate = isVaultDonate(ctx_.view().rules(), ctx_.tx); auto const& vaultAccount = vault->at(sfAccount); // Note, vault owner is always authorized diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 3c088993df..075935bd91 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -5434,6 +5434,81 @@ class Vault_test : public beast::unit_test::suite BEAST_EXPECT(assetsAvailableAfterWithdraw == 0); BEAST_EXPECT(assetsTotalAfterWithdraw == 0); } + + // Test donation with non-1:1 share ratio. + // A prior donation skews the ratio so that 1 share > 1 asset. + // The donated amount must land exactly, not rounded via shares. + { + testcase(prefix + " succeeds with non-1:1 share ratio"); + + // Create a fresh vault + auto const [createTx, vk] = vault.create({.owner = owner, .asset = xrpIssue()}); + env(createTx, ter{tesSUCCESS}); + env.close(); + + // Depositor puts in 10 XRP → gets 10 shares at 1:1 + env(vault.deposit({ + .depositor = depositor, + .id = vk.key, + .amount = XRP(10), + }), + ter{tesSUCCESS}); + env.close(); + + // Owner donates 7 XRP → ratio becomes 17 assets / 10 shares + env(vault.deposit({ + .depositor = owner, + .id = vk.key, + .amount = XRP(7), + .flags = tfVaultDonate, + }), + ter{tesSUCCESS}); + env.close(); + + auto const sharesAfterFirstDonate = vaultShareBalance(vk); + auto const [availAfterFirstDonate, totalAfterFirstDonate] = vaultAssetBalance(vk); + + // Shares unchanged (donation doesn't mint shares) + BEAST_EXPECT(sharesAfterFirstDonate == 10'000'000); + // Assets increased by exactly the donated amount + BEAST_EXPECT(availAfterFirstDonate == 17'000'000); + BEAST_EXPECT(totalAfterFirstDonate == 17'000'000); + + // Donate again at the skewed 17:10 ratio — 3 XRP + env(vault.deposit({ + .depositor = owner, + .id = vk.key, + .amount = XRP(3), + .flags = tfVaultDonate, + }), + ter{tesSUCCESS}); + env.close(); + + auto const sharesAfterSecondDonate = vaultShareBalance(vk); + auto const [availAfterSecondDonate, totalAfterSecondDonate] = vaultAssetBalance(vk); + + // Shares still unchanged + BEAST_EXPECT(sharesAfterSecondDonate == 10'000'000); + // Assets increased by exactly 3 XRP (20 total) + BEAST_EXPECT(availAfterSecondDonate == 20'000'000); + BEAST_EXPECT(totalAfterSecondDonate == 20'000'000); + + // Depositor withdraws all shares — should get all 20 XRP + auto const sleVault = env.le(vk); + if (!BEAST_EXPECT(sleVault)) + return; + Asset shareAsset(sleVault->at(sfShareMPTID)); + env(vault.withdraw( + {.depositor = depositor, + .id = vk.key, + .amount = shareAsset(sharesAfterSecondDonate)}), + ter{tesSUCCESS}); + env.close(); + + BEAST_EXPECT(vaultShareBalance(vk) == 0); + BEAST_EXPECT(vaultAssetBalance(vk).first == 0); + BEAST_EXPECT(vaultAssetBalance(vk).second == 0); + } } public: