Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 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
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
57 changes: 57 additions & 0 deletions include/xrpl/ledger/helpers/TokenHelpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ isIndividualFrozen(ReadView const& view, AccountID const& account, Asset const&
[[nodiscard]] TER
checkIndividualFrozen(ReadView const& view, AccountID const& account, Asset const& asset);

[[nodiscard]] TER
checkIndividualDeepFrozen(ReadView const& view, AccountID const& account, Asset const& asset);

/**
* isFrozen check is recursive for MPT shares in a vault, descending to
* assets in the vault, up to maxAssetCheckDepth recursion depth. This is
Expand Down Expand Up @@ -131,6 +134,60 @@ 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.
*
* 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. checkFrozen(sourceAcct, asset) — the pseudo-account's trustline /
* global freeze must not block sending.
* 2. checkFrozen(submitterAcct, asset) — skipped when submitter == dst
* (self-withdrawal); a regular freeze should not prevent recovering
* one's own funds.
* 3. checkDeepFrozen(dstAcct, asset) — 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.
*/
[[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).
*
* Asserts that dstAcct is a pseudo-account.
*
* Checks, in order:
* 1. checkFrozen(srcAcct, asset) — the depositor must not be frozen
* (global or individual) for the asset.
* 2. checkFrozen(dstAcct, asset) — the pseudo-account must not be
* 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.
*/
[[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
89 changes: 89 additions & 0 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 @@ -79,6 +80,14 @@ checkIndividualFrozen(ReadView const& view, AccountID const& account, Asset cons
return tesSUCCESS;
}

TER
checkIndividualDeepFrozen(ReadView const& view, AccountID const& account, Asset const& asset)
{
if (isDeepFrozen(view, account, asset))
return asset.holds<MPTIssue>() ? tecLOCKED : tecFROZEN;
return tesSUCCESS;
}

bool
isFrozen(ReadView const& view, AccountID const& account, Asset const& asset, std::uint8_t depth)
{
Expand Down Expand Up @@ -164,6 +173,86 @@ checkDeepFrozen(ReadView const& view, AccountID const& account, Asset const& ass
[&](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 must be a pseudo-account");

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

// If the asset is globally frozen, other checks are redundant
if (auto const ret = checkGlobalFrozen(view, asset))
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;
}

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

// The destination account must not be deep frozen to receive the funds
return checkIndividualDeepFrozen(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 must be a pseudo-account");

// An Issuer cannot deposit when:
// 1. Asset is globally frozen
// 2. The trustline of the pseudo-account is frozen

if (auto const ret = checkGlobalFrozen(view, asset))
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))
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
68 changes: 49 additions & 19 deletions src/libxrpl/tx/transactors/dex/AMMDeposit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,36 @@ AMMDeposit::preclaim(PreclaimContext const& ctx)
: tecUNFUNDED_AMM;
};

if (ctx.view.rules().enabled(featureAMMClawback))
auto const amount = ctx.tx[~sfAmount];
auto const amount2 = ctx.tx[~sfAmount2];
auto const ammAccountID = ammSle->getAccountID(sfAccount);

if (ctx.view.rules().enabled(fixCleanup3_3_0))
{
// Unified deposit freeze check for both pool assets.
// AMMDeposit is not allowed if either asset is frozen.
auto checkAsset = [&](Asset const& asset) -> TER {
if (auto const ter = requireAuth(ctx.view, asset, accountID, AuthType::WeakAuth))
{
JLOG(ctx.j.debug()) << "AMM Deposit: account is not authorized, " << asset;
return ter;
}
if (auto const ter = checkDepositFreeze(ctx.view, accountID, ammAccountID, asset))
{
JLOG(ctx.j.debug())
<< "AMM Deposit: frozen, " << to_string(accountID) << " " << to_string(asset);
return ter;
}
return tesSUCCESS;
};

if (auto const ter = checkAsset(ctx.tx[sfAsset]))
return ter;

if (auto const ter = checkAsset(ctx.tx[sfAsset2]))
return ter;
}
else if (ctx.view.rules().enabled(featureAMMClawback))
{
// Check if either of the assets is frozen, AMMDeposit is not allowed
// if either asset is frozen
Expand Down Expand Up @@ -283,10 +312,6 @@ AMMDeposit::preclaim(PreclaimContext const& ctx)
return ter;
}

auto const amount = ctx.tx[~sfAmount];
auto const amount2 = ctx.tx[~sfAmount2];
auto const ammAccountID = ammSle->getAccountID(sfAccount);

auto checkAmount = [&](std::optional<STAmount> const& amount, bool checkBalance) -> TER {
if (amount)
{
Expand All @@ -301,21 +326,26 @@ AMMDeposit::preclaim(PreclaimContext const& ctx)
return ter;
// LCOV_EXCL_STOP
}
// AMM account or currency frozen
if (auto const ter = checkFrozen(ctx.view, ammAccountID, amount->asset());
!isTesSuccess(ter))
{
JLOG(ctx.j.debug()) << "AMM Deposit: AMM account or currency is frozen or locked, "
<< to_string(accountID);
return ter;
}
// Account frozen
if (auto const ter = checkIndividualFrozen(ctx.view, accountID, amount->asset());
!isTesSuccess(ter))
if (!ctx.view.rules().enabled(fixCleanup3_3_0))
{
JLOG(ctx.j.debug()) << "AMM Deposit: account is frozen or locked, "
<< to_string(accountID) << " " << to_string(amount->asset());
return ter;
// AMM account or currency frozen
if (auto const ter = checkFrozen(ctx.view, ammAccountID, amount->asset());
!isTesSuccess(ter))
{
JLOG(ctx.j.debug())
<< "AMM Deposit: AMM account or currency is frozen or locked, "
<< to_string(accountID);
return ter;
}
// Account frozen
if (auto const ter = checkIndividualFrozen(ctx.view, accountID, amount->asset());
!isTesSuccess(ter))
{
JLOG(ctx.j.debug())
<< "AMM Deposit: account is frozen or locked, " << to_string(accountID)
<< " " << to_string(amount->asset());
return ter;
}
}
if (checkBalance)
{
Expand Down
65 changes: 50 additions & 15 deletions src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -238,21 +238,36 @@ AMMWithdraw::preclaim(PreclaimContext const& ctx)
<< "AMM Withdraw: account is not authorized, " << amount->asset();
return ter;
}
// AMM account or currency frozen
if (auto const ter = checkFrozen(ctx.view, ammAccountID, amount->asset());
!isTesSuccess(ter))
if (ctx.view.rules().enabled(fixCleanup3_3_0))
{
JLOG(ctx.j.debug()) << "AMM Withdraw: AMM account or currency is frozen or locked, "
<< to_string(accountID);
return ter;
if (auto const ret = checkWithdrawFreeze(
ctx.view, ammAccountID, accountID, accountID, amount->asset()))
{
JLOG(ctx.j.debug()) << "AMM Withdraw: frozen, " << to_string(accountID) << " "
<< to_string(amount->asset());
return ret;
}
}
// Account frozen
if (auto const ter = checkIndividualFrozen(ctx.view, accountID, amount->asset());
!isTesSuccess(ter))
else
{
JLOG(ctx.j.debug()) << "AMM Withdraw: account is frozen or locked, "
<< to_string(accountID) << " " << to_string(amount->asset());
return ter;
// AMM account or currency frozen
if (auto const ter = checkFrozen(ctx.view, ammAccountID, amount->asset());
!isTesSuccess(ter))
{
JLOG(ctx.j.debug())
<< "AMM Withdraw: AMM account or currency is frozen or locked, "
<< to_string(accountID);
return ter;
}
// Account frozen
if (auto const ter = checkIndividualFrozen(ctx.view, accountID, amount->asset());
!isTesSuccess(ter))
{
JLOG(ctx.j.debug())
<< "AMM Withdraw: account is frozen or locked, " << to_string(accountID)
<< " " << to_string(amount->asset());
return ter;
}
}
}
return tesSUCCESS;
Expand Down Expand Up @@ -302,6 +317,24 @@ AMMWithdraw::preclaim(PreclaimContext const& ctx)
return tesSUCCESS;
}

FreezeHandling
AMMWithdraw::issuerFreezeHandling() const
{
// When the withdrawer is the issuer of a pool asset, the issuer can
// always receive their own token — even when the pool is frozen.
// Use IgnoreFreeze so ammHolds returns real balances instead of zero.
if (ctx_.view().rules().enabled(fixCleanup3_3_0))
{
auto const asset1 = Asset{ctx_.tx[sfAsset]};
auto const asset2 = Asset{ctx_.tx[sfAsset2]};
if (!asset1.native() && accountID_ == asset1.getIssuer())
return FreezeHandling::IgnoreFreeze;
if (!asset2.native() && accountID_ == asset2.getIssuer())
return FreezeHandling::IgnoreFreeze;
}
return FreezeHandling::ZeroIfFrozen;
}

std::pair<TER, bool>
AMMWithdraw::applyGuts(Sandbox& sb)
{
Expand Down Expand Up @@ -329,12 +362,14 @@ AMMWithdraw::applyGuts(Sandbox& sb)

auto const tfee = getTradingFee(ctx_.view(), *ammSle, accountID_);

auto const freezeHandling = issuerFreezeHandling();

auto const expected = ammHolds(
sb,
*ammSle,
amount ? amount->asset() : std::optional<Asset>{},
amount2 ? amount2->asset() : std::optional<Asset>{},
FreezeHandling::ZeroIfFrozen,
freezeHandling,
AuthHandling::ZeroIfUnauthorized,
ctx_.journal);
if (!expected)
Expand Down Expand Up @@ -459,7 +494,7 @@ AMMWithdraw::withdraw(
lpTokensAMMBalance,
lpTokensWithdraw,
tfee,
FreezeHandling::ZeroIfFrozen,
issuerFreezeHandling(),
AuthHandling::ZeroIfUnauthorized,
isWithdrawAll(ctx_.tx),
preFeeBalance_,
Expand Down Expand Up @@ -746,7 +781,7 @@ AMMWithdraw::equalWithdrawTokens(
lpTokens,
lpTokensWithdraw,
tfee,
FreezeHandling::ZeroIfFrozen,
issuerFreezeHandling(),
AuthHandling::ZeroIfUnauthorized,
isWithdrawAll(ctx_.tx),
preFeeBalance_,
Expand Down
Loading
Loading