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

### Fixed

- Apply occurrence-floor spam filtering to tokens received through account-activity (websocket) updates: new tokens are enriched with Token API occurrence counts before detection and dropped when below the per-chain suggested floor, and stub metadata of filtered-out assets is stripped from the pipeline response so spam tokens never persist to state ([#9803](https://github.com/MetaMask/core/pull/9803))
- Checksum the lower-case ERC-20 asset IDs delivered by `AccountActivityDataSource` before they enter the assets pipeline, so middleware comparisons against state (which keys assets by checksummed ID) no longer treat an existing holding as a brand-new asset — previously such balance updates could be re-detected, or dropped entirely by spam filtering, and never reach state ([#9803](https://github.com/MetaMask/core/pull/9803))
- Preserve pooled-staking balances across Accounts API chain-slice updates (e.g. network switch / `replaceCoveredChainBalances`): exclude staking contract asset IDs from `AccountsApiDataSource` v5/v6 balance processing, and keep prior staked amounts when a merge replace omits them so Accounts API cannot reset staked ETH to missing/0 ([#9753](https://github.com/MetaMask/core/pull/9753))

## [13.1.1]
Expand Down
16 changes: 16 additions & 0 deletions packages/assets-controller/src/AssetsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3874,10 +3874,26 @@ export class AssetsController extends BaseController<
sourceId === 'AccountsApiDataSource' ||
sourceId === 'AccountActivityDataSource';

// Websocket updates can carry brand-new spam airdrops: enrich them
// with Token API occurrences and drop below-floor tokens BEFORE
// detection, so spam is never detected, enriched, priced or persisted.
const shouldFilterOccurrences =
sourceId === 'AccountActivityDataSource' &&
this.#isBasicFunctionality();

const enrichmentSources: AssetsDataSource[] = [
...(shouldGraduateCustomAssets
? [this.#customAssetGraduationMiddleware]
: []),
...(shouldFilterOccurrences
? [
{
getName: () => 'OccurrenceFloorFilter',
assetsMiddleware:
this.#tokenDataSource.occurrenceFilterMiddleware,
},
]
: []),
this.#detectionMiddleware,
];
if (this.#isBasicFunctionality()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ const EVM_ADDRESS = '0x1234567890123456789012345678901234567890';
const SOLANA_ADDRESS = 'DjVE6JNiYqPL2QXyCUUh8rNjHrbz9hXHNYt99MQ59qw1';

const ETH_ASSET = 'eip155:1/slip44:60' as Caip19AssetId;
// The websocket delivers lower-case addresses; state keys them checksummed.
const USDC_ASSET_LOWERCASE =
'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' as Caip19AssetId;
const USDC_ASSET_CHECKSUMMED =
'eip155:1/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' as Caip19AssetId;

/**
* Build an InternalAccount for tests.
Expand Down Expand Up @@ -354,6 +359,60 @@ describe('AccountActivityDataSource', () => {
cleanup();
});

it('checksums the lower-case ERC-20 asset IDs the websocket delivers', async () => {
const account = createMockAccount();
const { onAssetsUpdate, triggerBalanceUpdated, cleanup } = setup({
groupAccounts: [account],
});

triggerBalanceUpdated({
address: EVM_ADDRESS,
chain: CHAIN_MAINNET,
updates: [
createBalanceUpdate({
asset: { type: USDC_ASSET_LOWERCASE, unit: 'USDC', decimals: 6 },
postBalance: { amount: '1000000' },
}),
],
});

await Promise.resolve();

const [response] = onAssetsUpdate.mock.calls[0];
expect(response.assetsBalance[account.id]).toHaveProperty(
USDC_ASSET_CHECKSUMMED,
);
expect(response.assetsBalance[account.id]).not.toHaveProperty(
USDC_ASSET_LOWERCASE,
);
expect(response.assetsInfo).toHaveProperty(USDC_ASSET_CHECKSUMMED);

cleanup();
});

it('keeps an unparseable asset ID as-is', async () => {
const account = createMockAccount();
const { onAssetsUpdate, triggerBalanceUpdated, cleanup } = setup({
groupAccounts: [account],
});

const malformedAssetId = 'eip155:1/erc20:not-an-address' as Caip19AssetId;
triggerBalanceUpdated({
address: EVM_ADDRESS,
chain: CHAIN_MAINNET,
updates: [createBalanceUpdate({ asset: { type: malformedAssetId } })],
});

await Promise.resolve();

const [response] = onAssetsUpdate.mock.calls[0];
expect(response.assetsBalance[account.id]).toHaveProperty(
malformedAssetId,
);

cleanup();
});

it.each([
['address is empty', { address: '' }],
['chain is empty', { chain: '' }],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {
DataRequest,
DataResponse,
} from '../types.js';
import { safeNormalizeAssetId } from '../utils/index.js';
import { AbstractDataSource } from './AbstractDataSource.js';
import type { DataSourceState } from './AbstractDataSource.js';

Expand Down Expand Up @@ -66,7 +67,11 @@ function processAccountActivityBalanceUpdates(
continue;
}

const assetId = asset.type as Caip19AssetId;
// The websocket sends lower-case ERC-20 addresses, while state and the
// rest of the pipeline key assets by their checksummed ID. Normalizing
// here keeps middleware comparisons (detection, custom-asset graduation,
// occurrence filtering) from treating an existing holding as brand new.
const assetId = safeNormalizeAssetId(asset.type as Caip19AssetId);

if (asset.decimals === undefined) {
continue;
Expand Down
Loading