Skip to content

Commit 2934d05

Browse files
authored
fix(swap): align flash-loan and zero-LTV guards with cow-swap-adapter sellToken (#2960)
1 parent ba67298 commit 2934d05

3 files changed

Lines changed: 65 additions & 43 deletions

File tree

‎src/components/transactions/Swap/errors/SwapErrors.tsx‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -116,11 +116,7 @@ export const SwapErrors = ({
116116
);
117117
}
118118

119-
if (
120-
isProtocolSwapState(state) &&
121-
hasInsufficientLiquidity(state) &&
122-
state.swapType !== SwapType.RepayWithCollateral
123-
) {
119+
if (isProtocolSwapState(state) && hasInsufficientLiquidity(state)) {
124120
return (
125121
<InsufficientLiquidityBlockingGuard
126122
state={state}

‎src/components/transactions/Swap/errors/shared/InsufficientLiquidityBlockingGuard.tsx‎

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,26 +14,45 @@ import {
1414
import { isProtocolSwapState } from '../../types/state.types';
1515
import { InsufficientLiquidityBlockingError } from './InsufficientLiquidityBlockingError';
1616

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

27-
const availableBorrowCap =
28-
reserve.borrowCap === '0'
29-
? valueToBigNumber(ethers.constants.MaxUint256.toString())
30-
: valueToBigNumber(reserve.borrowCap).minus(valueToBigNumber(reserve.totalDebt));
31-
const availableLiquidity = BigNumber.max(
32-
BigNumber.min(valueToBigNumber(reserve.formattedAvailableLiquidity), availableBorrowCap),
33-
0
34+
const flashLoanedAddress = state.sellAmountToken.underlyingAddress?.toLowerCase();
35+
if (!flashLoanedAddress) return false;
36+
37+
const reserve = [state.sourceReserve?.reserve, state.destinationReserve?.reserve].find(
38+
(r) => r?.underlyingAsset?.toLowerCase() === flashLoanedAddress
3439
);
40+
if (!reserve) return false;
41+
42+
const liquidity = BigNumber.max(valueToBigNumber(reserve.formattedAvailableLiquidity), 0);
43+
44+
// Borrow cap only matters for DebtSwap, which leaves the user holding new
45+
// debt in the flash-loaned asset. Other flash-loan flows repay the loan
46+
// in-flight and never touch the borrow cap.
47+
const borrowCapRoom =
48+
state.swapType === SwapType.DebtSwap
49+
? reserve.borrowCap === '0'
50+
? valueToBigNumber(ethers.constants.MaxUint256.toString())
51+
: valueToBigNumber(reserve.borrowCap).minus(valueToBigNumber(reserve.totalDebt))
52+
: valueToBigNumber(ethers.constants.MaxUint256.toString());
3553

36-
return valueToBigNumber(buyAmount).gt(availableLiquidity);
54+
const effectiveLimit = BigNumber.max(BigNumber.min(liquidity, borrowCapRoom), 0);
55+
return valueToBigNumber(state.sellAmountFormatted).gt(effectiveLimit);
3756
};
3857

3958
export const InsufficientLiquidityBlockingGuard = ({
@@ -82,16 +101,20 @@ export const InsufficientLiquidityBlockingGuard = ({
82101
}
83102
}
84103
}, [
85-
state.buyAmountFormatted,
86-
state.destinationReserve?.reserve?.formattedAvailableLiquidity,
104+
state.swapType,
105+
state.sellAmountFormatted,
106+
state.sellAmountToken?.underlyingAddress,
87107
state.sourceReserve?.reserve?.formattedAvailableLiquidity,
88-
state.isInvertedSwap,
108+
state.sourceReserve?.reserve?.borrowCap,
109+
state.sourceReserve?.reserve?.totalDebt,
110+
state.destinationReserve?.reserve?.formattedAvailableLiquidity,
111+
state.destinationReserve?.reserve?.borrowCap,
112+
state.destinationReserve?.reserve?.totalDebt,
89113
]);
90114

91115
if (hasInsufficientLiquidity(state)) {
92-
const symbol = state.isInvertedSwap
93-
? state.sourceReserve?.reserve?.symbol
94-
: state.destinationReserve?.reserve?.symbol;
116+
// hasInsufficientLiquidity ensures sellAmountToken is defined.
117+
const symbol = state.sellAmountToken?.symbol ?? '';
95118
return (
96119
<InsufficientLiquidityBlockingError
97120
symbol={symbol}

‎src/components/transactions/Swap/errors/shared/ZeroLTVBlockingGuard.tsx‎

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,25 @@ import { useZeroLTVBlockingWithdraw } from 'src/hooks/useZeroLTVBlockingWithdraw
55
import { ActionsBlockedReason, SwapError, SwapState, SwapType } from '../../types';
66
import { ZeroLTVBlockingError } from './ZeroLTVBlockingError';
77

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

2629
export const ZeroLTVBlockingGuard = ({
@@ -71,7 +74,7 @@ export const ZeroLTVBlockingGuard = ({
7174
});
7275
}
7376
}
74-
}, [assetsBlockingWithdraw, state.sourceToken.symbol, state.swapType]);
77+
}, [assetsBlockingWithdraw, state.sellAmountToken?.symbol, state.swapType]);
7578

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

0 commit comments

Comments
 (0)