From c808c46049aac67b8b7b36907cdfe5879e806700 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:27:19 +0100 Subject: [PATCH 01/16] adds flag definitions --- include/xrpl/protocol/LedgerFormats.h | 1 + include/xrpl/protocol/TxFlags.h | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/include/xrpl/protocol/LedgerFormats.h b/include/xrpl/protocol/LedgerFormats.h index fa39c158438..80ffaa5c6ef 100644 --- a/include/xrpl/protocol/LedgerFormats.h +++ b/include/xrpl/protocol/LedgerFormats.h @@ -185,6 +185,7 @@ enum LedgerSpecificFlags { // ltVAULT lsfVaultPrivate = 0x00010000, + lsfVaultDepositBlocked = 0x00020000, // True, vault deposit is blocked // ltLOAN lsfLoanDefault = 0x00010000, diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index 6c6ef5e3698..9a4e00e3d65 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -270,11 +270,13 @@ constexpr std::uint32_t const tfBatchMask = // LoanPay: True, indicates any excess in this payment can be used // as an overpayment. False, no overpayments will be taken. constexpr std::uint32_t const tfLoanOverpayment = 0x00010000; + // LoanPay exclusive flags: // tfLoanFullPayment: True, indicates that the payment is an early // full payment. It must pay the entire loan including close // interest and fees, or it will fail. False: Not a full payment. constexpr std::uint32_t const tfLoanFullPayment = 0x00020000; + // tfLoanLatePayment: True, indicates that the payment is late, // and includes late interest and fees. If the loan is not late, // it will fail. False: not a late payment. If the current payment @@ -291,6 +293,11 @@ constexpr std::uint32_t const tfLoanImpair = 0x00020000; constexpr std::uint32_t const tfLoanUnimpair = 0x00040000; constexpr std::uint32_t const tfLoanManageMask = ~(tfUniversal | tfLoanDefault | tfLoanImpair | tfLoanUnimpair); +// VaultSet flags: +constexpr std::uint32_t const tfVaultDepositBlock = 0x00010000; +constexpr std::uint32_t const tfVaultDepositUnblock = 0x00020000; +constexpr std::uint32_t const tfVaultManageMask = ~(tfUniversal | tfVaultDepositBlock | tfVaultDepositUnblock); + // clang-format on } // namespace xrpl From a2198146a80669375e2e46d00d6fec1e4cf7d1c8 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:42:27 +0100 Subject: [PATCH 02/16] adds BLockDeposit flagsto VaultSet --- include/xrpl/protocol/TxFlags.h | 2 +- src/test/app/Vault_test.cpp | 92 ++++++++++++++++++++++++++++ src/test/jtx/impl/vault.cpp | 2 + src/test/jtx/vault.h | 1 + src/xrpld/app/tx/detail/VaultSet.cpp | 57 ++++++++++++++++- src/xrpld/app/tx/detail/VaultSet.h | 3 + 6 files changed, 155 insertions(+), 2 deletions(-) diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index 9a4e00e3d65..a1c18f28276 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -296,7 +296,7 @@ constexpr std::uint32_t const tfLoanManageMask = ~(tfUniversal | tfLoanDefault | // VaultSet flags: constexpr std::uint32_t const tfVaultDepositBlock = 0x00010000; constexpr std::uint32_t const tfVaultDepositUnblock = 0x00020000; -constexpr std::uint32_t const tfVaultManageMask = ~(tfUniversal | tfVaultDepositBlock | tfVaultDepositUnblock); +constexpr std::uint32_t const tfVaultSetMask = ~(tfUniversal | tfVaultDepositBlock | tfVaultDepositUnblock); // clang-format on diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 5d31c674c04..5b29ce68a26 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -4990,6 +4990,97 @@ class Vault_test : public beast::unit_test::suite } } + void + testVaultSetVaultDepositFlagValidation() + { + using namespace test::jtx; + + auto const all = testable_amendments(); + + { + testcase("VaultSet VaultDepositBlock fixLendingProtocolV1_1 disabled"); + Env env{*this, all - fixLendingProtocolV1_1}; + + Account owner{"owner"}; + env.fund(XRP(1'000'000), owner); + env.close(); + + PrettyAsset asset = xrpIssue(); + Vault vault{env}; + + auto const [tx, keylet] = vault.create({.owner = owner, .asset = asset, .flags = tfVaultPrivate}); + env(tx, ter(tesSUCCESS), THISLINE); + env.close(); + + env(vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositBlock}), + ter(temDISABLED), + THISLINE); + env.close(); + } + + { + std::string const prefix = "VaultSet(VaultDepositBlock): "; + Env env{*this, all | fixLendingProtocolV1_1}; + + Account owner{"owner"}; + env.fund(XRP(1'000'000), owner); + env.close(); + + PrettyAsset asset = xrpIssue(); + Vault vault{env}; + + auto const [tx, keylet] = vault.create({.owner = owner, .asset = asset, .flags = tfVaultPrivate}); + env(tx, ter(tesSUCCESS), THISLINE); + env.close(); + + { + testcase(prefix + "invalid flags"); + env(vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositBlock | tfVaultDepositUnblock}), + ter(temMALFORMED), + THISLINE); + env.close(); + } + + { + testcase(prefix + "unblock already unblocked vault"); + // Cannot unblock an already unblocked vault + env(vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositUnblock}), + ter(tecNO_PERMISSION), + THISLINE); + } + + { + testcase(prefix + "set and clear vault flag"); + env(vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositBlock}), + ter(tesSUCCESS), + THISLINE); + + auto sleVault = env.le(keylet); + if (!BEAST_EXPECT(sleVault)) + return; + + if (!BEAST_EXPECT(sleVault->isFlag(lsfVaultDepositBlocked))) + return; + + // Cannot block an already blocked vault + env(vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositBlock}), + ter(tecNO_PERMISSION), + THISLINE); + + // Cannot unblock an already unblocked vault + env(vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositUnblock}), + ter(tesSUCCESS), + THISLINE); + + sleVault = env.le(keylet); + if (!BEAST_EXPECT(sleVault)) + return; + + BEAST_EXPECT(!sleVault->isFlag(lsfVaultDepositBlocked)); + } + } + } + public: void run() override @@ -5011,6 +5102,7 @@ class Vault_test : public beast::unit_test::suite testVaultClawbackBurnShares(); testVaultClawbackAssets(); testAssetsMaximum(); + testVaultSetVaultDepositFlagValidation(); } }; diff --git a/src/test/jtx/impl/vault.cpp b/src/test/jtx/impl/vault.cpp index 90250aece0e..f096187b130 100644 --- a/src/test/jtx/impl/vault.cpp +++ b/src/test/jtx/impl/vault.cpp @@ -31,6 +31,8 @@ Vault::set(SetArgs const& args) jv[jss::TransactionType] = jss::VaultSet; jv[jss::Account] = args.owner.human(); jv[sfVaultID] = to_string(args.id); + 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 65a5706354b..4b33c9231de 100644 --- a/src/test/jtx/vault.h +++ b/src/test/jtx/vault.h @@ -36,6 +36,7 @@ struct Vault { Account owner; uint256 id; + std::optional flags{}; }; Json::Value diff --git a/src/xrpld/app/tx/detail/VaultSet.cpp b/src/xrpld/app/tx/detail/VaultSet.cpp index c0aaf5bac66..8042c81f5a2 100644 --- a/src/xrpld/app/tx/detail/VaultSet.cpp +++ b/src/xrpld/app/tx/detail/VaultSet.cpp @@ -21,9 +21,34 @@ VaultSet::checkExtraFeatures(PreflightContext const& ctx) return true; } +std::uint32_t +VaultSet::getFlagsMask(PreflightContext const& ctx) +{ + return tfVaultSetMask; +} + +static bool +isValidVaultUpdate(PreflightContext const& ctx) +{ + auto const checkFlags = ctx.rules.enabled(fixLendingProtocolV1_1); + + auto const atLeastOneFieldPresent = + ctx.tx.isFieldPresent(sfDomainID) || ctx.tx.isFieldPresent(sfAssetsMaximum) || ctx.tx.isFieldPresent(sfData); + + return atLeastOneFieldPresent || + (checkFlags && (ctx.tx.isFlag(tfVaultDepositBlock) || ctx.tx.isFlag(tfVaultDepositUnblock))); +} + NotTEC VaultSet::preflight(PreflightContext const& ctx) { + if ((ctx.tx.isFlag(tfVaultDepositBlock) || ctx.tx.isFlag(tfVaultDepositUnblock)) && + !ctx.rules.enabled(fixLendingProtocolV1_1)) + { + JLOG(ctx.j.debug()) << "VaultSet: flags not supported without fixLendingProtocolV1_1."; + return temDISABLED; + } + if (ctx.tx[sfVaultID] == beast::zero) { JLOG(ctx.j.debug()) << "VaultSet: zero/empty vault ID."; @@ -48,12 +73,18 @@ VaultSet::preflight(PreflightContext const& ctx) } } - if (!ctx.tx.isFieldPresent(sfDomainID) && !ctx.tx.isFieldPresent(sfAssetsMaximum) && !ctx.tx.isFieldPresent(sfData)) + if (!isValidVaultUpdate(ctx)) { JLOG(ctx.j.debug()) << "VaultSet: nothing is being updated."; return temMALFORMED; } + if (ctx.tx.isFlag(tfVaultDepositBlock) && ctx.tx.isFlag(tfVaultDepositUnblock)) + { + JLOG(ctx.j.debug()) << "VaultSet: cannot set tfVaultDepositBlock and tfVaultDepositUnblock simultaneously."; + return temMALFORMED; + } + return tesSUCCESS; } @@ -107,6 +138,21 @@ VaultSet::preclaim(PreclaimContext const& ctx) } } + if (ctx.view.rules().enabled(fixLendingProtocolV1_1)) + { + if (vault->isFlag(lsfVaultDepositBlocked) && ctx.tx.isFlag(tfVaultDepositBlock)) + { + JLOG(ctx.j.debug()) << "VaultSet: vault deposit is already blocked"; + return tecNO_PERMISSION; + } + + if (!vault->isFlag(lsfVaultDepositBlocked) && ctx.tx.isFlag(tfVaultDepositUnblock)) + { + JLOG(ctx.j.debug()) << "VaultSet: vault deposit is already unblocked"; + return tecNO_PERMISSION; + } + } + return tesSUCCESS; } @@ -164,6 +210,15 @@ VaultSet::doApply() view().update(sleIssuance); } + if (view().rules().enabled(fixLendingProtocolV1_1)) + { + if (tx.isFlag(tfVaultDepositBlock)) + vault->setFlag(lsfVaultDepositBlocked); + + if (tx.isFlag(tfVaultDepositUnblock)) + vault->clearFlag(lsfVaultDepositBlocked); + } + // Note, we must update Vault object even if only DomainID is being updated // in Issuance object. Otherwise it's really difficult for Vault invariants // to verify the operation. diff --git a/src/xrpld/app/tx/detail/VaultSet.h b/src/xrpld/app/tx/detail/VaultSet.h index 1e8a15291e1..0e41ac5919c 100644 --- a/src/xrpld/app/tx/detail/VaultSet.h +++ b/src/xrpld/app/tx/detail/VaultSet.h @@ -13,6 +13,9 @@ class VaultSet : public Transactor { } + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static bool checkExtraFeatures(PreflightContext const& ctx); From 1010866ba05c55a71a27e7f9bba2f7250e1534b7 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Fri, 13 Feb 2026 15:00:01 +0100 Subject: [PATCH 03/16] adds amendment validation in flags and better tests --- src/test/app/Vault_test.cpp | 163 +++++++++++---------------- src/xrpld/app/tx/detail/VaultSet.cpp | 9 +- 2 files changed, 74 insertions(+), 98 deletions(-) diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 5b29ce68a26..d67b000a185 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -178,6 +178,34 @@ class Vault_test : public beast::unit_test::suite env.close(); } + { + testcase(prefix + " fail to unblock a non-blocked vault"); + auto const tx = vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositUnblock}); + env(tx, ter(tecNO_PERMISSION)); + env.close(); + } + + { + testcase(prefix + " block a vault"); + auto const tx = vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositBlock}); + env(tx, ter(tesSUCCESS)); + env.close(); + } + + { + testcase(prefix + " fail to block an already blocked vault"); + auto const tx = vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositBlock}); + env(tx, ter(tecNO_PERMISSION)); + env.close(); + } + + { + testcase(prefix + " unblock a blocked vault"); + auto const tx = vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositUnblock}); + env(tx, ter(tesSUCCESS)); + env.close(); + } + { testcase(prefix + " fail to withdraw more than assets held"); auto tx = vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(1000)}); @@ -885,13 +913,26 @@ class Vault_test : public beast::unit_test::suite }); testCase([&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) { - testcase("invalid set immutable flag"); + testcase("set flags fail without fixLendingProtocolV1_1"); + + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + + { + env.disableFeature(fixLendingProtocolV1_1); + auto tx = vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositBlock}); + env(tx, ter(temINVALID_FLAG)); + env.enableFeature(fixLendingProtocolV1_1); + } + }); + + testCase([&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) { + testcase("invalid set flag combination"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); { auto tx = vault.set({.owner = owner, .id = keylet.key}); - tx[sfFlags] = tfVaultPrivate; + tx[sfFlags] = tfVaultDepositBlock | tfVaultDepositUnblock; env(tx, ter(temINVALID_FLAG)); } }); @@ -2762,7 +2803,7 @@ class Vault_test : public beast::unit_test::suite } void - testWithDomainCheck() + testPrivateVault() { using namespace test::jtx; @@ -2817,6 +2858,28 @@ class Vault_test : public beast::unit_test::suite env(tx, ter{tecOBJECT_NOT_FOUND}); } + { + testcase("blocking a private vault does not change lsfVaultPrivate flag"); + auto tx = vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositBlock}); + env(tx, ter{tesSUCCESS}); + auto const sleVault = env.le(keylet); + if (!BEAST_EXPECT(sleVault)) + return; + BEAST_EXPECT(sleVault->isFlag(lsfVaultDepositBlocked)); + BEAST_EXPECT(sleVault->isFlag(lsfVaultPrivate)); + } + + { + testcase("unblocking a private vault does not change lsfVaultPrivate flag"); + auto tx = vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositUnblock}); + env(tx, ter{tesSUCCESS}); + auto const sleVault = env.le(keylet); + if (!BEAST_EXPECT(sleVault)) + return; + BEAST_EXPECT(!sleVault->isFlag(lsfVaultDepositBlocked)); + BEAST_EXPECT(sleVault->isFlag(lsfVaultPrivate)); + } + { testcase("private vault set domainId"); @@ -4990,97 +5053,6 @@ class Vault_test : public beast::unit_test::suite } } - void - testVaultSetVaultDepositFlagValidation() - { - using namespace test::jtx; - - auto const all = testable_amendments(); - - { - testcase("VaultSet VaultDepositBlock fixLendingProtocolV1_1 disabled"); - Env env{*this, all - fixLendingProtocolV1_1}; - - Account owner{"owner"}; - env.fund(XRP(1'000'000), owner); - env.close(); - - PrettyAsset asset = xrpIssue(); - Vault vault{env}; - - auto const [tx, keylet] = vault.create({.owner = owner, .asset = asset, .flags = tfVaultPrivate}); - env(tx, ter(tesSUCCESS), THISLINE); - env.close(); - - env(vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositBlock}), - ter(temDISABLED), - THISLINE); - env.close(); - } - - { - std::string const prefix = "VaultSet(VaultDepositBlock): "; - Env env{*this, all | fixLendingProtocolV1_1}; - - Account owner{"owner"}; - env.fund(XRP(1'000'000), owner); - env.close(); - - PrettyAsset asset = xrpIssue(); - Vault vault{env}; - - auto const [tx, keylet] = vault.create({.owner = owner, .asset = asset, .flags = tfVaultPrivate}); - env(tx, ter(tesSUCCESS), THISLINE); - env.close(); - - { - testcase(prefix + "invalid flags"); - env(vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositBlock | tfVaultDepositUnblock}), - ter(temMALFORMED), - THISLINE); - env.close(); - } - - { - testcase(prefix + "unblock already unblocked vault"); - // Cannot unblock an already unblocked vault - env(vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositUnblock}), - ter(tecNO_PERMISSION), - THISLINE); - } - - { - testcase(prefix + "set and clear vault flag"); - env(vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositBlock}), - ter(tesSUCCESS), - THISLINE); - - auto sleVault = env.le(keylet); - if (!BEAST_EXPECT(sleVault)) - return; - - if (!BEAST_EXPECT(sleVault->isFlag(lsfVaultDepositBlocked))) - return; - - // Cannot block an already blocked vault - env(vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositBlock}), - ter(tecNO_PERMISSION), - THISLINE); - - // Cannot unblock an already unblocked vault - env(vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositUnblock}), - ter(tesSUCCESS), - THISLINE); - - sleVault = env.le(keylet); - if (!BEAST_EXPECT(sleVault)) - return; - - BEAST_EXPECT(!sleVault->isFlag(lsfVaultDepositBlocked)); - } - } - } - public: void run() override @@ -5092,7 +5064,7 @@ class Vault_test : public beast::unit_test::suite testCreateFailMPT(); testWithMPT(); testWithIOU(); - testWithDomainCheck(); + testPrivateVault(); testWithDomainCheckXRP(); testNonTransferableShares(); testFailedPseudoAccount(); @@ -5102,7 +5074,6 @@ class Vault_test : public beast::unit_test::suite testVaultClawbackBurnShares(); testVaultClawbackAssets(); testAssetsMaximum(); - testVaultSetVaultDepositFlagValidation(); } }; diff --git a/src/xrpld/app/tx/detail/VaultSet.cpp b/src/xrpld/app/tx/detail/VaultSet.cpp index 8042c81f5a2..1cc39f0edcf 100644 --- a/src/xrpld/app/tx/detail/VaultSet.cpp +++ b/src/xrpld/app/tx/detail/VaultSet.cpp @@ -24,7 +24,12 @@ VaultSet::checkExtraFeatures(PreflightContext const& ctx) std::uint32_t VaultSet::getFlagsMask(PreflightContext const& ctx) { - return tfVaultSetMask; + // VaultSet mask is built assuming fixLendingProtocolV1_1 is enabled + if (ctx.rules.enabled(fixLendingProtocolV1_1)) + return tfVaultSetMask; + + // Add tfVaultDepositBlock and tfVaultDepositUnblock flags to indicate they are disabled + return tfVaultSetMask | tfVaultDepositBlock | tfVaultDepositUnblock; } static bool @@ -82,7 +87,7 @@ VaultSet::preflight(PreflightContext const& ctx) if (ctx.tx.isFlag(tfVaultDepositBlock) && ctx.tx.isFlag(tfVaultDepositUnblock)) { JLOG(ctx.j.debug()) << "VaultSet: cannot set tfVaultDepositBlock and tfVaultDepositUnblock simultaneously."; - return temMALFORMED; + return temINVALID_FLAG; } return tesSUCCESS; From 087a9c1cf34c308d80f736556e24957bc49b6a07 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Tue, 17 Feb 2026 11:58:24 +0100 Subject: [PATCH 04/16] add vault deposit logic and tests --- src/test/app/Vault_test.cpp | 324 +++++++++++++++++++++++ src/xrpld/app/tx/detail/VaultDeposit.cpp | 26 +- 2 files changed, 346 insertions(+), 4 deletions(-) diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index d67b000a185..21e5be2d8cf 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -2131,6 +2131,167 @@ class Vault_test : public beast::unit_test::suite // Delete vault with zero balance env(vault.del({.owner = owner, .id = keylet.key})); }); + + testCase([&, this]( + Env& env, + Account const&, + Account const& owner, + Account const& depositor, + PrettyAsset const& asset, + Vault& vault, + MPTTester const& mptt) { + testcase("lsfVaultDepositBlocked prevents deposits"); + auto const [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + env(tx); + env.close(); + + // First deposit assets to later show that withdrawals are not blocked + { + auto const tx = vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(20)}); + env(tx, ter{tesSUCCESS}, THISLINE); + env.close(); + } + + // Block Vault deposits + { + auto const tx = vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositBlock}); + env(tx, ter(tesSUCCESS), THISLINE); + env.close(); + } + + { + auto const tx = vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(20)}); + env(tx, ter{tecNO_PERMISSION}, THISLINE); + env.close(); + } + + { + auto tx = vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(20)}); + env(tx, ter{tesSUCCESS}, THISLINE); + env.close(); + } + + // Unblock Vault Deposits + { + auto const tx = vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositUnblock}); + env(tx, ter(tesSUCCESS), THISLINE); + env.close(); + } + + // Deposits now succeed + { + auto const tx = vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(20)}); + env(tx, ter{tesSUCCESS}, THISLINE); + env.close(); + } + + // Withdraw assets from the vault to delete it + { + auto const tx = vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(20)}); + env(tx, ter{tesSUCCESS}, THISLINE); + env.close(); + } + + env(vault.del({.owner = owner, .id = keylet.key})); + env.close(); + }); + + testCase([&, this]( + Env& env, + Account const&, + Account const& owner, + Account const& depositor, + PrettyAsset const& asset, + Vault& vault, + MPTTester const& mptt) { + testcase("insolvent vault blocks deposits"); + + auto const depositAmount = asset(20); + + auto const [tx, vaultKeylet] = vault.create({.owner = owner, .asset = asset}); + env(tx); + env.close(); + + // First deposit assets to later show that withdrawals are not blocked + { + auto const tx = vault.deposit({.depositor = depositor, .id = vaultKeylet.key, .amount = depositAmount}); + env(tx, ter{tesSUCCESS}, THISLINE); + env.close(); + } + + auto const& brokerKeylet = keylet::loanbroker(owner.id(), env.seq(owner)); + auto const& loanKeylet = keylet::loan(brokerKeylet.key, 1); + + // Create a LoanBroker and a Loan, to drain the vault + { + using namespace loanBroker; + using namespace loan; + + env(set(owner, vaultKeylet.key), THISLINE); + env.close(); + + // Create a simple Loan for the full amount of Vault assets + env(set(depositor, brokerKeylet.key, depositAmount.value()), + loan::interestRate(TenthBips32(0)), + paymentInterval(120), + paymentTotal(1), + sig(sfCounterpartySignature, owner), + fee(env.current()->fees().base * 2), + ter(tesSUCCESS), + THISLINE); + env.close(); + + env.close(std::chrono::seconds{120 + 60}); + + env(manage(owner, loanKeylet.key, tfLoanDefault), ter(tesSUCCESS), THISLINE); + + auto const sleVault = env.le(vaultKeylet); + if (!BEAST_EXPECT(sleVault)) + return; + + auto const sleIssuance = env.le(keylet::mptIssuance(sleVault->at(sfShareMPTID))); + if (!BEAST_EXPECT(sleIssuance)) + return; + + auto const shareBalance = sleIssuance->at(sfOutstandingAmount); + auto const expectedShares = Number{ + depositAmount.number().mantissa(), depositAmount.number().exponent() + sleVault->at(sfScale)}; + + // verify that the vault is insolvent + if (!BEAST_EXPECT( + sleVault->at(sfAssetsTotal) == 0 && sleVault->at(sfAssetsAvailable) == 0 && + shareBalance == expectedShares)) + return; + } + + // The vault is insolvent, deposit must fail + { + auto const tx = vault.deposit({.depositor = depositor, .id = vaultKeylet.key, .amount = asset(20)}); + env(tx, ter{tecNO_PERMISSION}, THISLINE); + env.close(); + } + + // Clean up the vault to delete it + { + auto const sleVault = env.le(vaultKeylet); + if (!BEAST_EXPECT(sleVault)) + return; + + Asset share = sleVault->at(sfShareMPTID); + env(vault.clawback( + {.issuer = owner, .id = vaultKeylet.key, .holder = depositor, .amount = share(0).value()}), + ter(tesSUCCESS), + THISLINE); + env.close(); + } + + { + env(loan::del(owner, loanKeylet.key), ter(tesSUCCESS), THISLINE); + env(loanBroker::del(owner, brokerKeylet.key), ter(tesSUCCESS), THISLINE); + env(vault.del({.owner = owner, .id = vaultKeylet.key})); + env.close(); + } + }); } void @@ -2800,6 +2961,169 @@ class Vault_test : public beast::unit_test::suite env(vault.del({.owner = owner, .id = keylet.key})); env.close(); }); + + testCase([&, this]( + Env& env, + Account const& owner, + Account const& issuer, + Account const&, + auto vaultAccount, + Vault& vault, + PrettyAsset const& asset, + auto&&...) { + testcase("lsfVaultDepositBlocked prevents deposits"); + auto const [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + env(tx); + env.close(); + + // First deposit assets to later show that withdrawals are not blocked + { + auto const tx = vault.deposit({.depositor = issuer, .id = keylet.key, .amount = asset(20)}); + env(tx, ter{tesSUCCESS}, THISLINE); + env.close(); + } + + // Block Vault deposits + { + auto const tx = vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositBlock}); + env(tx, ter(tesSUCCESS), THISLINE); + env.close(); + } + + { + auto const tx = vault.deposit({.depositor = issuer, .id = keylet.key, .amount = asset(20)}); + env(tx, ter{tecNO_PERMISSION}, THISLINE); + env.close(); + } + + { + auto tx = vault.withdraw({.depositor = issuer, .id = keylet.key, .amount = asset(20)}); + env(tx, ter{tesSUCCESS}, THISLINE); + env.close(); + } + + // Unblock Vault Deposits + { + auto const tx = vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositUnblock}); + env(tx, ter(tesSUCCESS), THISLINE); + env.close(); + } + + // Deposits now succeed + { + auto const tx = vault.deposit({.depositor = issuer, .id = keylet.key, .amount = asset(20)}); + env(tx, ter{tesSUCCESS}, THISLINE); + env.close(); + } + + // Withdraw assets from the vault to delete it + { + auto const tx = vault.withdraw({.depositor = issuer, .id = keylet.key, .amount = asset(20)}); + env(tx, ter{tesSUCCESS}, THISLINE); + env.close(); + } + + env(vault.del({.owner = owner, .id = keylet.key})); + env.close(); + }); + + testCase([&, this]( + Env& env, + Account const& owner, + Account const& issuer, + Account const&, + auto vaultAccount, + Vault& vault, + PrettyAsset const& asset, + auto&&...) { + testcase("insolvent vault blocks deposits"); + + auto const depositAmount = asset(20); + + auto const [tx, vaultKeylet] = vault.create({.owner = owner, .asset = asset}); + env(tx); + env.close(); + + // First deposit assets to later show that withdrawals are not blocked + { + auto const tx = vault.deposit({.depositor = issuer, .id = vaultKeylet.key, .amount = depositAmount}); + env(tx, ter{tesSUCCESS}, THISLINE); + env.close(); + } + + auto const& brokerKeylet = keylet::loanbroker(owner.id(), env.seq(owner)); + auto const& loanKeylet = keylet::loan(brokerKeylet.key, 1); + + // Create a LoanBroker and a Loan, to drain the vault + { + using namespace loanBroker; + using namespace loan; + + env(set(owner, vaultKeylet.key), THISLINE); + env.close(); + + // Create a simple Loan for the full amount of Vault assets + env(set(issuer, brokerKeylet.key, depositAmount.value()), + loan::interestRate(TenthBips32(0)), + paymentInterval(120), + paymentTotal(1), + sig(sfCounterpartySignature, owner), + fee(env.current()->fees().base * 2), + ter(tesSUCCESS), + THISLINE); + env.close(); + + env.close(std::chrono::seconds{120 + 60}); + + env(manage(owner, loanKeylet.key, tfLoanDefault), ter(tesSUCCESS), THISLINE); + + auto const sleVault = env.le(vaultKeylet); + if (!BEAST_EXPECT(sleVault)) + return; + + auto const sleIssuance = env.le(keylet::mptIssuance(sleVault->at(sfShareMPTID))); + if (!BEAST_EXPECT(sleIssuance)) + return; + + auto const shareBalance = sleIssuance->at(sfOutstandingAmount); + auto const expectedShares = Number{ + depositAmount.number().mantissa(), depositAmount.number().exponent() + sleVault->at(sfScale)}; + + // verify that the vault is insolvent + if (!BEAST_EXPECT( + sleVault->at(sfAssetsTotal) == 0 && sleVault->at(sfAssetsAvailable) == 0 && + shareBalance == expectedShares)) + return; + } + + // The vault is insolvent, deposit must fail + { + auto const tx = vault.deposit({.depositor = issuer, .id = vaultKeylet.key, .amount = asset(20)}); + env(tx, ter{tecNO_PERMISSION}, THISLINE); + env.close(); + } + + // Clean up the vault to delete it + { + auto const sleVault = env.le(vaultKeylet); + if (!BEAST_EXPECT(sleVault)) + return; + + Asset share = sleVault->at(sfShareMPTID); + env(vault.clawback( + {.issuer = owner, .id = vaultKeylet.key, .holder = issuer, .amount = share(0).value()}), + ter(tesSUCCESS), + THISLINE); + env.close(); + } + + { + env(loan::del(owner, loanKeylet.key), ter(tesSUCCESS), THISLINE); + env(loanBroker::del(owner, brokerKeylet.key), ter(tesSUCCESS), THISLINE); + env(vault.del({.owner = owner, .id = vaultKeylet.key})); + env.close(); + } + }); } void diff --git a/src/xrpld/app/tx/detail/VaultDeposit.cpp b/src/xrpld/app/tx/detail/VaultDeposit.cpp index d5fc0e4ad6c..0ed722f4082 100644 --- a/src/xrpld/app/tx/detail/VaultDeposit.cpp +++ b/src/xrpld/app/tx/detail/VaultDeposit.cpp @@ -60,8 +60,8 @@ VaultDeposit::preclaim(PreclaimContext const& ctx) // LCOV_EXCL_STOP } - auto const sleIssuance = ctx.view.read(keylet::mptIssuance(mptIssuanceID)); - if (!sleIssuance) + auto const sleShareIssuance = ctx.view.read(keylet::mptIssuance(mptIssuanceID)); + if (!sleShareIssuance) { // LCOV_EXCL_START JLOG(ctx.j.error()) << "VaultDeposit: missing issuance of vault shares."; @@ -69,7 +69,7 @@ VaultDeposit::preclaim(PreclaimContext const& ctx) // LCOV_EXCL_STOP } - if (sleIssuance->isFlag(lsfMPTLocked)) + if (sleShareIssuance->isFlag(lsfMPTLocked)) { // LCOV_EXCL_START JLOG(ctx.j.error()) << "VaultDeposit: issuance of vault shares is locked."; @@ -77,6 +77,24 @@ VaultDeposit::preclaim(PreclaimContext const& ctx) // LCOV_EXCL_STOP } + if (ctx.view.rules().enabled(fixLendingProtocolV1_1)) + { + // Perform these checks early to avoid unnecessary processing + + // The Vault is insolvent, deposits are not allowed + if (vault->at(sfAssetsTotal) == 0 && sleShareIssuance->at(sfOutstandingAmount) > 0) + { + JLOG(ctx.j.debug()) << "VaultDeposit: Vault is insolvent, deposits are not allowed"; + return tecNO_PERMISSION; + } + + if (vault->isFlag(lsfVaultDepositBlocked)) + { + JLOG(ctx.j.debug()) << "VaultDeposit: Vault deposits are blocked"; + return tecNO_PERMISSION; + } + } + // Cannot deposit inside Vault an Asset frozen for the depositor if (isFrozen(ctx.view, account, vaultAsset)) return vaultAsset.holds() ? tecFROZEN : tecLOCKED; @@ -87,7 +105,7 @@ VaultDeposit::preclaim(PreclaimContext const& ctx) if (vault->isFlag(lsfVaultPrivate) && account != vault->at(sfOwner)) { - auto const maybeDomainID = sleIssuance->at(~sfDomainID); + auto const maybeDomainID = sleShareIssuance->at(~sfDomainID); // Since this is a private vault and the account is not its owner, we // perform authorization check based on DomainID read from sleIssuance. // Had the vault shares been a regular MPToken, we would allow From 82b0d57aac5b281d7dbbacb3fbd5ee31d17bca05 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Tue, 17 Feb 2026 13:33:47 +0100 Subject: [PATCH 05/16] minor code improvements --- include/xrpl/ledger/View.h | 5 +++++ include/xrpl/protocol/TxFlags.h | 2 -- src/libxrpl/ledger/View.cpp | 9 +++++++++ src/xrpld/app/tx/detail/VaultDeposit.cpp | 2 +- src/xrpld/app/tx/detail/VaultSet.cpp | 4 ++-- 5 files changed, 17 insertions(+), 5 deletions(-) diff --git a/include/xrpl/ledger/View.h b/include/xrpl/ledger/View.h index fa16e16006a..d6f3bdfe5e4 100644 --- a/include/xrpl/ledger/View.h +++ b/include/xrpl/ledger/View.h @@ -985,6 +985,11 @@ sharesToAssetsWithdraw( std::shared_ptr const& issuance, STAmount const& shares); +// Determine if a vault is insolvent. A vault is considered insolvent when +// the total assets in the vault are zero, and outstanding shares are non-zero. +[[nodiscard]] bool +isVaultInsolvent(std::shared_ptr const& vault, std::shared_ptr const& shareIssuance); + /** 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 a1c18f28276..e424f914aa1 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -270,13 +270,11 @@ constexpr std::uint32_t const tfBatchMask = // LoanPay: True, indicates any excess in this payment can be used // as an overpayment. False, no overpayments will be taken. constexpr std::uint32_t const tfLoanOverpayment = 0x00010000; - // LoanPay exclusive flags: // tfLoanFullPayment: True, indicates that the payment is an early // full payment. It must pay the entire loan including close // interest and fees, or it will fail. False: Not a full payment. constexpr std::uint32_t const tfLoanFullPayment = 0x00020000; - // tfLoanLatePayment: True, indicates that the payment is late, // and includes late interest and fees. If the loan is not late, // it will fail. False: not a late payment. If the current payment diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index 72df44ec4a0..01f7dcf8811 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -3438,4 +3438,13 @@ after(NetClock::time_point now, std::uint32_t mark) return now.time_since_epoch().count() > mark; } +[[nodiscard]] bool +isVaultInsolvent(std::shared_ptr const& vault, std::shared_ptr const& shareIssuance) +{ + auto const assetsTotal = vault->at(sfAssetsTotal); + auto const sharesOutstanding = shareIssuance->at(sfOutstandingAmount); + + return assetsTotal == 0 && sharesOutstanding > 0; +} + } // namespace xrpl diff --git a/src/xrpld/app/tx/detail/VaultDeposit.cpp b/src/xrpld/app/tx/detail/VaultDeposit.cpp index 0ed722f4082..5b8afa2a851 100644 --- a/src/xrpld/app/tx/detail/VaultDeposit.cpp +++ b/src/xrpld/app/tx/detail/VaultDeposit.cpp @@ -82,7 +82,7 @@ VaultDeposit::preclaim(PreclaimContext const& ctx) // Perform these checks early to avoid unnecessary processing // The Vault is insolvent, deposits are not allowed - if (vault->at(sfAssetsTotal) == 0 && sleShareIssuance->at(sfOutstandingAmount) > 0) + if (isVaultInsolvent(vault, sleShareIssuance)) { JLOG(ctx.j.debug()) << "VaultDeposit: Vault is insolvent, deposits are not allowed"; return tecNO_PERMISSION; diff --git a/src/xrpld/app/tx/detail/VaultSet.cpp b/src/xrpld/app/tx/detail/VaultSet.cpp index 1cc39f0edcf..f8aafb6a916 100644 --- a/src/xrpld/app/tx/detail/VaultSet.cpp +++ b/src/xrpld/app/tx/detail/VaultSet.cpp @@ -35,13 +35,13 @@ VaultSet::getFlagsMask(PreflightContext const& ctx) static bool isValidVaultUpdate(PreflightContext const& ctx) { - auto const checkFlags = ctx.rules.enabled(fixLendingProtocolV1_1); + auto const shouldCheckFlags = ctx.rules.enabled(fixLendingProtocolV1_1); auto const atLeastOneFieldPresent = ctx.tx.isFieldPresent(sfDomainID) || ctx.tx.isFieldPresent(sfAssetsMaximum) || ctx.tx.isFieldPresent(sfData); return atLeastOneFieldPresent || - (checkFlags && (ctx.tx.isFlag(tfVaultDepositBlock) || ctx.tx.isFlag(tfVaultDepositUnblock))); + (shouldCheckFlags && (ctx.tx.isFlag(tfVaultDepositBlock) || ctx.tx.isFlag(tfVaultDepositUnblock))); } NotTEC From f57b7159360d30af49954e0f2931c06d091e3c29 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Tue, 17 Feb 2026 13:47:50 +0100 Subject: [PATCH 06/16] removes redundant check --- src/xrpld/app/tx/detail/VaultSet.cpp | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/xrpld/app/tx/detail/VaultSet.cpp b/src/xrpld/app/tx/detail/VaultSet.cpp index f8aafb6a916..b1d1c36d07d 100644 --- a/src/xrpld/app/tx/detail/VaultSet.cpp +++ b/src/xrpld/app/tx/detail/VaultSet.cpp @@ -47,13 +47,6 @@ isValidVaultUpdate(PreflightContext const& ctx) NotTEC VaultSet::preflight(PreflightContext const& ctx) { - if ((ctx.tx.isFlag(tfVaultDepositBlock) || ctx.tx.isFlag(tfVaultDepositUnblock)) && - !ctx.rules.enabled(fixLendingProtocolV1_1)) - { - JLOG(ctx.j.debug()) << "VaultSet: flags not supported without fixLendingProtocolV1_1."; - return temDISABLED; - } - if (ctx.tx[sfVaultID] == beast::zero) { JLOG(ctx.j.debug()) << "VaultSet: zero/empty vault ID."; From 3cfb5fe56dca2baa259b6cd925e1d1e90485f948 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Tue, 17 Feb 2026 13:50:02 +0100 Subject: [PATCH 07/16] additional unit test --- src/test/app/Vault_test.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 7eae7250dfa..52215a0eb75 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -919,8 +919,9 @@ class Vault_test : public beast::unit_test::suite { env.disableFeature(fixLendingProtocolV1_1); - auto tx = vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositBlock}); - env(tx, ter(temINVALID_FLAG)); + env(vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositBlock}), ter(temINVALID_FLAG)); + env(vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositUnblock}), + ter(temINVALID_FLAG)); env.enableFeature(fixLendingProtocolV1_1); } }); From cd1e8ebbc3374ccc7808d9dd1104872c22275b49 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:40:03 +0100 Subject: [PATCH 08/16] debug attempt --- src/test/app/Vault_test.cpp | 57 ++++++++++++----------- src/xrpld/app/tx/detail/VaultClawback.cpp | 3 +- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 52215a0eb75..f50f3966601 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -1011,14 +1011,13 @@ class Vault_test : public beast::unit_test::suite { using namespace test::jtx; - auto testCase = [this]( - std::function test) { + auto testCase = [this](std::function test) { Env env{*this, testable_amendments() | featureSingleAssetVault}; Account issuer{"issuer"}; Account owner{"owner"}; @@ -1298,14 +1297,13 @@ class Vault_test : public beast::unit_test::suite { using namespace test::jtx; - auto testCase = [this]( - std::function test) { + auto testCase = [this](std::function test) { Env env{*this, testable_amendments() | featureSingleAssetVault}; Account issuer{"issuer"}; Account owner{"owner"}; @@ -2143,7 +2141,7 @@ class Vault_test : public beast::unit_test::suite PrettyAsset const& asset, Vault& vault, MPTTester const& mptt) { - testcase("lsfVaultDepositBlocked prevents deposits"); + testcase("MPT lsfVaultDepositBlocked prevents deposits"); auto const [tx, keylet] = vault.create({.owner = owner, .asset = asset}); env(tx); env.close(); @@ -2207,7 +2205,7 @@ class Vault_test : public beast::unit_test::suite PrettyAsset const& asset, Vault& vault, MPTTester const& mptt) { - testcase("insolvent vault blocks deposits"); + testcase("MPT insolvent vault blocks deposits"); auto const depositAmount = asset(20); @@ -2280,9 +2278,11 @@ class Vault_test : public beast::unit_test::suite if (!BEAST_EXPECT(sleVault)) return; + std::cout << "assets total: " << sleVault->at(sfAssetsTotal) << std::endl; + std::cout << "assets available: " << sleVault->at(sfAssetsAvailable) << std::endl; + Asset share = sleVault->at(sfShareMPTID); - env(vault.clawback( - {.issuer = owner, .id = vaultKeylet.key, .holder = depositor, .amount = share(0).value()}), + env(vault.clawback({.issuer = owner, .id = vaultKeylet.key, .holder = depositor, .amount = share(0)}), ter(tesSUCCESS), THISLINE); env.close(); @@ -2974,7 +2974,7 @@ class Vault_test : public beast::unit_test::suite Vault& vault, PrettyAsset const& asset, auto&&...) { - testcase("lsfVaultDepositBlocked prevents deposits"); + testcase("IOU lsfVaultDepositBlocked prevents deposits"); auto const [tx, keylet] = vault.create({.owner = owner, .asset = asset}); env(tx); env.close(); @@ -3039,7 +3039,7 @@ class Vault_test : public beast::unit_test::suite Vault& vault, PrettyAsset const& asset, auto&&...) { - testcase("insolvent vault blocks deposits"); + testcase("IOU insolvent vault blocks deposits"); auto const depositAmount = asset(20); @@ -3054,15 +3054,15 @@ class Vault_test : public beast::unit_test::suite env.close(); } - auto const& brokerKeylet = keylet::loanbroker(owner.id(), env.seq(owner)); - auto const& loanKeylet = keylet::loan(brokerKeylet.key, 1); + auto const brokerKeylet = keylet::loanbroker(owner.id(), env.seq(owner)); + auto const loanKeylet = keylet::loan(brokerKeylet.key, 1); // Create a LoanBroker and a Loan, to drain the vault { using namespace loanBroker; using namespace loan; - env(set(owner, vaultKeylet.key), THISLINE); + env(set(owner, vaultKeylet.key), ter{tesSUCCESS}, THISLINE); env.close(); // Create a simple Loan for the full amount of Vault assets @@ -3072,7 +3072,7 @@ class Vault_test : public beast::unit_test::suite paymentTotal(1), sig(sfCounterpartySignature, owner), fee(env.current()->fees().base * 2), - ter(tesSUCCESS), + ter{tesSUCCESS}, THISLINE); env.close(); @@ -3111,10 +3111,11 @@ class Vault_test : public beast::unit_test::suite auto const sleVault = env.le(vaultKeylet); if (!BEAST_EXPECT(sleVault)) return; + std::cout << "assets total: " << sleVault->at(sfAssetsTotal) << std::endl; + std::cout << "assets available: " << sleVault->at(sfAssetsAvailable) << std::endl; Asset share = sleVault->at(sfShareMPTID); - env(vault.clawback( - {.issuer = owner, .id = vaultKeylet.key, .holder = issuer, .amount = share(0).value()}), + env(vault.clawback({.issuer = owner, .id = vaultKeylet.key, .holder = issuer, .amount = share(0)}), ter(tesSUCCESS), THISLINE); env.close(); diff --git a/src/xrpld/app/tx/detail/VaultClawback.cpp b/src/xrpld/app/tx/detail/VaultClawback.cpp index 5aa63fac1a3..d56554a54f6 100644 --- a/src/xrpld/app/tx/detail/VaultClawback.cpp +++ b/src/xrpld/app/tx/detail/VaultClawback.cpp @@ -47,9 +47,8 @@ clawbackAmount( if (maybeAmount) return *maybeAmount; - Asset const share = MPTIssue{vault->at(sfShareMPTID)}; if (account == vault->at(sfOwner)) - return STAmount{share}; + return STAmount{MPTIssue{vault->at(sfShareMPTID)}}; return STAmount{vault->at(sfAsset)}; } From 884530e4150230fa3f72953a7dbd7fe75c8b937e Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:59:56 +0100 Subject: [PATCH 09/16] adds missing env.close --- src/test/app/Vault_test.cpp | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index f50f3966601..99dd5402daf 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -2240,11 +2240,10 @@ class Vault_test : public beast::unit_test::suite fee(env.current()->fees().base * 2), ter(tesSUCCESS), THISLINE); - env.close(); - env.close(std::chrono::seconds{120 + 60}); env(manage(owner, loanKeylet.key, tfLoanDefault), ter(tesSUCCESS), THISLINE); + env.close(); auto const sleVault = env.le(vaultKeylet); if (!BEAST_EXPECT(sleVault)) @@ -2278,9 +2277,6 @@ class Vault_test : public beast::unit_test::suite if (!BEAST_EXPECT(sleVault)) return; - std::cout << "assets total: " << sleVault->at(sfAssetsTotal) << std::endl; - std::cout << "assets available: " << sleVault->at(sfAssetsAvailable) << std::endl; - Asset share = sleVault->at(sfShareMPTID); env(vault.clawback({.issuer = owner, .id = vaultKeylet.key, .holder = depositor, .amount = share(0)}), ter(tesSUCCESS), @@ -3074,11 +3070,10 @@ class Vault_test : public beast::unit_test::suite fee(env.current()->fees().base * 2), ter{tesSUCCESS}, THISLINE); - env.close(); - env.close(std::chrono::seconds{120 + 60}); env(manage(owner, loanKeylet.key, tfLoanDefault), ter(tesSUCCESS), THISLINE); + env.close(); auto const sleVault = env.le(vaultKeylet); if (!BEAST_EXPECT(sleVault)) @@ -3111,8 +3106,6 @@ class Vault_test : public beast::unit_test::suite auto const sleVault = env.le(vaultKeylet); if (!BEAST_EXPECT(sleVault)) return; - std::cout << "assets total: " << sleVault->at(sfAssetsTotal) << std::endl; - std::cout << "assets available: " << sleVault->at(sfAssetsAvailable) << std::endl; Asset share = sleVault->at(sfShareMPTID); env(vault.clawback({.issuer = owner, .id = vaultKeylet.key, .holder = issuer, .amount = share(0)}), From 8732e84e54da66bdab2fbd5e8a5c607cf750e538 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Tue, 24 Feb 2026 12:56:51 +0100 Subject: [PATCH 10/16] refactor: Extract vault helper functions into VaultHelpers module Move vault share/asset conversion functions (assetsToSharesDeposit, sharesToAssetsDeposit, assetsToSharesWithdraw, sharesToAssetsWithdraw) and isVaultInsolvent from View.h/View.cpp into a dedicated VaultHelpers.h/VaultHelpers.cpp module. Reorder includes in Vault transactor .cpp files to place own header first. Fix VaultSet flag validation logic. --- include/xrpl/ledger/View.h | 45 ------- .../xrpl/tx/transactors/Vault/VaultClawback.h | 1 + .../xrpl/tx/transactors/Vault/VaultDeposit.h | 1 + .../xrpl/tx/transactors/Vault/VaultHelpers.h | 54 ++++++++ .../xrpl/tx/transactors/Vault/VaultWithdraw.h | 1 + src/libxrpl/ledger/View.cpp | 97 -------------- .../tx/transactors/Vault/VaultClawback.cpp | 4 +- .../tx/transactors/Vault/VaultCreate.cpp | 3 +- .../tx/transactors/Vault/VaultDelete.cpp | 3 +- .../tx/transactors/Vault/VaultDeposit.cpp | 3 +- .../tx/transactors/Vault/VaultHelpers.cpp | 123 ++++++++++++++++++ src/libxrpl/tx/transactors/Vault/VaultSet.cpp | 8 +- .../tx/transactors/Vault/VaultWithdraw.cpp | 3 +- 13 files changed, 194 insertions(+), 152 deletions(-) create mode 100644 include/xrpl/tx/transactors/Vault/VaultHelpers.h create mode 100644 src/libxrpl/tx/transactors/Vault/VaultHelpers.cpp diff --git a/include/xrpl/ledger/View.h b/include/xrpl/ledger/View.h index d6f3bdfe5e4..a485f8decce 100644 --- a/include/xrpl/ledger/View.h +++ b/include/xrpl/ledger/View.h @@ -945,51 +945,6 @@ deleteAMMTrustLine( std::optional const& ammAccountID, beast::Journal j); -// From the perspective of a vault, return the number of shares to give the -// depositor when they deposit a fixed amount of assets. Since shares are MPT -// this number is integral and always truncated in this calculation. -[[nodiscard]] std::optional -assetsToSharesDeposit( - std::shared_ptr const& vault, - std::shared_ptr const& issuance, - STAmount const& assets); - -// From the perspective of a vault, return the number of assets to take from -// depositor when they receive a fixed amount of shares. Note, since shares are -// MPT, they are always an integral number. -[[nodiscard]] std::optional -sharesToAssetsDeposit( - std::shared_ptr const& vault, - std::shared_ptr const& issuance, - STAmount const& shares); - -enum class TruncateShares : bool { no = false, yes = true }; - -// From the perspective of a vault, return the number of shares to demand from -// the depositor when they ask to withdraw a fixed amount of assets. Since -// shares are MPT this number is integral, and it will be rounded to nearest -// unless explicitly requested to be truncated instead. -[[nodiscard]] std::optional -assetsToSharesWithdraw( - std::shared_ptr const& vault, - std::shared_ptr const& issuance, - STAmount const& assets, - TruncateShares truncate = TruncateShares::no); - -// From the perspective of a vault, return the number of assets to give the -// depositor when they redeem a fixed amount of shares. Note, since shares are -// MPT, they are always an integral number. -[[nodiscard]] std::optional -sharesToAssetsWithdraw( - std::shared_ptr const& vault, - std::shared_ptr const& issuance, - STAmount const& shares); - -// Determine if a vault is insolvent. A vault is considered insolvent when -// the total assets in the vault are zero, and outstanding shares are non-zero. -[[nodiscard]] bool -isVaultInsolvent(std::shared_ptr const& vault, std::shared_ptr const& shareIssuance); - /** Has the specified time passed? @param now the current time diff --git a/include/xrpl/tx/transactors/Vault/VaultClawback.h b/include/xrpl/tx/transactors/Vault/VaultClawback.h index 131a1d87e78..3a38a0661bf 100644 --- a/include/xrpl/tx/transactors/Vault/VaultClawback.h +++ b/include/xrpl/tx/transactors/Vault/VaultClawback.h @@ -1,6 +1,7 @@ #pragma once #include +#include namespace xrpl { diff --git a/include/xrpl/tx/transactors/Vault/VaultDeposit.h b/include/xrpl/tx/transactors/Vault/VaultDeposit.h index 0943596f20d..43fa4566b37 100644 --- a/include/xrpl/tx/transactors/Vault/VaultDeposit.h +++ b/include/xrpl/tx/transactors/Vault/VaultDeposit.h @@ -1,6 +1,7 @@ #pragma once #include +#include namespace xrpl { diff --git a/include/xrpl/tx/transactors/Vault/VaultHelpers.h b/include/xrpl/tx/transactors/Vault/VaultHelpers.h new file mode 100644 index 00000000000..fd5f9b7b955 --- /dev/null +++ b/include/xrpl/tx/transactors/Vault/VaultHelpers.h @@ -0,0 +1,54 @@ +#pragma once + +#include +#include +#include + +namespace xrpl { + +enum class TruncateShares : bool { no = false, yes = true }; + +// From the perspective of a vault, return the number of shares to give the +// depositor when they deposit a fixed amount of assets. Since shares are MPT +// this number is integral and always truncated in this calculation. +[[nodiscard]] std::optional +assetsToSharesDeposit( + std::shared_ptr const& vault, + std::shared_ptr const& issuance, + STAmount const& assets); + +// From the perspective of a vault, return the number of assets to take from +// depositor when they receive a fixed amount of shares. Note, since shares are +// MPT, they are always an integral number. +[[nodiscard]] std::optional +sharesToAssetsDeposit( + std::shared_ptr const& vault, + std::shared_ptr const& issuance, + STAmount const& shares); + +// From the perspective of a vault, return the number of shares to demand from +// the depositor when they ask to withdraw a fixed amount of assets. Since +// shares are MPT this number is integral, and it will be rounded to nearest +// unless explicitly requested to be truncated instead. +[[nodiscard]] std::optional +assetsToSharesWithdraw( + std::shared_ptr const& vault, + std::shared_ptr const& issuance, + STAmount const& assets, + TruncateShares truncate = TruncateShares::no); + +// From the perspective of a vault, return the number of assets to give the +// depositor when they redeem a fixed amount of shares. Note, since shares are +// MPT, they are always an integral number. +[[nodiscard]] std::optional +sharesToAssetsWithdraw( + std::shared_ptr const& vault, + std::shared_ptr const& issuance, + STAmount const& shares); + +// Determine if a vault is insolvent. A vault is considered insolvent when +// the total assets in the vault are zero, and outstanding shares are non-zero. +[[nodiscard]] bool +isVaultInsolvent(std::shared_ptr const& vault, std::shared_ptr const& shareIssuance); + +} // namespace xrpl diff --git a/include/xrpl/tx/transactors/Vault/VaultWithdraw.h b/include/xrpl/tx/transactors/Vault/VaultWithdraw.h index ffe14a71411..154fa2d09b7 100644 --- a/include/xrpl/tx/transactors/Vault/VaultWithdraw.h +++ b/include/xrpl/tx/transactors/Vault/VaultWithdraw.h @@ -1,6 +1,7 @@ #pragma once #include +#include namespace xrpl { diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index 01f7dcf8811..444443fdeac 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -3103,94 +3103,6 @@ rippleCredit( saAmount.asset().value()); } -[[nodiscard]] std::optional -assetsToSharesDeposit( - std::shared_ptr const& vault, - std::shared_ptr const& issuance, - STAmount const& assets) -{ - XRPL_ASSERT(!assets.negative(), "xrpl::assetsToSharesDeposit : non-negative assets"); - XRPL_ASSERT(assets.asset() == vault->at(sfAsset), "xrpl::assetsToSharesDeposit : assets and vault match"); - if (assets.negative() || assets.asset() != vault->at(sfAsset)) - return std::nullopt; // LCOV_EXCL_LINE - - Number const assetTotal = vault->at(sfAssetsTotal); - STAmount shares{vault->at(sfShareMPTID)}; - if (assetTotal == 0) - return STAmount{shares.asset(), Number(assets.mantissa(), assets.exponent() + vault->at(sfScale)).truncate()}; - - Number const shareTotal = issuance->at(sfOutstandingAmount); - shares = ((shareTotal * assets) / assetTotal).truncate(); - return shares; -} - -[[nodiscard]] std::optional -sharesToAssetsDeposit( - std::shared_ptr const& vault, - std::shared_ptr const& issuance, - STAmount const& shares) -{ - XRPL_ASSERT(!shares.negative(), "xrpl::sharesToAssetsDeposit : non-negative shares"); - XRPL_ASSERT(shares.asset() == vault->at(sfShareMPTID), "xrpl::sharesToAssetsDeposit : shares and vault match"); - if (shares.negative() || shares.asset() != vault->at(sfShareMPTID)) - return std::nullopt; // LCOV_EXCL_LINE - - Number const assetTotal = vault->at(sfAssetsTotal); - STAmount assets{vault->at(sfAsset)}; - if (assetTotal == 0) - return STAmount{assets.asset(), shares.mantissa(), shares.exponent() - vault->at(sfScale), false}; - - Number const shareTotal = issuance->at(sfOutstandingAmount); - assets = (assetTotal * shares) / shareTotal; - return assets; -} - -[[nodiscard]] std::optional -assetsToSharesWithdraw( - std::shared_ptr const& vault, - std::shared_ptr const& issuance, - STAmount const& assets, - TruncateShares truncate) -{ - XRPL_ASSERT(!assets.negative(), "xrpl::assetsToSharesDeposit : non-negative assets"); - XRPL_ASSERT(assets.asset() == vault->at(sfAsset), "xrpl::assetsToSharesWithdraw : assets and vault match"); - if (assets.negative() || assets.asset() != vault->at(sfAsset)) - return std::nullopt; // LCOV_EXCL_LINE - - Number assetTotal = vault->at(sfAssetsTotal); - assetTotal -= vault->at(sfLossUnrealized); - STAmount shares{vault->at(sfShareMPTID)}; - if (assetTotal == 0) - return shares; - Number const shareTotal = issuance->at(sfOutstandingAmount); - Number result = (shareTotal * assets) / assetTotal; - if (truncate == TruncateShares::yes) - result = result.truncate(); - shares = result; - return shares; -} - -[[nodiscard]] std::optional -sharesToAssetsWithdraw( - std::shared_ptr const& vault, - std::shared_ptr const& issuance, - STAmount const& shares) -{ - XRPL_ASSERT(!shares.negative(), "xrpl::sharesToAssetsDeposit : non-negative shares"); - XRPL_ASSERT(shares.asset() == vault->at(sfShareMPTID), "xrpl::sharesToAssetsWithdraw : shares and vault match"); - if (shares.negative() || shares.asset() != vault->at(sfShareMPTID)) - return std::nullopt; // LCOV_EXCL_LINE - - Number assetTotal = vault->at(sfAssetsTotal); - assetTotal -= vault->at(sfLossUnrealized); - STAmount assets{vault->at(sfAsset)}; - if (assetTotal == 0) - return assets; - Number const shareTotal = issuance->at(sfOutstandingAmount); - assets = (assetTotal * shares) / shareTotal; - return assets; -} - TER rippleLockEscrowMPT(ApplyView& view, AccountID const& sender, STAmount const& amount, beast::Journal j) { @@ -3438,13 +3350,4 @@ after(NetClock::time_point now, std::uint32_t mark) return now.time_since_epoch().count() > mark; } -[[nodiscard]] bool -isVaultInsolvent(std::shared_ptr const& vault, std::shared_ptr const& shareIssuance) -{ - auto const assetsTotal = vault->at(sfAssetsTotal); - auto const sharesOutstanding = shareIssuance->at(sfOutstandingAmount); - - return assetsTotal == 0 && sharesOutstanding > 0; -} - } // namespace xrpl diff --git a/src/libxrpl/tx/transactors/Vault/VaultClawback.cpp b/src/libxrpl/tx/transactors/Vault/VaultClawback.cpp index 4be1b52eb7c..89932d69070 100644 --- a/src/libxrpl/tx/transactors/Vault/VaultClawback.cpp +++ b/src/libxrpl/tx/transactors/Vault/VaultClawback.cpp @@ -1,4 +1,5 @@ -#include +#include +// #include #include #include @@ -8,7 +9,6 @@ #include #include #include -#include #include diff --git a/src/libxrpl/tx/transactors/Vault/VaultCreate.cpp b/src/libxrpl/tx/transactors/Vault/VaultCreate.cpp index c911c496ffc..40192a56b63 100644 --- a/src/libxrpl/tx/transactors/Vault/VaultCreate.cpp +++ b/src/libxrpl/tx/transactors/Vault/VaultCreate.cpp @@ -1,3 +1,5 @@ +#include +// #include #include #include @@ -12,7 +14,6 @@ #include #include #include -#include namespace xrpl { diff --git a/src/libxrpl/tx/transactors/Vault/VaultDelete.cpp b/src/libxrpl/tx/transactors/Vault/VaultDelete.cpp index 7d8bf35e5f1..c1be184d166 100644 --- a/src/libxrpl/tx/transactors/Vault/VaultDelete.cpp +++ b/src/libxrpl/tx/transactors/Vault/VaultDelete.cpp @@ -1,3 +1,5 @@ +#include +// #include #include #include @@ -5,7 +7,6 @@ #include #include #include -#include namespace xrpl { diff --git a/src/libxrpl/tx/transactors/Vault/VaultDeposit.cpp b/src/libxrpl/tx/transactors/Vault/VaultDeposit.cpp index 029b36c2cba..3837e35a54a 100644 --- a/src/libxrpl/tx/transactors/Vault/VaultDeposit.cpp +++ b/src/libxrpl/tx/transactors/Vault/VaultDeposit.cpp @@ -1,3 +1,5 @@ +#include +// #include #include #include @@ -10,7 +12,6 @@ #include #include #include -#include namespace xrpl { diff --git a/src/libxrpl/tx/transactors/Vault/VaultHelpers.cpp b/src/libxrpl/tx/transactors/Vault/VaultHelpers.cpp new file mode 100644 index 00000000000..82b04cdbe52 --- /dev/null +++ b/src/libxrpl/tx/transactors/Vault/VaultHelpers.cpp @@ -0,0 +1,123 @@ +#include + +namespace xrpl { + +[[nodiscard]] std::optional +assetsToSharesDeposit( + std::shared_ptr const& vault, + std::shared_ptr const& issuance, + STAmount const& assets) +{ + XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::assetsToSharesDeposit : Vault sle"); + XRPL_ASSERT( + issuance && issuance->getType() == ltMPTOKEN_ISSUANCE, "xrpl::assetsToSharesDeposit : MPTokenIssuance sle"); + + XRPL_ASSERT(!assets.negative(), "xrpl::assetsToSharesDeposit : non-negative assets"); + XRPL_ASSERT(assets.asset() == vault->at(sfAsset), "xrpl::assetsToSharesDeposit : assets and vault match"); + if (assets.negative() || assets.asset() != vault->at(sfAsset)) + return std::nullopt; // LCOV_EXCL_LINE + + Number const assetTotal = vault->at(sfAssetsTotal); + STAmount shares{vault->at(sfShareMPTID)}; + if (assetTotal == 0) + return STAmount{shares.asset(), Number(assets.mantissa(), assets.exponent() + vault->at(sfScale)).truncate()}; + + Number const shareTotal = issuance->at(sfOutstandingAmount); + shares = ((shareTotal * assets) / assetTotal).truncate(); + return shares; +} + +[[nodiscard]] std::optional +sharesToAssetsDeposit( + std::shared_ptr const& vault, + std::shared_ptr const& issuance, + STAmount const& shares) +{ + XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::sharesToAssetsDeposit : Vault sle"); + XRPL_ASSERT( + issuance && issuance->getType() == ltMPTOKEN_ISSUANCE, "xrpl::sharesToAssetsDeposit : MPTokenIssuance sle"); + + XRPL_ASSERT(!shares.negative(), "xrpl::sharesToAssetsDeposit : non-negative shares"); + XRPL_ASSERT(shares.asset() == vault->at(sfShareMPTID), "xrpl::sharesToAssetsDeposit : shares and vault match"); + if (shares.negative() || shares.asset() != vault->at(sfShareMPTID)) + return std::nullopt; // LCOV_EXCL_LINE + + Number const assetTotal = vault->at(sfAssetsTotal); + STAmount assets{vault->at(sfAsset)}; + if (assetTotal == 0) + return STAmount{assets.asset(), shares.mantissa(), shares.exponent() - vault->at(sfScale), false}; + + Number const shareTotal = issuance->at(sfOutstandingAmount); + assets = (assetTotal * shares) / shareTotal; + return assets; +} + +[[nodiscard]] std::optional +assetsToSharesWithdraw( + std::shared_ptr const& vault, + std::shared_ptr const& issuance, + STAmount const& assets, + TruncateShares truncate) +{ + XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::assetsToSharesWithdraw : Vault sle"); + XRPL_ASSERT( + issuance && issuance->getType() == ltMPTOKEN_ISSUANCE, "xrpl::assetsToSharesWithdraw : MPTokenIssuance sle"); + + XRPL_ASSERT(!assets.negative(), "xrpl::assetsToSharesDeposit : non-negative assets"); + XRPL_ASSERT(assets.asset() == vault->at(sfAsset), "xrpl::assetsToSharesWithdraw : assets and vault match"); + if (assets.negative() || assets.asset() != vault->at(sfAsset)) + return std::nullopt; // LCOV_EXCL_LINE + + Number assetTotal = vault->at(sfAssetsTotal); + assetTotal -= vault->at(sfLossUnrealized); + STAmount shares{vault->at(sfShareMPTID)}; + if (assetTotal == 0) + return shares; + Number const shareTotal = issuance->at(sfOutstandingAmount); + Number result = (shareTotal * assets) / assetTotal; + if (truncate == TruncateShares::yes) + result = result.truncate(); + shares = result; + return shares; +} + +[[nodiscard]] std::optional +sharesToAssetsWithdraw( + std::shared_ptr const& vault, + std::shared_ptr const& issuance, + STAmount const& shares) +{ + XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::sharesToAssetsWithdraw : Vault sle"); + XRPL_ASSERT( + issuance && issuance->getType() == ltMPTOKEN_ISSUANCE, "xrpl::sharesToAssetsWithdraw : MPTokenIssuance sle"); + + XRPL_ASSERT(!shares.negative(), "xrpl::sharesToAssetsDeposit : non-negative shares"); + XRPL_ASSERT(shares.asset() == vault->at(sfShareMPTID), "xrpl::sharesToAssetsWithdraw : shares and vault match"); + if (shares.negative() || shares.asset() != vault->at(sfShareMPTID)) + return std::nullopt; // LCOV_EXCL_LINE + + Number assetTotal = vault->at(sfAssetsTotal); + assetTotal -= vault->at(sfLossUnrealized); + STAmount assets{vault->at(sfAsset)}; + if (assetTotal == 0) + return assets; + Number const shareTotal = issuance->at(sfOutstandingAmount); + assets = (assetTotal * shares) / shareTotal; + return assets; +} + +[[nodiscard]] bool +isVaultInsolvent(std::shared_ptr const& vault, std::shared_ptr const& shareIssuance) +{ + XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::isVaultInsolvent : Vault sle"); + XRPL_ASSERT( + shareIssuance && shareIssuance->getType() == ltMPTOKEN_ISSUANCE, + "xrpl::isVaultInsolvent : MPTokenIssuance sle"); + + auto const assetsTotal = vault->at(sfAssetsTotal); + auto const sharesOutstanding = shareIssuance->at(sfOutstandingAmount); + + return assetsTotal == 0 && sharesOutstanding > 0; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/Vault/VaultSet.cpp b/src/libxrpl/tx/transactors/Vault/VaultSet.cpp index 17064977a7a..03bf3fc686d 100644 --- a/src/libxrpl/tx/transactors/Vault/VaultSet.cpp +++ b/src/libxrpl/tx/transactors/Vault/VaultSet.cpp @@ -34,13 +34,13 @@ VaultSet::getFlagsMask(PreflightContext const& ctx) static bool isValidVaultUpdate(PreflightContext const& ctx) { - auto const shouldCheckFlags = ctx.rules.enabled(fixLendingProtocolV1_1); - auto const atLeastOneFieldPresent = ctx.tx.isFieldPresent(sfDomainID) || ctx.tx.isFieldPresent(sfAssetsMaximum) || ctx.tx.isFieldPresent(sfData); - return atLeastOneFieldPresent || - (shouldCheckFlags && (ctx.tx.isFlag(tfVaultDepositBlock) || ctx.tx.isFlag(tfVaultDepositUnblock))); + auto const shouldCheckFlags = ctx.rules.enabled(fixLendingProtocolV1_1); + auto const expectedFlags = ~(VaultSet::getFlagsMask(ctx) | tfUniversal); + + return atLeastOneFieldPresent || (shouldCheckFlags && (ctx.tx.getFlags() & expectedFlags)); } NotTEC diff --git a/src/libxrpl/tx/transactors/Vault/VaultWithdraw.cpp b/src/libxrpl/tx/transactors/Vault/VaultWithdraw.cpp index 4d0a3c20a59..95a576052a5 100644 --- a/src/libxrpl/tx/transactors/Vault/VaultWithdraw.cpp +++ b/src/libxrpl/tx/transactors/Vault/VaultWithdraw.cpp @@ -1,3 +1,5 @@ +#include +// #include #include #include @@ -7,7 +9,6 @@ #include #include #include -#include namespace xrpl { From 872347224d1f2dfe83457f30f754e4c9ac969e41 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Tue, 24 Feb 2026 16:19:11 +0100 Subject: [PATCH 11/16] fixes broken unit-tests --- src/test/app/Vault_test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index f82e55677a4..8cc819c5073 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -1006,7 +1006,7 @@ class Vault_test : public beast::unit_test::suite { auto tx = vault.set({.owner = owner, .id = keylet.key}); - tx[sfFlags] = tfVaultPrivate; + tx[sfFlags] = tfVaultDepositBlock | tfVaultDepositUnblock; env(tx, ter(temINVALID_FLAG)); } }); From 2b716eb4c74020fab91393678435a9b53c137986 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Wed, 25 Feb 2026 12:44:56 +0100 Subject: [PATCH 12/16] adds lsfVaultOwnerCanBlockDeposit flag --- include/xrpl/protocol/LedgerFormats.h | 1 + include/xrpl/protocol/TxFlags.h | 16 +- .../tx/transactors/Vault/VaultCreate.cpp | 20 +- src/libxrpl/tx/transactors/Vault/VaultSet.cpp | 11 +- src/test/app/Vault_test.cpp | 344 ++++++++---------- 5 files changed, 188 insertions(+), 204 deletions(-) diff --git a/include/xrpl/protocol/LedgerFormats.h b/include/xrpl/protocol/LedgerFormats.h index 80ffaa5c6ef..adfbbe2c559 100644 --- a/include/xrpl/protocol/LedgerFormats.h +++ b/include/xrpl/protocol/LedgerFormats.h @@ -186,6 +186,7 @@ enum LedgerSpecificFlags { // ltVAULT lsfVaultPrivate = 0x00010000, lsfVaultDepositBlocked = 0x00020000, // True, vault deposit is blocked + lsfVaultOwnerCanBlockDeposit = 0x00040000, // True, vault owner can block deposit // ltLOAN lsfLoanDefault = 0x00010000, diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index e424f914aa1..a366518fe11 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -250,7 +250,15 @@ constexpr std::uint32_t tfBridgeModifyMask = ~(tfUniversal | tfClearAccountCreat constexpr std::uint32_t const tfVaultPrivate = 0x00010000; static_assert(tfVaultPrivate == lsfVaultPrivate); constexpr std::uint32_t const tfVaultShareNonTransferable = 0x00020000; -constexpr std::uint32_t const tfVaultCreateMask = ~(tfUniversal | tfVaultPrivate | tfVaultShareNonTransferable); +constexpr std::uint32_t const tfVaultOwnerCanBlockDeposit = 0x00040000; +static_assert(tfVaultOwnerCanBlockDeposit == lsfVaultOwnerCanBlockDeposit); + +constexpr std::uint32_t const tfVaultCreateMask = ~(tfUniversal | tfVaultPrivate | tfVaultShareNonTransferable | tfVaultOwnerCanBlockDeposit); + +// VaultSet flags: +constexpr std::uint32_t const tfVaultDepositBlock = 0x00010000; +constexpr std::uint32_t const tfVaultDepositUnblock = 0x00020000; +constexpr std::uint32_t const tfVaultSetMask = ~(tfUniversal | tfVaultDepositBlock | tfVaultDepositUnblock); // Batch Flags: constexpr std::uint32_t tfAllOrNothing = 0x00010000; @@ -290,12 +298,6 @@ constexpr std::uint32_t const tfLoanDefault = 0x00010000; constexpr std::uint32_t const tfLoanImpair = 0x00020000; constexpr std::uint32_t const tfLoanUnimpair = 0x00040000; constexpr std::uint32_t const tfLoanManageMask = ~(tfUniversal | tfLoanDefault | tfLoanImpair | tfLoanUnimpair); - -// VaultSet flags: -constexpr std::uint32_t const tfVaultDepositBlock = 0x00010000; -constexpr std::uint32_t const tfVaultDepositUnblock = 0x00020000; -constexpr std::uint32_t const tfVaultSetMask = ~(tfUniversal | tfVaultDepositBlock | tfVaultDepositUnblock); - // clang-format on } // namespace xrpl diff --git a/src/libxrpl/tx/transactors/Vault/VaultCreate.cpp b/src/libxrpl/tx/transactors/Vault/VaultCreate.cpp index d8f1335e90e..972a74c0551 100644 --- a/src/libxrpl/tx/transactors/Vault/VaultCreate.cpp +++ b/src/libxrpl/tx/transactors/Vault/VaultCreate.cpp @@ -32,7 +32,10 @@ VaultCreate::checkExtraFeatures(PreflightContext const& ctx) std::uint32_t VaultCreate::getFlagsMask(PreflightContext const& ctx) { - return tfVaultCreateMask; + if (ctx.rules.enabled(fixLendingProtocolV1_1)) + return tfVaultCreateMask; + + return tfVaultCreateMask | tfVaultOwnerCanBlockDeposit; } NotTEC @@ -155,11 +158,11 @@ VaultCreate::doApply() ? 0 : ctx_.tx[~sfScale].value_or(vaultDefaultIOUScale); - auto txFlags = tx.getFlags(); std::uint32_t mptFlags = 0; - if ((txFlags & tfVaultShareNonTransferable) == 0) + if (!tx.isFlag(tfVaultShareNonTransferable)) mptFlags |= (lsfMPTCanEscrow | lsfMPTCanTrade | lsfMPTCanTransfer); - if (txFlags & tfVaultPrivate) + + if (tx.isFlag(tfVaultPrivate)) mptFlags |= lsfMPTRequireAuth; // Note, here we are **not** creating an MPToken for the assets held in @@ -183,7 +186,12 @@ VaultCreate::doApply() auto const& mptIssuanceID = *maybeShare; vault->setFieldIssue(sfAsset, STIssue{sfAsset, asset}); - vault->at(sfFlags) = txFlags & tfVaultPrivate; + if (tx.isFlag(tfVaultPrivate)) + vault->setFlag(lsfVaultPrivate); + + if (view().rules().enabled(fixLendingProtocolV1_1) && tx.isFlag(tfVaultOwnerCanBlockDeposit)) + vault->setFlag(lsfVaultOwnerCanBlockDeposit); + vault->at(sfSequence) = sequence; vault->at(sfOwner) = account_; vault->at(sfAccount) = pseudoId; @@ -212,7 +220,7 @@ VaultCreate::doApply() return err; // If the vault is private, set the authorized flag for the vault owner - if (txFlags & tfVaultPrivate) + if (vault->isFlag(lsfVaultPrivate)) { if (auto const err = authorizeMPToken( view(), mPriorBalance, mptIssuanceID, pseudoId, ctx_.journal, {}, account_); diff --git a/src/libxrpl/tx/transactors/Vault/VaultSet.cpp b/src/libxrpl/tx/transactors/Vault/VaultSet.cpp index 6778df9ebfd..8ac905192cb 100644 --- a/src/libxrpl/tx/transactors/Vault/VaultSet.cpp +++ b/src/libxrpl/tx/transactors/Vault/VaultSet.cpp @@ -37,10 +37,9 @@ isValidVaultUpdate(PreflightContext const& ctx) auto const atLeastOneFieldPresent = ctx.tx.isFieldPresent(sfDomainID) || ctx.tx.isFieldPresent(sfAssetsMaximum) || ctx.tx.isFieldPresent(sfData); - auto const shouldCheckFlags = ctx.rules.enabled(fixLendingProtocolV1_1); auto const expectedFlags = ~(VaultSet::getFlagsMask(ctx) | tfUniversal); - return atLeastOneFieldPresent || (shouldCheckFlags && (ctx.tx.getFlags() & expectedFlags)); + return atLeastOneFieldPresent || (ctx.tx.getFlags() & expectedFlags); } NotTEC @@ -138,6 +137,14 @@ VaultSet::preclaim(PreclaimContext const& ctx) if (ctx.view.rules().enabled(fixLendingProtocolV1_1)) { + // The Vault does not configured to support deposit blocking + if (!vault->isFlag(lsfVaultOwnerCanBlockDeposit) && + (ctx.tx.isFlag(tfVaultDepositBlock) || ctx.tx.isFlag(tfVaultDepositUnblock))) + { + JLOG(ctx.j.debug()) << "VaultSet: vault does not support blocking deposits"; + return tecNO_PERMISSION; + } + if (vault->isFlag(lsfVaultDepositBlocked) && ctx.tx.isFlag(tfVaultDepositBlock)) { JLOG(ctx.j.debug()) << "VaultSet: vault deposit is already blocked"; diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 8cc819c5073..9d7c497ab0c 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -185,38 +185,6 @@ class Vault_test : public beast::unit_test::suite env.close(); } - { - testcase(prefix + " fail to unblock a non-blocked vault"); - auto const tx = - vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositUnblock}); - env(tx, ter(tecNO_PERMISSION)); - env.close(); - } - - { - testcase(prefix + " block a vault"); - auto const tx = - vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositBlock}); - env(tx, ter(tesSUCCESS)); - env.close(); - } - - { - testcase(prefix + " fail to block an already blocked vault"); - auto const tx = - vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositBlock}); - env(tx, ter(tecNO_PERMISSION)); - env.close(); - } - - { - testcase(prefix + " unblock a blocked vault"); - auto const tx = - vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositUnblock}); - env(tx, ter(tesSUCCESS)); - env.close(); - } - { testcase(prefix + " fail to withdraw more than assets held"); auto tx = vault.withdraw( @@ -693,6 +661,14 @@ class Vault_test : public beast::unit_test::suite tx[sfFlags] = tfClearDeepFreeze; env(tx, ter{temINVALID_FLAG}); + { + env.disableFeature(fixLendingProtocolV1_1); + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + tx[sfFlags] = tfVaultOwnerCanBlockDeposit; + env(tx, ter{temINVALID_FLAG}); + env.enableFeature(fixLendingProtocolV1_1); + } + { auto tx = vault.set({.owner = owner, .id = keylet.key}); tx[sfFlags] = tfClearDeepFreeze; @@ -998,19 +974,6 @@ class Vault_test : public beast::unit_test::suite } }); - testCase( - [&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) { - testcase("invalid set immutable flag"); - - auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); - - { - auto tx = vault.set({.owner = owner, .id = keylet.key}); - tx[sfFlags] = tfVaultDepositBlock | tfVaultDepositUnblock; - env(tx, ter(temINVALID_FLAG)); - } - }); - testCase( [&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) { testcase("invalid withdraw amount"); @@ -2372,77 +2335,6 @@ class Vault_test : public beast::unit_test::suite env(vault.del({.owner = owner, .id = keylet.key})); }); - testCase([&, this]( - Env& env, - Account const&, - Account const& owner, - Account const& depositor, - PrettyAsset const& asset, - Vault& vault, - MPTTester const& mptt) { - testcase("MPT lsfVaultDepositBlocked prevents deposits"); - auto const [tx, keylet] = vault.create({.owner = owner, .asset = asset}); - env(tx); - env.close(); - - // First deposit assets to later show that withdrawals are not blocked - { - auto const tx = - vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(20)}); - env(tx, ter{tesSUCCESS}, THISLINE); - env.close(); - } - - // Block Vault deposits - { - auto const tx = - vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositBlock}); - env(tx, ter(tesSUCCESS), THISLINE); - env.close(); - } - - { - auto const tx = - vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(20)}); - env(tx, ter{tecNO_PERMISSION}, THISLINE); - env.close(); - } - - { - auto tx = - vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(20)}); - env(tx, ter{tesSUCCESS}, THISLINE); - env.close(); - } - - // Unblock Vault Deposits - { - auto const tx = - vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositUnblock}); - env(tx, ter(tesSUCCESS), THISLINE); - env.close(); - } - - // Deposits now succeed - { - auto const tx = - vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(20)}); - env(tx, ter{tesSUCCESS}, THISLINE); - env.close(); - } - - // Withdraw assets from the vault to delete it - { - auto const tx = - vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(20)}); - env(tx, ter{tesSUCCESS}, THISLINE); - env.close(); - } - - env(vault.del({.owner = owner, .id = keylet.key})); - env.close(); - }); - testCase([&, this]( Env& env, Account const&, @@ -3242,78 +3134,6 @@ class Vault_test : public beast::unit_test::suite env.close(); }); - testCase([&, this]( - Env& env, - Account const& owner, - Account const& issuer, - Account const&, - auto vaultAccount, - Vault& vault, - PrettyAsset const& asset, - auto&&...) { - testcase("IOU lsfVaultDepositBlocked prevents deposits"); - auto const [tx, keylet] = vault.create({.owner = owner, .asset = asset}); - env(tx); - env.close(); - - // First deposit assets to later show that withdrawals are not blocked - { - auto const tx = - vault.deposit({.depositor = issuer, .id = keylet.key, .amount = asset(20)}); - env(tx, ter{tesSUCCESS}, THISLINE); - env.close(); - } - - // Block Vault deposits - { - auto const tx = - vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositBlock}); - env(tx, ter(tesSUCCESS), THISLINE); - env.close(); - } - - { - auto const tx = - vault.deposit({.depositor = issuer, .id = keylet.key, .amount = asset(20)}); - env(tx, ter{tecNO_PERMISSION}, THISLINE); - env.close(); - } - - { - auto tx = - vault.withdraw({.depositor = issuer, .id = keylet.key, .amount = asset(20)}); - env(tx, ter{tesSUCCESS}, THISLINE); - env.close(); - } - - // Unblock Vault Deposits - { - auto const tx = - vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositUnblock}); - env(tx, ter(tesSUCCESS), THISLINE); - env.close(); - } - - // Deposits now succeed - { - auto const tx = - vault.deposit({.depositor = issuer, .id = keylet.key, .amount = asset(20)}); - env(tx, ter{tesSUCCESS}, THISLINE); - env.close(); - } - - // Withdraw assets from the vault to delete it - { - auto const tx = - vault.withdraw({.depositor = issuer, .id = keylet.key, .amount = asset(20)}); - env(tx, ter{tesSUCCESS}, THISLINE); - env.close(); - } - - env(vault.del({.owner = owner, .id = keylet.key})); - env.close(); - }); - testCase([&, this]( Env& env, Account const& owner, @@ -3450,7 +3270,10 @@ class Vault_test : public beast::unit_test::suite env(pay(issuer, charlie, asset(5))); env.close(); - auto [tx, keylet] = vault.create({.owner = owner, .asset = asset, .flags = tfVaultPrivate}); + auto [tx, keylet] = vault.create( + {.owner = owner, + .asset = asset, + .flags = tfVaultPrivate | tfVaultOwnerCanBlockDeposit}); env(tx); env.close(); BEAST_EXPECT(env.le(keylet)); @@ -5871,6 +5694,148 @@ class Vault_test : public beast::unit_test::suite } } + void + testVaultDepositBlockGeneral() + { + using namespace test::jtx; + + Env env{*this}; + Account const owner{"owner"}; + Account const other{"other"}; + + env.fund(XRP(100'000'000), owner, other); + Vault vault{env}; + PrettyAsset const asset = xrpIssue(); + std::string const prefix = "VaultDepositBlock: "; + + auto const blockVault = [&](TER expectedTer, Keylet const& keylet) { + env(vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositBlock}), + ter(expectedTer), + THISLINE); + }; + + auto const unblockVault = [&](TER expectedTer, Keylet const& keylet) { + env(vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositUnblock}), + ter(expectedTer), + THISLINE); + }; + + // Blocking Vault with the amendment disabled fails + { + testcase(prefix + "block/unblock fails when amendment is disabled"); + + env.disableFeature(fixLendingProtocolV1_1); + auto const [tx, keylet] = vault.create( + {.owner = owner, .asset = asset, .flags = tfVaultOwnerCanBlockDeposit}); + env(tx, ter(temINVALID_FLAG), THISLINE); + env.close(); + + blockVault(temINVALID_FLAG, keylet); + unblockVault(temINVALID_FLAG, keylet); + + env.enableFeature(fixLendingProtocolV1_1); + } + + // Block Vault deposits fails if the vault is not configured to allow blocking deposits + { + testcase(prefix + "block/unblock fails when vault is not configured"); + auto const [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + env(tx); + env.close(); + + blockVault(tecNO_PERMISSION, keylet); + unblockVault(tecNO_PERMISSION, keylet); + + env(vault.del({.owner = owner, .id = keylet.key}), ter(tesSUCCESS), THISLINE); + env.close(); + } + + auto const [tx, keylet] = + vault.create({.owner = owner, .asset = asset, .flags = tfVaultOwnerCanBlockDeposit}); + env(tx); + env.close(); + + { + testcase(prefix + "block/unblock succeeds"); + // deposit assets to show that blocking deposit does not block withdrawals + env(vault.deposit({ + .depositor = owner, + .id = keylet.key, + .amount = XRP(10'000), + }), + ter(tesSUCCESS), + THISLINE); + + blockVault(tesSUCCESS, keylet); + + env(vault.deposit({ + .depositor = owner, + .id = keylet.key, + .amount = XRP(10'000), + }), + ter(tecNO_PERMISSION), + THISLINE); + + // Block vault withdrawal works as normal + env(vault.withdraw({ + .depositor = owner, + .id = keylet.key, + .amount = XRP(10'000), + }), + ter(tesSUCCESS), + THISLINE); + + unblockVault(tesSUCCESS, keylet); + + env(vault.deposit({ + .depositor = owner, + .id = keylet.key, + .amount = XRP(10'000), + }), + ter(tesSUCCESS), + THISLINE); + + // Withdraw to keep the vault empty + env(vault.withdraw({ + .depositor = owner, + .id = keylet.key, + .amount = XRP(10'000), + }), + ter(tesSUCCESS), + THISLINE); + } + + { + testcase(prefix + "block/unblock fails when caller is not owner"); + + env(vault.set({.owner = other, .id = keylet.key, .flags = tfVaultDepositBlock}), + ter(tecNO_PERMISSION), + THISLINE); + + blockVault(tesSUCCESS, keylet); + + env(vault.set({.owner = other, .id = keylet.key, .flags = tfVaultDepositUnblock}), + ter(tecNO_PERMISSION), + THISLINE); + + unblockVault(tesSUCCESS, keylet); + } + + { + testcase(prefix + "unblock fails when vault is already unblocked"); + unblockVault(tecNO_PERMISSION, keylet); + } + + { + testcase(prefix + "block fails when vault is already blocked"); + blockVault(tesSUCCESS, keylet); + blockVault(tecNO_PERMISSION, keylet); + unblockVault(tesSUCCESS, keylet); + } + + env(vault.del({.owner = owner, .id = keylet.key})); + } + public: void run() override @@ -5892,6 +5857,7 @@ class Vault_test : public beast::unit_test::suite testVaultClawbackBurnShares(); testVaultClawbackAssets(); testAssetsMaximum(); + testVaultDepositBlockGeneral(); } }; From ebfa65959389b12d16fcbcd4e30ef13c3ae2b002 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Wed, 4 Mar 2026 12:09:51 +0100 Subject: [PATCH 13/16] fixes formattting issues after merge --- include/xrpl/tx/transactors/Vault/VaultHelpers.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/include/xrpl/tx/transactors/Vault/VaultHelpers.h b/include/xrpl/tx/transactors/Vault/VaultHelpers.h index fd5f9b7b955..7489e9b443a 100644 --- a/include/xrpl/tx/transactors/Vault/VaultHelpers.h +++ b/include/xrpl/tx/transactors/Vault/VaultHelpers.h @@ -49,6 +49,8 @@ sharesToAssetsWithdraw( // Determine if a vault is insolvent. A vault is considered insolvent when // the total assets in the vault are zero, and outstanding shares are non-zero. [[nodiscard]] bool -isVaultInsolvent(std::shared_ptr const& vault, std::shared_ptr const& shareIssuance); +isVaultInsolvent( + std::shared_ptr const& vault, + std::shared_ptr const& shareIssuance); } // namespace xrpl From cc9dbe2243f644f35244d03044fa5171c4e6eef4 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Wed, 4 Mar 2026 13:03:51 +0100 Subject: [PATCH 14/16] fixes typos and improves test coverage --- .../tx/transactors/Vault/VaultDelete.cpp | 4 +- .../tx/transactors/Vault/VaultHelpers.cpp | 43 +++++++++++++------ src/libxrpl/tx/transactors/Vault/VaultSet.cpp | 3 +- src/test/app/Vault_test.cpp | 41 ++++++++++++++++++ 4 files changed, 74 insertions(+), 17 deletions(-) diff --git a/src/libxrpl/tx/transactors/Vault/VaultDelete.cpp b/src/libxrpl/tx/transactors/Vault/VaultDelete.cpp index bea335a4511..d1e09f97d9a 100644 --- a/src/libxrpl/tx/transactors/Vault/VaultDelete.cpp +++ b/src/libxrpl/tx/transactors/Vault/VaultDelete.cpp @@ -60,7 +60,7 @@ VaultDelete::preclaim(PreclaimContext const& ctx) if (!sleMPT) { // LCOV_EXCL_START - JLOG(ctx.j.error()) << "VaultDeposit: missing issuance of vault shares."; + JLOG(ctx.j.error()) << "VaultDelete: missing issuance of vault shares."; return tecOBJECT_NOT_FOUND; // LCOV_EXCL_STOP } @@ -68,7 +68,7 @@ VaultDelete::preclaim(PreclaimContext const& ctx) if (sleMPT->at(sfIssuer) != vault->getAccountID(sfAccount)) { // LCOV_EXCL_START - JLOG(ctx.j.error()) << "VaultDeposit: invalid owner of vault shares."; + JLOG(ctx.j.error()) << "VaultDelete: invalid owner of vault shares."; return tecNO_PERMISSION; // LCOV_EXCL_STOP } diff --git a/src/libxrpl/tx/transactors/Vault/VaultHelpers.cpp b/src/libxrpl/tx/transactors/Vault/VaultHelpers.cpp index 82b04cdbe52..7380b7f0a9b 100644 --- a/src/libxrpl/tx/transactors/Vault/VaultHelpers.cpp +++ b/src/libxrpl/tx/transactors/Vault/VaultHelpers.cpp @@ -10,17 +10,22 @@ assetsToSharesDeposit( { XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::assetsToSharesDeposit : Vault sle"); XRPL_ASSERT( - issuance && issuance->getType() == ltMPTOKEN_ISSUANCE, "xrpl::assetsToSharesDeposit : MPTokenIssuance sle"); + issuance && issuance->getType() == ltMPTOKEN_ISSUANCE, + "xrpl::assetsToSharesDeposit : MPTokenIssuance sle"); XRPL_ASSERT(!assets.negative(), "xrpl::assetsToSharesDeposit : non-negative assets"); - XRPL_ASSERT(assets.asset() == vault->at(sfAsset), "xrpl::assetsToSharesDeposit : assets and vault match"); + XRPL_ASSERT( + assets.asset() == vault->at(sfAsset), + "xrpl::assetsToSharesDeposit : assets and vault match"); if (assets.negative() || assets.asset() != vault->at(sfAsset)) return std::nullopt; // LCOV_EXCL_LINE Number const assetTotal = vault->at(sfAssetsTotal); STAmount shares{vault->at(sfShareMPTID)}; if (assetTotal == 0) - return STAmount{shares.asset(), Number(assets.mantissa(), assets.exponent() + vault->at(sfScale)).truncate()}; + return STAmount{ + shares.asset(), + Number(assets.mantissa(), assets.exponent() + vault->at(sfScale)).truncate()}; Number const shareTotal = issuance->at(sfOutstandingAmount); shares = ((shareTotal * assets) / assetTotal).truncate(); @@ -35,17 +40,21 @@ sharesToAssetsDeposit( { XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::sharesToAssetsDeposit : Vault sle"); XRPL_ASSERT( - issuance && issuance->getType() == ltMPTOKEN_ISSUANCE, "xrpl::sharesToAssetsDeposit : MPTokenIssuance sle"); + issuance && issuance->getType() == ltMPTOKEN_ISSUANCE, + "xrpl::sharesToAssetsDeposit : MPTokenIssuance sle"); XRPL_ASSERT(!shares.negative(), "xrpl::sharesToAssetsDeposit : non-negative shares"); - XRPL_ASSERT(shares.asset() == vault->at(sfShareMPTID), "xrpl::sharesToAssetsDeposit : shares and vault match"); + XRPL_ASSERT( + shares.asset() == vault->at(sfShareMPTID), + "xrpl::sharesToAssetsDeposit : shares and vault match"); if (shares.negative() || shares.asset() != vault->at(sfShareMPTID)) return std::nullopt; // LCOV_EXCL_LINE Number const assetTotal = vault->at(sfAssetsTotal); STAmount assets{vault->at(sfAsset)}; if (assetTotal == 0) - return STAmount{assets.asset(), shares.mantissa(), shares.exponent() - vault->at(sfScale), false}; + return STAmount{ + assets.asset(), shares.mantissa(), shares.exponent() - vault->at(sfScale), false}; Number const shareTotal = issuance->at(sfOutstandingAmount); assets = (assetTotal * shares) / shareTotal; @@ -61,10 +70,13 @@ assetsToSharesWithdraw( { XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::assetsToSharesWithdraw : Vault sle"); XRPL_ASSERT( - issuance && issuance->getType() == ltMPTOKEN_ISSUANCE, "xrpl::assetsToSharesWithdraw : MPTokenIssuance sle"); + issuance && issuance->getType() == ltMPTOKEN_ISSUANCE, + "xrpl::assetsToSharesWithdraw : MPTokenIssuance sle"); - XRPL_ASSERT(!assets.negative(), "xrpl::assetsToSharesDeposit : non-negative assets"); - XRPL_ASSERT(assets.asset() == vault->at(sfAsset), "xrpl::assetsToSharesWithdraw : assets and vault match"); + XRPL_ASSERT(!assets.negative(), "xrpl::assetsToSharesWithdraw : non-negative assets"); + XRPL_ASSERT( + assets.asset() == vault->at(sfAsset), + "xrpl::assetsToSharesWithdraw : assets and vault match"); if (assets.negative() || assets.asset() != vault->at(sfAsset)) return std::nullopt; // LCOV_EXCL_LINE @@ -89,10 +101,13 @@ sharesToAssetsWithdraw( { XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::sharesToAssetsWithdraw : Vault sle"); XRPL_ASSERT( - issuance && issuance->getType() == ltMPTOKEN_ISSUANCE, "xrpl::sharesToAssetsWithdraw : MPTokenIssuance sle"); + issuance && issuance->getType() == ltMPTOKEN_ISSUANCE, + "xrpl::sharesToAssetsWithdraw : MPTokenIssuance sle"); - XRPL_ASSERT(!shares.negative(), "xrpl::sharesToAssetsDeposit : non-negative shares"); - XRPL_ASSERT(shares.asset() == vault->at(sfShareMPTID), "xrpl::sharesToAssetsWithdraw : shares and vault match"); + XRPL_ASSERT(!shares.negative(), "xrpl::sharesToAssetsWithdraw : non-negative shares"); + XRPL_ASSERT( + shares.asset() == vault->at(sfShareMPTID), + "xrpl::sharesToAssetsWithdraw : shares and vault match"); if (shares.negative() || shares.asset() != vault->at(sfShareMPTID)) return std::nullopt; // LCOV_EXCL_LINE @@ -107,7 +122,9 @@ sharesToAssetsWithdraw( } [[nodiscard]] bool -isVaultInsolvent(std::shared_ptr const& vault, std::shared_ptr const& shareIssuance) +isVaultInsolvent( + std::shared_ptr const& vault, + std::shared_ptr const& shareIssuance) { XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::isVaultInsolvent : Vault sle"); XRPL_ASSERT( diff --git a/src/libxrpl/tx/transactors/Vault/VaultSet.cpp b/src/libxrpl/tx/transactors/Vault/VaultSet.cpp index 8ac905192cb..54024dcb529 100644 --- a/src/libxrpl/tx/transactors/Vault/VaultSet.cpp +++ b/src/libxrpl/tx/transactors/Vault/VaultSet.cpp @@ -23,7 +23,6 @@ VaultSet::checkExtraFeatures(PreflightContext const& ctx) std::uint32_t VaultSet::getFlagsMask(PreflightContext const& ctx) { - // VaultSet mask is built assuming fixLendingProtocolV1_1 is enabled if (ctx.rules.enabled(fixLendingProtocolV1_1)) return tfVaultSetMask; @@ -137,7 +136,7 @@ VaultSet::preclaim(PreclaimContext const& ctx) if (ctx.view.rules().enabled(fixLendingProtocolV1_1)) { - // The Vault does not configured to support deposit blocking + // The Vault is not configured to support deposit blocking if (!vault->isFlag(lsfVaultOwnerCanBlockDeposit) && (ctx.tx.isFlag(tfVaultDepositBlock) || ctx.tx.isFlag(tfVaultDepositUnblock))) { diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 66101fc2525..88733d48342 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -5768,9 +5768,17 @@ class Vault_test : public beast::unit_test::suite }), ter(tesSUCCESS), THISLINE); + env(vault.deposit({ + .depositor = other, + .id = keylet.key, + .amount = XRP(10'000), + }), + ter(tesSUCCESS), + THISLINE); blockVault(tesSUCCESS, keylet); + // Owner is blocked from depositing to the vault env(vault.deposit({ .depositor = owner, .id = keylet.key, @@ -5779,6 +5787,15 @@ class Vault_test : public beast::unit_test::suite ter(tecNO_PERMISSION), THISLINE); + // Other accounts are also blocked from depositing to the vault + env(vault.deposit({ + .depositor = other, + .id = keylet.key, + .amount = XRP(10'000), + }), + ter(tecNO_PERMISSION), + THISLINE); + // Block vault withdrawal works as normal env(vault.withdraw({ .depositor = owner, @@ -5788,6 +5805,14 @@ class Vault_test : public beast::unit_test::suite ter(tesSUCCESS), THISLINE); + env(vault.withdraw({ + .depositor = other, + .id = keylet.key, + .amount = XRP(10'000), + }), + ter(tesSUCCESS), + THISLINE); + unblockVault(tesSUCCESS, keylet); env(vault.deposit({ @@ -5798,6 +5823,14 @@ class Vault_test : public beast::unit_test::suite ter(tesSUCCESS), THISLINE); + env(vault.deposit({ + .depositor = other, + .id = keylet.key, + .amount = XRP(10'000), + }), + ter(tesSUCCESS), + THISLINE); + // Withdraw to keep the vault empty env(vault.withdraw({ .depositor = owner, @@ -5806,6 +5839,14 @@ class Vault_test : public beast::unit_test::suite }), ter(tesSUCCESS), THISLINE); + + env(vault.withdraw({ + .depositor = other, + .id = keylet.key, + .amount = XRP(10'000), + }), + ter(tesSUCCESS), + THISLINE); } { From b970c66a372d62f6ff6196e1005e178176cb4c30 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Sat, 21 Mar 2026 17:43:08 +0100 Subject: [PATCH 15/16] fix: address PR review feedback --- .../transactions/VaultDelete.h | 37 ++++++ .../tx/transactors/vault/VaultCreate.cpp | 5 +- .../tx/transactors/vault/VaultDeposit.cpp | 4 +- src/libxrpl/tx/transactors/vault/VaultSet.cpp | 8 +- src/test/app/Vault_test.cpp | 106 ++++++++---------- .../transactions/VaultDeleteTests.cpp | 49 ++++++++ 6 files changed, 140 insertions(+), 69 deletions(-) diff --git a/include/xrpl/protocol_autogen/transactions/VaultDelete.h b/include/xrpl/protocol_autogen/transactions/VaultDelete.h index 89a4ef2a2fc..bc89ce2bb7f 100644 --- a/include/xrpl/protocol_autogen/transactions/VaultDelete.h +++ b/include/xrpl/protocol_autogen/transactions/VaultDelete.h @@ -57,6 +57,32 @@ class VaultDelete : public TransactionBase { return this->tx_->at(sfVaultID); } + + /** + * @brief Get sfMemoData (soeOPTIONAL) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getMemoData() const + { + if (hasMemoData()) + { + return this->tx_->at(sfMemoData); + } + return std::nullopt; + } + + /** + * @brief Check if sfMemoData is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasMemoData() const + { + return this->tx_->isFieldPresent(sfMemoData); + } }; /** @@ -112,6 +138,17 @@ class VaultDeleteBuilder : public TransactionBuilderBase return *this; } + /** + * @brief Set sfMemoData (soeOPTIONAL) + * @return Reference to this builder for method chaining. + */ + VaultDeleteBuilder& + setMemoData(std::decay_t const& value) + { + object_[sfMemoData] = value; + return *this; + } + /** * @brief Build and return the VaultDelete wrapper. * @param publicKey The public key for signing. diff --git a/src/libxrpl/tx/transactors/vault/VaultCreate.cpp b/src/libxrpl/tx/transactors/vault/VaultCreate.cpp index 28c38c43db3..36093582a60 100644 --- a/src/libxrpl/tx/transactors/vault/VaultCreate.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultCreate.cpp @@ -32,7 +32,7 @@ VaultCreate::checkExtraFeatures(PreflightContext const& ctx) std::uint32_t VaultCreate::getFlagsMask(PreflightContext const& ctx) { - if (ctx.rules.enabled(fixLendingProtocolV1_1)) + if (ctx.rules.enabled(featureLendingProtocolV1_1)) return tfVaultCreateMask; return tfVaultCreateMask | tfVaultOwnerCanBlockDeposit; @@ -193,7 +193,8 @@ VaultCreate::doApply() if (tx.isFlag(tfVaultPrivate)) vault->setFlag(lsfVaultPrivate); - if (view().rules().enabled(fixLendingProtocolV1_1) && tx.isFlag(tfVaultOwnerCanBlockDeposit)) + if (view().rules().enabled(featureLendingProtocolV1_1) && + tx.isFlag(tfVaultOwnerCanBlockDeposit)) vault->setFlag(lsfVaultOwnerCanBlockDeposit); vault->at(sfSequence) = sequence; diff --git a/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp b/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp index 20ca98aaa63..f4c4133404c 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(fixLendingProtocolV1_1)) + if (ctx.view.rules().enabled(featureLendingProtocolV1_1)) { // Perform these checks early to avoid unnecessary processing @@ -85,7 +85,7 @@ VaultDeposit::preclaim(PreclaimContext const& ctx) if (isVaultInsolvent(vault, sleShareIssuance)) { JLOG(ctx.j.debug()) << "VaultDeposit: Vault is insolvent, deposits are not allowed"; - return tecNO_PERMISSION; + return tecLOCKED; } if (vault->isFlag(lsfVaultDepositBlocked)) diff --git a/src/libxrpl/tx/transactors/vault/VaultSet.cpp b/src/libxrpl/tx/transactors/vault/VaultSet.cpp index 776fd22ae3b..41801436ed1 100644 --- a/src/libxrpl/tx/transactors/vault/VaultSet.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultSet.cpp @@ -23,7 +23,7 @@ VaultSet::checkExtraFeatures(PreflightContext const& ctx) std::uint32_t VaultSet::getFlagsMask(PreflightContext const& ctx) { - if (ctx.rules.enabled(fixLendingProtocolV1_1)) + if (ctx.rules.enabled(featureLendingProtocolV1_1)) return tfVaultSetMask; // Add tfVaultDepositBlock and tfVaultDepositUnblock flags to indicate they are disabled @@ -36,6 +36,8 @@ isValidVaultUpdate(PreflightContext const& ctx) auto const atLeastOneFieldPresent = ctx.tx.isFieldPresent(sfDomainID) || ctx.tx.isFieldPresent(sfAssetsMaximum) || ctx.tx.isFieldPresent(sfData); + // Mask of valid, non-universal flags: any bit set here means the + // transaction is requesting a meaningful flag change. auto const expectedFlags = ~(VaultSet::getFlagsMask(ctx) | tfUniversal); return atLeastOneFieldPresent || (ctx.tx.getFlags() & expectedFlags); @@ -134,7 +136,7 @@ VaultSet::preclaim(PreclaimContext const& ctx) } } - if (ctx.view.rules().enabled(fixLendingProtocolV1_1)) + if (ctx.view.rules().enabled(featureLendingProtocolV1_1)) { // The Vault is not configured to support deposit blocking if (!vault->isFlag(lsfVaultOwnerCanBlockDeposit) && @@ -214,7 +216,7 @@ VaultSet::doApply() view().update(sleIssuance); } - if (view().rules().enabled(fixLendingProtocolV1_1)) + if (view().rules().enabled(featureLendingProtocolV1_1)) { if (tx.isFlag(tfVaultDepositBlock)) vault->setFlag(lsfVaultDepositBlocked); diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index a65bad14e1d..19aaa803aec 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -669,11 +669,11 @@ class Vault_test : public beast::unit_test::suite env(tx, ter{temINVALID_FLAG}); { - env.disableFeature(fixLendingProtocolV1_1); + env.disableFeature(featureLendingProtocolV1_1); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); tx[sfFlags] = tfVaultOwnerCanBlockDeposit; env(tx, ter{temINVALID_FLAG}); - env.enableFeature(fixLendingProtocolV1_1); + env.enableFeature(featureLendingProtocolV1_1); } { @@ -1067,18 +1067,18 @@ class Vault_test : public beast::unit_test::suite testCase( [&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) { - testcase("set flags fail without fixLendingProtocolV1_1"); + testcase("set flags fail without featureLendingProtocolV1_1"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); { - env.disableFeature(fixLendingProtocolV1_1); + env.disableFeature(featureLendingProtocolV1_1); env(vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositBlock}), ter(temINVALID_FLAG)); env(vault.set( {.owner = owner, .id = keylet.key, .flags = tfVaultDepositUnblock}), ter(temINVALID_FLAG)); - env.enableFeature(fixLendingProtocolV1_1); + env.enableFeature(featureLendingProtocolV1_1); } }); @@ -2371,7 +2371,7 @@ class Vault_test : public beast::unit_test::suite { auto const tx = vault.deposit( {.depositor = depositor, .id = vaultKeylet.key, .amount = depositAmount}); - env(tx, ter{tesSUCCESS}, THISLINE); + env(tx, ter{tesSUCCESS}); env.close(); } @@ -2383,7 +2383,7 @@ class Vault_test : public beast::unit_test::suite using namespace loanBroker; using namespace loan; - env(set(owner, vaultKeylet.key), THISLINE); + env(set(owner, vaultKeylet.key)); env.close(); // Create a simple Loan for the full amount of Vault assets @@ -2393,11 +2393,10 @@ class Vault_test : public beast::unit_test::suite paymentTotal(1), sig(sfCounterpartySignature, owner), fee(env.current()->fees().base * 2), - ter(tesSUCCESS), - THISLINE); + ter(tesSUCCESS)); env.close(std::chrono::seconds{120 + 60}); - env(manage(owner, loanKeylet.key, tfLoanDefault), ter(tesSUCCESS), THISLINE); + env(manage(owner, loanKeylet.key, tfLoanDefault), ter(tesSUCCESS)); env.close(); auto const sleVault = env.le(vaultKeylet); @@ -2424,7 +2423,7 @@ class Vault_test : public beast::unit_test::suite { auto const tx = vault.deposit( {.depositor = depositor, .id = vaultKeylet.key, .amount = asset(20)}); - env(tx, ter{tecNO_PERMISSION}, THISLINE); + env(tx, ter{tecNO_PERMISSION}); env.close(); } @@ -2440,14 +2439,13 @@ class Vault_test : public beast::unit_test::suite .id = vaultKeylet.key, .holder = depositor, .amount = share(0)}), - ter(tesSUCCESS), - THISLINE); + ter(tesSUCCESS)); env.close(); } { - env(loan::del(owner, loanKeylet.key), ter(tesSUCCESS), THISLINE); - env(loanBroker::del(owner, brokerKeylet.key), ter(tesSUCCESS), THISLINE); + env(loan::del(owner, loanKeylet.key), ter(tesSUCCESS)); + env(loanBroker::del(owner, brokerKeylet.key), ter(tesSUCCESS)); env(vault.del({.owner = owner, .id = vaultKeylet.key})); env.close(); } @@ -3173,7 +3171,7 @@ class Vault_test : public beast::unit_test::suite { auto const tx = vault.deposit( {.depositor = issuer, .id = vaultKeylet.key, .amount = depositAmount}); - env(tx, ter{tesSUCCESS}, THISLINE); + env(tx, ter{tesSUCCESS}); env.close(); } @@ -3185,7 +3183,7 @@ class Vault_test : public beast::unit_test::suite using namespace loanBroker; using namespace loan; - env(set(owner, vaultKeylet.key), ter{tesSUCCESS}, THISLINE); + env(set(owner, vaultKeylet.key), ter{tesSUCCESS}); env.close(); // Create a simple Loan for the full amount of Vault assets @@ -3195,11 +3193,10 @@ class Vault_test : public beast::unit_test::suite paymentTotal(1), sig(sfCounterpartySignature, owner), fee(env.current()->fees().base * 2), - ter{tesSUCCESS}, - THISLINE); + ter{tesSUCCESS}); env.close(std::chrono::seconds{120 + 60}); - env(manage(owner, loanKeylet.key, tfLoanDefault), ter(tesSUCCESS), THISLINE); + env(manage(owner, loanKeylet.key, tfLoanDefault), ter(tesSUCCESS)); env.close(); auto const sleVault = env.le(vaultKeylet); @@ -3226,7 +3223,7 @@ class Vault_test : public beast::unit_test::suite { auto const tx = vault.deposit( {.depositor = issuer, .id = vaultKeylet.key, .amount = asset(20)}); - env(tx, ter{tecNO_PERMISSION}, THISLINE); + env(tx, ter{tecNO_PERMISSION}); env.close(); } @@ -3242,14 +3239,13 @@ class Vault_test : public beast::unit_test::suite .id = vaultKeylet.key, .holder = issuer, .amount = share(0)}), - ter(tesSUCCESS), - THISLINE); + ter(tesSUCCESS)); env.close(); } { - env(loan::del(owner, loanKeylet.key), ter(tesSUCCESS), THISLINE); - env(loanBroker::del(owner, brokerKeylet.key), ter(tesSUCCESS), THISLINE); + env(loan::del(owner, loanKeylet.key), ter(tesSUCCESS)); + env(loanBroker::del(owner, brokerKeylet.key), ter(tesSUCCESS)); env(vault.del({.owner = owner, .id = vaultKeylet.key})); env.close(); } @@ -5585,30 +5581,28 @@ class Vault_test : public beast::unit_test::suite auto const blockVault = [&](TER expectedTer, Keylet const& keylet) { env(vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositBlock}), - ter(expectedTer), - THISLINE); + ter(expectedTer)); }; auto const unblockVault = [&](TER expectedTer, Keylet const& keylet) { env(vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositUnblock}), - ter(expectedTer), - THISLINE); + ter(expectedTer)); }; // Blocking Vault with the amendment disabled fails { testcase(prefix + "block/unblock fails when amendment is disabled"); - env.disableFeature(fixLendingProtocolV1_1); + env.disableFeature(featureLendingProtocolV1_1); auto const [tx, keylet] = vault.create( {.owner = owner, .asset = asset, .flags = tfVaultOwnerCanBlockDeposit}); - env(tx, ter(temINVALID_FLAG), THISLINE); + env(tx, ter(temINVALID_FLAG)); env.close(); blockVault(temINVALID_FLAG, keylet); unblockVault(temINVALID_FLAG, keylet); - env.enableFeature(fixLendingProtocolV1_1); + env.enableFeature(featureLendingProtocolV1_1); } // Block Vault deposits fails if the vault is not configured to allow blocking deposits @@ -5621,7 +5615,7 @@ class Vault_test : public beast::unit_test::suite blockVault(tecNO_PERMISSION, keylet); unblockVault(tecNO_PERMISSION, keylet); - env(vault.del({.owner = owner, .id = keylet.key}), ter(tesSUCCESS), THISLINE); + env(vault.del({.owner = owner, .id = keylet.key}), ter(tesSUCCESS)); env.close(); } @@ -5638,15 +5632,13 @@ class Vault_test : public beast::unit_test::suite .id = keylet.key, .amount = XRP(10'000), }), - ter(tesSUCCESS), - THISLINE); + ter(tesSUCCESS)); env(vault.deposit({ .depositor = other, .id = keylet.key, .amount = XRP(10'000), }), - ter(tesSUCCESS), - THISLINE); + ter(tesSUCCESS)); blockVault(tesSUCCESS, keylet); @@ -5656,8 +5648,7 @@ class Vault_test : public beast::unit_test::suite .id = keylet.key, .amount = XRP(10'000), }), - ter(tecNO_PERMISSION), - THISLINE); + ter(tecNO_PERMISSION)); // Other accounts are also blocked from depositing to the vault env(vault.deposit({ @@ -5665,8 +5656,7 @@ class Vault_test : public beast::unit_test::suite .id = keylet.key, .amount = XRP(10'000), }), - ter(tecNO_PERMISSION), - THISLINE); + ter(tecNO_PERMISSION)); // Block vault withdrawal works as normal env(vault.withdraw({ @@ -5674,16 +5664,14 @@ class Vault_test : public beast::unit_test::suite .id = keylet.key, .amount = XRP(10'000), }), - ter(tesSUCCESS), - THISLINE); + ter(tesSUCCESS)); env(vault.withdraw({ .depositor = other, .id = keylet.key, .amount = XRP(10'000), }), - ter(tesSUCCESS), - THISLINE); + ter(tesSUCCESS)); unblockVault(tesSUCCESS, keylet); @@ -5692,16 +5680,14 @@ class Vault_test : public beast::unit_test::suite .id = keylet.key, .amount = XRP(10'000), }), - ter(tesSUCCESS), - THISLINE); + ter(tesSUCCESS)); env(vault.deposit({ .depositor = other, .id = keylet.key, .amount = XRP(10'000), }), - ter(tesSUCCESS), - THISLINE); + ter(tesSUCCESS)); // Withdraw to keep the vault empty env(vault.withdraw({ @@ -5709,30 +5695,26 @@ class Vault_test : public beast::unit_test::suite .id = keylet.key, .amount = XRP(10'000), }), - ter(tesSUCCESS), - THISLINE); + ter(tesSUCCESS)); env(vault.withdraw({ .depositor = other, .id = keylet.key, .amount = XRP(10'000), }), - ter(tesSUCCESS), - THISLINE); + ter(tesSUCCESS)); } { testcase(prefix + "block/unblock fails when caller is not owner"); env(vault.set({.owner = other, .id = keylet.key, .flags = tfVaultDepositBlock}), - ter(tecNO_PERMISSION), - THISLINE); + ter(tecNO_PERMISSION)); blockVault(tesSUCCESS, keylet); env(vault.set({.owner = other, .id = keylet.key, .flags = tfVaultDepositUnblock}), - ter(tecNO_PERMISSION), - THISLINE); + ter(tecNO_PERMISSION)); unblockVault(tesSUCCESS, keylet); } @@ -5772,7 +5754,7 @@ class Vault_test : public beast::unit_test::suite testcase("VaultDelete data featureLendingProtocolV1_1 disabled"); env.disableFeature(featureLendingProtocolV1_1); delTx[sfMemoData] = strHex(std::string(maxDataPayloadLength, 'A')); - env(delTx, ter(temDISABLED), THISLINE); + env(delTx, ter(temDISABLED)); env.close(); env.enableFeature(featureLendingProtocolV1_1); } @@ -5781,7 +5763,7 @@ class Vault_test : public beast::unit_test::suite { testcase("VaultDelete data featureLendingProtocolV1_1 enabled data too large"); delTx[sfMemoData] = strHex(std::string(maxDataPayloadLength + 1, 'A')); - env(delTx, ter(temMALFORMED), THISLINE); + env(delTx, ter(temMALFORMED)); env.close(); } @@ -5789,7 +5771,7 @@ class Vault_test : public beast::unit_test::suite { testcase("VaultDelete data featureLendingProtocolV1_1 enabled data empty"); delTx[sfMemoData] = strHex(std::string(0, 'A')); - env(delTx, ter(temMALFORMED), THISLINE); + env(delTx, ter(temMALFORMED)); env.close(); } @@ -5797,12 +5779,12 @@ class Vault_test : public beast::unit_test::suite testcase("VaultDelete data featureLendingProtocolV1_1 enabled data valid"); PrettyAsset const xrpAsset = xrpIssue(); auto [tx, keylet] = vault.create({.owner = owner, .asset = xrpAsset}); - env(tx, ter(tesSUCCESS), THISLINE); + env(tx, ter(tesSUCCESS)); env.close(); // Recreate the transaction as the vault keylet changed auto delTx = vault.del({.owner = owner, .id = keylet.key}); delTx[sfMemoData] = strHex(std::string(maxDataPayloadLength, 'A')); - env(delTx, ter(tesSUCCESS), THISLINE); + env(delTx, ter(tesSUCCESS)); env.close(); } } diff --git a/src/tests/libxrpl/protocol_autogen/transactions/VaultDeleteTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/VaultDeleteTests.cpp index 24c89d249a5..2b4ec486a65 100644 --- a/src/tests/libxrpl/protocol_autogen/transactions/VaultDeleteTests.cpp +++ b/src/tests/libxrpl/protocol_autogen/transactions/VaultDeleteTests.cpp @@ -30,6 +30,7 @@ TEST(TransactionsVaultDeleteTests, BuilderSettersRoundTrip) // Transaction-specific field values auto const vaultIDValue = canonical_UINT256(); + auto const memoDataValue = canonical_VL(); VaultDeleteBuilder builder{ accountValue, @@ -39,6 +40,7 @@ TEST(TransactionsVaultDeleteTests, BuilderSettersRoundTrip) }; // Set optional fields + builder.setMemoData(memoDataValue); auto tx = builder.build(publicKey, secretKey); @@ -62,6 +64,14 @@ TEST(TransactionsVaultDeleteTests, BuilderSettersRoundTrip) } // Verify optional fields + { + auto const& expected = memoDataValue; + auto const actualOpt = tx.getMemoData(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfMemoData should be present"; + expectEqualField(expected, *actualOpt, "sfMemoData"); + EXPECT_TRUE(tx.hasMemoData()); + } + } // 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper, @@ -79,6 +89,7 @@ TEST(TransactionsVaultDeleteTests, BuilderFromStTxRoundTrip) // Transaction-specific field values auto const vaultIDValue = canonical_UINT256(); + auto const memoDataValue = canonical_VL(); // Build an initial transaction VaultDeleteBuilder initialBuilder{ @@ -88,6 +99,7 @@ TEST(TransactionsVaultDeleteTests, BuilderFromStTxRoundTrip) feeValue }; + initialBuilder.setMemoData(memoDataValue); auto initialTx = initialBuilder.build(publicKey, secretKey); @@ -112,6 +124,13 @@ TEST(TransactionsVaultDeleteTests, BuilderFromStTxRoundTrip) } // Verify optional fields + { + auto const& expected = memoDataValue; + auto const actualOpt = rebuiltTx.getMemoData(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfMemoData should be present"; + expectEqualField(expected, *actualOpt, "sfMemoData"); + } + } // 3) Verify wrapper throws when constructed from wrong transaction type. @@ -142,5 +161,35 @@ TEST(TransactionsVaultDeleteTests, BuilderThrowsOnWrongTxType) EXPECT_THROW(VaultDeleteBuilder{wrongTx.getSTTx()}, std::runtime_error); } +// 5) Build with only required fields and verify optional fields return nullopt. +TEST(TransactionsVaultDeleteTests, OptionalFieldsReturnNullopt) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::secp256k1, generateSeed("testVaultDeleteNullopt")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 3; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific required field values + auto const vaultIDValue = canonical_UINT256(); + + VaultDeleteBuilder builder{ + accountValue, + vaultIDValue, + sequenceValue, + feeValue + }; + + // Do NOT set optional fields + + auto tx = builder.build(publicKey, secretKey); + + // Verify optional fields are not present + EXPECT_FALSE(tx.hasMemoData()); + EXPECT_FALSE(tx.getMemoData().has_value()); +} } From 5fe99dc2ae430c475525d7053b222a391091b242 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Mon, 23 Mar 2026 13:48:30 +0100 Subject: [PATCH 16/16] fixes failing unit-tests --- src/test/app/Vault_test.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 19aaa803aec..65a6053e1a0 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -2423,7 +2423,7 @@ class Vault_test : public beast::unit_test::suite { auto const tx = vault.deposit( {.depositor = depositor, .id = vaultKeylet.key, .amount = asset(20)}); - env(tx, ter{tecNO_PERMISSION}); + env(tx, ter{tecLOCKED}); env.close(); } @@ -3223,7 +3223,7 @@ class Vault_test : public beast::unit_test::suite { auto const tx = vault.deposit( {.depositor = issuer, .id = vaultKeylet.key, .amount = asset(20)}); - env(tx, ter{tecNO_PERMISSION}); + env(tx, ter{tecLOCKED}); env.close(); }