Summary
When a token's TokenMetadataEntity row is missing from the local Room DB but a balance row exists, three different surfaces in the app behave inconsistently — none of them correctly. For non-18-decimal tokens (USDC, USDT, WBTC, etc.) on Ethereum mainnet this produces a silent failure mode: the token disappears from the home view entirely, while the swap view displays a balance off by ~10¹². The transaction history shows the correct amount, which makes the bug confusing to diagnose from the user side.
Reproduction
- Device: dGEN1 / ethOS, WalletManager
versionCode 155 (commit d234e2c3 on update-token_carousel / feature-token_extensions).
- Hold any non-zero balance of a non-18-decimal token (USDC = 6 decimals reproduces it cleanly) on Ethereum mainnet.
- If the token's metadata row hasn't been populated:
- Home view: token absent from the asset list entirely.
- Swap view: token appears, but balance displays with ~10–12 leading zeros (e.g., real
N USDC shows as N × 10⁻¹²).
- Transaction history: token amounts display correctly.
Why this happens — three code paths, one root cause
Symptom 1: Home view drops the token
File: core/data/src/main/java/com/core/data/repository/DefaultGroupedTokenRepository.kt, ~lines 79–83
val metadata = token.tokenMetadataEntity
...
// Skip tokens without metadata
if (metadata == null) {
// ...skip
}
Defensible in isolation, but combined with Symptom 2 it produces a silent disappearance — the user has no signal that the token exists or that anything failed.
Symptom 2: Swap view defaults missing decimals to 18 (primary bug)
File: core/data/src/main/java/com/core/data/repository/AlchemyTokenBalanceRepository.kt, ~lines 91–95
val assetsWithoutMetadata = tokensWithoutMetadata.map { compositeToken ->
val balanceEntity = compositeToken.tokenBalanceEntity!!
// Use default decimals of 18 for tokens without metadata
val decimals = 18
val balance = balanceEntity.tokenBalance
.movePointLeft(decimals)
...
For a 6-decimal token, movePointLeft(18) is off by 10¹². The buggy balance then flows into anything that consumes getCombinedTokens() — including the swap UI.
Symptom 3 (related antipattern): Swap fallback also hardcodes 18
File: feature/swap/src/main/java/com/feature/swap/SwapViewModel.kt, ~lines 920–928
} else {
Log.w("SwapViewModel", "convertToSwapToken: No primary token found, using fallback")
TokenAsset(
address = "0x0000000000000000000000000000000000000000",
chainId = 1,
symbol = asset.symbol,
name = asset.name,
balance = asset.totalBalance,
decimals = 18,
...
)
}
Less likely to fire for top-tier tokens, but it's the same antipattern: when lookup fails, fabricate a TokenAsset with decimals = 18 and a zero address. Anything downstream that trusts this object will
be wrong.
Why the recent metadata-side fix didn't catch this
Commit b2b4b1f ("Fix token decimals defaulting to 18 for Clanker API fallback") patched the decimals = 18 default in AlchemyTokenMetadataRepository.kt and added an on-chain fallback via
OnChainTokenMetadataFetcher. The matching defaults in AlchemyTokenBalanceRepository.kt and SwapViewModel.kt were missed. Same antipattern, three places — only one fixed.
Suggested fixes
For Symptom 2 (AlchemyTokenBalanceRepository.kt:91) — apply the same pattern that b2b4b1f already established:
- When a balance exists without metadata, trigger an on-chain decimals() fetch via the existing OnChainTokenMetadataFetcher before constructing the TokenAsset.
- If that also fails, omit the entry rather than fabricate decimals = 18. Displaying a wrong number is worse than displaying nothing.
For Symptom 1 (DefaultGroupedTokenRepository.kt:83) — pair the silent skip with a refresh:
- When iterating composite tokens and finding a balance without metadata, enqueue a metadata refresh for that contract instead of silently dropping it. Optionally surface a "loading metadata…"
placeholder in the asset list so the user has feedback.
For Symptom 3 (SwapViewModel.kt:920) — fail closed:
- The fallback should disable the swap or surface an error, not synthesize a TokenAsset with decimals = 18 and address = 0x0.
Defaulting unknown tokens to 18 decimals only happens to work for ETH-class assets and breaks visibly for stablecoins (USDC/USDT = 6) and BTC-class wrappers (WBTC = 8). The right contract is "decimals
come from on-chain or from a verified source — never invented."
Examples/Pics
I can provide the ETH address if absolutely needed.
Home screen with no card for USDC

Swap screen showing 0.000000000010379... USDC (should show 10.379...)

Transaction log showing the original swap 10.3806 USDC

Summary
When a token's
TokenMetadataEntityrow is missing from the local Room DB but a balance row exists, three different surfaces in the app behave inconsistently — none of them correctly. For non-18-decimal tokens (USDC, USDT, WBTC, etc.) on Ethereum mainnet this produces a silent failure mode: the token disappears from the home view entirely, while the swap view displays a balance off by ~10¹². The transaction history shows the correct amount, which makes the bug confusing to diagnose from the user side.Reproduction
versionCode 155(commitd234e2c3onupdate-token_carousel/feature-token_extensions).N USDCshows asN × 10⁻¹²).Why this happens — three code paths, one root cause
Symptom 1: Home view drops the token
File:
core/data/src/main/java/com/core/data/repository/DefaultGroupedTokenRepository.kt, ~lines 79–83Defensible in isolation, but combined with Symptom 2 it produces a silent disappearance — the user has no signal that the token exists or that anything failed.
Symptom 2: Swap view defaults missing decimals to 18 (primary bug)
File: core/data/src/main/java/com/core/data/repository/AlchemyTokenBalanceRepository.kt, ~lines 91–95
For a 6-decimal token, movePointLeft(18) is off by 10¹². The buggy balance then flows into anything that consumes getCombinedTokens() — including the swap UI.
Symptom 3 (related antipattern): Swap fallback also hardcodes 18
File: feature/swap/src/main/java/com/feature/swap/SwapViewModel.kt, ~lines 920–928
Less likely to fire for top-tier tokens, but it's the same antipattern: when lookup fails, fabricate a TokenAsset with decimals = 18 and a zero address. Anything downstream that trusts this object will
be wrong.
Why the recent metadata-side fix didn't catch this
Commit b2b4b1f ("Fix token decimals defaulting to 18 for Clanker API fallback") patched the decimals = 18 default in AlchemyTokenMetadataRepository.kt and added an on-chain fallback via
OnChainTokenMetadataFetcher. The matching defaults in AlchemyTokenBalanceRepository.kt and SwapViewModel.kt were missed. Same antipattern, three places — only one fixed.
Suggested fixes
For Symptom 2 (AlchemyTokenBalanceRepository.kt:91) — apply the same pattern that b2b4b1f already established:
For Symptom 1 (DefaultGroupedTokenRepository.kt:83) — pair the silent skip with a refresh:
placeholder in the asset list so the user has feedback.
For Symptom 3 (SwapViewModel.kt:920) — fail closed:
Defaulting unknown tokens to 18 decimals only happens to work for ETH-class assets and breaks visibly for stablecoins (USDC/USDT = 6) and BTC-class wrappers (WBTC = 8). The right contract is "decimals
come from on-chain or from a verified source — never invented."
Examples/Pics
I can provide the ETH address if absolutely needed.
Home screen with no card for USDC

Swap screen showing 0.000000000010379... USDC (should show 10.379...)

Transaction log showing the original swap 10.3806 USDC
