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/actions/CollateralSwap/CollateralSwapActionsViaCoWAdapters.tsx b/src/components/transactions/Swap/actions/CollateralSwap/CollateralSwapActionsViaCoWAdapters.tsx index 07e1f32d15..2200d25cb0 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, @@ -172,6 +172,10 @@ export const CollateralSwapActionsViaCowAdapters = ({ ) 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] @@ -189,7 +193,7 @@ export const CollateralSwapActionsViaCowAdapters = ({ : undefined; const { flashLoanFeeAmount, sellAmountToSign } = flashLoanSdk.calculateFlashLoanAmounts({ - 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 9eb7782c2c..6e7e4de33d 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, @@ -187,6 +187,10 @@ export const DebtSwapActionsViaCoW = ({ ) 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] @@ -215,7 +219,7 @@ export const DebtSwapActionsViaCoW = ({ : undefined; const { flashLoanFeeAmount, sellAmountToSign } = flashLoanSdk.calculateFlashLoanAmounts({ - 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 b7e9051466..4ca5affaa3 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, @@ -185,6 +185,10 @@ export const RepayWithCollateralActionsViaCoW = ({ ) 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] @@ -213,7 +217,7 @@ export const RepayWithCollateralActionsViaCoW = ({ : undefined; const { flashLoanFeeAmount, sellAmountToSign } = flashLoanSdk.calculateFlashLoanAmounts({ - 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 b8e78231cf..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: 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: 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 1bb122358c..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), @@ -32,7 +34,7 @@ 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); + (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 new file mode 100644 index 0000000000..6a67e6078a --- /dev/null +++ b/src/components/transactions/Swap/hooks/useFlashLoanFeeBps.ts @@ -0,0 +1,98 @@ +import { + AaveV3Arbitrum, + AaveV3Avalanche, + AaveV3Base, + AaveV3BNB, + AaveV3Ethereum, + AaveV3EthereumEtherFi, + AaveV3EthereumLido, + AaveV3Gnosis, + AaveV3Linea, + AaveV3Optimism, + AaveV3Plasma, + 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)']; + +// 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, + [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, + [CustomMarket.proto_linea_v3]: AaveV3Linea.ACL_MANAGER, + [CustomMarket.proto_plasma_v3]: AaveV3Plasma.ACL_MANAGER, +}; + +/** + * 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, + swapType, + marketData, +}: { + provider: SwapProvider; + swapType: SwapType; + marketData: MarketDataType; +}): number | undefined => { + const isCow = provider === SwapProvider.COW_PROTOCOL; + const aclManager = ACL_MANAGER_BY_MARKET[marketData.market]; + const target = isCow + ? ADAPTER_FACTORY[marketData.chainId as unknown as SupportedChainId] || undefined + : undefined; + + const enabled = isCow && 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, + }); + + 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 : FLASH_LOAN_FEE_BPS; +}; diff --git a/src/components/transactions/Swap/hooks/useSwapOrderAmounts.ts b/src/components/transactions/Swap/hooks/useSwapOrderAmounts.ts index 9f9e2c18e6..51dd323fa2 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 ( @@ -95,9 +97,13 @@ 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) - ? getFlashLoanFeeBps(state.provider) - : 0; + const usingFlashloan = isPositionSwap(state.swapType, state.useFlashloan ?? false); + 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' ? valueToBigNumber(state.outputAmount).multipliedBy(flashLoanFeeBps).dividedBy(10000) @@ -354,6 +360,7 @@ export const useSwapOrderAmounts = ({ networkFeeAmountInBuyFormatted, partnerFeeAmountFormatted: partnerFeeAmount.toFixed(), flashLoanFeeAmountFormatted: flashLoanFeeAmount.toFixed(), + flashLoanFeeBps, partnerFeeBps: partnetFeeBps, }); }, [ @@ -366,5 +373,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..0a1a853ca7 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: undefined, partnerFeeBps: 0, limitsOrderButtonBlocked: false, showSlippageWarning: false,