fix: price staked ETH at parity with native currency instead of $0 - #10067
Open
gomesalexandre wants to merge 1 commit into
Open
fix: price staked ETH at parity with native currency instead of $0#10067gomesalexandre wants to merge 1 commit into
gomesalexandre wants to merge 1 commit into
Conversation
Staked positions were valued at $0 across the aggregated-balance selectors and every public asset getter (getAccountAssetByID, getAccountAssetsByIDs, getAccountAssetsByScope, getAssets, getAssetsPrice). The staking-vault contract's own asset ID is never a priced token -- the Price API always returns null for it -- and the new unified assets-controller re-derived the legacy balances.ts staked-native price aliasing without carrying it over. Adds a single source of truth (STAKING_CHAIN_CONFIG) mapping each staking-supported chain to its vault address AND native SLIP-44 coin type together, plus resolvePriceLookupAssetId() which both the selector and AssetsController#getAssetFromState now use unconditionally -- not just as an absence-fallback, so a stale price recorded directly under the vault's own key (persisted pre-fix state, or a partial currency-switch refresh) can never win over the correct native price. PriceDataSource now maps a staking-vault ID to its native asset ID before both fetch-request sites, so the correct price is actually requested rather than merely not-dropped, and isPriceableAsset excludes the vault address so the app stops issuing a request that can only return null. The same resolve-before-check fix is applied to AssetsController#fetchMissingPricesWithoutCache and DetectionMiddleware's price-queuing so a stale vault-keyed price entry can't suppress a legitimate re-fetch of the native price through any of the three enqueue paths.
gomesalexandre
marked this pull request as ready for review
September 2, 2026 00:00
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.
Summary
Staked ETH was valued at
$0throughout the new unified assets-controller — in the aggregated-balance selectors and in every public asset getter (getAccountAssetByID,getAccountAssetsByIDs,getAccountAssetsByScope,getAssets,getAssetsPrice).The staking-vault contract's own asset ID (e.g.
eip155:1/erc20:0x4fef9d741011476750a243ac70b9789a63dd47df) is never a priced token — the Price API always returnsnullfor it. The legacypackages/assets-controllers/src/balances.tshandles this by aliasing a staked-native position to the native token's price (added in #8141, whose own description says native/staked-native balances were previously excluded and undercounted). The newassets-controllerpackage re-derives that calculation from scratch and never carried the alias over.Evidence
Live API + real code, before the fix:
The fix
STAKING_CHAIN_CONFIG(new, instaking-contracts.ts) is a single source of truth mapping each staking-supported chain to both its vault address and its native SLIP-44 coin type together.STAKING_CONTRACT_ADDRESS_BY_CHAINID(the existing public export) is now derived from it rather than hand-maintained separately, so the two pieces of a chain's staking config can't drift apart.resolvePriceLookupAssetId(assetId)resolves a staking-vault asset ID to its chain's native asset ID, or returns the input unchanged. It's used unconditionally, not merely as a fallback on a missing entry — so a stale price recorded directly under the vault's own key (e.g. from persisted pre-fix state) can never win over the correct native price.selectors/balance.ts'sgetPriceDatumFastandAssetsController#getAssetFromState(which backs every public getter) both route through this resolver.PriceDataSourcemaps a staking-vault ID to its native asset ID before both fetch-request sites, so the native price is actually requested, not merely not-dropped by theisPriceableAssetfilter (which now also excludes the vault address, since requesting it can only ever returnnull).AssetsController#fetchMissingPricesWithoutCacheandDetectionMiddleware's price-queuing (maybeQueue) — both previously checked presence against the raw vault key, which could let a stale entry there suppress a legitimate re-fetch of the native price.Testing
packages/assets-controller/src/data-sources/evm-rpc-services/utils/staking-contracts.test.ts(new): direct coverage ofgetNativeAssetIdForStakedAsset/resolvePriceLookupAssetId— mainnet, Hoodi, case-insensitivity, wrong-chain, unrelated-erc20, malformed-input.selectors/balance.test.ts: a real mixed liquid+staked portfolio (was2000, now6000for 1 ETH + 2 staked ETH at 2000/ETH); a boundary test for an unrelated erc20 on a staking-supported chain; a regression test proving the native price wins even when a stale price is also recorded under the vault's own key.data-sources/PriceDataSource.test.ts: the fetch request for a staked-only balance targets the native asset, never the vault address; a mixed native+staked balance only requests the native asset once (no duplicate).middlewares/DetectionMiddleware.test.ts: a staking asset is still queued for a price update even when a stale price exists under its own vault key.AssetsController.test.ts: the publicgetAccountAssetByIDgetter returns the correct native-derived price andfiatValuefor a staked position.All new tests were verified genuinely red-before/green-after via
git stashon the source fix (each fails with the exact predicted symptom against unfixed code).yarn lint:tsc(the repo's real project-references-aware typecheck, run from a fully cache-cleared state) andyarn eslinton every changed file are both clean.Review notes
Record<string, string>collapseskeyof typeofback to plainstring, so nothing was enforced) — replaced with the singleSTAKING_CHAIN_CONFIGmap instead. It also found the same stale-key-suppression bug existed in three additional enqueue paths (#fetchMissingPricesWithoutCache,DetectionMiddleware,PriceDataSource's detected-assets branch) — all fixed and covered by new regression tests.parseCaipAssetTypeargument type mismatch) using a rawtsc -p packages/assets-controller/tsconfig.jsoninvocation. I verified this directly: that invocation bypasses this monorepo's TypeScript project-references build mode, and produces ~80 clearly-false errors on completely untouched pre-existing lines in the same files (e.g.Property 'messenger' does not exist on type 'AssetsController', which is trivially false). A fully cache-cleared run of the repo's actual CI command (yarn lint:tsc, i.e.tsc --build) is genuinely clean. Flagging this in case a reviewer's own localtsc -pinvocation reproduces the same false positive.Note
Medium Risk
Changes fiat valuation and price-fetch behavior for staking vault balances across selectors and getters; scope is well-tested but incorrect aliasing would misstate portfolio value.
Overview
Staked vault positions (e.g. mainnet stETH contract) were shown at $0 in the unified
assets-controllerbecause the Price API never prices the vault token itself. This PR aliases staking-vault asset IDs to the chain’s native asset for all price reads and fetches.STAKING_CHAIN_CONFIGnow pairs each supported chain’s vault address with its native SLIP-44 id;resolvePriceLookupAssetIdmaps vault IDs to native IDs (always, not only when the vault key is missing) so staleassetsPriceentries under the vault cannot block correct fiat. That resolver is used in aggregated balance selectors,getAccountAssetByID(and related getters),PriceDataSource(fetch + detected-asset queuing, with dedupe),DetectionMiddleware, and the force-refresh missing-price path.isPriceableAssettreats vault contracts as non-priceable so batches request native ETH instead.Regression tests cover public getters, price API request shape, duplicate native fetches, stale vault-key suppression, and portfolio totals.
Reviewed by Cursor Bugbot for commit ca59661. Bugbot is set up for automated code reviews on this repo. Configure here.