Both isFrozen and requireAuth are now recursive for vault shares

This commit is contained in:
Bronek Kozicki
2025-04-07 13:34:26 +01:00
parent 9c967f83be
commit db19760ee8
4 changed files with 126 additions and 20 deletions

View File

@@ -122,6 +122,8 @@ std::size_t constexpr maxDataPayloadLength = 256;
/** Vault withdrawal policies */ /** Vault withdrawal policies */
std::uint8_t constexpr vaultStrategyFirstComeFirstServe = 1; std::uint8_t constexpr vaultStrategyFirstComeFirstServe = 1;
std::uint8_t constexpr maxFreezeCheckDepth = 5;
/** A ledger index. */ /** A ledger index. */
using LedgerIndex = std::uint32_t; using LedgerIndex = std::uint32_t;

View File

@@ -78,6 +78,12 @@ class Vault_test : public beast::unit_test::suite
env.close(); env.close();
BEAST_EXPECT(env.le(keylet)); BEAST_EXPECT(env.le(keylet));
auto const share = [&env, keylet = keylet, this]() -> PrettyAsset {
auto const vault = env.le(keylet);
BEAST_EXPECT(vault != nullptr);
return MPTIssue(vault->at(sfShareMPTID));
}();
// Several 3rd party accounts which cannot receive funds // Several 3rd party accounts which cannot receive funds
Account alice{"alice"}; Account alice{"alice"};
Account dave{"dave"}; Account dave{"dave"};
@@ -352,6 +358,36 @@ class Vault_test : public beast::unit_test::suite
ter{asset.raw().holds<Issue>() ? tecNO_LINE : tecNO_AUTH}); ter{asset.raw().holds<Issue>() ? tecNO_LINE : tecNO_AUTH});
} }
if (!asset.raw().native() && asset.raw().holds<Issue>())
{
testcase(prefix + " temporary authorization for 3rd party");
env(trust(erin, asset(1000)));
env(trust(issuer, asset(0), erin, tfSetfAuth));
env(pay(issuer, erin, asset(10)));
// Erin deposits all in vault, then sends shares to depositor
auto tx = vault.deposit(
{.depositor = erin, .id = keylet.key, .amount = asset(10)});
env(tx);
env(pay(erin, depositor, share(10)));
testcase(prefix + " withdraw to authorized 3rd party");
// Depositor withdraws shares, destined to Erin
tx = vault.withdraw(
{.depositor = depositor,
.id = keylet.key,
.amount = asset(10)});
tx[sfDestination] = erin.human();
env(tx);
// Erin returns assets to issuer
env(pay(erin, issuer, asset(10)));
testcase(prefix + " fail to pay to unauthorized 3rd party");
env(trust(erin, asset(0)));
// Erin has MPToken but is no longer authorized to hold assets
env(pay(depositor, erin, share(1)), ter{tecNO_LINE});
}
{ {
testcase( testcase(
prefix + prefix +
@@ -1252,6 +1288,12 @@ class Vault_test : public beast::unit_test::suite
return vault->at(sfAccount); return vault->at(sfAccount);
}(); }();
auto const share = [&env, keylet = keylet, this]() -> Asset {
auto const vault = env.le(keylet);
BEAST_EXPECT(vault != nullptr);
return MPTIssue(vault->at(sfShareMPTID));
}();
auto const vaultBalance = // auto const vaultBalance = //
[&, account = vaultAccount, this]() -> PrettyAmount { [&, account = vaultAccount, this]() -> PrettyAmount {
auto const sle = env.le(keylet::line(account, issue)); auto const sle = env.le(keylet::line(account, issue));
@@ -1298,7 +1340,7 @@ class Vault_test : public beast::unit_test::suite
} }
{ {
testcase("IOU deleted trust line, withdraw to 3rd party"); testcase("IOU froze trust line, cannot withdraw to 3rd party");
env(trust(issuer, asset(0), owner, tfSetFreeze)); env(trust(issuer, asset(0), owner, tfSetFreeze));
auto tx = vault.withdraw( auto tx = vault.withdraw(
@@ -1306,10 +1348,16 @@ class Vault_test : public beast::unit_test::suite
env(tx, ter{tecFROZEN}); env(tx, ter{tecFROZEN});
tx[sfDestination] = charlie.human(); tx[sfDestination] = charlie.human();
env(tx); env(tx, ter{tecLOCKED}); // owner transitively locked via MPToken
env.close(); env.close();
BEAST_EXPECT(env.balance(charlie, issue) == asset(30)); auto tx1 = test::jtx::pay(owner, charlie, STAmount{share, 10});
env(tx1, ter{tecLOCKED});
auto tx2 = test::jtx::pay(charlie, owner, STAmount{share, 10});
env(tx2, ter{tecLOCKED});
BEAST_EXPECT(env.balance(charlie, issue) == asset(20));
} }
{ {

View File

@@ -88,8 +88,13 @@ isGlobalFrozen(ReadView const& view, MPTIssue const& mptIssue);
[[nodiscard]] bool [[nodiscard]] bool
isGlobalFrozen(ReadView const& view, Asset const& asset); isGlobalFrozen(ReadView const& view, Asset const& asset);
// Note, depth parameter is used to limit the recursion depth
[[nodiscard]] bool [[nodiscard]] bool
isVaultPseudoAccountFrozen(ReadView const& view, MPTIssue const& mptShare); isVaultPseudoAccountFrozen(
ReadView const& view,
AccountID const& account,
MPTIssue const& mptShare,
int depth);
[[nodiscard]] bool [[nodiscard]] bool
isIndividualFrozen( isIndividualFrozen(
@@ -134,7 +139,11 @@ isFrozen(
AccountID const& issuer); AccountID const& issuer);
[[nodiscard]] inline bool [[nodiscard]] inline bool
isFrozen(ReadView const& view, AccountID const& account, Issue const& issue) isFrozen(
ReadView const& view,
AccountID const& account,
Issue const& issue,
int = 0 /*ignored*/)
{ {
return isFrozen(view, account, issue.currency, issue.account); return isFrozen(view, account, issue.currency, issue.account);
} }
@@ -143,13 +152,20 @@ isFrozen(ReadView const& view, AccountID const& account, Issue const& issue)
isFrozen( isFrozen(
ReadView const& view, ReadView const& view,
AccountID const& account, AccountID const& account,
MPTIssue const& mptIssue); MPTIssue const& mptIssue,
int depth = 0);
[[nodiscard]] inline bool [[nodiscard]] inline bool
isFrozen(ReadView const& view, AccountID const& account, Asset const& asset) isFrozen(
ReadView const& view,
AccountID const& account,
Asset const& asset,
int depth = 0)
{ {
return std::visit( return std::visit(
[&](auto const& issue) { return isFrozen(view, account, issue); }, [&](auto const& issue) {
return isFrozen(view, account, issue, depth);
},
asset.value()); asset.value());
} }
@@ -158,7 +174,8 @@ isAnyFrozen(
ReadView const& view, ReadView const& view,
AccountID const& account1, AccountID const& account1,
AccountID const& account2, AccountID const& account2,
MPTIssue const& mptIssue); MPTIssue const& mptIssue,
int depth = 0);
/* /*
We do not have a use case for these (yet ?) We do not have a use case for these (yet ?)
@@ -687,9 +704,6 @@ struct TokenDescriptor
std::shared_ptr<SLE const> issuance; std::shared_ptr<SLE const> issuance;
}; };
[[nodiscard]] TER
requireAuth(ReadView const& view, Issue const& issue, AccountID const& account);
/** Check if the account lacks required authorization. /** Check if the account lacks required authorization.
* *
* Return tecNO_AUTH or tecNO_LINE if it does * Return tecNO_AUTH or tecNO_LINE if it does
@@ -709,7 +723,8 @@ requireAuth(ReadView const& view, Issue const& issue, AccountID const& account);
requireAuth( requireAuth(
ReadView const& view, ReadView const& view,
MPTIssue const& mptIssue, MPTIssue const& mptIssue,
AccountID const& account); AccountID const& account,
int depth = 0);
/** Enforce account has MPToken to match its authorization. /** Enforce account has MPToken to match its authorization.
* *

View File

@@ -274,11 +274,12 @@ bool
isFrozen( isFrozen(
ReadView const& view, ReadView const& view,
AccountID const& account, AccountID const& account,
MPTIssue const& mptIssue) MPTIssue const& mptIssue,
int depth)
{ {
return isGlobalFrozen(view, mptIssue) || return isGlobalFrozen(view, mptIssue) ||
isIndividualFrozen(view, account, mptIssue) || isIndividualFrozen(view, account, mptIssue) ||
isVaultPseudoAccountFrozen(view, mptIssue); isVaultPseudoAccountFrozen(view, account, mptIssue, depth);
} }
[[nodiscard]] bool [[nodiscard]] bool
@@ -286,16 +287,22 @@ isAnyFrozen(
ReadView const& view, ReadView const& view,
AccountID const& account1, AccountID const& account1,
AccountID const& account2, AccountID const& account2,
MPTIssue const& mptIssue) MPTIssue const& mptIssue,
int depth)
{ {
return isGlobalFrozen(view, mptIssue) || return isGlobalFrozen(view, mptIssue) ||
isIndividualFrozen(view, account1, mptIssue) || isIndividualFrozen(view, account1, mptIssue) ||
isIndividualFrozen(view, account2, mptIssue) || isIndividualFrozen(view, account2, mptIssue) ||
isVaultPseudoAccountFrozen(view, mptIssue); isVaultPseudoAccountFrozen(view, account1, mptIssue, depth) ||
isVaultPseudoAccountFrozen(view, account2, mptIssue, depth);
} }
bool bool
isVaultPseudoAccountFrozen(ReadView const& view, MPTIssue const& mptShare) isVaultPseudoAccountFrozen(
ReadView const& view,
AccountID const& account,
MPTIssue const& mptShare,
int depth)
{ {
if (!view.rules().enabled(featureSingleAssetVault)) if (!view.rules().enabled(featureSingleAssetVault))
return false; return false;
@@ -321,6 +328,9 @@ isVaultPseudoAccountFrozen(ReadView const& view, MPTIssue const& mptShare)
if (!mptIssuer->isFieldPresent(sfVaultID)) if (!mptIssuer->isFieldPresent(sfVaultID))
return false; // not a Vault pseudo-account, common case return false; // not a Vault pseudo-account, common case
if (depth > maxFreezeCheckDepth)
return true; // fail at maximum 2^maxFreezeCheckDepth checks
auto const vault = auto const vault =
view.read(keylet::vault(mptIssuer->getFieldH256(sfVaultID))); view.read(keylet::vault(mptIssuer->getFieldH256(sfVaultID)));
if (vault == nullptr) if (vault == nullptr)
@@ -331,7 +341,8 @@ isVaultPseudoAccountFrozen(ReadView const& view, MPTIssue const& mptShare)
} }
Asset const asset{vault->at(sfAsset)}; Asset const asset{vault->at(sfAsset)};
return isFrozen(view, issuer, asset); return isFrozen(view, issuer, asset, depth + 1) ||
isFrozen(view, account, asset, depth + 1);
} }
bool bool
@@ -2234,7 +2245,8 @@ TER
requireAuth( requireAuth(
ReadView const& view, ReadView const& view,
MPTIssue const& mptIssue, MPTIssue const& mptIssue,
AccountID const& account) AccountID const& account,
int depth)
{ {
auto const mptID = keylet::mptIssuance(mptIssue.getMptID()); auto const mptID = keylet::mptIssuance(mptIssue.getMptID());
auto const sleIssuance = view.read(mptID); auto const sleIssuance = view.read(mptID);
@@ -2247,6 +2259,35 @@ requireAuth(
if (mptIssuer == account) // Issuer won't have MPToken if (mptIssuer == account) // Issuer won't have MPToken
return tesSUCCESS; return tesSUCCESS;
if (view.rules().enabled(featureSingleAssetVault))
{
// requireAuth is recursive if the issuer is a vault pseudo-account
auto const sleIssuer = view.read(keylet::account(mptIssuer));
if (!sleIssuer)
return tefINTERNAL; // LCOV_EXCL_LINE
if (sleIssuer->isFieldPresent(sfVaultID))
{
auto const sleVault =
view.read(keylet::vault(sleIssuer->getFieldH256(sfVaultID)));
if (!sleVault)
return tefINTERNAL; // LCOV_EXCL_LINE
if (depth > maxFreezeCheckDepth)
return tecKILLED; // TODO: consider different code
auto const asset = sleVault->at(sfAsset);
return std::visit(
[&]<ValidIssueType TIss>(TIss const& issue) {
if constexpr (std::is_same_v<TIss, Issue>)
return requireAuth(view, issue, account);
else
return requireAuth(view, issue, account, depth + 1);
},
asset.value());
}
}
auto const mptokenID = keylet::mptoken(mptID.key, account); auto const mptokenID = keylet::mptoken(mptID.key, account);
auto const sleToken = view.read(mptokenID); auto const sleToken = view.read(mptokenID);
// Note, this is not amendment-gated because we do not want to maintain in // Note, this is not amendment-gated because we do not want to maintain in