diff --git a/src/components/transactions/Swap/errors/SwapErrors.tsx b/src/components/transactions/Swap/errors/SwapErrors.tsx index b3792e968d..913ec47b7d 100644 --- a/src/components/transactions/Swap/errors/SwapErrors.tsx +++ b/src/components/transactions/Swap/errors/SwapErrors.tsx @@ -116,11 +116,7 @@ export const SwapErrors = ({ ); } - if ( - isProtocolSwapState(state) && - hasInsufficientLiquidity(state) && - state.swapType !== SwapType.RepayWithCollateral - ) { + if (isProtocolSwapState(state) && hasInsufficientLiquidity(state)) { return ( { - // 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 = ({ @@ -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 ( { - // 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 = ({ @@ -71,7 +74,7 @@ export const ZeroLTVBlockingGuard = ({ }); } } - }, [assetsBlockingWithdraw, state.sourceToken.symbol, state.swapType]); + }, [assetsBlockingWithdraw, state.sellAmountToken?.symbol, state.swapType]); if (hasZeroLTVBlocking(state, assetsBlockingWithdraw)) { return ;