From 21b22921249da37d66773e5ac83e54125f5bb5d8 Mon Sep 17 00:00:00 2001 From: Martin Grabina Date: Fri, 8 May 2026 11:36:35 -0300 Subject: [PATCH 1/8] feat: resolve flashloan fee on-chain via ACLManager --- .../CollateralSwapActionsViaCoWAdapters.tsx | 2 +- .../DebtSwap/DebtSwapActionsViaCoW.tsx | 2 +- .../RepayWithCollateralActionsViaCoW.tsx | 2 +- .../Swap/helpers/cow/adapters.helpers.ts | 4 +- .../helpers/paraswap/flashloan.helpers.ts | 4 +- .../Swap/hooks/useFlashLoanFeeBps.ts | 104 ++++++++++++++++++ .../Swap/hooks/useSwapOrderAmounts.ts | 18 +-- .../transactions/Swap/types/state.types.ts | 3 + 8 files changed, 125 insertions(+), 14 deletions(-) create mode 100644 src/components/transactions/Swap/hooks/useFlashLoanFeeBps.ts diff --git a/src/components/transactions/Swap/actions/CollateralSwap/CollateralSwapActionsViaCoWAdapters.tsx b/src/components/transactions/Swap/actions/CollateralSwap/CollateralSwapActionsViaCoWAdapters.tsx index 07e1f32d15..33c5760389 100644 --- a/src/components/transactions/Swap/actions/CollateralSwap/CollateralSwapActionsViaCoWAdapters.tsx +++ b/src/components/transactions/Swap/actions/CollateralSwap/CollateralSwapActionsViaCoWAdapters.tsx @@ -189,7 +189,7 @@ export const CollateralSwapActionsViaCowAdapters = ({ : undefined; const { flashLoanFeeAmount, sellAmountToSign } = flashLoanSdk.calculateFlashLoanAmounts({ - flashLoanFeeBps: FLASH_LOAN_FEE_BPS, + flashLoanFeeBps: state.flashLoanFeeBps ?? FLASH_LOAN_FEE_BPS, sellAmount: state.sellAmountBigInt, }); diff --git a/src/components/transactions/Swap/actions/DebtSwap/DebtSwapActionsViaCoW.tsx b/src/components/transactions/Swap/actions/DebtSwap/DebtSwapActionsViaCoW.tsx index 9eb7782c2c..eccc9df775 100644 --- a/src/components/transactions/Swap/actions/DebtSwap/DebtSwapActionsViaCoW.tsx +++ b/src/components/transactions/Swap/actions/DebtSwap/DebtSwapActionsViaCoW.tsx @@ -215,7 +215,7 @@ export const DebtSwapActionsViaCoW = ({ : undefined; const { flashLoanFeeAmount, sellAmountToSign } = flashLoanSdk.calculateFlashLoanAmounts({ - flashLoanFeeBps: FLASH_LOAN_FEE_BPS, + flashLoanFeeBps: state.flashLoanFeeBps ?? FLASH_LOAN_FEE_BPS, sellAmount: BigInt(sellAmountWithMarginForDustProtection), }); diff --git a/src/components/transactions/Swap/actions/RepayWithCollateral/RepayWithCollateralActionsViaCoW.tsx b/src/components/transactions/Swap/actions/RepayWithCollateral/RepayWithCollateralActionsViaCoW.tsx index b7e9051466..cd2758c0cd 100644 --- a/src/components/transactions/Swap/actions/RepayWithCollateral/RepayWithCollateralActionsViaCoW.tsx +++ b/src/components/transactions/Swap/actions/RepayWithCollateral/RepayWithCollateralActionsViaCoW.tsx @@ -213,7 +213,7 @@ export const RepayWithCollateralActionsViaCoW = ({ : undefined; const { flashLoanFeeAmount, sellAmountToSign } = flashLoanSdk.calculateFlashLoanAmounts({ - flashLoanFeeBps: FLASH_LOAN_FEE_BPS, + flashLoanFeeBps: state.flashLoanFeeBps ?? FLASH_LOAN_FEE_BPS, sellAmount: BigInt(sellAmountWithMarginForDustProtection), }); diff --git a/src/components/transactions/Swap/helpers/cow/adapters.helpers.ts b/src/components/transactions/Swap/helpers/cow/adapters.helpers.ts index b8e78231cf..874bb69376 100644 --- a/src/components/transactions/Swap/helpers/cow/adapters.helpers.ts +++ b/src/components/transactions/Swap/helpers/cow/adapters.helpers.ts @@ -92,7 +92,7 @@ export const calculateInstanceAddress = async ({ }; const { flashLoanFeeAmount, sellAmountToSign } = flashLoanSdk.calculateFlashLoanAmounts({ - flashLoanFeeBps: FLASH_LOAN_FEE_BPS, + flashLoanFeeBps: state.flashLoanFeeBps ?? FLASH_LOAN_FEE_BPS, sellAmount: BigInt(sellAmountWithMarginForDustProtection), }); @@ -180,7 +180,7 @@ export const calculateFlashLoanAmounts = ( const { flashLoanFeeAmount, sellAmountToSign } = flashLoanSdk.calculateFlashLoanAmounts({ sellAmount: sellAmount, - flashLoanFeeBps: FLASH_LOAN_FEE_BPS, + flashLoanFeeBps: state.flashLoanFeeBps ?? FLASH_LOAN_FEE_BPS, }); return { diff --git a/src/components/transactions/Swap/helpers/paraswap/flashloan.helpers.ts b/src/components/transactions/Swap/helpers/paraswap/flashloan.helpers.ts index 1bb122358c..2e12b2f24d 100644 --- a/src/components/transactions/Swap/helpers/paraswap/flashloan.helpers.ts +++ b/src/components/transactions/Swap/helpers/paraswap/flashloan.helpers.ts @@ -31,8 +31,8 @@ export const calculateParaswapFlashLoanFee = ( // Calculate fee: flashloan amount * fee bps / 10000 // The flashloan amount is the sell amount (collateral being swapped) - const flashLoanFeeAmount = - (state.sellAmountBigInt * BigInt(PARASWAP_FLASH_LOAN_FEE_BPS)) / BigInt(10000); + const feeBps = state.flashLoanFeeBps ?? PARASWAP_FLASH_LOAN_FEE_BPS; + const flashLoanFeeAmount = (state.sellAmountBigInt * BigInt(feeBps)) / BigInt(10000); // Format the fee amount const flashLoanFeeFormatted = valueToBigNumber(flashLoanFeeAmount.toString()) diff --git a/src/components/transactions/Swap/hooks/useFlashLoanFeeBps.ts b/src/components/transactions/Swap/hooks/useFlashLoanFeeBps.ts new file mode 100644 index 0000000000..aee2d69821 --- /dev/null +++ b/src/components/transactions/Swap/hooks/useFlashLoanFeeBps.ts @@ -0,0 +1,104 @@ +import { + AaveV3Arbitrum, + AaveV3Avalanche, + AaveV3Base, + AaveV3BNB, + AaveV3Ethereum, + AaveV3EthereumEtherFi, + AaveV3EthereumLido, + AaveV3Gnosis, + AaveV3Optimism, + AaveV3Polygon, + AaveV3Sonic, +} from '@aave-dao/aave-address-book'; +import { SupportedChainId } from '@cowprotocol/cow-sdk'; +import { useQuery } from '@tanstack/react-query'; +import { Contract } from 'ethers'; +import { CustomMarket, MarketDataType } from 'src/ui-config/marketsConfig'; +import { getProvider } from 'src/utils/marketsAndNetworksConfig'; + +import { ADAPTER_FACTORY, FLASH_LOAN_FEE_BPS } from '../constants/cow.constants'; +import { PARASWAP_FLASH_LOAN_FEE_BPS } from '../constants/paraswap.constants'; +import { SwapProvider, SwapType } from '../types'; + +const ACL_MANAGER_ABI = ['function isFlashBorrower(address) view returns (bool)']; + +const ACL_MANAGER_BY_MARKET: Partial> = { + [CustomMarket.proto_mainnet_v3]: AaveV3Ethereum.ACL_MANAGER, + [CustomMarket.proto_lido_v3]: AaveV3EthereumLido.ACL_MANAGER, + [CustomMarket.proto_etherfi_v3]: AaveV3EthereumEtherFi.ACL_MANAGER, + [CustomMarket.proto_arbitrum_v3]: AaveV3Arbitrum.ACL_MANAGER, + [CustomMarket.proto_base_v3]: AaveV3Base.ACL_MANAGER, + [CustomMarket.proto_polygon_v3]: AaveV3Polygon.ACL_MANAGER, + [CustomMarket.proto_optimism_v3]: AaveV3Optimism.ACL_MANAGER, + [CustomMarket.proto_avalanche_v3]: AaveV3Avalanche.ACL_MANAGER, + [CustomMarket.proto_sonic_v3]: AaveV3Sonic.ACL_MANAGER, + [CustomMarket.proto_gnosis_v3]: AaveV3Gnosis.ACL_MANAGER, + [CustomMarket.proto_bnb_v3]: AaveV3BNB.ACL_MANAGER, +}; + +const fallbackBps = (provider: SwapProvider): number => + provider === SwapProvider.COW_PROTOCOL ? FLASH_LOAN_FEE_BPS : PARASWAP_FLASH_LOAN_FEE_BPS; + +const resolveTarget = ( + provider: SwapProvider, + swapType: SwapType, + marketData: MarketDataType +): string | undefined => { + if (provider === SwapProvider.COW_PROTOCOL) { + return ADAPTER_FACTORY[marketData.chainId as unknown as SupportedChainId] || undefined; + } + if (provider === SwapProvider.PARASWAP) { + switch (swapType) { + case SwapType.CollateralSwap: + return marketData.addresses.SWAP_COLLATERAL_ADAPTER; + case SwapType.RepayWithCollateral: + return marketData.addresses.REPAY_WITH_COLLATERAL_ADAPTER; + case SwapType.DebtSwap: + return marketData.addresses.DEBT_SWITCH_ADAPTER; + case SwapType.WithdrawAndSwap: + return marketData.addresses.WITHDRAW_SWITCH_ADAPTER; + default: + return undefined; + } + } + return undefined; +}; + +/** + * Resolve the flashloan fee bps for the active swap, defaulting to the + * provider's hardcoded bps when the on-chain ACLManager check is unavailable + * or pending. Returns 0 only when the target adapter (Paraswap per-flow) / + * factory (CoW) is registered as a FLASH_BORROWER on the market's ACLManager. + */ +export const useFlashLoanFeeBps = ({ + provider, + swapType, + marketData, +}: { + provider: SwapProvider; + swapType: SwapType; + marketData: MarketDataType; +}): number => { + const aclManager = ACL_MANAGER_BY_MARKET[marketData.market]; + const target = resolveTarget(provider, swapType, marketData); + const fallback = fallbackBps(provider); + + const enabled = Boolean(aclManager && target); + + const { data: isWhitelisted } = useQuery({ + queryFn: async (): Promise => { + const contract = new Contract( + aclManager as string, + ACL_MANAGER_ABI, + getProvider(marketData.chainId) + ); + return contract.isFlashBorrower(target); + }, + queryKey: ['flashBorrowerCheck', marketData.chainId, aclManager, target], + enabled, + staleTime: 5 * 60 * 1000, + }); + + return isWhitelisted === true ? 0 : fallback; +}; diff --git a/src/components/transactions/Swap/hooks/useSwapOrderAmounts.ts b/src/components/transactions/Swap/hooks/useSwapOrderAmounts.ts index 9f9e2c18e6..16f67ed625 100644 --- a/src/components/transactions/Swap/hooks/useSwapOrderAmounts.ts +++ b/src/components/transactions/Swap/hooks/useSwapOrderAmounts.ts @@ -3,8 +3,7 @@ import { OrderKind } from '@cowprotocol/cow-sdk'; import { Dispatch, useEffect } from 'react'; import { useRootStore } from 'src/store/root'; -import { COW_PARTNER_FEE, FLASH_LOAN_FEE_BPS } from '../constants/cow.constants'; -import { PARASWAP_FLASH_LOAN_FEE_BPS } from '../constants/paraswap.constants'; +import { COW_PARTNER_FEE } from '../constants/cow.constants'; import { isCowProtocolRates, OrderType, @@ -13,6 +12,7 @@ import { SwapState, SwapType, } from '../types'; +import { useFlashLoanFeeBps } from './useFlashLoanFeeBps'; import { swapTypesThatRequiresInvertedQuote } from './useSwapQuote'; const marketOrderKindPerSwapType: Record = { @@ -27,10 +27,6 @@ const isPositionSwap = (swapType: SwapType, usingFlashloan: boolean) => { return swapType != SwapType.Swap && usingFlashloan; }; -const getFlashLoanFeeBps = (provider: SwapProvider) => { - return provider === SwapProvider.COW_PROTOCOL ? FLASH_LOAN_FEE_BPS : PARASWAP_FLASH_LOAN_FEE_BPS; -}; - /** * Computes normalized sell/buy amounts used to build transactions. * @@ -50,6 +46,12 @@ export const useSwapOrderAmounts = ({ setState: Dispatch>; }) => { const currentMarket = useRootStore((state) => state.currentMarket); + const currentMarketData = useRootStore((store) => store.currentMarketData); + const resolvedFlashLoanFeeBps = useFlashLoanFeeBps({ + provider: state.provider, + swapType: state.swapType, + marketData: currentMarketData, + }); useEffect(() => { if ( @@ -96,7 +98,7 @@ export const useSwapOrderAmounts = ({ // const partnerFeeToken = state.side === 'sell' ? state.destinationToken : state.sourceToken; const flashLoanFeeBps = isPositionSwap(state.swapType, state.useFlashloan ?? false) - ? getFlashLoanFeeBps(state.provider) + ? resolvedFlashLoanFeeBps : 0; const flashLoanFeeAmount = state.side == 'sell' @@ -354,6 +356,7 @@ export const useSwapOrderAmounts = ({ networkFeeAmountInBuyFormatted, partnerFeeAmountFormatted: partnerFeeAmount.toFixed(), flashLoanFeeAmountFormatted: flashLoanFeeAmount.toFixed(), + flashLoanFeeBps, partnerFeeBps: partnetFeeBps, }); }, [ @@ -366,5 +369,6 @@ export const useSwapOrderAmounts = ({ state.swapType, state.orderType, state.useFlashloan, + resolvedFlashLoanFeeBps, ]); }; diff --git a/src/components/transactions/Swap/types/state.types.ts b/src/components/transactions/Swap/types/state.types.ts index f3bff46de9..c9f46c9acb 100644 --- a/src/components/transactions/Swap/types/state.types.ts +++ b/src/components/transactions/Swap/types/state.types.ts @@ -97,6 +97,8 @@ export type TokensSwapState = { partnerFeeAmountFormatted?: string; /** Flash loan fee amount applied to this order, normalized to the fee token units (depends on side). */ flashLoanFeeAmountFormatted?: string; + /** Flash loan fee in basis points used to compute flashLoanFeeAmountFormatted. Resolved on-chain from ACLManager.isFlashBorrower. */ + flashLoanFeeBps?: number; /** Partner fee in basis points used to compute partnerFeeAmountFormatted. */ partnerFeeBps?: number; @@ -287,6 +289,7 @@ export const swapDefaultState: SwapState = { networkFeeAmountInBuyFormatted: '0', partnerFeeAmountFormatted: '0', flashLoanFeeAmountFormatted: '0', + flashLoanFeeBps: 0, partnerFeeBps: 0, limitsOrderButtonBlocked: false, showSlippageWarning: false, From 1f1394c2fd0d3fc15680d74ec0ca434bb2e28f7e Mon Sep 17 00:00:00 2001 From: Martin Grabina Date: Fri, 8 May 2026 11:42:56 -0300 Subject: [PATCH 2/8] fix: gate flashloan-fee readers on resolved on-chain value --- .../CollateralSwapActionsViaCoWAdapters.tsx | 7 ++++--- .../DebtSwap/DebtSwapActionsViaCoW.tsx | 7 ++++--- .../RepayWithCollateralActionsViaCoW.tsx | 7 ++++--- .../Swap/helpers/cow/adapters.helpers.ts | 19 ++++++++++--------- .../helpers/paraswap/flashloan.helpers.ts | 12 +++++++----- .../Swap/hooks/useFlashLoanFeeBps.ts | 17 ++++++++++------- .../Swap/hooks/useSwapOrderAmounts.ts | 6 +++--- 7 files changed, 42 insertions(+), 33 deletions(-) diff --git a/src/components/transactions/Swap/actions/CollateralSwap/CollateralSwapActionsViaCoWAdapters.tsx b/src/components/transactions/Swap/actions/CollateralSwap/CollateralSwapActionsViaCoWAdapters.tsx index 33c5760389..c8e45d05ca 100644 --- a/src/components/transactions/Swap/actions/CollateralSwap/CollateralSwapActionsViaCoWAdapters.tsx +++ b/src/components/transactions/Swap/actions/CollateralSwap/CollateralSwapActionsViaCoWAdapters.tsx @@ -13,7 +13,7 @@ import { saveCowOrderToUserHistory } from 'src/utils/swapAdapterHistory'; import { useShallow } from 'zustand/react/shallow'; import { TrackAnalyticsHandlers } from '../../analytics/useTrackAnalytics'; -import { COW_PARTNER_FEE, FLASH_LOAN_FEE_BPS } from '../../constants/cow.constants'; +import { COW_PARTNER_FEE } from '../../constants/cow.constants'; import { APP_CODE_PER_SWAP_TYPE } from '../../constants/shared.constants'; import { addOrderTypeToAppData, @@ -168,7 +168,8 @@ export const CollateralSwapActionsViaCowAdapters = ({ !state.sellAmountBigInt || !state.sellAmountToken || !state.buyAmountBigInt || - !state.buyAmountToken + !state.buyAmountToken || + state.flashLoanFeeBps === undefined ) return; @@ -189,7 +190,7 @@ export const CollateralSwapActionsViaCowAdapters = ({ : undefined; const { flashLoanFeeAmount, sellAmountToSign } = flashLoanSdk.calculateFlashLoanAmounts({ - flashLoanFeeBps: state.flashLoanFeeBps ?? FLASH_LOAN_FEE_BPS, + flashLoanFeeBps: state.flashLoanFeeBps, sellAmount: state.sellAmountBigInt, }); diff --git a/src/components/transactions/Swap/actions/DebtSwap/DebtSwapActionsViaCoW.tsx b/src/components/transactions/Swap/actions/DebtSwap/DebtSwapActionsViaCoW.tsx index eccc9df775..9b16c3ee31 100644 --- a/src/components/transactions/Swap/actions/DebtSwap/DebtSwapActionsViaCoW.tsx +++ b/src/components/transactions/Swap/actions/DebtSwap/DebtSwapActionsViaCoW.tsx @@ -14,7 +14,7 @@ import { zeroAddress } from 'viem'; import { useShallow } from 'zustand/react/shallow'; import { TrackAnalyticsHandlers } from '../../analytics/useTrackAnalytics'; -import { COW_PARTNER_FEE, FLASH_LOAN_FEE_BPS } from '../../constants/cow.constants'; +import { COW_PARTNER_FEE } from '../../constants/cow.constants'; import { APP_CODE_PER_SWAP_TYPE } from '../../constants/shared.constants'; import { addOrderTypeToAppData, @@ -183,7 +183,8 @@ export const DebtSwapActionsViaCoW = ({ !state.sellAmountBigInt || !state.sellAmountToken || !state.buyAmountBigInt || - !state.buyAmountToken + !state.buyAmountToken || + state.flashLoanFeeBps === undefined ) return; @@ -215,7 +216,7 @@ export const DebtSwapActionsViaCoW = ({ : undefined; const { flashLoanFeeAmount, sellAmountToSign } = flashLoanSdk.calculateFlashLoanAmounts({ - flashLoanFeeBps: state.flashLoanFeeBps ?? FLASH_LOAN_FEE_BPS, + flashLoanFeeBps: state.flashLoanFeeBps, sellAmount: BigInt(sellAmountWithMarginForDustProtection), }); diff --git a/src/components/transactions/Swap/actions/RepayWithCollateral/RepayWithCollateralActionsViaCoW.tsx b/src/components/transactions/Swap/actions/RepayWithCollateral/RepayWithCollateralActionsViaCoW.tsx index cd2758c0cd..ef6f9bfdc9 100644 --- a/src/components/transactions/Swap/actions/RepayWithCollateral/RepayWithCollateralActionsViaCoW.tsx +++ b/src/components/transactions/Swap/actions/RepayWithCollateral/RepayWithCollateralActionsViaCoW.tsx @@ -13,7 +13,7 @@ import { saveCowOrderToUserHistory } from 'src/utils/swapAdapterHistory'; import { useShallow } from 'zustand/react/shallow'; import { TrackAnalyticsHandlers } from '../../analytics/useTrackAnalytics'; -import { COW_PARTNER_FEE, FLASH_LOAN_FEE_BPS } from '../../constants/cow.constants'; +import { COW_PARTNER_FEE } from '../../constants/cow.constants'; import { APP_CODE_PER_SWAP_TYPE } from '../../constants/shared.constants'; import { addOrderTypeToAppData, @@ -181,7 +181,8 @@ export const RepayWithCollateralActionsViaCoW = ({ !state.sellAmountBigInt || !state.sellAmountToken || !state.buyAmountBigInt || - !state.buyAmountToken + !state.buyAmountToken || + state.flashLoanFeeBps === undefined ) return; @@ -213,7 +214,7 @@ export const RepayWithCollateralActionsViaCoW = ({ : undefined; const { flashLoanFeeAmount, sellAmountToSign } = flashLoanSdk.calculateFlashLoanAmounts({ - flashLoanFeeBps: state.flashLoanFeeBps ?? FLASH_LOAN_FEE_BPS, + flashLoanFeeBps: state.flashLoanFeeBps, sellAmount: BigInt(sellAmountWithMarginForDustProtection), }); diff --git a/src/components/transactions/Swap/helpers/cow/adapters.helpers.ts b/src/components/transactions/Swap/helpers/cow/adapters.helpers.ts index 874bb69376..72975352c4 100644 --- a/src/components/transactions/Swap/helpers/cow/adapters.helpers.ts +++ b/src/components/transactions/Swap/helpers/cow/adapters.helpers.ts @@ -16,11 +16,7 @@ import { } from '@cowprotocol/sdk-flash-loans'; import { CustomMarket } from 'src/ui-config/marketsConfig'; -import { - COW_PARTNER_FEE, - DUST_PROTECTION_MULTIPLIER, - FLASH_LOAN_FEE_BPS, -} from '../../constants/cow.constants'; +import { COW_PARTNER_FEE, DUST_PROTECTION_MULTIPLIER } from '../../constants/cow.constants'; import { isCowProtocolRates, OrderType, SwapProvider, SwapState, SwapType } from '../../types'; import { getCowFlashLoanSdk } from './env.helpers'; @@ -53,7 +49,8 @@ export const calculateInstanceAddress = async ({ !state.sellAmountBigInt || !state.buyAmountBigInt || !state.sellAmountToken || - !state.buyAmountToken + !state.buyAmountToken || + state.flashLoanFeeBps === undefined ) return; @@ -92,7 +89,7 @@ export const calculateInstanceAddress = async ({ }; const { flashLoanFeeAmount, sellAmountToSign } = flashLoanSdk.calculateFlashLoanAmounts({ - flashLoanFeeBps: state.flashLoanFeeBps ?? FLASH_LOAN_FEE_BPS, + flashLoanFeeBps: state.flashLoanFeeBps, sellAmount: BigInt(sellAmountWithMarginForDustProtection), }); @@ -171,7 +168,11 @@ export const calculateFlashLoanAmounts = ( finalSellAmount: BigInt(0), }; - if (state.swapType === SwapType.Swap || state.provider !== SwapProvider.COW_PROTOCOL) { + if ( + state.swapType === SwapType.Swap || + state.provider !== SwapProvider.COW_PROTOCOL || + state.flashLoanFeeBps === undefined + ) { return { flashLoanFeeAmount: BigInt(0), finalSellAmount: sellAmount, @@ -180,7 +181,7 @@ export const calculateFlashLoanAmounts = ( const { flashLoanFeeAmount, sellAmountToSign } = flashLoanSdk.calculateFlashLoanAmounts({ sellAmount: sellAmount, - flashLoanFeeBps: state.flashLoanFeeBps ?? FLASH_LOAN_FEE_BPS, + flashLoanFeeBps: state.flashLoanFeeBps, }); return { diff --git a/src/components/transactions/Swap/helpers/paraswap/flashloan.helpers.ts b/src/components/transactions/Swap/helpers/paraswap/flashloan.helpers.ts index 2e12b2f24d..40d4a43117 100644 --- a/src/components/transactions/Swap/helpers/paraswap/flashloan.helpers.ts +++ b/src/components/transactions/Swap/helpers/paraswap/flashloan.helpers.ts @@ -1,11 +1,12 @@ import { valueToBigNumber } from '@aave/math-utils'; -import { PARASWAP_FLASH_LOAN_FEE_BPS } from '../../constants/paraswap.constants'; import { SwapProvider, SwapState, SwapType } from '../../types'; /** * Calculate flashloan fee amount for Paraswap adapter swaps. - * The fee is 0.05% (5 bps) of the flashloan amount, which is the sell amount. + * The fee bps is resolved on-chain via ACLManager.isFlashBorrower; while the + * check is in flight `state.flashLoanFeeBps` is undefined and we return zeros + * so the caller doesn't render a stale value. * * @param state - Swap state * @returns Object containing flashloan fee amount in bigint and formatted string @@ -21,7 +22,8 @@ export const calculateParaswapFlashLoanFee = ( state.swapType === SwapType.Swap || state.provider !== SwapProvider.PARASWAP || !state.useFlashloan || - !state.sellAmountBigInt + !state.sellAmountBigInt || + state.flashLoanFeeBps === undefined ) { return { flashLoanFeeAmount: BigInt(0), @@ -31,8 +33,8 @@ export const calculateParaswapFlashLoanFee = ( // Calculate fee: flashloan amount * fee bps / 10000 // The flashloan amount is the sell amount (collateral being swapped) - const feeBps = state.flashLoanFeeBps ?? PARASWAP_FLASH_LOAN_FEE_BPS; - const flashLoanFeeAmount = (state.sellAmountBigInt * BigInt(feeBps)) / BigInt(10000); + const flashLoanFeeAmount = + (state.sellAmountBigInt * BigInt(state.flashLoanFeeBps)) / BigInt(10000); // Format the fee amount const flashLoanFeeFormatted = valueToBigNumber(flashLoanFeeAmount.toString()) diff --git a/src/components/transactions/Swap/hooks/useFlashLoanFeeBps.ts b/src/components/transactions/Swap/hooks/useFlashLoanFeeBps.ts index aee2d69821..b8da9f9366 100644 --- a/src/components/transactions/Swap/hooks/useFlashLoanFeeBps.ts +++ b/src/components/transactions/Swap/hooks/useFlashLoanFeeBps.ts @@ -66,10 +66,11 @@ const resolveTarget = ( }; /** - * Resolve the flashloan fee bps for the active swap, defaulting to the - * provider's hardcoded bps when the on-chain ACLManager check is unavailable - * or pending. Returns 0 only when the target adapter (Paraswap per-flow) / - * factory (CoW) is registered as a FLASH_BORROWER on the market's ACLManager. + * Resolve the flashloan fee bps for the active swap by checking the market's + * ACLManager.isFlashBorrower for the provider's target address. Returns 0 when + * the target is whitelisted, the provider's default bps when it's not, and + * `undefined` while the on-chain query is in flight so callers can hold off + * computing order amounts until the answer is in. */ export const useFlashLoanFeeBps = ({ provider, @@ -79,10 +80,10 @@ export const useFlashLoanFeeBps = ({ provider: SwapProvider; swapType: SwapType; marketData: MarketDataType; -}): number => { +}): number | undefined => { const aclManager = ACL_MANAGER_BY_MARKET[marketData.market]; const target = resolveTarget(provider, swapType, marketData); - const fallback = fallbackBps(provider); + const defaultBps = fallbackBps(provider); const enabled = Boolean(aclManager && target); @@ -100,5 +101,7 @@ export const useFlashLoanFeeBps = ({ staleTime: 5 * 60 * 1000, }); - return isWhitelisted === true ? 0 : fallback; + if (!enabled) return defaultBps; + if (isWhitelisted === undefined) return undefined; + return isWhitelisted ? 0 : defaultBps; }; diff --git a/src/components/transactions/Swap/hooks/useSwapOrderAmounts.ts b/src/components/transactions/Swap/hooks/useSwapOrderAmounts.ts index 16f67ed625..d7c0ebb472 100644 --- a/src/components/transactions/Swap/hooks/useSwapOrderAmounts.ts +++ b/src/components/transactions/Swap/hooks/useSwapOrderAmounts.ts @@ -97,9 +97,9 @@ export const useSwapOrderAmounts = ({ : valueToBigNumber(state.inputAmount).multipliedBy(partnetFeeBps).dividedBy(10000); // const partnerFeeToken = state.side === 'sell' ? state.destinationToken : state.sourceToken; - const flashLoanFeeBps = isPositionSwap(state.swapType, state.useFlashloan ?? false) - ? resolvedFlashLoanFeeBps - : 0; + const usingFlashloan = isPositionSwap(state.swapType, state.useFlashloan ?? false); + if (usingFlashloan && resolvedFlashLoanFeeBps === undefined) return; + const flashLoanFeeBps = usingFlashloan ? (resolvedFlashLoanFeeBps as number) : 0; const flashLoanFeeAmount = state.side == 'sell' ? valueToBigNumber(state.outputAmount).multipliedBy(flashLoanFeeBps).dividedBy(10000) From 3e7f14b1d00126a1cc530acd03dd3e7c07756ca4 Mon Sep 17 00:00:00 2001 From: Martin Grabina Date: Fri, 8 May 2026 11:50:09 -0300 Subject: [PATCH 3/8] fix: throw on swap submit when flashloan fee unresolved --- .../CollateralSwap/CollateralSwapActionsViaCoWAdapters.tsx | 7 +++++-- .../CollateralSwapActionsViaParaswapAdapters.tsx | 4 ++++ .../Swap/actions/DebtSwap/DebtSwapActionsViaCoW.tsx | 7 +++++-- .../Swap/actions/DebtSwap/DebtSwapActionsViaParaswap.tsx | 4 ++++ .../RepayWithCollateralActionsViaCoW.tsx | 7 +++++-- .../RepayWithCollateralActionsViaParaswap.tsx | 4 ++++ .../WithdrawAndSwap/WithdrawAndSwapActionsViaParaswap.tsx | 4 ++++ .../transactions/Swap/hooks/useFlashLoanFeeBps.ts | 7 ++++--- 8 files changed, 35 insertions(+), 9 deletions(-) diff --git a/src/components/transactions/Swap/actions/CollateralSwap/CollateralSwapActionsViaCoWAdapters.tsx b/src/components/transactions/Swap/actions/CollateralSwap/CollateralSwapActionsViaCoWAdapters.tsx index c8e45d05ca..2200d25cb0 100644 --- a/src/components/transactions/Swap/actions/CollateralSwap/CollateralSwapActionsViaCoWAdapters.tsx +++ b/src/components/transactions/Swap/actions/CollateralSwap/CollateralSwapActionsViaCoWAdapters.tsx @@ -168,11 +168,14 @@ export const CollateralSwapActionsViaCowAdapters = ({ !state.sellAmountBigInt || !state.sellAmountToken || !state.buyAmountBigInt || - !state.buyAmountToken || - state.flashLoanFeeBps === undefined + !state.buyAmountToken ) return; + if (state.flashLoanFeeBps === undefined) { + throw new Error('Flashloan fee unavailable: on-chain ACLManager check has not resolved.'); + } + const tradingSdk = await getCowTradingSdkByChainIdAndAppCode( state.chainId, APP_CODE_PER_SWAP_TYPE[state.swapType] diff --git a/src/components/transactions/Swap/actions/CollateralSwap/CollateralSwapActionsViaParaswapAdapters.tsx b/src/components/transactions/Swap/actions/CollateralSwap/CollateralSwapActionsViaParaswapAdapters.tsx index c8567ed466..894feb8386 100644 --- a/src/components/transactions/Swap/actions/CollateralSwap/CollateralSwapActionsViaParaswapAdapters.tsx +++ b/src/components/transactions/Swap/actions/CollateralSwap/CollateralSwapActionsViaParaswapAdapters.tsx @@ -81,6 +81,10 @@ export const CollateralSwapActionsViaParaswapAdapters = ({ if (!state.swapRate || !isParaswapRates(state.swapRate)) throw new Error('Route required to build transaction'); + if (state.flashLoanFeeBps === undefined) { + throw new Error('Flashloan fee unavailable: on-chain ACLManager check has not resolved.'); + } + setMainTxState({ txHash: undefined, loading: true, diff --git a/src/components/transactions/Swap/actions/DebtSwap/DebtSwapActionsViaCoW.tsx b/src/components/transactions/Swap/actions/DebtSwap/DebtSwapActionsViaCoW.tsx index 9b16c3ee31..6e7e4de33d 100644 --- a/src/components/transactions/Swap/actions/DebtSwap/DebtSwapActionsViaCoW.tsx +++ b/src/components/transactions/Swap/actions/DebtSwap/DebtSwapActionsViaCoW.tsx @@ -183,11 +183,14 @@ export const DebtSwapActionsViaCoW = ({ !state.sellAmountBigInt || !state.sellAmountToken || !state.buyAmountBigInt || - !state.buyAmountToken || - state.flashLoanFeeBps === undefined + !state.buyAmountToken ) return; + if (state.flashLoanFeeBps === undefined) { + throw new Error('Flashloan fee unavailable: on-chain ACLManager check has not resolved.'); + } + const tradingSdk = await getCowTradingSdkByChainIdAndAppCode( state.chainId, APP_CODE_PER_SWAP_TYPE[state.swapType] diff --git a/src/components/transactions/Swap/actions/DebtSwap/DebtSwapActionsViaParaswap.tsx b/src/components/transactions/Swap/actions/DebtSwap/DebtSwapActionsViaParaswap.tsx index 0c8f21bbdd..dcf7b0902e 100644 --- a/src/components/transactions/Swap/actions/DebtSwap/DebtSwapActionsViaParaswap.tsx +++ b/src/components/transactions/Swap/actions/DebtSwap/DebtSwapActionsViaParaswap.tsx @@ -104,6 +104,10 @@ export const DebtSwapActionsViaParaswap = ({ throw new Error('No swap rate found'); } + if (state.flashLoanFeeBps === undefined) { + throw new Error('Flashloan fee unavailable: on-chain ACLManager check has not resolved.'); + } + const inferredKind = state.swapRate.optimalRateData.side === 'SELL' ? 'sell' : 'buy'; // CallData for ParaswapRoute, which is inversed to the actual swap (dest -> src) diff --git a/src/components/transactions/Swap/actions/RepayWithCollateral/RepayWithCollateralActionsViaCoW.tsx b/src/components/transactions/Swap/actions/RepayWithCollateral/RepayWithCollateralActionsViaCoW.tsx index ef6f9bfdc9..4ca5affaa3 100644 --- a/src/components/transactions/Swap/actions/RepayWithCollateral/RepayWithCollateralActionsViaCoW.tsx +++ b/src/components/transactions/Swap/actions/RepayWithCollateral/RepayWithCollateralActionsViaCoW.tsx @@ -181,11 +181,14 @@ export const RepayWithCollateralActionsViaCoW = ({ !state.sellAmountBigInt || !state.sellAmountToken || !state.buyAmountBigInt || - !state.buyAmountToken || - state.flashLoanFeeBps === undefined + !state.buyAmountToken ) return; + if (state.flashLoanFeeBps === undefined) { + throw new Error('Flashloan fee unavailable: on-chain ACLManager check has not resolved.'); + } + const tradingSdk = await getCowTradingSdkByChainIdAndAppCode( state.chainId, APP_CODE_PER_SWAP_TYPE[state.swapType] diff --git a/src/components/transactions/Swap/actions/RepayWithCollateral/RepayWithCollateralActionsViaParaswap.tsx b/src/components/transactions/Swap/actions/RepayWithCollateral/RepayWithCollateralActionsViaParaswap.tsx index 95a190db2c..bbae52ec66 100644 --- a/src/components/transactions/Swap/actions/RepayWithCollateral/RepayWithCollateralActionsViaParaswap.tsx +++ b/src/components/transactions/Swap/actions/RepayWithCollateral/RepayWithCollateralActionsViaParaswap.tsx @@ -103,6 +103,10 @@ export const RepayWithCollateralActionsViaParaswap = ({ if (!state.swapRate || !isParaswapRates(state.swapRate)) throw new Error('Route required to build transaction'); + if (state.flashLoanFeeBps === undefined) { + throw new Error('Flashloan fee unavailable: on-chain ACLManager check has not resolved.'); + } + setMainTxState({ txHash: undefined, loading: true, diff --git a/src/components/transactions/Swap/actions/WithdrawAndSwap/WithdrawAndSwapActionsViaParaswap.tsx b/src/components/transactions/Swap/actions/WithdrawAndSwap/WithdrawAndSwapActionsViaParaswap.tsx index 60c62fa3dc..3eb222ec88 100644 --- a/src/components/transactions/Swap/actions/WithdrawAndSwap/WithdrawAndSwapActionsViaParaswap.tsx +++ b/src/components/transactions/Swap/actions/WithdrawAndSwap/WithdrawAndSwapActionsViaParaswap.tsx @@ -82,6 +82,10 @@ export const WithdrawAndSwapActionsViaParaswap = ({ return; } + if (state.flashLoanFeeBps === undefined) { + throw new Error('Flashloan fee unavailable: on-chain ACLManager check has not resolved.'); + } + try { setMainTxState({ ...mainTxState, loading: true }); const { swapCallData, augustus } = await getTransactionParams( diff --git a/src/components/transactions/Swap/hooks/useFlashLoanFeeBps.ts b/src/components/transactions/Swap/hooks/useFlashLoanFeeBps.ts index b8da9f9366..e9b27a60a5 100644 --- a/src/components/transactions/Swap/hooks/useFlashLoanFeeBps.ts +++ b/src/components/transactions/Swap/hooks/useFlashLoanFeeBps.ts @@ -69,8 +69,9 @@ const resolveTarget = ( * Resolve the flashloan fee bps for the active swap by checking the market's * ACLManager.isFlashBorrower for the provider's target address. Returns 0 when * the target is whitelisted, the provider's default bps when it's not, and - * `undefined` while the on-chain query is in flight so callers can hold off - * computing order amounts until the answer is in. + * `undefined` whenever we don't have an on-chain answer (query pending, + * ACLManager unmapped for the market, or no target address resolvable). The + * caller MUST refuse to submit a transaction while the value is undefined. */ export const useFlashLoanFeeBps = ({ provider, @@ -101,7 +102,7 @@ export const useFlashLoanFeeBps = ({ staleTime: 5 * 60 * 1000, }); - if (!enabled) return defaultBps; + if (!enabled) return undefined; if (isWhitelisted === undefined) return undefined; return isWhitelisted ? 0 : defaultBps; }; From ed687af04be3b9b06fed57b1a6e473a424c9dea2 Mon Sep 17 00:00:00 2001 From: Martin Grabina Date: Fri, 8 May 2026 12:27:23 -0300 Subject: [PATCH 4/8] chore: instrument useFlashLoanFeeBps for preview debugging --- .gitignore | 1 + .../Swap/hooks/useFlashLoanFeeBps.ts | 40 ++++++++++++++++--- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 1d7c8b4031..b56c837373 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,4 @@ package-lock.json # Sentry Config File .env.sentry-build-plugin +.env*.local diff --git a/src/components/transactions/Swap/hooks/useFlashLoanFeeBps.ts b/src/components/transactions/Swap/hooks/useFlashLoanFeeBps.ts index e9b27a60a5..f3cc81a600 100644 --- a/src/components/transactions/Swap/hooks/useFlashLoanFeeBps.ts +++ b/src/components/transactions/Swap/hooks/useFlashLoanFeeBps.ts @@ -88,21 +88,51 @@ export const useFlashLoanFeeBps = ({ const enabled = Boolean(aclManager && target); - const { data: isWhitelisted } = useQuery({ + const { + data: isWhitelisted, + status, + error, + } = useQuery({ queryFn: async (): Promise => { const contract = new Contract( aclManager as string, ACL_MANAGER_ABI, getProvider(marketData.chainId) ); - return contract.isFlashBorrower(target); + const result: boolean = await contract.isFlashBorrower(target); + console.log('[FlashLoanFeeBps] queryFn result', { + chainId: marketData.chainId, + market: marketData.market, + provider, + swapType, + aclManager, + target, + isFlashBorrower: result, + }); + return result; }, queryKey: ['flashBorrowerCheck', marketData.chainId, aclManager, target], enabled, staleTime: 5 * 60 * 1000, }); - if (!enabled) return undefined; - if (isWhitelisted === undefined) return undefined; - return isWhitelisted ? 0 : defaultBps; + const effective = + !enabled || isWhitelisted === undefined ? undefined : isWhitelisted ? 0 : defaultBps; + + console.log('[FlashLoanFeeBps] resolve', { + chainId: marketData.chainId, + market: marketData.market, + provider, + swapType, + aclManager, + target, + enabled, + queryStatus: status, + queryError: error?.message, + isWhitelisted, + defaultBps, + effective, + }); + + return effective; }; From 435845689288b3ec4bbdde9bf1c6dc586e7ff088 Mon Sep 17 00:00:00 2001 From: Martin Grabina Date: Fri, 8 May 2026 13:27:44 -0300 Subject: [PATCH 5/8] chore: log useFlashloan + applied bps in swap order amounts --- .../transactions/Swap/hooks/useSwapOrderAmounts.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/transactions/Swap/hooks/useSwapOrderAmounts.ts b/src/components/transactions/Swap/hooks/useSwapOrderAmounts.ts index d7c0ebb472..420a0af579 100644 --- a/src/components/transactions/Swap/hooks/useSwapOrderAmounts.ts +++ b/src/components/transactions/Swap/hooks/useSwapOrderAmounts.ts @@ -100,6 +100,14 @@ export const useSwapOrderAmounts = ({ const usingFlashloan = isPositionSwap(state.swapType, state.useFlashloan ?? false); if (usingFlashloan && resolvedFlashLoanFeeBps === undefined) return; const flashLoanFeeBps = usingFlashloan ? (resolvedFlashLoanFeeBps as number) : 0; + console.log('[FlashLoanFeeBps] useSwapOrderAmounts', { + swapType: state.swapType, + provider: state.provider, + useFlashloanRaw: state.useFlashloan, + isPositionSwap: usingFlashloan, + resolvedFlashLoanFeeBps, + flashLoanFeeBpsApplied: flashLoanFeeBps, + }); const flashLoanFeeAmount = state.side == 'sell' ? valueToBigNumber(state.outputAmount).multipliedBy(flashLoanFeeBps).dividedBy(10000) From 864ad9a9626dbcb465819de8dfa234afad79a46c Mon Sep 17 00:00:00 2001 From: Martin Grabina Date: Fri, 8 May 2026 13:43:55 -0300 Subject: [PATCH 6/8] chore: remove preview debug logs --- .../Swap/hooks/useFlashLoanFeeBps.ts | 40 +++---------------- .../Swap/hooks/useSwapOrderAmounts.ts | 8 ---- 2 files changed, 5 insertions(+), 43 deletions(-) diff --git a/src/components/transactions/Swap/hooks/useFlashLoanFeeBps.ts b/src/components/transactions/Swap/hooks/useFlashLoanFeeBps.ts index f3cc81a600..e9b27a60a5 100644 --- a/src/components/transactions/Swap/hooks/useFlashLoanFeeBps.ts +++ b/src/components/transactions/Swap/hooks/useFlashLoanFeeBps.ts @@ -88,51 +88,21 @@ export const useFlashLoanFeeBps = ({ const enabled = Boolean(aclManager && target); - const { - data: isWhitelisted, - status, - error, - } = useQuery({ + const { data: isWhitelisted } = useQuery({ queryFn: async (): Promise => { const contract = new Contract( aclManager as string, ACL_MANAGER_ABI, getProvider(marketData.chainId) ); - const result: boolean = await contract.isFlashBorrower(target); - console.log('[FlashLoanFeeBps] queryFn result', { - chainId: marketData.chainId, - market: marketData.market, - provider, - swapType, - aclManager, - target, - isFlashBorrower: result, - }); - return result; + return contract.isFlashBorrower(target); }, queryKey: ['flashBorrowerCheck', marketData.chainId, aclManager, target], enabled, staleTime: 5 * 60 * 1000, }); - const effective = - !enabled || isWhitelisted === undefined ? undefined : isWhitelisted ? 0 : defaultBps; - - console.log('[FlashLoanFeeBps] resolve', { - chainId: marketData.chainId, - market: marketData.market, - provider, - swapType, - aclManager, - target, - enabled, - queryStatus: status, - queryError: error?.message, - isWhitelisted, - defaultBps, - effective, - }); - - return effective; + if (!enabled) return undefined; + if (isWhitelisted === undefined) return undefined; + return isWhitelisted ? 0 : defaultBps; }; diff --git a/src/components/transactions/Swap/hooks/useSwapOrderAmounts.ts b/src/components/transactions/Swap/hooks/useSwapOrderAmounts.ts index 420a0af579..d7c0ebb472 100644 --- a/src/components/transactions/Swap/hooks/useSwapOrderAmounts.ts +++ b/src/components/transactions/Swap/hooks/useSwapOrderAmounts.ts @@ -100,14 +100,6 @@ export const useSwapOrderAmounts = ({ const usingFlashloan = isPositionSwap(state.swapType, state.useFlashloan ?? false); if (usingFlashloan && resolvedFlashLoanFeeBps === undefined) return; const flashLoanFeeBps = usingFlashloan ? (resolvedFlashLoanFeeBps as number) : 0; - console.log('[FlashLoanFeeBps] useSwapOrderAmounts', { - swapType: state.swapType, - provider: state.provider, - useFlashloanRaw: state.useFlashloan, - isPositionSwap: usingFlashloan, - resolvedFlashLoanFeeBps, - flashLoanFeeBpsApplied: flashLoanFeeBps, - }); const flashLoanFeeAmount = state.side == 'sell' ? valueToBigNumber(state.outputAmount).multipliedBy(flashLoanFeeBps).dividedBy(10000) From f888f5728df472a0d6fd6740b73965101180baef Mon Sep 17 00:00:00 2001 From: Martin Grabina Date: Fri, 8 May 2026 14:00:31 -0300 Subject: [PATCH 7/8] fix: clear flashloan fee on pending; map Linea + Plasma ACLManagers --- .../transactions/Swap/hooks/useFlashLoanFeeBps.ts | 4 ++++ .../transactions/Swap/hooks/useSwapOrderAmounts.ts | 6 +++++- src/components/transactions/Swap/types/state.types.ts | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/transactions/Swap/hooks/useFlashLoanFeeBps.ts b/src/components/transactions/Swap/hooks/useFlashLoanFeeBps.ts index e9b27a60a5..3208c988b8 100644 --- a/src/components/transactions/Swap/hooks/useFlashLoanFeeBps.ts +++ b/src/components/transactions/Swap/hooks/useFlashLoanFeeBps.ts @@ -7,7 +7,9 @@ import { AaveV3EthereumEtherFi, AaveV3EthereumLido, AaveV3Gnosis, + AaveV3Linea, AaveV3Optimism, + AaveV3Plasma, AaveV3Polygon, AaveV3Sonic, } from '@aave-dao/aave-address-book'; @@ -35,6 +37,8 @@ const ACL_MANAGER_BY_MARKET: Partial> = { [CustomMarket.proto_sonic_v3]: AaveV3Sonic.ACL_MANAGER, [CustomMarket.proto_gnosis_v3]: AaveV3Gnosis.ACL_MANAGER, [CustomMarket.proto_bnb_v3]: AaveV3BNB.ACL_MANAGER, + [CustomMarket.proto_linea_v3]: AaveV3Linea.ACL_MANAGER, + [CustomMarket.proto_plasma_v3]: AaveV3Plasma.ACL_MANAGER, }; const fallbackBps = (provider: SwapProvider): number => diff --git a/src/components/transactions/Swap/hooks/useSwapOrderAmounts.ts b/src/components/transactions/Swap/hooks/useSwapOrderAmounts.ts index d7c0ebb472..51dd323fa2 100644 --- a/src/components/transactions/Swap/hooks/useSwapOrderAmounts.ts +++ b/src/components/transactions/Swap/hooks/useSwapOrderAmounts.ts @@ -98,7 +98,11 @@ export const useSwapOrderAmounts = ({ // const partnerFeeToken = state.side === 'sell' ? state.destinationToken : state.sourceToken; const usingFlashloan = isPositionSwap(state.swapType, state.useFlashloan ?? false); - if (usingFlashloan && resolvedFlashLoanFeeBps === undefined) return; + if (usingFlashloan && resolvedFlashLoanFeeBps === undefined) { + // Clear any stale fee from a previous render so submit handlers see the loading state. + if (state.flashLoanFeeBps !== undefined) setState({ flashLoanFeeBps: undefined }); + return; + } const flashLoanFeeBps = usingFlashloan ? (resolvedFlashLoanFeeBps as number) : 0; const flashLoanFeeAmount = state.side == 'sell' diff --git a/src/components/transactions/Swap/types/state.types.ts b/src/components/transactions/Swap/types/state.types.ts index c9f46c9acb..0a1a853ca7 100644 --- a/src/components/transactions/Swap/types/state.types.ts +++ b/src/components/transactions/Swap/types/state.types.ts @@ -289,7 +289,7 @@ export const swapDefaultState: SwapState = { networkFeeAmountInBuyFormatted: '0', partnerFeeAmountFormatted: '0', flashLoanFeeAmountFormatted: '0', - flashLoanFeeBps: 0, + flashLoanFeeBps: undefined, partnerFeeBps: 0, limitsOrderButtonBlocked: false, showSlippageWarning: false, From 13f32adb0b487a1f56899f6919c42617a7a35293 Mon Sep 17 00:00:00 2001 From: Martin Grabina Date: Fri, 8 May 2026 14:13:30 -0300 Subject: [PATCH 8/8] refactor: skip ACLManager check for Paraswap (msg.sender is user) --- ...llateralSwapActionsViaParaswapAdapters.tsx | 4 -- .../DebtSwap/DebtSwapActionsViaParaswap.tsx | 4 -- .../RepayWithCollateralActionsViaParaswap.tsx | 4 -- .../WithdrawAndSwapActionsViaParaswap.tsx | 4 -- .../Swap/hooks/useFlashLoanFeeBps.ts | 62 +++++++------------ 5 files changed, 24 insertions(+), 54 deletions(-) diff --git a/src/components/transactions/Swap/actions/CollateralSwap/CollateralSwapActionsViaParaswapAdapters.tsx b/src/components/transactions/Swap/actions/CollateralSwap/CollateralSwapActionsViaParaswapAdapters.tsx index 894feb8386..c8567ed466 100644 --- a/src/components/transactions/Swap/actions/CollateralSwap/CollateralSwapActionsViaParaswapAdapters.tsx +++ b/src/components/transactions/Swap/actions/CollateralSwap/CollateralSwapActionsViaParaswapAdapters.tsx @@ -81,10 +81,6 @@ export const CollateralSwapActionsViaParaswapAdapters = ({ if (!state.swapRate || !isParaswapRates(state.swapRate)) throw new Error('Route required to build transaction'); - if (state.flashLoanFeeBps === undefined) { - throw new Error('Flashloan fee unavailable: on-chain ACLManager check has not resolved.'); - } - setMainTxState({ txHash: undefined, loading: true, diff --git a/src/components/transactions/Swap/actions/DebtSwap/DebtSwapActionsViaParaswap.tsx b/src/components/transactions/Swap/actions/DebtSwap/DebtSwapActionsViaParaswap.tsx index dcf7b0902e..0c8f21bbdd 100644 --- a/src/components/transactions/Swap/actions/DebtSwap/DebtSwapActionsViaParaswap.tsx +++ b/src/components/transactions/Swap/actions/DebtSwap/DebtSwapActionsViaParaswap.tsx @@ -104,10 +104,6 @@ export const DebtSwapActionsViaParaswap = ({ throw new Error('No swap rate found'); } - if (state.flashLoanFeeBps === undefined) { - throw new Error('Flashloan fee unavailable: on-chain ACLManager check has not resolved.'); - } - const inferredKind = state.swapRate.optimalRateData.side === 'SELL' ? 'sell' : 'buy'; // CallData for ParaswapRoute, which is inversed to the actual swap (dest -> src) diff --git a/src/components/transactions/Swap/actions/RepayWithCollateral/RepayWithCollateralActionsViaParaswap.tsx b/src/components/transactions/Swap/actions/RepayWithCollateral/RepayWithCollateralActionsViaParaswap.tsx index bbae52ec66..95a190db2c 100644 --- a/src/components/transactions/Swap/actions/RepayWithCollateral/RepayWithCollateralActionsViaParaswap.tsx +++ b/src/components/transactions/Swap/actions/RepayWithCollateral/RepayWithCollateralActionsViaParaswap.tsx @@ -103,10 +103,6 @@ export const RepayWithCollateralActionsViaParaswap = ({ if (!state.swapRate || !isParaswapRates(state.swapRate)) throw new Error('Route required to build transaction'); - if (state.flashLoanFeeBps === undefined) { - throw new Error('Flashloan fee unavailable: on-chain ACLManager check has not resolved.'); - } - setMainTxState({ txHash: undefined, loading: true, diff --git a/src/components/transactions/Swap/actions/WithdrawAndSwap/WithdrawAndSwapActionsViaParaswap.tsx b/src/components/transactions/Swap/actions/WithdrawAndSwap/WithdrawAndSwapActionsViaParaswap.tsx index 3eb222ec88..60c62fa3dc 100644 --- a/src/components/transactions/Swap/actions/WithdrawAndSwap/WithdrawAndSwapActionsViaParaswap.tsx +++ b/src/components/transactions/Swap/actions/WithdrawAndSwap/WithdrawAndSwapActionsViaParaswap.tsx @@ -82,10 +82,6 @@ export const WithdrawAndSwapActionsViaParaswap = ({ return; } - if (state.flashLoanFeeBps === undefined) { - throw new Error('Flashloan fee unavailable: on-chain ACLManager check has not resolved.'); - } - try { setMainTxState({ ...mainTxState, loading: true }); const { swapCallData, augustus } = await getTransactionParams( diff --git a/src/components/transactions/Swap/hooks/useFlashLoanFeeBps.ts b/src/components/transactions/Swap/hooks/useFlashLoanFeeBps.ts index 3208c988b8..6a67e6078a 100644 --- a/src/components/transactions/Swap/hooks/useFlashLoanFeeBps.ts +++ b/src/components/transactions/Swap/hooks/useFlashLoanFeeBps.ts @@ -25,6 +25,9 @@ import { SwapProvider, SwapType } from '../types'; const ACL_MANAGER_ABI = ['function isFlashBorrower(address) view returns (bool)']; +// CoW per-market ACLManager addresses. Only CoW flows do the on-chain lookup — +// Paraswap flows always have the user EOA as msg.sender to Pool.flashLoan, so +// they pay the default premium and don't need a check. const ACL_MANAGER_BY_MARKET: Partial> = { [CustomMarket.proto_mainnet_v3]: AaveV3Ethereum.ACL_MANAGER, [CustomMarket.proto_lido_v3]: AaveV3EthereumLido.ACL_MANAGER, @@ -41,41 +44,16 @@ const ACL_MANAGER_BY_MARKET: Partial> = { [CustomMarket.proto_plasma_v3]: AaveV3Plasma.ACL_MANAGER, }; -const fallbackBps = (provider: SwapProvider): number => - provider === SwapProvider.COW_PROTOCOL ? FLASH_LOAN_FEE_BPS : PARASWAP_FLASH_LOAN_FEE_BPS; - -const resolveTarget = ( - provider: SwapProvider, - swapType: SwapType, - marketData: MarketDataType -): string | undefined => { - if (provider === SwapProvider.COW_PROTOCOL) { - return ADAPTER_FACTORY[marketData.chainId as unknown as SupportedChainId] || undefined; - } - if (provider === SwapProvider.PARASWAP) { - switch (swapType) { - case SwapType.CollateralSwap: - return marketData.addresses.SWAP_COLLATERAL_ADAPTER; - case SwapType.RepayWithCollateral: - return marketData.addresses.REPAY_WITH_COLLATERAL_ADAPTER; - case SwapType.DebtSwap: - return marketData.addresses.DEBT_SWITCH_ADAPTER; - case SwapType.WithdrawAndSwap: - return marketData.addresses.WITHDRAW_SWITCH_ADAPTER; - default: - return undefined; - } - } - return undefined; -}; - /** - * Resolve the flashloan fee bps for the active swap by checking the market's - * ACLManager.isFlashBorrower for the provider's target address. Returns 0 when - * the target is whitelisted, the provider's default bps when it's not, and - * `undefined` whenever we don't have an on-chain answer (query pending, - * ACLManager unmapped for the market, or no target address resolvable). The - * caller MUST refuse to submit a transaction while the value is undefined. + * Resolve the flashloan fee bps for the active swap. + * + * - CoW: msg.sender to Pool.flashLoan is the CoW factory. Check + * ACLManager.isFlashBorrower(factory) on chain. Returns 0 when whitelisted, + * FLASH_LOAN_FEE_BPS when not, and `undefined` while the query is pending or + * when we can't run the check (factory or ACLManager not mapped for the + * chain). Submit handlers MUST refuse to send while the value is undefined. + * - Paraswap: msg.sender is always the user EOA, so the role can never apply. + * Returns the constant immediately, no on-chain call. */ export const useFlashLoanFeeBps = ({ provider, @@ -86,11 +64,13 @@ export const useFlashLoanFeeBps = ({ swapType: SwapType; marketData: MarketDataType; }): number | undefined => { + const isCow = provider === SwapProvider.COW_PROTOCOL; const aclManager = ACL_MANAGER_BY_MARKET[marketData.market]; - const target = resolveTarget(provider, swapType, marketData); - const defaultBps = fallbackBps(provider); + const target = isCow + ? ADAPTER_FACTORY[marketData.chainId as unknown as SupportedChainId] || undefined + : undefined; - const enabled = Boolean(aclManager && target); + const enabled = isCow && Boolean(aclManager && target); const { data: isWhitelisted } = useQuery({ queryFn: async (): Promise => { @@ -106,7 +86,13 @@ export const useFlashLoanFeeBps = ({ staleTime: 5 * 60 * 1000, }); + if (!isCow) { + // Acknowledge swapType to keep the dep narrow even though Paraswap + // doesn't branch on it for fee resolution. + void swapType; + return PARASWAP_FLASH_LOAN_FEE_BPS; + } if (!enabled) return undefined; if (isWhitelisted === undefined) return undefined; - return isWhitelisted ? 0 : defaultBps; + return isWhitelisted ? 0 : FLASH_LOAN_FEE_BPS; };