Skip to content
Merged
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
6 changes: 1 addition & 5 deletions src/components/transactions/Swap/errors/SwapErrors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,7 @@ export const SwapErrors = ({
);
}

if (
isProtocolSwapState(state) &&
hasInsufficientLiquidity(state) &&
state.swapType !== SwapType.RepayWithCollateral
) {
if (isProtocolSwapState(state) && hasInsufficientLiquidity(state)) {
return (
<InsufficientLiquidityBlockingGuard
state={state}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,45 @@ import {
import { isProtocolSwapState } from '../../types/state.types';
import { InsufficientLiquidityBlockingError } from './InsufficientLiquidityBlockingError';

// The cow-swap-adapter flash-loans `_sellToken` for every position swap
// (CollateralSwap / DebtSwap / RepayWithCollateral, see
// cow-swap-adapters/src/adapters/v3/*Adapter.sol). The interface exposes that
// asset as `state.sellAmountToken`, so checking against it sidesteps the
// source/destination + isInvertedSwap question and mirrors what actually pulls
// from the lender on-chain.
export const hasInsufficientLiquidity = (state: SwapState) => {
// Only relevant for Debt Swaps where target asset availability and borrow cap matter.
// Collateral-related flows are handled via SupplyCapBlockingGuard and should not use borrow caps here.
if (!isProtocolSwapState(state) || state.swapType !== SwapType.DebtSwap) return false;
const reserve = state.isInvertedSwap
? state.sourceReserve?.reserve
: state.destinationReserve?.reserve;
const buyAmount = state.buyAmountFormatted;
if (!reserve || !buyAmount) return false;
// isProtocolSwapState narrows out SwapType.Swap (direct DEX, no Aave call).
if (!isProtocolSwapState(state)) return false;
// Don't gate on `state.useFlashloan`: several protocol paths flash-loan
// unconditionally regardless of the flag (CoW DebtSwap / RepayWithCollateral
// via forceFlashloanFlow, ParaSwap DebtSwap via DebtSwitchAdapter, ParaSwap
// CollateralSwap with `useFlashLoan: true` hardcoded). And even non-
// flashloan paths still withdraw/borrow from the pool, which decrements
// virtualUnderlyingBalance — the same liquidity ceiling we're guarding.
if (!state.sellAmountToken || !state.sellAmountFormatted) return false;

const availableBorrowCap =
reserve.borrowCap === '0'
? valueToBigNumber(ethers.constants.MaxUint256.toString())
: valueToBigNumber(reserve.borrowCap).minus(valueToBigNumber(reserve.totalDebt));
const availableLiquidity = BigNumber.max(
BigNumber.min(valueToBigNumber(reserve.formattedAvailableLiquidity), availableBorrowCap),
0
const flashLoanedAddress = state.sellAmountToken.underlyingAddress?.toLowerCase();
if (!flashLoanedAddress) return false;

const reserve = [state.sourceReserve?.reserve, state.destinationReserve?.reserve].find(
(r) => r?.underlyingAsset?.toLowerCase() === flashLoanedAddress
);
if (!reserve) return false;

const liquidity = BigNumber.max(valueToBigNumber(reserve.formattedAvailableLiquidity), 0);

// Borrow cap only matters for DebtSwap, which leaves the user holding new
// debt in the flash-loaned asset. Other flash-loan flows repay the loan
// in-flight and never touch the borrow cap.
const borrowCapRoom =
state.swapType === SwapType.DebtSwap
? reserve.borrowCap === '0'
? valueToBigNumber(ethers.constants.MaxUint256.toString())
: valueToBigNumber(reserve.borrowCap).minus(valueToBigNumber(reserve.totalDebt))
: valueToBigNumber(ethers.constants.MaxUint256.toString());

return valueToBigNumber(buyAmount).gt(availableLiquidity);
const effectiveLimit = BigNumber.max(BigNumber.min(liquidity, borrowCapRoom), 0);
return valueToBigNumber(state.sellAmountFormatted).gt(effectiveLimit);
};

export const InsufficientLiquidityBlockingGuard = ({
Expand Down Expand Up @@ -82,16 +101,20 @@ export const InsufficientLiquidityBlockingGuard = ({
}
}
}, [
state.buyAmountFormatted,
state.destinationReserve?.reserve?.formattedAvailableLiquidity,
state.swapType,
state.sellAmountFormatted,
state.sellAmountToken?.underlyingAddress,
state.sourceReserve?.reserve?.formattedAvailableLiquidity,
state.isInvertedSwap,
state.sourceReserve?.reserve?.borrowCap,
state.sourceReserve?.reserve?.totalDebt,
state.destinationReserve?.reserve?.formattedAvailableLiquidity,
state.destinationReserve?.reserve?.borrowCap,
state.destinationReserve?.reserve?.totalDebt,
]);

if (hasInsufficientLiquidity(state)) {
const symbol = state.isInvertedSwap
? state.sourceReserve?.reserve?.symbol
: state.destinationReserve?.reserve?.symbol;
// hasInsufficientLiquidity ensures sellAmountToken is defined.
const symbol = state.sellAmountToken?.symbol ?? '';
return (
<InsufficientLiquidityBlockingError
symbol={symbol}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,25 @@ import { useZeroLTVBlockingWithdraw } from 'src/hooks/useZeroLTVBlockingWithdraw
import { ActionsBlockedReason, SwapError, SwapState, SwapType } from '../../types';
import { ZeroLTVBlockingError } from './ZeroLTVBlockingError';

// Mirrors `validateHFAndLtvzero` in aave-v3-origin SupplyLogic: when the user
// has any zero-LTV collateral enabled, every withdrawn aToken must itself have
// zero LTV. The cow-swap-adapter withdraws `_sellToken` (see
// cow-swap-adapters/src/adapters/v3/*Adapter.sol), which the interface exposes
// as `state.sellAmountToken` — checking by symbol against the user's blocking
// asset list mirrors what the protocol validates on-chain.
export const hasZeroLTVBlocking = (state: SwapState, blockingAssets: string[]) => {
// DebtSwap (repay old debt + borrow new debt) never triggers validateHFAndLtv
// because neither repay nor borrow calls that validation.
if (state.swapType === SwapType.DebtSwap) {
return false;
}
// CollateralSwap does supply + withdraw. The withdraw triggers validateHFAndLtv
// which scans ALL collaterals. Block if any zero-LTV collateral exists.
if (state.swapType === SwapType.CollateralSwap) {
return blockingAssets.length > 0;
}
// RepayWithCollateral does repay + withdraw. The withdraw triggers
// validateHFAndLtv. Block if there are zero-LTV collateral assets that are
// NOT the source token being withdrawn. The pool allows withdrawing a
// zero-LTV asset itself (getLtv() == 0 passes the check).
return blockingAssets.length > 0 && !blockingAssets.includes(state.sourceToken.symbol);
if (blockingAssets.length === 0) return false;
// Direct DEX swaps don't touch Aave; the on-chain check never runs.
if (state.swapType === SwapType.Swap) return false;
// DebtSwap repays old debt and opens new debt. Neither path withdraws an
// aToken from the user, so validateHFAndLtvzero never fires.
if (state.swapType === SwapType.DebtSwap) return false;

const withdrawnSymbol = state.sellAmountToken?.symbol;
// Conservative: if we can't identify the withdrawn asset yet, block.
if (!withdrawnSymbol) return true;
// Withdrawing the LTV=0 asset itself is allowed by the protocol.
return !blockingAssets.includes(withdrawnSymbol);
};

export const ZeroLTVBlockingGuard = ({
Expand Down Expand Up @@ -71,7 +74,7 @@ export const ZeroLTVBlockingGuard = ({
});
}
}
}, [assetsBlockingWithdraw, state.sourceToken.symbol, state.swapType]);
}, [assetsBlockingWithdraw, state.sellAmountToken?.symbol, state.swapType]);

if (hasZeroLTVBlocking(state, assetsBlockingWithdraw)) {
return <ZeroLTVBlockingError sx={{ mb: !isSwapFlowSelected ? 0 : 4, ...sx }} />;
Expand Down
Loading