Skip to content
Draft
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 packages/bridge-controller/src/constants/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export const DEFAULT_FEATURE_FLAG_CONFIG: FeatureFlagsPlatformConfig = {
maxRefreshCount: DEFAULT_MAX_REFRESH_COUNT,
support: false,
chains: {},
stablecoins: [],
};

export const DEFAULT_BRIDGE_CONTROLLER_STATE: BridgeControllerState = {
Expand Down
6 changes: 4 additions & 2 deletions packages/bridge-controller/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,6 @@ export {

export {
selectBridgeQuotes,
selectDefaultSlippagePercentage,
type BridgeAppState,
selectExchangeRateByChainIdAndAddress,
selectIsQuoteExpired,
Expand All @@ -144,4 +143,7 @@ export { DEFAULT_FEATURE_FLAG_CONFIG } from './constants/bridge';

export { getBridgeFeatureFlags } from './utils/feature-flags';

export { BRIDGE_DEFAULT_SLIPPAGE } from './utils/slippage';
export {
BRIDGE_DEFAULT_SLIPPAGE,
getDefaultSlippagePercentage,
} from './utils/slippage';
183 changes: 49 additions & 134 deletions packages/bridge-controller/src/selectors.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AddressZero } from '@ethersproject/constants';
import type { MarketDataDetails } from '@metamask/assets-controllers';
import { toHex } from '@metamask/controller-utils';
import type { CaipAssetType } from '@metamask/keyring-api';
import { SolScope } from '@metamask/keyring-api';
import { BigNumber } from 'bignumber.js';

Expand All @@ -12,11 +13,11 @@ import {
selectIsQuoteExpired,
selectBridgeFeatureFlags,
selectMinimumBalanceForRentExemptionInSOL,
selectDefaultSlippagePercentage,
} from './selectors';
import type { BridgeAsset, QuoteResponse } from './types';
import { SortOrder, RequestStatus, ChainId } from './types';
import { isNativeAddress } from './utils/bridge';
import { getDefaultSlippagePercentage } from './utils/slippage';

describe('Bridge Selectors', () => {
describe('selectExchangeRateByChainIdAndAddress', () => {
Expand Down Expand Up @@ -1162,11 +1163,14 @@ describe('Bridge Selectors', () => {
refreshRate: 3,
maxRefreshCount: 1,
support: true,
stablecoins: [
'eips:1/erc20:0x123',
'eips:1/erc20:0x456',
] as CaipAssetType[],
chains: {
'1': {
isActiveSrc: true,
isActiveDest: true,
stablecoins: ['0x123', '0x456'],
},
'10': {
isActiveSrc: true,
Expand All @@ -1180,180 +1184,91 @@ describe('Bridge Selectors', () => {
};

it('should return swap default slippage when stablecoins list is not defined', () => {
const result = selectDefaultSlippagePercentage(
{
remoteFeatureFlags: {
bridgeConfig: mockValidBridgeConfig,
},
} as never,
{
srcTokenAddress: '0x123',
destTokenAddress: '0x456',
srcChainId: '10',
destChainId: '10',
},
);
const result = getDefaultSlippagePercentage({
srcAssetId: 'eips:10/erc20:0x123',
destAssetId: 'eips:10/erc20:0x456',
stablecoins: mockValidBridgeConfig.stablecoins,
});

expect(result).toBe(2);
});

it('should return bridge default slippage when requesting an EVM bridge quote', () => {
const result = selectDefaultSlippagePercentage(
{
remoteFeatureFlags: {
bridgeConfig: mockValidBridgeConfig,
},
} as never,
{
srcTokenAddress: '0x123',
destTokenAddress: '0x456',
srcChainId: '1',
destChainId: ChainId.SOLANA,
},
);
const result = getDefaultSlippagePercentage({
srcAssetId: 'eips:1/erc20:0x123',
destAssetId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:0x456',
stablecoins: mockValidBridgeConfig.stablecoins,
});

expect(result).toBe(0.5);
});

it('should return bridge default slippage when requesting a Solana bridge quote', () => {
const result = selectDefaultSlippagePercentage(
{
remoteFeatureFlags: {
bridgeConfig: mockValidBridgeConfig,
},
} as never,
{
srcTokenAddress: '0x123',
destTokenAddress: '0x456',
destChainId: '1',
srcChainId: ChainId.SOLANA,
},
);
const result = getDefaultSlippagePercentage({
srcAssetId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:0x123',
destAssetId: 'eips:1/erc20:0x456',
stablecoins: mockValidBridgeConfig.stablecoins,
});

expect(result).toBe(0.5);
});

it('should return swap auto slippage when requesting a Solana swap quote', () => {
const result = selectDefaultSlippagePercentage(
{
remoteFeatureFlags: {
bridgeConfig: mockValidBridgeConfig,
},
} as never,
{
srcTokenAddress: '0x123',
destTokenAddress: '0x456',
destChainId: ChainId.SOLANA,
srcChainId: ChainId.SOLANA,
},
);
const result = getDefaultSlippagePercentage({
srcAssetId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:0x123',
destAssetId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:0x456',
stablecoins: mockValidBridgeConfig.stablecoins,
});

expect(result).toBeUndefined();
});

it('should return swap default slippage when dest token is not a stablecoin', () => {
const result = selectDefaultSlippagePercentage(
{
remoteFeatureFlags: {
bridgeConfig: mockValidBridgeConfig,
},
} as never,
{
srcTokenAddress: '0x123',
destTokenAddress: '0x789',
destChainId: '1',
srcChainId: '1',
},
);
const result = getDefaultSlippagePercentage({
srcAssetId: 'eips:1/erc20:0x123',
destAssetId: 'eips:1/erc20:0x789',
stablecoins: mockValidBridgeConfig.stablecoins,
});

expect(result).toBe(2);
});

it('should return swap default slippage when src token is not a stablecoin', () => {
const result = selectDefaultSlippagePercentage(
{
remoteFeatureFlags: {
bridgeConfig: mockValidBridgeConfig,
},
} as never,
{
srcTokenAddress: '0x789',
destTokenAddress: '0x456',
destChainId: '1',
srcChainId: '1',
},
);
const result = getDefaultSlippagePercentage({
srcAssetId: 'eips:1/erc20:0x789',
destAssetId: 'eips:1/erc20:0x456',
stablecoins: mockValidBridgeConfig.stablecoins,
});

expect(result).toBe(2);
});

it('should return swap stablecoin slippage when both tokens are stablecoins', () => {
const result = selectDefaultSlippagePercentage(
{
remoteFeatureFlags: {
bridgeConfig: mockValidBridgeConfig,
},
} as never,
{
srcTokenAddress: '0x123',
destTokenAddress: '0x456',
destChainId: '1',
srcChainId: '1',
},
);
const result = getDefaultSlippagePercentage({
srcAssetId: 'eips:1/erc20:0x123',
destAssetId: 'eips:1/erc20:0x456',
stablecoins: mockValidBridgeConfig.stablecoins,
});

expect(result).toBe(0.5);
});

it('should return bridge default slippage when srcChainId is undefined', () => {
const result = selectDefaultSlippagePercentage(
{
remoteFeatureFlags: {
bridgeConfig: mockValidBridgeConfig,
},
} as never,
{
srcTokenAddress: '0x123',
destTokenAddress: '0x456',
destChainId: '1',
},
);
const result = getDefaultSlippagePercentage({
destAssetId: 'eips:1/erc20:0x456',
stablecoins: mockValidBridgeConfig.stablecoins,
});

expect(result).toBe(0.5);
});

it('should return swap stablecoin slippage when destChainId is undefined', () => {
const result = selectDefaultSlippagePercentage(
{
remoteFeatureFlags: {
bridgeConfig: mockValidBridgeConfig,
},
} as never,
{
srcTokenAddress: '0x123',
destTokenAddress: '0x456',
srcChainId: '1',
},
);
const result = getDefaultSlippagePercentage({
srcAssetId: 'eips:1/erc20:0x123',
stablecoins: mockValidBridgeConfig.stablecoins,
});

expect(result).toBe(0.5);
});

it('should return swap default slippage when destChainId is undefined', () => {
const result = selectDefaultSlippagePercentage(
{
remoteFeatureFlags: {
bridgeConfig: mockValidBridgeConfig,
},
} as never,
{
srcTokenAddress: '0x789',
destTokenAddress: '0x456',
srcChainId: '1',
},
);

expect(result).toBe(2);
});
});
});
37 changes: 0 additions & 37 deletions packages/bridge-controller/src/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ import {
calcTotalEstimatedNetworkFee,
calcTotalMaxNetworkFee,
} from './utils/quote';
import { getDefaultSlippagePercentage } from './utils/slippage';

/**
* The controller states that provide exchange rates
Expand Down Expand Up @@ -451,39 +450,3 @@ export const selectMinimumBalanceForRentExemptionInSOL = (
new BigNumber(state.minimumBalanceForRentExemptionInLamports ?? 0)
.div(10 ** 9)
.toString();

export const selectDefaultSlippagePercentage = createBridgeSelector(
[
(state) => selectBridgeFeatureFlags(state).chains,
(_, slippageParams: Parameters<typeof getDefaultSlippagePercentage>[0]) =>
slippageParams.srcTokenAddress,
(_, slippageParams: Parameters<typeof getDefaultSlippagePercentage>[0]) =>
slippageParams.destTokenAddress,
(_, slippageParams: Parameters<typeof getDefaultSlippagePercentage>[0]) =>
slippageParams.srcChainId
? formatChainIdToCaip(slippageParams.srcChainId)
: undefined,
(_, slippageParams: Parameters<typeof getDefaultSlippagePercentage>[0]) =>
slippageParams.destChainId
? formatChainIdToCaip(slippageParams.destChainId)
: undefined,
],
(
featureFlagsByChain,
srcTokenAddress,
destTokenAddress,
srcChainId,
destChainId,
) => {
return getDefaultSlippagePercentage(
{
srcTokenAddress,
destTokenAddress,
srcChainId,
destChainId,
},
srcChainId ? featureFlagsByChain[srcChainId]?.stablecoins : undefined,
destChainId ? featureFlagsByChain[destChainId]?.stablecoins : undefined,
);
},
);
25 changes: 16 additions & 9 deletions packages/bridge-controller/src/utils/caip-formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ export const formatChainIdToDec = (
*
* @param chainId - The chainId to convert
* @returns The hex string
*
* @throws {Error} If the chainId is not an EVM chainId
*/
export const formatChainIdToHex = (
chainId: Hex | CaipChainId | string | number,
Expand All @@ -108,6 +110,10 @@ export const formatChainIdToHex = (
*
* @param address - The address to convert
* @returns The converted address
*
* @deprecated This function should not be used
* // TODO find usages and deprecate
* @throws {Error} If the address is not a valid hex string or CAIP address
*/
export const formatAddressToCaipReference = (address: string) => {
if (isStrictHexString(address)) {
Expand All @@ -122,7 +128,7 @@ export const formatAddressToCaipReference = (address: string) => {
// If the address is not a valid hex string or CAIP address, throw an error
// This should never happen, but it's a sanity check
if (!addressWithoutPrefix) {
throw new Error('Invalid address');
throw new Error('Invalid CAIP asset');
}
return addressWithoutPrefix;
};
Expand All @@ -133,26 +139,27 @@ export const formatAddressToCaipReference = (address: string) => {
* @param addressOrAssetId - The address or assetId to convert
* @param chainId - The chainId of the asset
* @returns The CaipAssetType
*
* @throws {Error} If the chain is not supported by swap/bridge
*/
export const formatAddressToAssetId = (
addressOrAssetId: Hex | CaipAssetType | string,
chainId: GenericQuoteRequest['srcChainId'],
): CaipAssetType | undefined => {
): CaipAssetType => {
if (isCaipAssetType(addressOrAssetId)) {
return addressOrAssetId;
}
if (isNativeAddress(addressOrAssetId)) {
return getNativeAssetForChainId(chainId).assetId;
}
if (chainId === SolScope.Mainnet) {
return CaipAssetTypeStruct.create(`${chainId}/token:${addressOrAssetId}`);
}

// EVM assets
if (!isStrictHexString(addressOrAssetId)) {
return undefined;
if (isStrictHexString(addressOrAssetId)) {
return CaipAssetTypeStruct.create(
`${formatChainIdToCaip(chainId)}/erc20:${addressOrAssetId.toLowerCase()}`,
);
}
// Non-EVM assets
return CaipAssetTypeStruct.create(
`${formatChainIdToCaip(chainId)}/erc20:${addressOrAssetId}`,
`${formatChainIdToCaip(chainId)}/token:${addressOrAssetId}`,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Refactored Function Lacks Validation

The refactored formatAddressToAssetId function removes validation and always returns a CaipAssetType, even for invalid inputs. The old implementation returned undefined when addressOrAssetId was not a valid hex string (after checking for native addresses and Solana). The new implementation will create a "token:" type asset for ANY non-hex string input in the final fallthrough case, which could create invalid CAIP asset types for malformed or invalid addresses. This changes the function's contract (return type changed from CaipAssetType | undefined to CaipAssetType) without proper validation, potentially masking errors.

Fix in Cursor Fix in Web

);
};
Loading