Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,4 @@ package-lock.json

# Sentry Config File
.env.sentry-build-plugin
.env*.local
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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]
Expand All @@ -189,7 +193,7 @@ export const CollateralSwapActionsViaCowAdapters = ({
: undefined;

const { flashLoanFeeAmount, sellAmountToSign } = flashLoanSdk.calculateFlashLoanAmounts({
flashLoanFeeBps: FLASH_LOAN_FEE_BPS,
flashLoanFeeBps: state.flashLoanFeeBps,
sellAmount: state.sellAmountBigInt,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -215,7 +219,7 @@ export const DebtSwapActionsViaCoW = ({
: undefined;

const { flashLoanFeeAmount, sellAmountToSign } = flashLoanSdk.calculateFlashLoanAmounts({
flashLoanFeeBps: FLASH_LOAN_FEE_BPS,
flashLoanFeeBps: state.flashLoanFeeBps,
sellAmount: BigInt(sellAmountWithMarginForDustProtection),
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -213,7 +217,7 @@ export const RepayWithCollateralActionsViaCoW = ({
: undefined;

const { flashLoanFeeAmount, sellAmountToSign } = flashLoanSdk.calculateFlashLoanAmounts({
flashLoanFeeBps: FLASH_LOAN_FEE_BPS,
flashLoanFeeBps: state.flashLoanFeeBps,
sellAmount: BigInt(sellAmountWithMarginForDustProtection),
});

Expand Down
19 changes: 10 additions & 9 deletions src/components/transactions/Swap/helpers/cow/adapters.helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -53,7 +49,8 @@ export const calculateInstanceAddress = async ({
!state.sellAmountBigInt ||
!state.buyAmountBigInt ||
!state.sellAmountToken ||
!state.buyAmountToken
!state.buyAmountToken ||
state.flashLoanFeeBps === undefined
)
return;

Expand Down Expand Up @@ -92,7 +89,7 @@ export const calculateInstanceAddress = async ({
};

const { flashLoanFeeAmount, sellAmountToSign } = flashLoanSdk.calculateFlashLoanAmounts({
flashLoanFeeBps: FLASH_LOAN_FEE_BPS,
flashLoanFeeBps: state.flashLoanFeeBps,
sellAmount: BigInt(sellAmountWithMarginForDustProtection),
});

Expand Down Expand Up @@ -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,
Expand All @@ -180,7 +181,7 @@ export const calculateFlashLoanAmounts = (

const { flashLoanFeeAmount, sellAmountToSign } = flashLoanSdk.calculateFlashLoanAmounts({
sellAmount: sellAmount,
flashLoanFeeBps: FLASH_LOAN_FEE_BPS,
flashLoanFeeBps: state.flashLoanFeeBps,
});

return {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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),
Expand All @@ -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())
Expand Down
98 changes: 98 additions & 0 deletions src/components/transactions/Swap/hooks/useFlashLoanFeeBps.ts
Original file line number Diff line number Diff line change
@@ -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<Record<CustomMarket, string>> = {
[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<boolean> => {
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;
};
26 changes: 17 additions & 9 deletions src/components/transactions/Swap/hooks/useSwapOrderAmounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -13,6 +12,7 @@ import {
SwapState,
SwapType,
} from '../types';
import { useFlashLoanFeeBps } from './useFlashLoanFeeBps';
import { swapTypesThatRequiresInvertedQuote } from './useSwapQuote';

const marketOrderKindPerSwapType: Record<SwapType, OrderKind> = {
Expand All @@ -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.
*
Expand All @@ -50,6 +46,12 @@ export const useSwapOrderAmounts = ({
setState: Dispatch<Partial<SwapState>>;
}) => {
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 (
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -354,6 +360,7 @@ export const useSwapOrderAmounts = ({
networkFeeAmountInBuyFormatted,
partnerFeeAmountFormatted: partnerFeeAmount.toFixed(),
flashLoanFeeAmountFormatted: flashLoanFeeAmount.toFixed(),
flashLoanFeeBps,
partnerFeeBps: partnetFeeBps,
});
}, [
Expand All @@ -366,5 +373,6 @@ export const useSwapOrderAmounts = ({
state.swapType,
state.orderType,
state.useFlashloan,
resolvedFlashLoanFeeBps,
]);
};
3 changes: 3 additions & 0 deletions src/components/transactions/Swap/types/state.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -287,6 +289,7 @@ export const swapDefaultState: SwapState = {
networkFeeAmountInBuyFormatted: '0',
partnerFeeAmountFormatted: '0',
flashLoanFeeAmountFormatted: '0',
flashLoanFeeBps: undefined,
partnerFeeBps: 0,
limitsOrderButtonBlocked: false,
showSlippageWarning: false,
Expand Down
Loading