diff --git a/include/xrpl/ledger/helpers/VaultHelpers.h b/include/xrpl/ledger/helpers/VaultHelpers.h index 8aef30aa27b..ea19aa3dab3 100644 --- a/include/xrpl/ledger/helpers/VaultHelpers.h +++ b/include/xrpl/ledger/helpers/VaultHelpers.h @@ -78,4 +78,17 @@ 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. + + @param vault The vault SLE. + @param shareIssuance The MPTokenIssuance SLE for the vault's shares. + + @return True if the vault is insolvent, false otherwise. +*/ +[[nodiscard]] bool +isVaultInsolvent( + std::shared_ptr const& vault, + std::shared_ptr const& shareIssuance); + } // namespace xrpl diff --git a/include/xrpl/protocol/LedgerFormats.h b/include/xrpl/protocol/LedgerFormats.h index a43f6a71340..ff6557d27e9 100644 --- a/include/xrpl/protocol/LedgerFormats.h +++ b/include/xrpl/protocol/LedgerFormats.h @@ -193,7 +193,9 @@ enum LedgerEntryType : std::uint16_t { LSF_FLAG(lsfAccepted, 0x00010000)) \ \ LEDGER_OBJECT(Vault, \ - LSF_FLAG(lsfVaultPrivate, 0x00010000)) \ + LSF_FLAG(lsfVaultPrivate, 0x00010000) \ + LSF_FLAG(lsfVaultDepositBlocked, 0x00020000) /* True, vault deposit is blocked */ \ + LSF_FLAG(lsfVaultOwnerCanBlockDeposit, 0x00040000)) /* True, vault owner can block deposit */ \ \ LEDGER_OBJECT(Loan, \ LSF_FLAG(lsfLoanDefault, 0x00010000) \ diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index 7c2085109ff..ff278682135 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -183,7 +183,13 @@ inline constexpr FlagValue tfUniversalMask = ~tfUniversal; \ TRANSACTION(VaultCreate, \ TF_FLAG(tfVaultPrivate, lsfVaultPrivate) \ - TF_FLAG(tfVaultShareNonTransferable, 0x00020000), \ + TF_FLAG(tfVaultShareNonTransferable, 0x00020000) \ + TF_FLAG(tfVaultOwnerCanBlockDeposit, lsfVaultOwnerCanBlockDeposit), \ + MASK_ADJ(0)) \ + \ + TRANSACTION(VaultSet, \ + TF_FLAG(tfVaultDepositBlock, 0x00010000) \ + TF_FLAG(tfVaultDepositUnblock, 0x00020000), \ MASK_ADJ(0)) \ \ TRANSACTION(Batch, \ @@ -213,7 +219,6 @@ inline constexpr FlagValue tfUniversalMask = ~tfUniversal; TF_FLAG(tfLoanImpair, 0x00020000) \ TF_FLAG(tfLoanUnimpair, 0x00040000), \ MASK_ADJ(0)) - // clang-format on // Create all the flag values. diff --git a/include/xrpl/tx/transactors/vault/VaultClawback.h b/include/xrpl/tx/transactors/vault/VaultClawback.h index 131a1d87e78..8db661499c6 100644 --- a/include/xrpl/tx/transactors/vault/VaultClawback.h +++ b/include/xrpl/tx/transactors/vault/VaultClawback.h @@ -1,5 +1,6 @@ #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..f06f1b24323 100644 --- a/include/xrpl/tx/transactors/vault/VaultDeposit.h +++ b/include/xrpl/tx/transactors/vault/VaultDeposit.h @@ -1,5 +1,6 @@ #pragma once +#include #include namespace xrpl { diff --git a/include/xrpl/tx/transactors/vault/VaultSet.h b/include/xrpl/tx/transactors/vault/VaultSet.h index fb69f132b1e..bb12c0f2f90 100644 --- a/include/xrpl/tx/transactors/vault/VaultSet.h +++ b/include/xrpl/tx/transactors/vault/VaultSet.h @@ -13,6 +13,9 @@ class VaultSet : public Transactor { } + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static bool checkExtraFeatures(PreflightContext const& ctx); diff --git a/include/xrpl/tx/transactors/vault/VaultWithdraw.h b/include/xrpl/tx/transactors/vault/VaultWithdraw.h index ffe14a71411..009ff0557cd 100644 --- a/include/xrpl/tx/transactors/vault/VaultWithdraw.h +++ b/include/xrpl/tx/transactors/vault/VaultWithdraw.h @@ -1,5 +1,6 @@ #pragma once +#include #include namespace xrpl { diff --git a/src/libxrpl/ledger/helpers/VaultHelpers.cpp b/src/libxrpl/ledger/helpers/VaultHelpers.cpp index 83a1b9fc4f6..0861443fbb8 100644 --- a/src/libxrpl/ledger/helpers/VaultHelpers.cpp +++ b/src/libxrpl/ledger/helpers/VaultHelpers.cpp @@ -109,4 +109,20 @@ sharesToAssetsWithdraw( 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/VaultClawback.cpp b/src/libxrpl/tx/transactors/vault/VaultClawback.cpp index a650aed310a..4babf888e21 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 @@ -10,7 +11,6 @@ #include #include #include -#include #include @@ -51,9 +51,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)}; } diff --git a/src/libxrpl/tx/transactors/vault/VaultCreate.cpp b/src/libxrpl/tx/transactors/vault/VaultCreate.cpp index 02f8ecb57ba..1213e7ea11d 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 @@ -15,7 +17,6 @@ #include #include #include -#include namespace xrpl { @@ -34,7 +35,10 @@ VaultCreate::checkExtraFeatures(PreflightContext const& ctx) std::uint32_t VaultCreate::getFlagsMask(PreflightContext const& ctx) { - return tfVaultCreateMask; + if (ctx.rules.enabled(featureLendingProtocolV1_1)) + return tfVaultCreateMask; + + return tfVaultCreateMask | tfVaultOwnerCanBlockDeposit; } NotTEC @@ -161,11 +165,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) != 0u) + + if (tx.isFlag(tfVaultPrivate)) mptFlags |= lsfMPTRequireAuth; // Note, here we are **not** creating an MPToken for the assets held in @@ -189,7 +193,13 @@ 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(featureLendingProtocolV1_1) && + tx.isFlag(tfVaultOwnerCanBlockDeposit)) + vault->setFlag(lsfVaultOwnerCanBlockDeposit); + vault->at(sfSequence) = sequence; vault->at(sfOwner) = account_; vault->at(sfAccount) = pseudoId; @@ -222,7 +232,7 @@ VaultCreate::doApply() return err; // If the vault is private, set the authorized flag for the vault owner - if ((txFlags & tfVaultPrivate) != 0u) + if (vault->isFlag(lsfVaultPrivate)) { if (auto const err = authorizeMPToken( view(), preFeeBalance_, mptIssuanceID, pseudoId, ctx_.journal, {}, account_); diff --git a/src/libxrpl/tx/transactors/vault/VaultDelete.cpp b/src/libxrpl/tx/transactors/vault/VaultDelete.cpp index 2c6725172c9..922a2603f19 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 @@ -8,7 +10,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 04b249d2116..cbb1ff30fbd 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 @@ -13,7 +15,6 @@ #include #include #include -#include namespace xrpl { @@ -62,8 +63,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."; @@ -71,7 +72,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."; @@ -79,6 +80,24 @@ VaultDeposit::preclaim(PreclaimContext const& ctx) // LCOV_EXCL_STOP } + if (ctx.view.rules().enabled(featureLendingProtocolV1_1)) + { + // Perform these checks early to avoid unnecessary processing + + // The Vault is insolvent, deposits are not allowed + if (isVaultInsolvent(vault, sleShareIssuance)) + { + JLOG(ctx.j.debug()) << "VaultDeposit: Vault is insolvent, deposits are not allowed"; + return tecLOCKED; + } + + 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; @@ -89,7 +108,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 diff --git a/src/libxrpl/tx/transactors/vault/VaultSet.cpp b/src/libxrpl/tx/transactors/vault/VaultSet.cpp index b54389bcdeb..8a9a6361155 100644 --- a/src/libxrpl/tx/transactors/vault/VaultSet.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultSet.cpp @@ -17,6 +17,29 @@ VaultSet::checkExtraFeatures(PreflightContext const& ctx) return !ctx.tx.isFieldPresent(sfDomainID) || ctx.rules.enabled(featurePermissionedDomains); } +std::uint32_t +VaultSet::getFlagsMask(PreflightContext const& ctx) +{ + if (ctx.rules.enabled(featureLendingProtocolV1_1)) + return tfVaultSetMask; + + // Add tfVaultDepositBlock and tfVaultDepositUnblock flags to indicate they are disabled + return tfVaultSetMask | tfVaultDepositBlock | tfVaultDepositUnblock; +} + +static bool +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); +} + NotTEC VaultSet::preflight(PreflightContext const& ctx) { @@ -44,13 +67,19 @@ 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 temINVALID_FLAG; + } + return tesSUCCESS; } @@ -104,6 +133,29 @@ VaultSet::preclaim(PreclaimContext const& ctx) } } + if (ctx.view.rules().enabled(featureLendingProtocolV1_1)) + { + // The Vault is 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"; + 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; } @@ -161,6 +213,15 @@ VaultSet::doApply() view().update(sleIssuance); } + if (view().rules().enabled(featureLendingProtocolV1_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/libxrpl/tx/transactors/vault/VaultWithdraw.cpp b/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp index e4c671b4624..0c337fb1ef5 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 @@ -9,7 +11,6 @@ #include #include #include -#include namespace xrpl { diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 1cefda0d1e4..d95d2cadf13 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -669,6 +669,14 @@ class Vault_test : public beast::unit_test::suite tx[sfFlags] = tfClearDeepFreeze; env(tx, ter{temINVALID_FLAG}); + { + env.disableFeature(featureLendingProtocolV1_1); + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + tx[sfFlags] = tfVaultOwnerCanBlockDeposit; + env(tx, ter{temINVALID_FLAG}); + env.enableFeature(featureLendingProtocolV1_1); + } + { auto tx = vault.set({.owner = owner, .id = keylet.key}); tx[sfFlags] = tfClearDeepFreeze; @@ -976,13 +984,114 @@ 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("invalid withdraw amount"); + + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + + { + auto tx = vault.withdraw( + {.depositor = owner, .id = keylet.key, .amount = negativeAmount(asset)}); + env(tx, ter(temBAD_AMOUNT)); + } + + { + auto tx = + vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(0)}); + env(tx, ter(temBAD_AMOUNT)); + } + }); + + testCase( + [&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) { + testcase("set nothing updated"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); { auto tx = vault.set({.owner = owner, .id = keylet.key}); - tx[sfFlags] = tfVaultPrivate; + env(tx, ter{temMALFORMED}); + } + }); + + testCase( + [&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) { + testcase("create with invalid metadata"); + + auto [tx1, keylet] = vault.create({.owner = owner, .asset = asset}); + + { + auto tx = tx1; + tx[sfMPTokenMetadata] = ""; + env(tx, ter(temMALFORMED)); + } + + { + auto tx = tx1; + // This metadata is for the share token. + // A hexadecimal string of 1025 bytes. + tx[sfMPTokenMetadata] = std::string(2050, 'B'); + env(tx, ter(temMALFORMED)); + } + }); + + testCase( + [&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) { + testcase("set negative maximum"); + + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + + { + auto tx = vault.set({.owner = owner, .id = keylet.key}); + tx[sfAssetsMaximum] = negativeAmount(asset).number(); + env(tx, ter{temMALFORMED}); + } + }); + + testCase( + [&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) { + testcase("invalid deposit amount"); + + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + + { + auto tx = vault.deposit( + {.depositor = owner, .id = keylet.key, .amount = negativeAmount(asset)}); + env(tx, ter(temBAD_AMOUNT)); + } + + { + auto tx = + vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(0)}); + env(tx, ter(temBAD_AMOUNT)); + } + }); + + testCase( + [&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) { + testcase("set flags fail without featureLendingProtocolV1_1"); + + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + + { + 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(featureLendingProtocolV1_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] = tfVaultDepositBlock | tfVaultDepositUnblock; env(tx, ter(temINVALID_FLAG)); } }); @@ -2242,6 +2351,106 @@ 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("MPT 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}); + 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)); + 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)); + env.close(std::chrono::seconds{120 + 60}); + + env(manage(owner, loanKeylet.key, tfLoanDefault), ter(tesSUCCESS)); + env.close(); + + 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{tecLOCKED}); + 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)}), + ter(tesSUCCESS)); + env.close(); + } + + { + 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(); + } + }); } void @@ -2941,10 +3150,111 @@ 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("IOU 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}); + 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), ter{tesSUCCESS}); + 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}); + env.close(std::chrono::seconds{120 + 60}); + + env(manage(owner, loanKeylet.key, tfLoanDefault), ter(tesSUCCESS)); + env.close(); + + 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{tecLOCKED}); + 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)}), + ter(tesSUCCESS)); + env.close(); + } + + { + 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(); + } + }); } void - testWithDomainCheck() + testPrivateVault() { using namespace test::jtx; @@ -2975,7 +3285,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)); @@ -3000,6 +3313,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"); @@ -5233,12 +5568,179 @@ class Vault_test : public beast::unit_test::suite } void - testVaultDeleteData() + 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)); + }; + + auto const unblockVault = [&](TER expectedTer, Keylet const& keylet) { + env(vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositUnblock}), + ter(expectedTer)); + }; + + // Blocking Vault with the amendment disabled fails + { + testcase(prefix + "block/unblock fails when amendment is disabled"); + env.disableFeature(featureLendingProtocolV1_1); + auto const [tx, keylet] = vault.create( + {.owner = owner, .asset = asset, .flags = tfVaultOwnerCanBlockDeposit}); + env(tx, ter(temINVALID_FLAG)); + env.close(); + + blockVault(temINVALID_FLAG, keylet); + unblockVault(temINVALID_FLAG, keylet); + + env.enableFeature(featureLendingProtocolV1_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)); + 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)); + env(vault.deposit({ + .depositor = other, + .id = keylet.key, + .amount = XRP(10'000), + }), + ter(tesSUCCESS)); + + blockVault(tesSUCCESS, keylet); + + // Owner is blocked from depositing to the vault + env(vault.deposit({ + .depositor = owner, + .id = keylet.key, + .amount = XRP(10'000), + }), + ter(tecNO_PERMISSION)); + + // 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)); + + // Block vault withdrawal works as normal + env(vault.withdraw({ + .depositor = owner, + .id = keylet.key, + .amount = XRP(10'000), + }), + ter(tesSUCCESS)); + + env(vault.withdraw({ + .depositor = other, + .id = keylet.key, + .amount = XRP(10'000), + }), + ter(tesSUCCESS)); + + unblockVault(tesSUCCESS, keylet); + + env(vault.deposit({ + .depositor = owner, + .id = keylet.key, + .amount = XRP(10'000), + }), + ter(tesSUCCESS)); + + env(vault.deposit({ + .depositor = other, + .id = keylet.key, + .amount = XRP(10'000), + }), + ter(tesSUCCESS)); + + // Withdraw to keep the vault empty + env(vault.withdraw({ + .depositor = owner, + .id = keylet.key, + .amount = XRP(10'000), + }), + ter(tesSUCCESS)); + + env(vault.withdraw({ + .depositor = other, + .id = keylet.key, + .amount = XRP(10'000), + }), + 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)); + + blockVault(tesSUCCESS, keylet); + + env(vault.set({.owner = other, .id = keylet.key, .flags = tfVaultDepositUnblock}), + ter(tecNO_PERMISSION)); + + 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})); + } + + void + testVaultDeleteData() + { + using namespace test::jtx; + Env env{*this}; Account const owner{"owner"}; env.fund(XRP(1'000'000), owner); env.close(); @@ -5300,7 +5802,7 @@ class Vault_test : public beast::unit_test::suite testCreateFailMPT(); testWithMPT(); testWithIOU(); - testWithDomainCheck(); + testPrivateVault(); testWithDomainCheckXRP(); testNonTransferableShares(); testFailedPseudoAccount(); @@ -5309,6 +5811,7 @@ class Vault_test : public beast::unit_test::suite testVaultClawbackBurnShares(); testVaultClawbackAssets(); testAssetsMaximum(); + testVaultDepositBlockGeneral(); testVaultDeleteData(); } }; diff --git a/src/test/jtx/impl/vault.cpp b/src/test/jtx/impl/vault.cpp index 49c0dddaec8..149bf11613b 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 748d3341a54..4ea1a2a352d 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{}; }; static Json::Value