fix(assets-controller): price deduper drops erc20 prices for inflight joiners and can mix currencies - #10063
Open
gomesalexandre wants to merge 2 commits into
Open
Conversation
… joiners and can mix currencies Two coupled bugs in PriceDataSource's price-fetching deduper: 1. The deduper's inflight-joiner path looked up the batch's raw API response by the requested key. MetaMask requests/stores checksummed CAIP-19 asset IDs, but the Price API's response echoes back lowercase-cased ERC-20 addresses. A caller starting the batch was rescued by AssetsController's own downstream key re-normalization, but a caller joining the same fetch as an inflight promise looked its checksummed key up directly against the raw (lowercase) response and got nothing back -- every ERC-20 price silently vanished for inflight joiners, self-healing only after the freshness TTL expired. 2. The deduper's cache/inflight key had no currency component, so a currency switch racing an in-flight fetch could join (or later receive) a price fetched under the previously-selected currency. Fix: introduce a composite `currency:assetId` deduper key (`PriceDeduperKey`), normalize asset IDs consistently at the point a key is built and at the point the API response is matched back against it (using `safeNormalizeAssetId` throughout, since response data is untrusted and a single malformed key must not poison an otherwise-valid batch), and decode the currency from the key inside `#executeBatchFetch` instead of re-reading the live selected currency at execution time (a second, independent source of the same race). Composite keys alone stop a *new* request from joining a stale-currency fetch, but a slow stale-currency fetch already independently in flight would otherwise still complete and let its now-superseded values reach the caller. `#executeBatchFetch` now discards its own result if the selected currency has moved on by the time the batch settles, closing that second half of the race. Two new regression-test describe blocks, plus a `forceUpdate`/ `invalidateKeys` normalization test and a checksummed-vs-lowercase caller-coalescing test, all verified red-before/green-after against a stashed pre-fix version of the source. Existing tests using an all-lowercase mock asset ID (a coincidentally-unaffected 0.3% of real address space) updated to assert the now-normalized key. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lh6V2uPTUqauqq45BM7m5k
gomesalexandre
marked this pull request as ready for review
September 1, 2026 21:42
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Explanation
Two coupled bugs in
PriceDataSource's price-fetching deduper (DedupingBatchFetcher+#executeBatchFetch/#fetchSpotPrices):Bug A — inflight joiners silently lose ERC-20 prices
assetsMiddlewarerequests/stores CAIP-19 asset IDs in checksummed form (normalizeAssetId), but the Price API's response echoes back ERC-20 addresses lowercase. The deduper's inflight-joiner path (dedupingBatchFetcher.ts:186) looks up the batch's raw response by the requested key. A caller starting the batch happened to be rescued downstream (AssetsController's own response-key re-normalization at merge time), but a caller joining the same fetch as an in-flight promise looked its checksummed key up directly against the raw (lowercase) response and gotundefined— the price silently vanished, self-healing only after the freshness TTL expired.Bug B — currency-blind cache key
The deduper's cache/inflight key was just the asset ID, with no currency component.
invalidate()/invalidateKeys()(called on a currency change) are documented to leave in-flight promises alone. So a currency switch racing an in-flight fetch could join — or, worse, later receive on settlement — a price fetched under the previously-selected currency.Fix
currency:assetIddeduper key (PriceDeduperKey).safeNormalizeAssetIdthroughout — response data is untrusted, and one malformed key must not poison an otherwise-valid batch (a real gap in my first draft, caught in review — see receipts).#executeBatchFetchinstead of re-reading the live selected currency at execution time, which is itself a second, independent source of the same race (the currency could move on between when a batch is queued and when its async callback actually runs).#executeBatchFetchnow discards its own result if the selected currency has moved on by the time the batch settles — closing the other half of the race.#executeBatchFetchthrows if a batch is ever handed keys spanning more than one currency (currently unreachable given how keys are built, but cheap to make loud rather than silently wrong if that invariant is ever broken by a future change).References
None — found via independent code-review/testing during this session, not tied to a filed issue.
Changelog
receipts
Real, independently-reproduced repros (not just reasoning from source) for both bugs, plus genuine red-before/green-after on every new test:
New regression describe blocks:
regression: checksummed vs lowercase asset ID casing— an inflight joiner receivingundefinedfor a checksummed key against a lowercase-keyed response, plus a lowercase-vs-checksummed caller-coalescing test (2 API calls → 1)regression: forceUpdate invalidation uses the normalized deduper key— provesinvalidateKeystargets the same normalized key the fetch populated, not a silent no-opregression: currency switch racing an inflight fetch— a USD fetch hangs, currency switches to EUR mid-flight, the EUR request gets the EUR price (not the stale USD one), and the stale USD result is discarded on settlement rather than delivered to anyoneGenuine red-before/green-after — stashed just the source fix (kept the new tests), reran:
Full package suite (all consumers, incl.
AssetsController.test.ts):Real repo-wide typecheck (the actual CI command, not a scoped
tsc --noEmitthat hits unrelated project-reference build-order noise):Lint clean:
Changelog validated:
Adversarial review (Codex, synchronous)
Ran Codex against the diff before opening this PR. It found real, substantive gaps in my first draft, all fixed before this PR was opened:
normalizeAssetId(which can throw on malformed input) was used unguarded in three places touching untrusted/batch data, so one malformed asset ID could poison an entire otherwise-valid batch. → switched tosafeNormalizeAssetIdat all three sites.forceUpdate/invalidateKeysstill worked correctly against the new composite key, and no test proved a genuinely different-cased caller (not just a differently-cased API response) coalesces correctly. → added both.Checklist
README, e.g.) for new or updated code as neededNote
Medium Risk
Changes core price-fetch caching and coalescing used across the assets pipeline; behavior is well-covered by new regression tests but incorrect keying could still affect displayed fiat values after currency switches or concurrent fetches.
Overview
Fixes two bugs in
PriceDataSource’s price-fetch deduper: inflight joiners could miss ERC-20 prices when API response keys were lowercase but request keys were checksummed, and a currency change could coalesce onto or apply stale in-flight prices because cache keys were asset-only.The deduper now uses
currency:normalizedAssetIdkeys, maps API responses back throughsafeNormalizeAssetId(including the USD companion batch),forceUpdateinvalidates those composite keys, and#executeBatchFetchdrops results if the selected currency moved on while the request was in flight. Regression tests cover address casing, forced refresh, and currency-switch races; the package changelog documents the fixes.Reviewed by Cursor Bugbot for commit 363f6cb. Bugbot is set up for automated code reviews on this repo. Configure here.