Skip to content

USDC (and other non-18-decimal tokens) silently missing or off-by-10¹² when metadata row is absent #9

@benwoody

Description

@benwoody

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:

  1. When a balance exists without metadata, trigger an on-chain decimals() fetch via the existing OnChainTokenMetadataFetcher before constructing the TokenAsset.
  2. 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
Image

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

Transaction log showing the original swap 10.3806 USDC
Image

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions