diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 79584f7a88..83d2c6e748 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -1288,6 +1288,72 @@ class Vault_test : public beast::unit_test::suite env(tx); }); + testCase([this]( + Env& env, + Account const& issuer, + Account const& owner, + Account const& depositor, + Asset const& asset, + Vault& vault, + MPTTester& mptt) { + testcase("MPT lock of vault pseudo-account"); + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + env(tx); + env.close(); + + auto const vaultAccount = + [&env, keylet = keylet, this]() -> AccountID { + auto const vault = env.le(keylet); + BEAST_EXPECT(vault != nullptr); + return vault->at(sfAccount); + }(); + + tx = vault.deposit( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(100)}); + env(tx); + env.close(); + + tx = [&]() { + Json::Value jv; + jv[jss::Account] = issuer.human(); + jv[sfMPTokenIssuanceID] = + to_string(asset.get().getMptID()); + jv[jss::Holder] = toBase58(vaultAccount); + jv[jss::TransactionType] = jss::MPTokenIssuanceSet; + jv[jss::Flags] = tfMPTLock; + return jv; + }(); + env(tx); + env.close(); + + tx = vault.deposit( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(100)}); + env(tx, ter(tecLOCKED)); + + tx = vault.withdraw( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(100)}); + env(tx, ter(tecLOCKED)); + + // Clawback works, even when locked + tx = vault.clawback( + {.issuer = issuer, + .id = keylet.key, + .holder = depositor, + .amount = asset(100)}); + env(tx); + + // Cannot delete an empty vault, because its shares are + // (transitively, by asset) locked. + tx = vault.del({.owner = owner, .id = keylet.key}); + env(tx, ter{tecNO_PERMISSION}); + }); + { testcase("MPT shares to a vault"); @@ -1627,6 +1693,7 @@ class Vault_test : public beast::unit_test::suite Account const charlie{"charlie"}; Vault vault{env}; env.fund(XRP(1000), issuer, owner, charlie); + env(fset(issuer, asfAllowTrustLineClawback)); env.close(); PrettyAsset const asset = issuer["IOU"]; @@ -1666,6 +1733,81 @@ class Vault_test : public beast::unit_test::suite }; BEAST_EXPECT(vaultBalance() == asset(0)); + { + testcase("IOU cannot update random trustline"); + PrettyAsset const foo = issuer["FOO"]; + + auto tx = [&]() { + Json::Value jv; + jv[jss::Account] = issuer.human(); + { + auto& ja = jv[jss::LimitAmount] = + foo(0).value().getJson(JsonOptions::none); + ja[jss::issuer] = toBase58(vaultAccount); + } + jv[jss::TransactionType] = jss::TrustSet; + jv[jss::Flags] = tfSetFreeze; + return jv; + }(); + env(tx, ter{tecNO_PERMISSION}); + env.close(); + } + + { + testcase("IOU cannot deposit when frozen"); + + env(vault.deposit( + {.depositor = owner, .id = keylet.key, .amount = asset(100)})); + env.close(); + + auto tx0 = [&]() { + Json::Value jv; + jv[jss::Account] = issuer.human(); + { + auto& ja = jv[jss::LimitAmount] = + asset(0).value().getJson(JsonOptions::none); + ja[jss::issuer] = toBase58(vaultAccount); + } + jv[jss::TransactionType] = jss::TrustSet; + jv[jss::Flags] = tfSetFreeze; + return jv; + }(); + env(tx0); + env.close(); + + // Note, the "frozen" state of the trust line is reported as + // "locked" state of the vault shares, because this state is + // attached to shares by means of the transitive isFrozen check. + env(vault.deposit( + {.depositor = owner, + .id = keylet.key, + .amount = asset(100)}), + ter{tecLOCKED}); + env.close(); + + // Clawback works, even when locked + auto tx1 = vault.clawback( + {.issuer = issuer, + .id = keylet.key, + .holder = owner, + .amount = asset(0)}); + env(tx1); + env.close(); + + // Cannot delete an empty vault, because its shares are + // (transitively, by asset) locked. + auto tx2 = vault.del({.owner = owner, .id = keylet.key}); + env(tx2, ter{tecNO_PERMISSION}); + env.close(); + + tx0[jss::Flags] = tfClearFreeze; + env(tx0); + env.close(); + + env(pay(issuer, owner, asset(100))); + env.close(); + } + { testcase("IOU zero fee on deposit"); env(vault.deposit( @@ -1757,7 +1899,7 @@ class Vault_test : public beast::unit_test::suite env.close(); auto tx = vault.withdraw( - {.depositor = owner, .id = keylet.key, .amount = asset(10)}); + {.depositor = owner, .id = keylet.key, .amount = asset(1)}); env(tx, ter{tecFROZEN}); tx[sfDestination] = issuer.human(); @@ -1768,6 +1910,21 @@ class Vault_test : public beast::unit_test::suite auto tx2 = test::jtx::pay(charlie, owner, STAmount{share, 10}); env(tx2, ter{tecLOCKED}); env.close(); + + // Clawback is permitted + auto tx3 = vault.clawback( + {.issuer = issuer, + .id = keylet.key, + .holder = owner, + .amount = asset(0)}); + env(tx3); + env.close(); + + // Cannot delete an empty vault, because its shares are + // (transitively, by asset) locked. + auto tx4 = vault.del({.owner = owner, .id = keylet.key}); + env(tx4, ter{tecNO_PERMISSION}); + env.close(); } } diff --git a/src/xrpld/app/tx/detail/SetTrust.cpp b/src/xrpld/app/tx/detail/SetTrust.cpp index f0340040bc..cd9bf6ea7e 100644 --- a/src/xrpld/app/tx/detail/SetTrust.cpp +++ b/src/xrpld/app/tx/detail/SetTrust.cpp @@ -25,6 +25,8 @@ #include #include #include +#include +#include namespace { @@ -228,16 +230,26 @@ SetTrust::preclaim(PreclaimContext const& ctx) } } - // Pseudo-accounts cannot receive trustlines, other than these native to - // their underlying ledger object - implemented in their respective - // transaction types. Note, this is not amendment-gated because all writes - // to pseudo-account discriminator fields **are** amendment gated, hence the - // behaviour of this check will always match the currently active - // amendments. - // The AMM destination is handled above, so exclude it from check here - // since AMM does allow trustline in certain conditions. - if (sleDst && !sleDst->isFieldPresent(sfAMMID) && isPseudoAccount(sleDst)) - return tecPSEUDO_ACCOUNT; + // If destination is Vault, allow changes to the existing trustline. + if (ctx.view.rules().enabled(featureSingleAssetVault)) + { + if (!sleDst) + return tecNO_DST; + + bool const isVault = sleDst->isFieldPresent(sfVaultID); + if (isVault && + !ctx.view.read(keylet::line(id, uDstAccountID, currency))) + return tecNO_PERMISSION; + else if (isVault) + { + if (ctx.view.read(keylet::vault(sleDst->getFieldH256(sfVaultID)))) + { + // Allow updating the state of existing trust line for the vault + } + else + return tecINTERNAL; + } + } // Checking all freeze/deep freeze flag invariants. if (ctx.view.rules().enabled(featureDeepFreeze)) diff --git a/src/xrpld/ledger/detail/View.cpp b/src/xrpld/ledger/detail/View.cpp index 65d5d5ccae..3f1dcabb4e 100644 --- a/src/xrpld/ledger/detail/View.cpp +++ b/src/xrpld/ledger/detail/View.cpp @@ -1344,6 +1344,17 @@ removeEmptyHolding( if (line->at(sfBalance)->iou() != beast::zero) return tecHAS_OBLIGATIONS; + if (view.rules().enabled(featureSingleAssetVault)) + { + // Check frozen flag, disallow delete if set + if (auto const issuer = view.read(keylet::account(issue.account)); + issuer && issuer->isFlag(lsfGlobalFreeze)) + return tecNO_PERMISSION; + if (line->isFlag( + (issue.account > accountID) ? lsfHighFreeze : lsfLowFreeze)) + return tecNO_PERMISSION; + } + // Adjust the owner count(s) if (line->isFlag(lsfLowReserve)) {