Skip to content

Commit e55b2b0

Browse files
authored
feat: resolve flashloan fee on-chain via ACLManager (#2972)
1 parent cf24dad commit e55b2b0

9 files changed

Lines changed: 153 additions & 28 deletions

File tree

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,4 @@ package-lock.json
5151

5252
# Sentry Config File
5353
.env.sentry-build-plugin
54+
.env*.local

‎src/components/transactions/Swap/actions/CollateralSwap/CollateralSwapActionsViaCoWAdapters.tsx‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { saveCowOrderToUserHistory } from 'src/utils/swapAdapterHistory';
1313
import { useShallow } from 'zustand/react/shallow';
1414

1515
import { TrackAnalyticsHandlers } from '../../analytics/useTrackAnalytics';
16-
import { COW_PARTNER_FEE, FLASH_LOAN_FEE_BPS } from '../../constants/cow.constants';
16+
import { COW_PARTNER_FEE } from '../../constants/cow.constants';
1717
import { APP_CODE_PER_SWAP_TYPE } from '../../constants/shared.constants';
1818
import {
1919
addOrderTypeToAppData,
@@ -172,6 +172,10 @@ export const CollateralSwapActionsViaCowAdapters = ({
172172
)
173173
return;
174174

175+
if (state.flashLoanFeeBps === undefined) {
176+
throw new Error('Flashloan fee unavailable: on-chain ACLManager check has not resolved.');
177+
}
178+
175179
const tradingSdk = await getCowTradingSdkByChainIdAndAppCode(
176180
state.chainId,
177181
APP_CODE_PER_SWAP_TYPE[state.swapType]
@@ -189,7 +193,7 @@ export const CollateralSwapActionsViaCowAdapters = ({
189193
: undefined;
190194

191195
const { flashLoanFeeAmount, sellAmountToSign } = flashLoanSdk.calculateFlashLoanAmounts({
192-
flashLoanFeeBps: FLASH_LOAN_FEE_BPS,
196+
flashLoanFeeBps: state.flashLoanFeeBps,
193197
sellAmount: state.sellAmountBigInt,
194198
});
195199

‎src/components/transactions/Swap/actions/DebtSwap/DebtSwapActionsViaCoW.tsx‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { zeroAddress } from 'viem';
1414
import { useShallow } from 'zustand/react/shallow';
1515

1616
import { TrackAnalyticsHandlers } from '../../analytics/useTrackAnalytics';
17-
import { COW_PARTNER_FEE, FLASH_LOAN_FEE_BPS } from '../../constants/cow.constants';
17+
import { COW_PARTNER_FEE } from '../../constants/cow.constants';
1818
import { APP_CODE_PER_SWAP_TYPE } from '../../constants/shared.constants';
1919
import {
2020
addOrderTypeToAppData,
@@ -187,6 +187,10 @@ export const DebtSwapActionsViaCoW = ({
187187
)
188188
return;
189189

190+
if (state.flashLoanFeeBps === undefined) {
191+
throw new Error('Flashloan fee unavailable: on-chain ACLManager check has not resolved.');
192+
}
193+
190194
const tradingSdk = await getCowTradingSdkByChainIdAndAppCode(
191195
state.chainId,
192196
APP_CODE_PER_SWAP_TYPE[state.swapType]
@@ -215,7 +219,7 @@ export const DebtSwapActionsViaCoW = ({
215219
: undefined;
216220

217221
const { flashLoanFeeAmount, sellAmountToSign } = flashLoanSdk.calculateFlashLoanAmounts({
218-
flashLoanFeeBps: FLASH_LOAN_FEE_BPS,
222+
flashLoanFeeBps: state.flashLoanFeeBps,
219223
sellAmount: BigInt(sellAmountWithMarginForDustProtection),
220224
});
221225

‎src/components/transactions/Swap/actions/RepayWithCollateral/RepayWithCollateralActionsViaCoW.tsx‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { saveCowOrderToUserHistory } from 'src/utils/swapAdapterHistory';
1313
import { useShallow } from 'zustand/react/shallow';
1414

1515
import { TrackAnalyticsHandlers } from '../../analytics/useTrackAnalytics';
16-
import { COW_PARTNER_FEE, FLASH_LOAN_FEE_BPS } from '../../constants/cow.constants';
16+
import { COW_PARTNER_FEE } from '../../constants/cow.constants';
1717
import { APP_CODE_PER_SWAP_TYPE } from '../../constants/shared.constants';
1818
import {
1919
addOrderTypeToAppData,
@@ -185,6 +185,10 @@ export const RepayWithCollateralActionsViaCoW = ({
185185
)
186186
return;
187187

188+
if (state.flashLoanFeeBps === undefined) {
189+
throw new Error('Flashloan fee unavailable: on-chain ACLManager check has not resolved.');
190+
}
191+
188192
const tradingSdk = await getCowTradingSdkByChainIdAndAppCode(
189193
state.chainId,
190194
APP_CODE_PER_SWAP_TYPE[state.swapType]
@@ -213,7 +217,7 @@ export const RepayWithCollateralActionsViaCoW = ({
213217
: undefined;
214218

215219
const { flashLoanFeeAmount, sellAmountToSign } = flashLoanSdk.calculateFlashLoanAmounts({
216-
flashLoanFeeBps: FLASH_LOAN_FEE_BPS,
220+
flashLoanFeeBps: state.flashLoanFeeBps,
217221
sellAmount: BigInt(sellAmountWithMarginForDustProtection),
218222
});
219223

‎src/components/transactions/Swap/helpers/cow/adapters.helpers.ts‎

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,7 @@ import {
1616
} from '@cowprotocol/sdk-flash-loans';
1717
import { CustomMarket } from 'src/ui-config/marketsConfig';
1818

19-
import {
20-
COW_PARTNER_FEE,
21-
DUST_PROTECTION_MULTIPLIER,
22-
FLASH_LOAN_FEE_BPS,
23-
} from '../../constants/cow.constants';
19+
import { COW_PARTNER_FEE, DUST_PROTECTION_MULTIPLIER } from '../../constants/cow.constants';
2420
import { isCowProtocolRates, OrderType, SwapProvider, SwapState, SwapType } from '../../types';
2521
import { getCowFlashLoanSdk } from './env.helpers';
2622

@@ -53,7 +49,8 @@ export const calculateInstanceAddress = async ({
5349
!state.sellAmountBigInt ||
5450
!state.buyAmountBigInt ||
5551
!state.sellAmountToken ||
56-
!state.buyAmountToken
52+
!state.buyAmountToken ||
53+
state.flashLoanFeeBps === undefined
5754
)
5855
return;
5956

@@ -92,7 +89,7 @@ export const calculateInstanceAddress = async ({
9289
};
9390

9491
const { flashLoanFeeAmount, sellAmountToSign } = flashLoanSdk.calculateFlashLoanAmounts({
95-
flashLoanFeeBps: FLASH_LOAN_FEE_BPS,
92+
flashLoanFeeBps: state.flashLoanFeeBps,
9693
sellAmount: BigInt(sellAmountWithMarginForDustProtection),
9794
});
9895

@@ -171,7 +168,11 @@ export const calculateFlashLoanAmounts = (
171168
finalSellAmount: BigInt(0),
172169
};
173170

174-
if (state.swapType === SwapType.Swap || state.provider !== SwapProvider.COW_PROTOCOL) {
171+
if (
172+
state.swapType === SwapType.Swap ||
173+
state.provider !== SwapProvider.COW_PROTOCOL ||
174+
state.flashLoanFeeBps === undefined
175+
) {
175176
return {
176177
flashLoanFeeAmount: BigInt(0),
177178
finalSellAmount: sellAmount,
@@ -180,7 +181,7 @@ export const calculateFlashLoanAmounts = (
180181

181182
const { flashLoanFeeAmount, sellAmountToSign } = flashLoanSdk.calculateFlashLoanAmounts({
182183
sellAmount: sellAmount,
183-
flashLoanFeeBps: FLASH_LOAN_FEE_BPS,
184+
flashLoanFeeBps: state.flashLoanFeeBps,
184185
});
185186

186187
return {

‎src/components/transactions/Swap/helpers/paraswap/flashloan.helpers.ts‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { valueToBigNumber } from '@aave/math-utils';
22

3-
import { PARASWAP_FLASH_LOAN_FEE_BPS } from '../../constants/paraswap.constants';
43
import { SwapProvider, SwapState, SwapType } from '../../types';
54

65
/**
76
* Calculate flashloan fee amount for Paraswap adapter swaps.
8-
* The fee is 0.05% (5 bps) of the flashloan amount, which is the sell amount.
7+
* The fee bps is resolved on-chain via ACLManager.isFlashBorrower; while the
8+
* check is in flight `state.flashLoanFeeBps` is undefined and we return zeros
9+
* so the caller doesn't render a stale value.
910
*
1011
* @param state - Swap state
1112
* @returns Object containing flashloan fee amount in bigint and formatted string
@@ -21,7 +22,8 @@ export const calculateParaswapFlashLoanFee = (
2122
state.swapType === SwapType.Swap ||
2223
state.provider !== SwapProvider.PARASWAP ||
2324
!state.useFlashloan ||
24-
!state.sellAmountBigInt
25+
!state.sellAmountBigInt ||
26+
state.flashLoanFeeBps === undefined
2527
) {
2628
return {
2729
flashLoanFeeAmount: BigInt(0),
@@ -32,7 +34,7 @@ export const calculateParaswapFlashLoanFee = (
3234
// Calculate fee: flashloan amount * fee bps / 10000
3335
// The flashloan amount is the sell amount (collateral being swapped)
3436
const flashLoanFeeAmount =
35-
(state.sellAmountBigInt * BigInt(PARASWAP_FLASH_LOAN_FEE_BPS)) / BigInt(10000);
37+
(state.sellAmountBigInt * BigInt(state.flashLoanFeeBps)) / BigInt(10000);
3638

3739
// Format the fee amount
3840
const flashLoanFeeFormatted = valueToBigNumber(flashLoanFeeAmount.toString())
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import {
2+
AaveV3Arbitrum,
3+
AaveV3Avalanche,
4+
AaveV3Base,
5+
AaveV3BNB,
6+
AaveV3Ethereum,
7+
AaveV3EthereumEtherFi,
8+
AaveV3EthereumLido,
9+
AaveV3Gnosis,
10+
AaveV3Linea,
11+
AaveV3Optimism,
12+
AaveV3Plasma,
13+
AaveV3Polygon,
14+
AaveV3Sonic,
15+
} from '@aave-dao/aave-address-book';
16+
import { SupportedChainId } from '@cowprotocol/cow-sdk';
17+
import { useQuery } from '@tanstack/react-query';
18+
import { Contract } from 'ethers';
19+
import { CustomMarket, MarketDataType } from 'src/ui-config/marketsConfig';
20+
import { getProvider } from 'src/utils/marketsAndNetworksConfig';
21+
22+
import { ADAPTER_FACTORY, FLASH_LOAN_FEE_BPS } from '../constants/cow.constants';
23+
import { PARASWAP_FLASH_LOAN_FEE_BPS } from '../constants/paraswap.constants';
24+
import { SwapProvider, SwapType } from '../types';
25+
26+
const ACL_MANAGER_ABI = ['function isFlashBorrower(address) view returns (bool)'];
27+
28+
// CoW per-market ACLManager addresses. Only CoW flows do the on-chain lookup —
29+
// Paraswap flows always have the user EOA as msg.sender to Pool.flashLoan, so
30+
// they pay the default premium and don't need a check.
31+
const ACL_MANAGER_BY_MARKET: Partial<Record<CustomMarket, string>> = {
32+
[CustomMarket.proto_mainnet_v3]: AaveV3Ethereum.ACL_MANAGER,
33+
[CustomMarket.proto_lido_v3]: AaveV3EthereumLido.ACL_MANAGER,
34+
[CustomMarket.proto_etherfi_v3]: AaveV3EthereumEtherFi.ACL_MANAGER,
35+
[CustomMarket.proto_arbitrum_v3]: AaveV3Arbitrum.ACL_MANAGER,
36+
[CustomMarket.proto_base_v3]: AaveV3Base.ACL_MANAGER,
37+
[CustomMarket.proto_polygon_v3]: AaveV3Polygon.ACL_MANAGER,
38+
[CustomMarket.proto_optimism_v3]: AaveV3Optimism.ACL_MANAGER,
39+
[CustomMarket.proto_avalanche_v3]: AaveV3Avalanche.ACL_MANAGER,
40+
[CustomMarket.proto_sonic_v3]: AaveV3Sonic.ACL_MANAGER,
41+
[CustomMarket.proto_gnosis_v3]: AaveV3Gnosis.ACL_MANAGER,
42+
[CustomMarket.proto_bnb_v3]: AaveV3BNB.ACL_MANAGER,
43+
[CustomMarket.proto_linea_v3]: AaveV3Linea.ACL_MANAGER,
44+
[CustomMarket.proto_plasma_v3]: AaveV3Plasma.ACL_MANAGER,
45+
};
46+
47+
/**
48+
* Resolve the flashloan fee bps for the active swap.
49+
*
50+
* - CoW: msg.sender to Pool.flashLoan is the CoW factory. Check
51+
* ACLManager.isFlashBorrower(factory) on chain. Returns 0 when whitelisted,
52+
* FLASH_LOAN_FEE_BPS when not, and `undefined` while the query is pending or
53+
* when we can't run the check (factory or ACLManager not mapped for the
54+
* chain). Submit handlers MUST refuse to send while the value is undefined.
55+
* - Paraswap: msg.sender is always the user EOA, so the role can never apply.
56+
* Returns the constant immediately, no on-chain call.
57+
*/
58+
export const useFlashLoanFeeBps = ({
59+
provider,
60+
swapType,
61+
marketData,
62+
}: {
63+
provider: SwapProvider;
64+
swapType: SwapType;
65+
marketData: MarketDataType;
66+
}): number | undefined => {
67+
const isCow = provider === SwapProvider.COW_PROTOCOL;
68+
const aclManager = ACL_MANAGER_BY_MARKET[marketData.market];
69+
const target = isCow
70+
? ADAPTER_FACTORY[marketData.chainId as unknown as SupportedChainId] || undefined
71+
: undefined;
72+
73+
const enabled = isCow && Boolean(aclManager && target);
74+
75+
const { data: isWhitelisted } = useQuery({
76+
queryFn: async (): Promise<boolean> => {
77+
const contract = new Contract(
78+
aclManager as string,
79+
ACL_MANAGER_ABI,
80+
getProvider(marketData.chainId)
81+
);
82+
return contract.isFlashBorrower(target);
83+
},
84+
queryKey: ['flashBorrowerCheck', marketData.chainId, aclManager, target],
85+
enabled,
86+
staleTime: 5 * 60 * 1000,
87+
});
88+
89+
if (!isCow) {
90+
// Acknowledge swapType to keep the dep narrow even though Paraswap
91+
// doesn't branch on it for fee resolution.
92+
void swapType;
93+
return PARASWAP_FLASH_LOAN_FEE_BPS;
94+
}
95+
if (!enabled) return undefined;
96+
if (isWhitelisted === undefined) return undefined;
97+
return isWhitelisted ? 0 : FLASH_LOAN_FEE_BPS;
98+
};

‎src/components/transactions/Swap/hooks/useSwapOrderAmounts.ts‎

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ import { OrderKind } from '@cowprotocol/cow-sdk';
33
import { Dispatch, useEffect } from 'react';
44
import { useRootStore } from 'src/store/root';
55

6-
import { COW_PARTNER_FEE, FLASH_LOAN_FEE_BPS } from '../constants/cow.constants';
7-
import { PARASWAP_FLASH_LOAN_FEE_BPS } from '../constants/paraswap.constants';
6+
import { COW_PARTNER_FEE } from '../constants/cow.constants';
87
import {
98
isCowProtocolRates,
109
OrderType,
@@ -13,6 +12,7 @@ import {
1312
SwapState,
1413
SwapType,
1514
} from '../types';
15+
import { useFlashLoanFeeBps } from './useFlashLoanFeeBps';
1616
import { swapTypesThatRequiresInvertedQuote } from './useSwapQuote';
1717

1818
const marketOrderKindPerSwapType: Record<SwapType, OrderKind> = {
@@ -27,10 +27,6 @@ const isPositionSwap = (swapType: SwapType, usingFlashloan: boolean) => {
2727
return swapType != SwapType.Swap && usingFlashloan;
2828
};
2929

30-
const getFlashLoanFeeBps = (provider: SwapProvider) => {
31-
return provider === SwapProvider.COW_PROTOCOL ? FLASH_LOAN_FEE_BPS : PARASWAP_FLASH_LOAN_FEE_BPS;
32-
};
33-
3430
/**
3531
* Computes normalized sell/buy amounts used to build transactions.
3632
*
@@ -50,6 +46,12 @@ export const useSwapOrderAmounts = ({
5046
setState: Dispatch<Partial<SwapState>>;
5147
}) => {
5248
const currentMarket = useRootStore((state) => state.currentMarket);
49+
const currentMarketData = useRootStore((store) => store.currentMarketData);
50+
const resolvedFlashLoanFeeBps = useFlashLoanFeeBps({
51+
provider: state.provider,
52+
swapType: state.swapType,
53+
marketData: currentMarketData,
54+
});
5355

5456
useEffect(() => {
5557
if (
@@ -95,9 +97,13 @@ export const useSwapOrderAmounts = ({
9597
: valueToBigNumber(state.inputAmount).multipliedBy(partnetFeeBps).dividedBy(10000);
9698
// const partnerFeeToken = state.side === 'sell' ? state.destinationToken : state.sourceToken;
9799

98-
const flashLoanFeeBps = isPositionSwap(state.swapType, state.useFlashloan ?? false)
99-
? getFlashLoanFeeBps(state.provider)
100-
: 0;
100+
const usingFlashloan = isPositionSwap(state.swapType, state.useFlashloan ?? false);
101+
if (usingFlashloan && resolvedFlashLoanFeeBps === undefined) {
102+
// Clear any stale fee from a previous render so submit handlers see the loading state.
103+
if (state.flashLoanFeeBps !== undefined) setState({ flashLoanFeeBps: undefined });
104+
return;
105+
}
106+
const flashLoanFeeBps = usingFlashloan ? (resolvedFlashLoanFeeBps as number) : 0;
101107
const flashLoanFeeAmount =
102108
state.side == 'sell'
103109
? valueToBigNumber(state.outputAmount).multipliedBy(flashLoanFeeBps).dividedBy(10000)
@@ -354,6 +360,7 @@ export const useSwapOrderAmounts = ({
354360
networkFeeAmountInBuyFormatted,
355361
partnerFeeAmountFormatted: partnerFeeAmount.toFixed(),
356362
flashLoanFeeAmountFormatted: flashLoanFeeAmount.toFixed(),
363+
flashLoanFeeBps,
357364
partnerFeeBps: partnetFeeBps,
358365
});
359366
}, [
@@ -366,5 +373,6 @@ export const useSwapOrderAmounts = ({
366373
state.swapType,
367374
state.orderType,
368375
state.useFlashloan,
376+
resolvedFlashLoanFeeBps,
369377
]);
370378
};

‎src/components/transactions/Swap/types/state.types.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ export type TokensSwapState = {
9797
partnerFeeAmountFormatted?: string;
9898
/** Flash loan fee amount applied to this order, normalized to the fee token units (depends on side). */
9999
flashLoanFeeAmountFormatted?: string;
100+
/** Flash loan fee in basis points used to compute flashLoanFeeAmountFormatted. Resolved on-chain from ACLManager.isFlashBorrower. */
101+
flashLoanFeeBps?: number;
100102
/** Partner fee in basis points used to compute partnerFeeAmountFormatted. */
101103
partnerFeeBps?: number;
102104

@@ -287,6 +289,7 @@ export const swapDefaultState: SwapState = {
287289
networkFeeAmountInBuyFormatted: '0',
288290
partnerFeeAmountFormatted: '0',
289291
flashLoanFeeAmountFormatted: '0',
292+
flashLoanFeeBps: undefined,
290293
partnerFeeBps: 0,
291294
limitsOrderButtonBlocked: false,
292295
showSlippageWarning: false,

0 commit comments

Comments
 (0)