diff --git a/packages/assets-controller/CHANGELOG.md b/packages/assets-controller/CHANGELOG.md index ad35f5f1fd..a0eba5b54d 100644 --- a/packages/assets-controller/CHANGELOG.md +++ b/packages/assets-controller/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `decimals: 0` is treated as valid; `name` and `symbol` are not required. - Decimals resolution in `#handleBalanceUpdate` and the manual-fetch path no longer relies on `??` to fall through from state to pipeline metadata. A new `#pickValidDecimals` helper picks the first source whose `decimals` is finite and non-negative, so a stale `decimals: NaN` (or `decimals: -1`) in state can no longer shadow the chain-status stub's `decimals: 18` and silently produce `amount: '0'` while `assetsInfo` reports `decimals: 18`. - `#convertToHumanReadable` now defensively returns `'0'` when `decimals` isn't a finite non-negative number or when the raw balance can't be parsed, matching the existing safe fallback used in the error path. +- The mUSD (`MetaMask USD`) contract address stored in `defaults.ts` is now EIP-55 checksummed (`0xacA92E438df0B2401fF60dA7E4337B687a2435DA`) ([#8786](https://github.com/MetaMask/core/pull/8786)). Previously the address was all-lowercase, causing the CAIP-19 keys pre-seeded into `assetsInfo` by `buildDefaultAssetsInfo()` to differ from the checksummed keys written by data sources (which always normalise asset IDs via `normalizeAssetId`). The mismatch resulted in two separate `assetsInfo` entries for the same token after the first balance or token-data poll. ## [7.1.1] diff --git a/packages/assets-controller/src/AssetsController.test.ts b/packages/assets-controller/src/AssetsController.test.ts index da37526a53..ab609ea176 100644 --- a/packages/assets-controller/src/AssetsController.test.ts +++ b/packages/assets-controller/src/AssetsController.test.ts @@ -260,6 +260,23 @@ describe('AssetsController', () => { selectedCurrency: 'usd', }); }); + + it('pre-seeds assetsInfo with EIP-55 checksummed CAIP-19 keys', () => { + // Regression: MUSD_ADDRESS was previously all-lowercase, so + // buildDefaultAssetsInfo() produced lowercase CAIP-19 keys while data + // sources (which call normalizeAssetId) wrote checksummed keys. + // After the first balance poll both keys existed in assetsInfo. + const defaultState = getDefaultAssetsControllerState(); + const assetIds = Object.keys(defaultState.assetsInfo); + expect(assetIds.length).toBeGreaterThan(0); + // Every erc20 asset ID must contain at least one uppercase hex letter + // (EIP-55 checksum property) so that keys match normalizeAssetId output. + const erc20Ids = assetIds.filter((id) => id.includes('/erc20:')); + expect(erc20Ids.length).toBeGreaterThan(0); + for (const id of erc20Ids) { + expect(id).toMatch(/\/erc20:0x[0-9a-fA-F]*[A-F][0-9a-fA-F]*/u); + } + }); }); describe('constructor', () => { diff --git a/packages/assets-controller/src/defaults.ts b/packages/assets-controller/src/defaults.ts index 963008a604..edb5be687b 100644 --- a/packages/assets-controller/src/defaults.ts +++ b/packages/assets-controller/src/defaults.ts @@ -6,10 +6,18 @@ import type { } from './types'; /** - * Address of MetaMask USD (mUSD) — same canonical contract address - * across every chain we deploy it to. + * EIP-55 checksummed address of MetaMask USD (mUSD) — same canonical contract + * address across every chain we deploy it to. + * + * Must be checksummed so that the CAIP-19 asset IDs produced by + * `musdAssetId()` (and therefore the keys seeded into `assetsInfo` by + * `buildDefaultAssetsInfo()`) match the keys written by data sources, which + * always pass IDs through `normalizeAssetId` → `toChecksumAddress` before + * emitting a `DataResponse`. Using a lowercase address would cause the + * pre-seeded keys to diverge from the data-source keys, leaving a duplicate + * entry in `assetsInfo` after the first balance or token-data poll. */ -const MUSD_ADDRESS = '0xaca92e438df0b2401ff60da7e4337b687a2435da'; +const MUSD_ADDRESS = '0xacA92E438df0B2401fF60dA7E4337B687a2435DA'; /** * Hardcoded metadata for MetaMask USD. Pre-seeding this in default @@ -63,14 +71,14 @@ export const CHAINS_WITH_DEFAULT_TRACKED_ASSETS: ReadonlySet = new Set( /** * Pre-seeded metadata for every default tracked asset, keyed by the - * lowercase CAIP-19 id so callers can look up without worrying about - * checksum case. + * checksummed CAIP-19 id. All callers must pass a checksummed asset ID; + * use `normalizeAssetId` to ensure the correct format before looking up. */ export const DEFAULT_ASSET_METADATA: ReadonlyMap = new Map([ - [musdAssetId('eip155:1' as ChainId).toLowerCase(), MUSD_METADATA], - [musdAssetId('eip155:59144' as ChainId).toLowerCase(), MUSD_METADATA], - [musdAssetId('eip155:143' as ChainId).toLowerCase(), MUSD_METADATA], + [musdAssetId('eip155:1' as ChainId), MUSD_METADATA], + [musdAssetId('eip155:59144' as ChainId), MUSD_METADATA], + [musdAssetId('eip155:143' as ChainId), MUSD_METADATA], ]); /** @@ -89,14 +97,14 @@ export function getDefaultTrackedAssetsForChain( /** * Look up pre-seeded metadata for a default tracked asset. * - * @param assetId - CAIP-19 asset id (any case). + * @param assetId - CAIP-19 asset id (must be EIP-55 checksummed for EVM tokens). * @returns The metadata if the asset is a default tracked asset, * otherwise `undefined`. */ export function getDefaultAssetMetadata( assetId: Caip19AssetId, ): AssetMetadata | undefined { - return DEFAULT_ASSET_METADATA.get(assetId.toLowerCase()); + return DEFAULT_ASSET_METADATA.get(assetId); } /**