Allow freezeing trust line to vault, disallow deleting vault when asset is locked

This commit is contained in:
Bronek Kozicki
2025-04-29 12:16:44 +01:00
parent 298aaac456
commit 071beca306
3 changed files with 191 additions and 11 deletions

View File

@@ -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<MPTIssue>().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();
}
}

View File

@@ -25,6 +25,8 @@
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Quality.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/TER.h>
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))

View File

@@ -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))
{