Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
488e1be
fix: Check vault sender freeze and use checkDeepFrozen for destination
Tapanito Jun 2, 2026
250b0d2
Merge branch 'develop' into tapanito/vault-freeze-check
Tapanito Jun 2, 2026
45fa34d
test: Add freeze-check tests for VaultWithdraw issuer guard
Tapanito Jun 3, 2026
43fc095
fix: Use IgnoreFreeze in doApply for issuer-redemption withdrawals
Tapanito Jun 3, 2026
3daf40c
fix: clarifying comment
Tapanito Jun 3, 2026
42b7e85
test: Restore clawback-under-MPT-global-lock coverage
Tapanito Jun 3, 2026
5abb72e
test: Add withdrawal-to-issuer assertion in IOU global-freeze test
Tapanito Jun 3, 2026
2f1846c
Merge remote-tracking branch 'origin/develop' into tapanito/vault-fre…
Tapanito Jun 11, 2026
9624f48
fix: Consolidate vault withdrawal freeze checks into checkWithdrawFre…
Tapanito Jun 12, 2026
36ab234
ci: Run sanitizers on release builds too (#7527)
mathbunnyru Jun 11, 2026
4760849
ci: Patch conan recipe for Nix to be able to use on macOS (#7532)
mathbunnyru Jun 11, 2026
94e1d75
test: Add null check unit test for `Oracle::aggregatePrice` (#7306)
pratikmankawde Jun 11, 2026
612c31d
test: Update Vault_test deposit/withdraw freeze expectations for chec…
Tapanito Jun 12, 2026
e42cdbe
fix: Add checkDepositFreeze and relax vault deposit freeze semantics
Tapanito Jun 12, 2026
cfd3d51
adds unified freeze checks for CoverDeposit
Tapanito Jun 18, 2026
2b6b277
test: Add testVaultDepositFreeze and testVaultWithdrawFreeze
Tapanito Jun 18, 2026
34b4bb4
Merge branch 'develop' into tapanito/vault-freeze-check
Tapanito Jun 18, 2026
7b63288
fix: Unify AMM Deposit/Withdraw freeze checks with checkDepositFreeze…
Tapanito Jun 18, 2026
0ce3751
fix: typo in assertion
Tapanito Jun 18, 2026
72f96cb
undo nix/devshell.nix changes
Tapanito Jun 22, 2026
53e5c8c
fix: Align pseudo-account withdraw freeze handling for issuer redemption
Tapanito Jun 22, 2026
4acefde
fix: Add missing initializer_list include in AMM_test
Tapanito Jun 22, 2026
e0872d9
Merge remote-tracking branch 'origin/develop' into tapanito/vault-fre…
Tapanito Jun 22, 2026
0e3cd2e
fix: failing unit-tests
Tapanito Jun 22, 2026
19deac2
clang-tidy
Tapanito Jun 22, 2026
1a3aed2
Merge remote-tracking branch 'origin/develop' into tapanito/vault-fre…
Tapanito Jun 22, 2026
884d159
fix: removes redundant global freeze checks
Tapanito Jun 23, 2026
8edf253
tests: adds explict IOU self-withdrawal tests
Tapanito Jun 23, 2026
44af7c2
adds explicit pseudo-account freeze handling
Tapanito Jun 23, 2026
799f126
removes junk files
Tapanito Jun 23, 2026
786a163
tests: invariant test for vault-share freeze gate
Tapanito Jun 24, 2026
f3d1567
tests: add vault-share freeze checks in AMM operations
Tapanito Jun 24, 2026
b2b240e
refactor: extract amendmentCombinations; document vault-share freeze …
Tapanito Jun 24, 2026
d77da24
address review feedback
Tapanito Jun 24, 2026
cdbc52f
clang-tidy
Tapanito Jun 24, 2026
c3e8bba
assertion typo
Tapanito Jun 24, 2026
1e135ab
Merge remote-tracking branch 'origin/develop' into tapanito/vault-fre…
Tapanito Jun 24, 2026
0addb3f
Update src/libxrpl/ledger/helpers/TokenHelpers.cpp
Tapanito Jun 24, 2026
95ddd1d
fix: move amendment permutation generation to env
Tapanito Jun 25, 2026
ea8c6ff
fix: Authorize AMM pseudo-accounts holding vault shares
Tapanito Jun 25, 2026
a91c3b2
Merge branch 'develop' into tapanito/vault-freeze-check
Tapanito Jun 25, 2026
160b9ab
Merge branch 'develop' into tapanito/vault-freeze-check
Tapanito Jun 25, 2026
a65eeea
fix: missing ret check
Tapanito Jun 25, 2026
6251805
Merge branch 'develop' into tapanito/vault-freeze-check
Tapanito Jun 25, 2026
e8bc09c
Merge branch 'develop' into tapanito/vault-freeze-check
Tapanito Jun 26, 2026
c7ee3c2
fix: fix failing unit-test \w higher fee
Tapanito Jun 26, 2026
c3aae7b
AI feedback
Tapanito Jun 26, 2026
3a8f212
Merge remote-tracking branch 'origin/develop' into tapanito/vault-fre…
Tapanito Jun 26, 2026
661e5a7
ai feedback
Tapanito Jun 26, 2026
09e6aa1
one more
Tapanito Jun 26, 2026
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
69 changes: 69 additions & 0 deletions include/xrpl/ledger/helpers/TokenHelpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,75 @@ checkDeepFrozen(ReadView const& view, AccountID const& account, MPTIssue const&
[[nodiscard]] TER
checkDeepFrozen(ReadView const& view, AccountID const& account, Asset const& asset);

/**
* Checks freeze compliance for withdrawing an asset from a pseudo-account (e.g. Vault, AMM,
* LoanBroker) to a destination account.
*
* Asserts that sourceAcct is a pseudo-account and that submitterAcct and dstAcct are not.
*
* Issuer exemption: returns tesSUCCESS immediately when dstAcct is the asset issuer — the issuer
* can always receive their own token, even when the pool is frozen. Callers that need to block
* withdrawals from a frozen pool even for the issuer (e.g. because the pool math cannot handle it)
* must check checkFrozen(sourceAcct, asset) separately before calling this function.
*
* 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).
*
* 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
* blocked.
*
* @param view Ledger view to read freeze state from.
* @param srcAcct 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.
* @return tesSUCCESS if the withdrawal is permitted, otherwise a freeze
* result (tecFROZEN for IOUs, tecLOCKED for MPTs).
*/
[[nodiscard]] TER
checkWithdrawFreeze(
ReadView const& view,
AccountID const& srcAcct,
AccountID const& submitterAcct,
AccountID const& dstAcct,
Asset const& asset);

/**
* 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,
* 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 asset Asset being deposited.
* @return tesSUCCESS if the deposit is permitted, otherwise a freeze result
* (tecFROZEN for IOUs, tecLOCKED for MPTs).
*/
[[nodiscard]] TER
checkDepositFreeze(
ReadView const& view,
AccountID const& srcAcct,
AccountID const& dstAcct,
Asset const& asset);

//------------------------------------------------------------------------------
//
// Account balance functions (Asset-based dispatchers)
Expand Down
5 changes: 5 additions & 0 deletions include/xrpl/tx/transactors/dex/AMMWithdraw.h
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,11 @@ class AMMWithdraw : public Transactor
beast::Journal const& journal);

private:
/** Returns IgnoreFreeze when the withdrawer is the issuer of a pool
* asset (post-fixCleanup3_3_0), ZeroIfFrozen otherwise. */
[[nodiscard]] FreezeHandling
issuerFreezeHandling() const;

std::pair<TER, bool>
applyGuts(Sandbox& view);

Expand Down
26 changes: 18 additions & 8 deletions src/libxrpl/ledger/helpers/MPTokenHelpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,18 @@ requireAuth(
AuthType authType,
std::uint8_t depth)
{
bool const fix330Enabled = view.rules().enabled(fixCleanup3_3_0);
bool const featureSAVEnabled = view.rules().enabled(featureSingleAssetVault);
bool const featureMPTV2Enabled = view.rules().enabled(featureMPTokensV2);

// Pseudo-accounts (Vault, LoanBroker, AMM) hold assets on behalf of their participants.
// They are implicitly authorized for any MPT they hold, including vault shares whose
// underlying asset would otherwise require auth.
auto const isPseudoAccountExempt = [&] {
return (featureSAVEnabled || featureMPTV2Enabled) &&
isPseudoAccount(view, account, {&sfVaultID, &sfLoanBrokerID, &sfAMMID});
};

auto const mptID = keylet::mptokenIssuance(mptIssue.getMptID());
auto const sleIssuance = view.read(mptID);
if (!sleIssuance)
Expand All @@ -319,7 +331,9 @@ requireAuth(
if (mptIssuer == account) // Issuer won't have MPToken
return tesSUCCESS;

bool const featureSAVEnabled = view.rules().enabled(featureSingleAssetVault);
// Post-fix330: exempt before the recursive underlying-asset auth check.
if (fix330Enabled && isPseudoAccountExempt())
return tesSUCCESS;

if (featureSAVEnabled)
{
Expand Down Expand Up @@ -382,13 +396,9 @@ requireAuth(
// belong to someone who is explicitly authorized e.g. a vault owner.
}

bool const featureMPTV2Enabled = view.rules().enabled(featureMPTokensV2);
if (featureSAVEnabled || featureMPTV2Enabled)
{
// Implicitly authorize Vault, LoanBroker, and AMM pseudo-accounts
if (isPseudoAccount(view, account, {&sfVaultID, &sfLoanBrokerID, &sfAMMID}))
return tesSUCCESS;
}
// Pre-fix330: exempt after domain/sleToken checks, preserving prior behavior.
if (!fix330Enabled && isPseudoAccountExempt())
return tesSUCCESS;

// mptoken must be authorized if issuance enabled requireAuth
if (sleIssuance->isFlag(lsfMPTRequireAuth) &&
Expand Down
96 changes: 87 additions & 9 deletions src/libxrpl/ledger/helpers/TokenHelpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.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/ledger/helpers/RippleStateHelpers.h>
Expand Down Expand Up @@ -34,14 +35,6 @@

namespace xrpl {

// Forward declaration for function that remains in View.h/cpp
bool
isLPTokenFrozen(
ReadView const& view,
AccountID const& account,
Asset const& asset,
Asset const& asset2);

//------------------------------------------------------------------------------
//
// Freeze checking (Asset-based)
Expand Down Expand Up @@ -164,6 +157,90 @@
[&](auto const& issue) { return checkDeepFrozen(view, account, issue); }, asset.value());
}

[[nodiscard]] TER
checkWithdrawFreeze(
ReadView const& view,
AccountID const& srcAcct,
AccountID const& submitterAcct,
AccountID const& dstAcct,
Asset const& asset)
{
XRPL_ASSERT(
isPseudoAccount(view, srcAcct), "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");

// Funds can always be sent to the issuer
if (dstAcct == asset.getIssuer())
Comment thread
Tapanito marked this conversation as resolved.
Comment thread
Tapanito marked this conversation as resolved.
return tesSUCCESS;
Comment thread
Tapanito marked this conversation as resolved.
Comment thread
Tapanito marked this conversation as resolved.

// If the asset is globally frozen, other checks are redundant
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;

Check warning on line 189 in src/libxrpl/ledger/helpers/TokenHelpers.cpp

View check run for this annotation

Codecov / codecov/patch

src/libxrpl/ledger/helpers/TokenHelpers.cpp#L189

Added line #L189 was not covered by tests
}

// 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))
return ret;

// Check submitter's individual freeze only when Submitter != Destination (a regular freeze
// should not block self-withdrawal).
if (submitterAcct != dstAcct)
{
if (auto const ret = checkIndividualFrozen(view, submitterAcct, asset); !isTesSuccess(ret))
return ret;
}

// The destination account must not be deep frozen to receive the funds
return checkDeepFrozen(view, dstAcct, asset);
}

[[nodiscard]] TER
checkDepositFreeze(
ReadView const& view,
AccountID const& srcAcct,
AccountID const& dstAcct,
Asset const& asset)
{
XRPL_ASSERT(
isPseudoAccount(view, dstAcct),
"xrpl::checkDepositFreeze : destination is a pseudo-account");
XRPL_ASSERT(
!isPseudoAccount(view, srcAcct),
"xrpl::checkDepositFreeze : source is not 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;

Check warning on line 230 in src/libxrpl/ledger/helpers/TokenHelpers.cpp

View check run for this annotation

Codecov / codecov/patch

src/libxrpl/ledger/helpers/TokenHelpers.cpp#L230

Added line #L230 was not covered by tests
}

if (srcAcct != asset.getIssuer())
{
if (auto const ret = checkIndividualFrozen(view, srcAcct, asset); !isTesSuccess(ret))
return ret;
}

// Unlike regular accounts, pseudo-accounts cannot receive deposits under a regular freeze
// because those funds cannot be later withdrawn
return checkIndividualFrozen(view, dstAcct, asset);
}

//------------------------------------------------------------------------------
//
// Account balance functions
Expand Down Expand Up @@ -776,7 +853,8 @@
if (senderID == issuer || receiverID == issuer || issuer == noAccount())
{
// Direct send: redeeming IOUs and/or sending own IOUs.
if (auto const ter = directSendNoFeeIOU(view, senderID, receiverID, amount, false, j))
if (auto const ter = directSendNoFeeIOU(view, senderID, receiverID, amount, false, j);
!isTesSuccess(ter))
return ter;
actual += amount;
// Do not add amount to takeFromSender, because directSendNoFeeIOU took
Expand Down
40 changes: 35 additions & 5 deletions src/libxrpl/tx/invariants/MPTInvariant.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
#include <xrpl/beast/utility/Journal.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>
#include <xrpl/protocol/Feature.h>
Expand Down Expand Up @@ -509,6 +511,15 @@ ValidMPTTransfer::isAuthorized(
AccountID const& holder,
bool reqAuth) const
{
// Pseudo-accounts (Vault, LoanBroker, AMM) hold assets on behalf of their
// participants and are implicitly authorized for any MPT they hold,
// including vault shares whose underlying asset would otherwise require
// auth. Exempt them here rather than relying on requireAuth: the recursive
// share -> underlying descent in requireAuth fails for a pseudo-account
// that holds the share but not the underlying.
if (isPseudoAccount(view, holder, {&sfVaultID, &sfLoanBrokerID, &sfAMMID}))
return true;

auto const key = keylet::mptoken(mptid, holder);
auto const it = deletedAuthorized_.find(key.key);
if (it != deletedAuthorized_.end())
Expand All @@ -524,6 +535,8 @@ 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 @@ -584,12 +597,29 @@ ValidMPTTransfer::finalize(
++senders;
}

// 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.
// 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();
Comment thread
Tapanito marked this conversation as resolved.
if (!invalidTransfer &&
(isFrozen(view, account, MPTIssue{mptID}) ||
!isAuthorized(view, mptID, account, reqAuth)))
(accountFrozen || !isAuthorized(view, mptID, account, reqAuth)))
{
invalidTransfer = true;
}
Expand Down
Loading
Loading