Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions include/xrpl/ledger/helpers/MPTokenHelpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ namespace xrpl {
[[nodiscard]] bool
isGlobalFrozen(ReadView const& view, MPTIssue const& mptIssue);

/** Returns true if @p account's MPToken for @p mptIssue carries the
* individual-lock flag (lsfMPTLocked).
*
* @warning This checks only the raw per-holder lock bit. It does **not**
* perform the transitive vault pseudo-account check: if @p mptIssue is a
* vault share whose underlying asset is frozen, this function returns false.
* Call @ref isFrozen instead when determining whether an account may send or
* receive tokens — it combines isIndividualFrozen, isGlobalFrozen, and
* isVaultPseudoAccountFrozen into a single complete check. */
[[nodiscard]] bool
isIndividualFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mptIssue);

Expand Down
32 changes: 14 additions & 18 deletions include/xrpl/ledger/helpers/TokenHelpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -144,19 +144,18 @@ checkDeepFrozen(ReadView const& view, AccountID const& account, Asset const& ass
*
* Otherwise checks, in order:
* 1. If the asset is globally frozen the remaining checks are redundant.
* 2. For MPT shares: The pseudo-account's vault share must not be transitively frozen via its
* underlying asset.
* 3. The pseudo-account's trustline / MPToken must not be frozen for sending.
* 4. Skipped when submitter == dst (self-withdrawal); a regular freeze should not prevent
* recovering one's own funds.
* 5. The destination must not be deep-frozen (cannot receive under any circumstance).
* 2. The pseudo-account's trustline / MPToken must not be individually frozen for sending.
* 3. The submitter's trustline / MPToken must not be individually frozen. Skipped when
* submitter == dst (self-withdrawal) so a regular freeze does not prevent recovering one's own
* funds. (Enforced as defensive code; no current caller exercises a frozen submitter ≠ dst.)
* 4. The destination must not be deep-frozen.
*
* For IOUs a regular individual freeze on the withdrawer does NOT block self-withdrawal; only deep
* freeze does. For MPTs "locked" is equivalent to deep-frozen, so locked MPT holders are always
* For IOUs a regular individual freeze on the submitter does NOT block self-withdrawal; only deep
* freeze does. For MPTs "locked" is equivalent to deep-frozen, so locked MPT holders are always
* blocked.
*
* @param view Ledger view to read freeze state from.
* @param srcAcct Pseudo-account the funds are withdrawn from (sender).
* @param pseudoAcct Pseudo-account the funds are withdrawn from (sender).
* @param submitterAcct Account that submitted the withdrawal transaction.
* @param dstAcct Account receiving the withdrawn funds.
* @param asset Asset being withdrawn.
Expand All @@ -166,7 +165,7 @@ checkDeepFrozen(ReadView const& view, AccountID const& account, Asset const& ass
[[nodiscard]] TER
checkWithdrawFreeze(
ReadView const& view,
AccountID const& srcAcct,
AccountID const& pseudoAcct,
AccountID const& submitterAcct,
AccountID const& dstAcct,
Asset const& asset);
Expand All @@ -175,20 +174,17 @@ checkWithdrawFreeze(
* Checks freeze compliance for depositing an asset into a pseudo-account (e.g. Vault, AMM,
* LoanBroker).
*
*
* Checks, in order:
* 1. If the asset is globally frozen the remaining checks are redundant.
* 2. For MPT shares: the pseudo-account's vault share must not be transitively frozen via its
* underlying asset (returns tecLOCKED).
* 3. The depositor must not be individually frozen. Skipped when srcAcct is the asset issuer,
* since the issuer can always send its own asset.
* 4. The pseudo-account must not be individually frozen for the asset. Unlike regular accounts,
* 2. The depositor must not be individually frozen for the asset. Skipped when srcAcct is the
* asset issuer, since the issuer can always send its own asset.
* 3. The pseudo-account must not be individually frozen for the asset. Unlike regular accounts,
* pseudo-accounts cannot receive deposits under a regular freeze because the deposited funds
* could not later be withdrawn.
*
* @param view Ledger view to read freeze state from.
* @param srcAcct Depositor sending the funds.
* @param dstAcct Pseudo-account receiving the deposit.
* @param pseudoAcct Pseudo-account receiving the deposit.
* @param asset Asset being deposited.
* @return tesSUCCESS if the deposit is permitted, otherwise a freeze result
* (tecFROZEN for IOUs, tecLOCKED for MPTs).
Expand All @@ -197,7 +193,7 @@ checkWithdrawFreeze(
checkDepositFreeze(
ReadView const& view,
AccountID const& srcAcct,
AccountID const& dstAcct,
AccountID const& pseudoAcct,
Asset const& asset);

//------------------------------------------------------------------------------
Expand Down
61 changes: 40 additions & 21 deletions src/libxrpl/ledger/helpers/TokenHelpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -160,19 +160,24 @@ checkDeepFrozen(ReadView const& view, AccountID const& account, Asset const& ass
[[nodiscard]] TER
checkWithdrawFreeze(
ReadView const& view,
AccountID const& srcAcct,
AccountID const& pseudoAcct,
AccountID const& submitterAcct,
AccountID const& dstAcct,
Asset const& asset)
{
XRPL_ASSERT(
isPseudoAccount(view, srcAcct), "xrpl::checkWithdrawFreeze : source is a pseudo-account");
isPseudoAccount(view, pseudoAcct),
"xrpl::checkWithdrawFreeze : source is a pseudo-account");
XRPL_ASSERT(
!isPseudoAccount(view, submitterAcct),
"xrpl::checkWithdrawFreeze : submitter is not a pseudo-account");
XRPL_ASSERT(
!isPseudoAccount(view, dstAcct),
"xrpl::checkWithdrawFreeze : destination is not a pseudo-account");
// The asset being withdrawn must not be issued by a pseudo-account
XRPL_ASSERT(
!isPseudoAccount(view, asset.getIssuer()),
"xrpl::checkWithdrawFreeze : asset issuer cannot be a pseudo-account");

// Funds can always be sent to the issuer
if (dstAcct == asset.getIssuer())
Expand All @@ -182,16 +187,9 @@ checkWithdrawFreeze(
if (auto const ret = checkGlobalFrozen(view, asset); !isTesSuccess(ret))
return ret;

// Special case for shares - check if the shares (and the transitive asset) is not frozen
if (asset.holds<MPTIssue>() &&
isVaultPseudoAccountFrozen(view, srcAcct, asset.get<MPTIssue>(), 0))
{
return tecLOCKED;
}

// The transfer is from Submitter to Destination via Source (pseudo-account)
// Both Source and Submitter must not be frozen to allow sending funds
if (auto const ret = checkIndividualFrozen(view, srcAcct, asset); !isTesSuccess(ret))
if (auto const ret = checkIndividualFrozen(view, pseudoAcct, asset); !isTesSuccess(ret))
return ret;

// Check submitter's individual freeze only when Submitter != Destination (a regular freeze
Expand All @@ -203,33 +201,42 @@ checkWithdrawFreeze(
}

// The destination account must not be deep frozen to receive the funds
return checkDeepFrozen(view, dstAcct, asset);
if (auto const ret = checkDeepFrozen(view, dstAcct, asset); !isTesSuccess(ret))
return ret;

if (asset.holds<MPTIssue>() &&
Comment thread
Tapanito marked this conversation as resolved.
isVaultPseudoAccountFrozen(view, pseudoAcct, asset.get<MPTIssue>(), 0))
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::checkWithdrawFreeze : pseudo-account backed object holds shares");
return tecINTERNAL;
// LCOV_EXCL_STOP
}

return tesSUCCESS;
}

[[nodiscard]] TER
checkDepositFreeze(
ReadView const& view,
AccountID const& srcAcct,
AccountID const& dstAcct,
AccountID const& pseudoAcct,
Asset const& asset)
{
XRPL_ASSERT(
isPseudoAccount(view, dstAcct),
isPseudoAccount(view, pseudoAcct),
"xrpl::checkDepositFreeze : destination is a pseudo-account");
XRPL_ASSERT(
!isPseudoAccount(view, srcAcct),
"xrpl::checkDepositFreeze : source is not a pseudo-account");
// The asset being deposited must not be issued by a pseudo-account
XRPL_ASSERT(
!isPseudoAccount(view, asset.getIssuer()),
"xrpl::checkDepositFreeze : asset issuer cannot be a pseudo-account");

if (auto const ret = checkGlobalFrozen(view, asset); !isTesSuccess(ret))
return ret;

// Special case for shares - check if the shares and the transitive asset is not frozen
if (asset.holds<MPTIssue>() &&
isVaultPseudoAccountFrozen(view, dstAcct, asset.get<MPTIssue>(), 0))
{
return tecLOCKED;
}

if (srcAcct != asset.getIssuer())
{
if (auto const ret = checkIndividualFrozen(view, srcAcct, asset); !isTesSuccess(ret))
Expand All @@ -238,7 +245,19 @@ checkDepositFreeze(

// Unlike regular accounts, pseudo-accounts cannot receive deposits under a regular freeze
// because those funds cannot be later withdrawn
return checkIndividualFrozen(view, dstAcct, asset);
if (auto const ret = checkIndividualFrozen(view, pseudoAcct, asset); !isTesSuccess(ret))
return ret;

if (asset.holds<MPTIssue>() &&
isVaultPseudoAccountFrozen(view, pseudoAcct, asset.get<MPTIssue>(), 0))
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::checkDepositFreeze : pseudo-account backed object holds shares");
Comment thread
Tapanito marked this conversation as resolved.
return tecINTERNAL;
// LCOV_EXCL_STOP
}

return tesSUCCESS;
}

//------------------------------------------------------------------------------
Expand Down
25 changes: 2 additions & 23 deletions src/libxrpl/tx/invariants/MPTInvariant.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/protocol/AccountID.h>
Expand Down Expand Up @@ -837,8 +836,6 @@ ValidMPTTransfer::finalize(
ReadView const& view,
beast::Journal const& j)
{
auto const fix330Enabled = view.rules().enabled(fixCleanup3_3_0);

if (hasPrivilege(tx, OverrideFreeze))
return true;

Expand Down Expand Up @@ -901,27 +898,9 @@ ValidMPTTransfer::finalize(

// Check once: if any involved account is frozen, the whole issuance transfer is
// considered frozen. Only need to check for frozen if there is a transfer of funds.
//
// Post-fix330: full isFrozen() applies — vault-share transitive freeze is part of
// the freeze semantics for all changed holders.
//
// Pre-fix330: legacy AMM withdraw only checked individual freeze on the
// destination, not the transitive vault freeze. All other paths (and the AMM
// account itself as sender) did apply the full check.
MPTIssue const issue{mptID};
auto const legacyAccountFrozen = [&] {
if (isGlobalFrozen(view, issue) || isIndividualFrozen(view, account, issue))
return true;
bool const isReceiver =
!value.amtBefore.has_value() || *value.amtAfter > *value.amtBefore;
if (txnType == ttAMM_WITHDRAW && isReceiver)
return false;
return isVaultPseudoAccountFrozen(view, account, issue, 0);
};
bool const accountFrozen =
fix330Enabled ? isFrozen(view, account, issue) : legacyAccountFrozen();
if (!invalidTransfer &&
(accountFrozen || !isAuthorized(view, mptID, account, reqAuth)))
(isFrozen(view, account, MPTIssue{mptID}) ||
!isAuthorized(view, mptID, account, reqAuth)))
{
invalidTransfer = true;
}
Expand Down
17 changes: 17 additions & 0 deletions src/libxrpl/tx/transactors/dex/AMMCreate.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,23 @@ AMMCreate::preclaim(PreclaimContext const& ctx)
pseudoAccountAddress(ctx.view, keylet::amm(amount.asset(), amount2.asset()).key);
accountId == beast::kZero)
return terADDRESS_COLLISION;

auto const isMPTIssuerPseudo = [&](Asset const& asset) {
if (asset.native())
return false;

if (asset.holds<Issue>())
return false;

return isPseudoAccount(ctx.view, asset.getIssuer());
};

if (isMPTIssuerPseudo(amount.asset()) || isMPTIssuerPseudo(amount2.asset()))
Comment thread
Tapanito marked this conversation as resolved.
{
JLOG(ctx.j.debug()) << "AMM Instance: can't create with vault shares " << amount << " "
<< amount2;
return tecWRONG_ASSET;
}
}

if (auto const ter = canMPTTradeAndTransfer(ctx.view, amount.asset(), accountID, accountID);
Expand Down
Loading
Loading