diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index 9a4e00e3d6..a1c18f2827 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 5d31c674c0..5b29ce68a2 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 @@ public: testVaultClawbackBurnShares(); testVaultClawbackAssets(); testAssetsMaximum(); + testVaultSetVaultDepositFlagValidation(); } }; diff --git a/src/test/jtx/impl/vault.cpp b/src/test/jtx/impl/vault.cpp index 90250aece0..f096187b13 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 65a5706354..4b33c9231d 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 c0aaf5bac6..8042c81f5a 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 1e8a15291e..0e41ac5919 100644 --- a/src/xrpld/app/tx/detail/VaultSet.h +++ b/src/xrpld/app/tx/detail/VaultSet.h @@ -13,6 +13,9 @@ public: { } + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static bool checkExtraFeatures(PreflightContext const& ctx);