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
10 changes: 10 additions & 0 deletions packages/assets-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add `isNativeAsset` utility that centralizes native asset detection across all CAIP-19 representations (`slip44:` namespace, known native asset IDs from `SPOT_PRICES_SUPPORT_INFO`, and `erc20:` with zero address) ([#8483](https://github.com/MetaMask/core/pull/8483))

### Changed

- Bump `@metamask/keyring-api` from `^21.6.0` to `^23.0.1` ([#8464](https://github.com/MetaMask/core/pull/8464))
- Bump `@metamask/keyring-internal-api` from `^10.0.0` to `^10.1.1` ([#8464](https://github.com/MetaMask/core/pull/8464))
- Bump `@metamask/keyring-snap-client` from `^8.2.0` to `^9.0.1` ([#8464](https://github.com/MetaMask/core/pull/8464))
- Bump `@metamask/transaction-controller` from `^64.2.0` to `^64.3.0` ([#8482](https://github.com/MetaMask/core/pull/8482))

### Fixed

- Native asset detection now correctly identifies native assets across all CAIP-19 representations, not just `slip44:` namespace checks ([#TBD](https://github.com/MetaMask/core/pull/TBD))
- Previously, native assets represented as ERC-20 tokens (e.g., Polygon's POL at `0x…1010`) were not recognized as native, causing incorrect token type classification, balance handling, and missing entries in bridge exchange rates and transaction pay legacy formats.
- Legacy format conversions (bridge exchange rates and transaction pay) now use the correct chain-specific native token address via `getNativeTokenAddress()` instead of always using the zero address ([#TBD](https://github.com/MetaMask/core/pull/TBD))

## [6.0.0]

### Added
Expand Down
5 changes: 3 additions & 2 deletions packages/assets-controller/src/AssetsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ import type {
BridgeExchangeRatesFormat,
TransactionPayLegacyFormat,
} from './utils';
import { isNativeAsset } from './utils/isNativeAsset';

// ============================================================================
// PENDING TOKEN METADATA (UI input format for addCustomAsset)
Expand Down Expand Up @@ -1520,7 +1521,7 @@ export class AssetsController extends BaseController<
if (pendingMetadata) {
const parsed = parseCaipAssetType(normalizedAssetId);
let tokenType: FungibleAssetMetadata['type'] = 'erc20';
if (parsed.assetNamespace === 'slip44') {
if (isNativeAsset(normalizedAssetId)) {
tokenType = 'native';
} else if (parsed.assetNamespace === 'spl') {
tokenType = 'spl';
Expand Down Expand Up @@ -2140,7 +2141,7 @@ export class AssetsController extends BaseController<
}

// Check if it's a native token (either by metadata type or assetId format)
const isNative = metadata.type === 'native' || assetId.includes('/slip44:');
const isNative = metadata.type === 'native' || isNativeAsset(assetId);

return isNative;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type {
AssetBalance,
DataResponse,
} from '../types';
import { isNativeAsset } from '../utils/isNativeAsset';
import { AbstractDataSource } from './AbstractDataSource';
import type {
DataSourceState,
Expand Down Expand Up @@ -639,7 +640,7 @@ export class BackendWebsocketDataSource extends AbstractDataSource<
const assetId = asset.type as Caip19AssetId;

// Determine token type from asset type string
const isNative = asset.type.includes('/slip44:');
const isNative = isNativeAsset(assetId);
const tokenType = isNative ? 'native' : 'erc20';

// We assume decimals are always present; skip malformed updates
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import type {
} from '../types';
import { normalizeAssetId } from '../utils';
import { ZERO_ADDRESS } from '../utils/constants';
import { isNativeAsset } from '../utils/isNativeAsset';
import { AbstractDataSource } from './AbstractDataSource';
import type {
DataSourceState,
Expand Down Expand Up @@ -343,7 +344,7 @@ export class RpcDataSource extends AbstractDataSource<
const existingMetadata = this.#getExistingAssetsMetadata();

for (const balance of balances) {
const isNative = balance.assetId.includes('/slip44:');
const isNative = isNativeAsset(balance.assetId);
if (isNative) {
const chainStatus = this.#chainStatuses[chainId];

Expand Down Expand Up @@ -1367,7 +1368,9 @@ export class RpcDataSource extends AbstractDataSource<
'NetworkEnablementController:getState',
);

return nativeAssetIdentifiers[chainId] ?? `${chainId}/slip44:60`;
return (
nativeAssetIdentifiers[chainId] ?? `${chainId}/erc20:${ZERO_ADDRESS}`
);
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
bergarces marked this conversation as resolved.
}

/**
Expand Down
20 changes: 10 additions & 10 deletions packages/assets-controller/src/data-sources/TokenDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {
Middleware,
FungibleAssetMetadata,
} from '../types';
import { isNativeAsset } from '../utils/isNativeAsset';
import {
isStakingContractAssetId,
reduceInBatchesSerially,
Expand Down Expand Up @@ -85,13 +86,13 @@ export type TokenDataSourceAllowedActions =
* @returns FungibleAssetMetadata for state storage.
*/
function transformV3AssetResponseToMetadata(
assetId: string,
assetId: Caip19AssetId,
assetData: V3AssetResponse,
): AssetMetadata {
const parsed = parseCaipAssetType(assetId as CaipAssetType);
const parsed = parseCaipAssetType(assetId);
let tokenType: 'native' | 'erc20' | 'spl' = 'erc20';

if (parsed.assetNamespace === 'slip44') {
if (isNativeAsset(assetId)) {
tokenType = 'native';
} else if (parsed.assetNamespace === 'spl') {
tokenType = 'spl';
Expand Down Expand Up @@ -396,18 +397,17 @@ export class TokenDataSource {
const nonEvmTokenIds: string[] = [];

for (const assetData of metadataResponse) {
const { assetNamespace, chain } = parseCaipAssetType(
assetData.assetId as CaipAssetType,
);
if (assetNamespace === CaipAssetNamespace.Slip44) {
const assetId = assetData.assetId as Caip19AssetId;
const { assetNamespace, chain } = parseCaipAssetType(assetId);
if (isNativeAsset(assetId)) {
// Native assets are always kept — no filtering.
} else if (
assetNamespace === CaipAssetNamespace.Erc20 &&
chain.namespace === KnownCaipNamespace.Eip155
) {
evmErc20Ids.push(assetData.assetId);
evmErc20Ids.push(assetId);
} else if (assetNamespace === CaipAssetNamespace.Token) {
nonEvmTokenIds.push(assetData.assetId);
nonEvmTokenIds.push(assetId);
}
}

Expand Down Expand Up @@ -461,7 +461,7 @@ export class TokenDataSource {

const caipAssetId = assetData.assetId as Caip19AssetId;
response.assetsInfo[caipAssetId] = transformV3AssetResponseToMetadata(
assetData.assetId,
caipAssetId,
assetData,
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { StaticIntervalPollingControllerOnly } from '@metamask/polling-controlle
import { parseCaipAssetType } from '@metamask/utils';

import { ZERO_ADDRESS } from '../../../utils/constants';
import { isNativeAsset } from '../../../utils/isNativeAsset';
import type { MulticallClient } from '../clients';
import type {
AccountId,
Expand Down Expand Up @@ -139,7 +140,6 @@ export class BalanceFetcher extends StaticIntervalPollingControllerOnly<BalanceP
for (const assetId of Object.keys(accountBalances) as CaipAssetType[]) {
const {
chain: { reference: chainReference },
assetNamespace,
assetReference,
} = parseCaipAssetType(assetId);

Expand All @@ -149,9 +149,9 @@ export class BalanceFetcher extends StaticIntervalPollingControllerOnly<BalanceP
continue;
}

const isNative = assetNamespace === 'slip44';
const isNative = isNativeAsset(assetId);
const tokenAddress = isNative
? ZERO_ADDRESS
? ZERO_ADDRESS // BalanceFetcher requires the use of zero address even for chains in which the native token has a non-zero address
: (assetReference.toLowerCase() as Address);

assetsToFetch.set(assetIdLowerCase, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,8 @@ describe('formatExchangeRatesForBridge', () => {
conversionRate: 0.5,
usdConversionRate: 0.5,
});
const nativeAddress = '0x0000000000000000000000000000000000000000';
expect(result.marketData['0x89']?.[nativeAddress]?.currency).toBe('POL');
const nativeAddressPol = '0x0000000000000000000000000000000000001010';
expect(result.marketData['0x89']?.[nativeAddressPol]?.currency).toBe('POL');
});

it('includes EVM native asset in marketData and currencyRates', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import {
MarketDataDetails,
MultichainAssetsRatesControllerState,
TokenRatesControllerState,
getNativeTokenAddress,
} from '@metamask/assets-controllers';
import { Hex, KnownCaipNamespace, numberToHex } from '@metamask/utils';
import { parseCaipAssetType, parseCaipChainId } from '@metamask/utils';

import type { AssetPrice, FungibleAssetPrice, Caip19AssetId } from '../types';
import { isNativeAsset } from './isNativeAsset';

/**
* Exchange rates in the format expected by the bridge controller:
Expand Down Expand Up @@ -83,15 +85,16 @@ export function formatExchangeRatesForBridge(params: {
const expirationTime = lastUpdatedInSeconds + expirationOffsetInSeconds;

try {
const isNative = isNativeAsset(assetId as Caip19AssetId);
const parsed = parseCaipAssetType(assetId as Caip19AssetId);
const chainIdParsed = parseCaipChainId(parsed.chainId);

if (chainIdParsed.namespace === KnownCaipNamespace.Eip155) {
const chainIdHex = numberToHex(parseInt(chainIdParsed.reference, 10));

const nativeAssetId = nativeAssetIdentifiers[parsed.chainId] as
| Caip19AssetId
| undefined;
const nativeAssetId = (
isNative ? assetId : nativeAssetIdentifiers[parsed.chainId]
) as Caip19AssetId | undefined;

const nativeCurrencySymbol =
networkConfigurationsByChainId[chainIdHex]?.nativeCurrency;
Expand All @@ -110,12 +113,12 @@ export function formatExchangeRatesForBridge(params: {

let tokenAddress: Hex | undefined;
if (parsed.assetNamespace === 'erc20') {
tokenAddress = toChecksumAddress(String(parsed.assetReference));
} else if (parsed.assetNamespace === 'slip44') {
tokenAddress = '0x0000000000000000000000000000000000000000';
tokenAddress = toChecksumAddress(parsed.assetReference);
} else if (isNative) {
tokenAddress = toChecksumAddress(getNativeTokenAddress(chainIdHex));
}

if (tokenAddress && nativeAssetId) {
if (tokenAddress) {
const priceInNative =
nativeAssetUsdPrice > 0 ? usdPrice / nativeAssetUsdPrice : usdPrice;
if (!marketData[chainIdHex]) {
Expand All @@ -131,7 +134,7 @@ export function formatExchangeRatesForBridge(params: {
} as MarketDataDetails;
}

if (parsed.assetNamespace === 'slip44' && nativeAssetId) {
if (isNative) {
currencyRates[nativeCurrencySymbol] = {
conversionDate: lastUpdatedInSeconds,
conversionRate: price,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { toChecksumAddress } from '@ethereumjs/util';
import { getNativeTokenAddress } from '@metamask/assets-controllers';
import { numberToHex } from '@metamask/utils';
import { parseCaipAssetType, parseCaipChainId } from '@metamask/utils';

Expand All @@ -10,6 +11,7 @@ import type {
} from '../types';
import { formatExchangeRatesForBridge } from './formatExchangeRatesForBridge';
import type { BridgeExchangeRatesFormat } from './formatExchangeRatesForBridge';
import { isNativeAsset } from './isNativeAsset';

/** Account with id and address for mapping state to legacy format. */
export type AccountForLegacyFormat = { id: string; address: string };
Expand Down Expand Up @@ -115,9 +117,10 @@ export function formatStateForTransactionPay(params: {
const amount = getAmountFromBalance(balance);
const balanceHex = amountToHex(amount);

if (parsed.assetNamespace === 'slip44') {
const nativeAddress =
'0x0000000000000000000000000000000000000000' as const;
if (isNativeAsset(assetId as Caip19AssetId)) {
const nativeAddress = toChecksumAddress(
getNativeTokenAddress(chainIdHex),
);
const checksumAddress = toChecksumAddress(account.address);
tokenBalances[accountAddressLower] ??= {};
tokenBalances[accountAddressLower][chainIdHex] ??= {};
Expand Down Expand Up @@ -148,10 +151,9 @@ export function formatStateForTransactionPay(params: {
continue;
}
const chainIdHex = numberToHex(parseInt(chainIdParsed.reference, 10));
const address =
parsed.assetNamespace === 'slip44'
? '0x0000000000000000000000000000000000000000'
: toChecksumAddress(String(parsed.assetReference));
const address = isNativeAsset(assetId as Caip19AssetId)
? getNativeTokenAddress(chainIdHex)
: toChecksumAddress(String(parsed.assetReference));
const token: LegacyToken = {
address,
decimals: metadata.decimals,
Expand Down
31 changes: 31 additions & 0 deletions packages/assets-controller/src/utils/isNativeAsset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { SPOT_PRICES_SUPPORT_INFO } from '@metamask/assets-controllers';
import { parseCaipAssetType } from '@metamask/utils';

import type { Caip19AssetId } from '../types';
import { ZERO_ADDRESS } from './constants';

export function isNativeAsset(assetId: Caip19AssetId): boolean {
const { assetNamespace, assetReference } = parseCaipAssetType(assetId);

// All SLIP44 assets are native assets
if (assetNamespace === 'slip44') {
return true;
}

// All assets in this list are native assets
if (
Object.values(SPOT_PRICES_SUPPORT_INFO).some(
(nativeAssetId) => nativeAssetId.toLowerCase() === assetId.toLowerCase(),
)
) {
return true;
}

// ERC20 assets with a zero address are native assets
if (assetNamespace === 'erc20' && assetReference === ZERO_ADDRESS) {
return true;
}

// Not a native asset
return false;
}
1 change: 1 addition & 0 deletions packages/assets-controllers/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- `MultichainAssetsController`: periodic Blockaid re-scan of stored SPL-style `token:` assets (default once per day) so tokens that become malicious after a prior scan are dropped; use constructor option `blockaidTokenRescanInterval` (ms), or `0` to disable. ([#8400](https://github.com/MetaMask/core/pull/8400))
- Export `SPOT_PRICES_SUPPORT_INFO` from the token prices service ([#8483](https://github.com/MetaMask/core/pull/8483))

### Changed

Expand Down
1 change: 1 addition & 0 deletions packages/assets-controllers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ export {
CodefiTokenPricesServiceV2,
SUPPORTED_CHAIN_IDS,
getNativeTokenAddress,
SPOT_PRICES_SUPPORT_INFO,
} from './token-prices-service';
export {
searchTokens,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ describe('token-prices-service', () => {
"fetchSupportedNetworks",
"getSupportedNetworks",
"resetSupportedNetworksCache",
"SPOT_PRICES_SUPPORT_INFO",
]
`);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ export {
fetchSupportedNetworks,
getSupportedNetworks,
resetSupportedNetworksCache,
SPOT_PRICES_SUPPORT_INFO,
} from './codefi-v2';
Loading