Skip to content
Merged
4 changes: 4 additions & 0 deletions packages/bridge-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- Fix `calcSentAmount` double-counting fees for intent-based swap quotes ([#8845](https://github.com/MetaMask/core/pull/8845))

## [73.0.0]

### Added
Expand Down
35 changes: 35 additions & 0 deletions packages/bridge-controller/src/utils/quote.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,41 @@ describe('Quote Metadata Utils', () => {
expect(result.valueInCurrency).toBe('2.2');
expect(result.usd).toBe('1.65');
});

it('should not add feeData fees for intent-based quotes', () => {
// For intent-based swaps (e.g. CoW Protocol), srcTokenAmount is already
// the total fixed commitment including protocol fees. Adding feeData fees
// on top would double-count them.
const intentQuote = {
srcTokenAmount: '10000000', // 10 USDT (6 decimals), fee already included
srcAsset: {
decimals: 6,
assetId: 'eip155:1/erc20:0xdAC17F958D2ee523a2206206994597C13D831ec7',
},
feeData: {
metabridge: {
amount: '500000', // 0.5 USDT protocol fee — already inside srcTokenAmount
asset: {
assetId:
'eip155:1/erc20:0xdAC17F958D2ee523a2206206994597C13D831ec7',
address: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
decimals: 6,
},
},
},
intent: { protocol: 'cow', order: {} },
} as unknown as Quote;

const result = calcSentAmount(intentQuote, {
exchangeRate: '1',
usdExchangeRate: '1',
});

// Should be exactly 10 USDT — not 10.5 (which would double-count the fee)
expect(result.amount).toBe('10');
expect(result.valueInCurrency).toBe('10');
expect(result.usd).toBe('10');
});
});

describe('calcNonEvmTotalNetworkFee', () => {
Expand Down
25 changes: 16 additions & 9 deletions packages/bridge-controller/src/utils/quote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,17 +141,24 @@ export const calcToAmount = (
};

export const calcSentAmount = (
{ srcTokenAmount, srcAsset, feeData }: Quote,
{ srcTokenAmount, srcAsset, feeData, intent }: Quote,
{ exchangeRate, usdExchangeRate }: ExchangeRate,
) => {
// Find all fees that will be taken from the src token
const srcTokenFees = Object.values(feeData).filter(
(fee) => fee && fee.amount && fee.asset?.assetId === srcAsset.assetId,
);
const sentAmount = srcTokenFees.reduce(
(acc, { amount }) => acc.plus(amount),
new BigNumber(srcTokenAmount),
);
// For intent-based swaps (e.g. CoW Protocol), srcTokenAmount is the total
// fixed commitment the user makes to the protocol — the protocol fee is
// already baked in. Adding feeData fees on top would double-count them.
// For conventional swaps, srcTokenAmount is the net routing amount (fees
// excluded), so the src-token fees must be added to get the wallet deduction.
const sentAmount = intent
? new BigNumber(srcTokenAmount)
: Object.values(feeData)
.filter(
(fee) => fee && fee.amount && fee.asset?.assetId === srcAsset.assetId,
)
.reduce(
(acc, { amount }) => acc.plus(amount),
new BigNumber(srcTokenAmount),
);
const normalizedSentAmount = calcTokenAmount(sentAmount, srcAsset.decimals);
return {
amount: normalizedSentAmount.toString(),
Expand Down
Loading