diff --git a/include/xrpl/ledger/helpers/VaultHelpers.h b/include/xrpl/ledger/helpers/VaultHelpers.h index 5681cc57e8..e4692d240b 100644 --- a/include/xrpl/ledger/helpers/VaultHelpers.h +++ b/include/xrpl/ledger/helpers/VaultHelpers.h @@ -5,6 +5,7 @@ #include #include #include +#include #include @@ -123,4 +124,7 @@ isSoleShareholder(ReadView const& view, AccountID const& account, SLE::const_ref [[nodiscard]] VaultVersion getVaultVersion(SLE::const_ref vault); +[[nodiscard]] bool +isVaultDonate(Rules const& rules, STTx const& tx); + } // namespace xrpl diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index 0afdebb898..97fdd1a632 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -191,6 +191,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) \ diff --git a/include/xrpl/tx/transactors/vault/VaultDeposit.h b/include/xrpl/tx/transactors/vault/VaultDeposit.h index d49f504243..3131f1cf47 100644 --- a/include/xrpl/tx/transactors/vault/VaultDeposit.h +++ b/include/xrpl/tx/transactors/vault/VaultDeposit.h @@ -20,6 +20,9 @@ public: { } + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); diff --git a/merged-prs.md b/merged-prs.md index 7c63e4eecc..53e3a4b9d3 100644 --- a/merged-prs.md +++ b/merged-prs.md @@ -5,3 +5,4 @@ PRs merged into the `ripple/lending-protocol-fv` branch. | PR | Title | Author | Branch | Merged | | --------------------------------------------------- | ------------------------------------------------ | --------- | ----------------------------- | ---------- | | [#7817](https://github.com/XRPLF/rippled/pull/7817) | feat: Implement LoanBroker cash-basis accounting | @Tapanito | `tapanito/lending-cash-basis` | 2026-07-27 | +| [#6383](https://github.com/XRPLF/rippled/pull/6383) | feat: Add tfVaultDonate feature | @Tapanito | `tapanito/vault-donation` | 2026-07-27 | diff --git a/src/libxrpl/ledger/helpers/VaultHelpers.cpp b/src/libxrpl/ledger/helpers/VaultHelpers.cpp index 78f64d2077..2ffb8e975b 100644 --- a/src/libxrpl/ledger/helpers/VaultHelpers.cpp +++ b/src/libxrpl/ledger/helpers/VaultHelpers.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include // IWYU pragma: keep #include @@ -11,6 +12,8 @@ #include #include #include // IWYU pragma: keep +#include +#include #include #include @@ -119,6 +122,12 @@ sharesToAssetsWithdraw( return assets; } +[[nodiscard]] bool +isVaultDonate(Rules const& rules, STTx const& tx) +{ + return rules.enabled(featureLendingProtocolV1_1) && tx.isFlag(tfVaultDonate); +} + [[nodiscard]] bool isSoleShareholder(ReadView const& view, AccountID const& account, SLE::const_ref issuance) { diff --git a/src/libxrpl/tx/invariants/VaultInvariant.cpp b/src/libxrpl/tx/invariants/VaultInvariant.cpp index a9ba0ec874..69f8d70989 100644 --- a/src/libxrpl/tx/invariants/VaultInvariant.cpp +++ b/src/libxrpl/tx/invariants/VaultInvariant.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -527,10 +528,14 @@ ValidVault::finalize( return std::nullopt; }(); - if (!beforeShares && - (tx.getTxnType() == ttVAULT_DEPOSIT || // - tx.getTxnType() == ttVAULT_WITHDRAW || // - tx.getTxnType() == ttVAULT_CLAWBACK)) + 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) || // + tx.getTxnType() == ttVAULT_WITHDRAW || // + tx.getTxnType() == ttVAULT_CLAWBACK); + + if (!beforeShares && shouldUpdateShares) { JLOG(j.fatal()) << "Invariant failed: vault operation succeeded " "without updating shares"; @@ -741,34 +746,57 @@ ValidVault::finalize( result = false; } - auto const maybeAccDeltaShares = deltaShares(tx[sfAccount]); - if (!maybeAccDeltaShares) + // If assets are donated, check share invariants + if (isDonate) { - JLOG(j.fatal()) << "Invariant failed: deposit must change depositor shares"; - return false; // That's all we can do - } - // We don't round shares, they are integral MPT - auto const& accountDeltaShares = *maybeAccDeltaShares; - if (accountDeltaShares.delta <= kZero) - { - JLOG(j.fatal()) << "Invariant failed: deposit must increase depositor shares"; - result = false; - } + auto const accountDeltaShares = deltaShares(tx[sfAccount]); + if (accountDeltaShares) + { + JLOG(j.fatal()) << // + "Invariant failed: donation must not change depositor shares"; + return false; // That's all we can do + } - auto const maybeVaultDeltaShares = deltaShares(afterVault.pseudoId); - if (!maybeVaultDeltaShares || maybeVaultDeltaShares->delta == kZero) - { - JLOG(j.fatal()) << "Invariant failed: deposit must change vault shares"; - return false; // That's all we can do + auto const vaultDeltaShares = deltaShares(afterVault.pseudoId); + if (vaultDeltaShares) + { + JLOG(j.fatal()) << // + "Invariant failed: donation must not change vault shares"; + return false; // That's all we can do + } } - - // We don't round shares, they are integral MPT - auto const& vaultDeltaShares = *maybeVaultDeltaShares; - if (vaultDeltaShares.delta * -1 != accountDeltaShares.delta) + else { - JLOG(j.fatal()) << "Invariant failed: " << // - "deposit must change depositor and vault shares by equal amount"; - result = false; + auto const maybeAccDeltaShares = deltaShares(tx[sfAccount]); + if (!maybeAccDeltaShares) + { + JLOG(j.fatal()) << "Invariant failed: deposit must change depositor shares"; + return false; // That's all we can do + } + // We don't need to round shares, they are integral MPT + auto const& accountDeltaShares = *maybeAccDeltaShares; + if (accountDeltaShares.delta <= kZero) + { + JLOG(j.fatal()) + << "Invariant failed: deposit must increase depositor shares"; + result = false; + } + + auto const maybeVaultDeltaShares = deltaShares(afterVault.pseudoId); + if (!maybeVaultDeltaShares || maybeVaultDeltaShares->delta == kZero) + { + JLOG(j.fatal()) << "Invariant failed: deposit must change vault shares"; + return false; // That's all we can do + } + + // We don't need to round shares, they are integral MPT + auto const& vaultDeltaShares = *maybeVaultDeltaShares; + if (vaultDeltaShares.delta * -1 != accountDeltaShares.delta) + { + JLOG(j.fatal()) << "Invariant failed: " << // + "deposit must change depositor and vault shares by equal amount"; + result = false; + } } auto const assetTotalDelta = roundToAsset( diff --git a/src/libxrpl/tx/transactors/vault/VaultDelete.cpp b/src/libxrpl/tx/transactors/vault/VaultDelete.cpp index 497a2f2465..9c6c41654b 100644 --- a/src/libxrpl/tx/transactors/vault/VaultDelete.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultDelete.cpp @@ -33,6 +33,7 @@ VaultDelete::preflight(PreflightContext const& ctx) if (ctx.tx.isFieldPresent(sfMemoData) && !ctx.rules.enabled(featureLendingProtocolV1_1)) return temDISABLED; + // The sfMemoData field is an optional field used to record the deletion reason. if (!validDataLength(ctx.tx[~sfMemoData], kMaxDataPayloadLength)) return temMALFORMED; diff --git a/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp b/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp index aa9cfc8537..1437377c02 100644 --- a/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp @@ -20,14 +20,25 @@ #include #include #include +#include #include #include +#include #include #include namespace xrpl { +std::uint32_t +VaultDeposit::getFlagsMask(PreflightContext const& ctx) +{ + if (ctx.rules.enabled(featureLendingProtocolV1_1)) + return tfVaultDepositMask; + + return tfVaultDepositMask | tfVaultDonate; +} + [[nodiscard]] static STAmount roundToVaultScale(STAmount const& amount, SLE::const_ref vault) @@ -103,6 +114,22 @@ VaultDeposit::preclaim(PreclaimContext const& ctx) // LCOV_EXCL_STOP } + if (isVaultDonate(ctx.view.rules(), ctx.tx)) + { + if (account != vault->at(sfOwner)) + { + JLOG(ctx.j.debug()) << "VaultDeposit: only owner can donate to vault."; + return tecNO_PERMISSION; + } + + // Cannot donate to a vault with no shares + if (sleIssuance->at(sfOutstandingAmount) == 0) + { + JLOG(ctx.j.debug()) << "VaultDeposit: empty vault cannot receive donations."; + return tecNO_PERMISSION; + } + } + if (sleIssuance->isFlag(lsfMPTLocked)) { // LCOV_EXCL_START @@ -227,6 +254,8 @@ VaultDeposit::doApply() // LCOV_EXCL_STOP } + auto const isDonate = isVaultDonate(ctx_.view().rules(), ctx_.tx); + auto const& vaultAccount = vault->at(sfAccount); // Note, vault owner is always authorized if (vault->isFlag(lsfVaultPrivate) && accountID_ != vault->at(sfOwner)) @@ -270,44 +299,54 @@ VaultDeposit::doApply() return err; } } - STAmount sharesCreated = {vault->at(sfShareMPTID)}, assetsDeposited; - try + if (isDonate) { - // Compute exchange before transferring any amounts. - { - auto const maybeShares = assetsToSharesDeposit(vault, sleIssuance, amount); - if (!maybeShares) - return tecINTERNAL; // LCOV_EXCL_LINE - sharesCreated = *maybeShares; - } - if (sharesCreated == beast::kZero) - return tecPRECISION_LOSS; - - auto const maybeAssets = sharesToAssetsDeposit(vault, sleIssuance, sharesCreated); - if (!maybeAssets) - { - return tecINTERNAL; // LCOV_EXCL_LINE - } - if (*maybeAssets > amount) - { - // LCOV_EXCL_START - JLOG(j_.error()) << "VaultDeposit: would take more than offered."; - return tecINTERNAL; - // LCOV_EXCL_STOP - } - assetsDeposited = *maybeAssets; + XRPL_ASSERT( + accountID_ == vault->at(sfOwner), "xrpl::VaultDeposit::doApply : account is owner"); + assetsDeposited = amount; } - catch (std::overflow_error const&) + else { - // It's easy to hit this exception from Number with large enough Scale - // so we avoid spamming the log and only use debug here. - JLOG(j_.debug()) // - << "VaultDeposit: overflow error with" - << " scale=" << (int)vault->at(sfScale).value() // - << ", assetsTotal=" << vault->at(sfAssetsTotal).value() - << ", sharesTotal=" << sleIssuance->at(sfOutstandingAmount) << ", amount=" << amount; - return tecPATH_DRY; + try + { + // Compute exchange before transferring any amounts. + { + auto const maybeShares = assetsToSharesDeposit(vault, sleIssuance, amount); + if (!maybeShares) + return tecINTERNAL; // LCOV_EXCL_LINE + sharesCreated = *maybeShares; + } + if (sharesCreated == beast::kZero) + return tecPRECISION_LOSS; + + auto const maybeAssets = sharesToAssetsDeposit(vault, sleIssuance, sharesCreated); + if (!maybeAssets) + { + return tecINTERNAL; // LCOV_EXCL_LINE + } + + if (*maybeAssets > amount) + { + // LCOV_EXCL_START + JLOG(j_.error()) << "VaultDeposit: would take more than offered."; + return tecINTERNAL; + // LCOV_EXCL_STOP + } + assetsDeposited = *maybeAssets; + } + catch (std::overflow_error const&) + { + // It's easy to hit this exception from Number with large enough Scale + // so we avoid spamming the log and only use debug here. + JLOG(j_.debug()) // + << "VaultDeposit: overflow error with" + << " scale=" << (int)vault->at(sfScale).value() // + << ", assetsTotal=" << vault->at(sfAssetsTotal).value() + << ", sharesTotal=" << sleIssuance->at(sfOutstandingAmount) + << ", amount=" << amount; + return tecPATH_DRY; + } } XRPL_ASSERT( @@ -351,11 +390,19 @@ VaultDeposit::doApply() } } - // Transfer shares from vault to depositor. - if (auto const ter = accountSend( - view(), vaultAccount, accountID_, sharesCreated, j_, {}, WaiveTransferFee::Yes); - !isTesSuccess(ter)) - return ter; + if (isDonate) + { + XRPL_ASSERT( + sharesCreated == beast::kZero, "xrpl::VaultDeposit::doApply : donation issued shares"); + } + else + { + // Transfer shares from vault to depositor. + if (auto const ter = accountSend( + view(), vaultAccount, accountID_, sharesCreated, j_, {}, WaiveTransferFee::Yes); + !isTesSuccess(ter)) + return ter; + } associateAsset(*vault, vaultAsset); diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index eaf1f2704c..8d25d9ddd9 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -3894,6 +3894,21 @@ class Invariants_test : public beast::unit_test::Suite precloseXrp, TxAccount::A2); + doInvariantCheck( + Env{*this, testableAmendments() - featureLendingProtocolV1_1}, + {"deposit must change depositor shares"}, + [&](Account const& a1, Account const& a2, ApplyContext& ac) { + auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) { + sample.accountShares.reset(); + })); + }, + XRPAmount{}, + STTx{ttVAULT_DEPOSIT, [](STObject& tx) { tx[sfAmount] = XRPAmount(10); }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + doInvariantCheck( {"deposit must change depositor shares"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { @@ -3981,6 +3996,45 @@ class Invariants_test : public beast::unit_test::Suite precloseXrp, TxAccount::A2); + doInvariantCheck( + {"donation must not change depositor shares"}, + [&](Account const& a1, Account const& a2, ApplyContext& ac) { + auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) { + sample.accountShares->amount = 10; + })); + }, + XRPAmount{}, + STTx{ + ttVAULT_DEPOSIT, + [](STObject& tx) { + tx[sfAmount] = XRPAmount(10); + tx[sfFlags] = tfVaultDonate; + }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + doInvariantCheck( + {"donation must not change vault shares"}, + [&](Account const& a1, Account const& a2, ApplyContext& ac) { + auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) { + sample.sharesTotal = 10; + sample.accountShares = std::nullopt; + })); + }, + XRPAmount{}, + STTx{ + ttVAULT_DEPOSIT, + [](STObject& tx) { + tx[sfAmount] = XRPAmount(10); + tx[sfFlags] = tfVaultDonate; + }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + testcase << "Vault withdrawal"; doInvariantCheck( {"withdrawal must change vault balance"}, diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index bd596d6149..3adb3c253f 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -8365,6 +8365,230 @@ class Vault_test : public beast::unit_test::Suite env.enableFeature(fixCleanup3_3_0); } + void + testVaultDepositDonate() + { + using namespace test::jtx; + std::string const prefix = "VaultDeposit donate"; + + Env env{*this}; + Vault const vault{env}; + + auto const vaultShareBalance = [&](Keylet const& vaultKeylet) { + auto const sleVault = env.le(vaultKeylet); + BEAST_EXPECT(sleVault != nullptr); + + auto const sleIssuance = env.le(keylet::mptokenIssuance(sleVault->at(sfShareMPTID))); + BEAST_EXPECT(sleIssuance != nullptr); + + return sleIssuance->at(sfOutstandingAmount); + }; + + auto const vaultAssetBalance = [&](Keylet const& vaultKeylet) { + auto const sleVault = env.le(vaultKeylet); + BEAST_EXPECT(sleVault != nullptr); + + return std::make_pair(sleVault->at(sfAssetsAvailable), sleVault->at(sfAssetsTotal)); + }; + + Account const owner{"owner"}; + Account const depositor{"depositor"}; + env.fund(XRP(1'000'000), owner, depositor); + env.close(); + + auto const depositAmount = XRP(10); + + auto const [tx, keylet] = vault.create({.owner = owner, .asset = xrpIssue()}); + env(tx, Ter(tesSUCCESS)); + env.close(); + + // With featureLendingProtocolV1_1 disabled, donations fail + { + testcase(prefix + " fails with featureLendingProtocolV1_1 disabled"); + env.disableFeature(featureLendingProtocolV1_1); + auto const tx = vault.deposit({ + .depositor = owner, + .id = keylet.key, + .amount = depositAmount, + .flags = tfVaultDonate, + }); + env(tx, Ter(temINVALID_FLAG)); + env.enableFeature(featureLendingProtocolV1_1); + env.close(); + } + + // Donation is not allowed to an empty vault + { + testcase(prefix + " fails to an empty vault"); + auto const tx = vault.deposit({ + .depositor = owner, + .id = keylet.key, + .amount = depositAmount, + .flags = tfVaultDonate, + }); + env(tx, Ter(tecNO_PERMISSION)); + env.close(); + } + + // Further unit tests require assets in the Vault + env(vault.deposit({ + .depositor = depositor, + .id = keylet.key, + .amount = depositAmount, + }), + Ter(tesSUCCESS)); + env.close(); + + // Donation is not allowed by a non-owner + { + testcase(prefix + " fails by a non-owner"); + auto const tx = vault.deposit({ + .depositor = depositor, + .id = keylet.key, + .amount = depositAmount, + .flags = tfVaultDonate, + }); + env(tx, Ter(tecNO_PERMISSION)); + env.close(); + } + + // Donation cannot exceed assets maximum + { + testcase(prefix + " cannot exceed assets maximum"); + auto tx = vault.set({ + .owner = owner, + .id = keylet.key, + }); + tx[sfAssetsMaximum] = XRP(30).number(); + env(tx, Ter(tesSUCCESS)); + + tx = vault.deposit({ + .depositor = owner, + .id = keylet.key, + .amount = depositAmount + XRP(30), + .flags = tfVaultDonate, + }); + + env(tx, Ter(tecLIMIT_EXCEEDED)); + env.close(); + } + + { + testcase(prefix + " succeeds"); + auto const shareBalance = vaultShareBalance(keylet); + auto const [assetsAvailable, assetsTotal] = vaultAssetBalance(keylet); + + auto tx = vault.deposit({ + .depositor = owner, + .id = keylet.key, + .amount = depositAmount, + .flags = tfVaultDonate, + }); + env(tx, Ter(tesSUCCESS)); + env.close(); + + auto const shareBalanceAfterDeposit = vaultShareBalance(keylet); + auto const [assetsAvailableAfterDeposit, assetsTotalAfterDeposit] = + vaultAssetBalance(keylet); + + BEAST_EXPECT(shareBalance == shareBalanceAfterDeposit); + BEAST_EXPECT(assetsAvailable + depositAmount.number() == assetsAvailableAfterDeposit); + BEAST_EXPECT(assetsTotal + depositAmount.number() == assetsTotalAfterDeposit); + + auto const sleVault = env.le(keylet); + if (!BEAST_EXPECT(sleVault)) + return; + + // The depositor can withdraw their assets and the donated amount + Asset const shareAsset(sleVault->at(sfShareMPTID)); + tx = vault.withdraw( + {.depositor = depositor, .id = keylet.key, .amount = shareAsset(shareBalance)}); + env(tx, Ter(tesSUCCESS)); + + auto const shareBalanceAfterWithdraw = vaultShareBalance(keylet); + auto const [assetsAvailableAfterWithdraw, assetsTotalAfterWithdraw] = + vaultAssetBalance(keylet); + BEAST_EXPECT(shareBalanceAfterWithdraw == 0); + 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 const 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: void run() override @@ -8395,6 +8619,7 @@ public: testAssetsMaximum(); testVaultDeleteMemoData(); testVaultCreateLEVersion(); + testVaultDepositDonate(); testBug6LimitBypassWithShares(); testRemoveEmptyHoldingLockedAmount(); testRemoveEmptyHoldingConfidentialBalances(); diff --git a/src/test/jtx/impl/vault.cpp b/src/test/jtx/impl/vault.cpp index 7084347763..998a7acdd1 100644 --- a/src/test/jtx/impl/vault.cpp +++ b/src/test/jtx/impl/vault.cpp @@ -57,6 +57,8 @@ Vault::deposit(DepositArgs const& args) jv[jss::Account] = args.depositor.human(); jv[sfVaultID] = to_string(args.id); jv[jss::Amount] = toJson(args.amount); + if (args.flags) + jv[jss::Flags] = *args.flags; return jv; } diff --git a/src/test/jtx/vault.h b/src/test/jtx/vault.h index e72eae89b7..c2a8775a39 100644 --- a/src/test/jtx/vault.h +++ b/src/test/jtx/vault.h @@ -56,6 +56,7 @@ struct Vault Account depositor; uint256 id; STAmount amount; + std::optional flags = std::nullopt; }; static json::Value