diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index d67b000a18..21e5be2d8c 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 d5fc0e4ad6..0ed722f408 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