Skip to content

Token metadata RPC failures are cached and never retried, dropping events or storing wrong metadata #13285

Description

@kelsos

Problem Definition

When rotki creates an EVM token on the fly (pool/LP tokens during decoding, but the same path serves every ERC20), it reads decimals, symbol and name from chain through one multicall. If that RPC call fails, the failure is treated the same as a contract that has no such methods, the result is cached for the whole session, and nothing ever retries. Depending on the caller, the outcome is either a silently missing history event or permanently wrong token metadata.

Found by reading the code on develop at a0fc8a5a0e.

What happens on an RPC failure

  1. _query_token_contract (node_inquirer.py#L1416) catches RemoteError and returns (False, b'') for each call, as if every method reverted.
  2. _process_contract_info turns that into decimals=18 (DEFAULT_TOKEN_DECIMALS), name=None, symbol=None.
  3. get_erc20_contract_info (node_inquirer.py#L1431) stores the result in contract_info_erc20_cache without any check. The cache is an LRU of 1024 entries with no TTL and nothing evicts from it, so the address looks non-conformant for the rest of the session even after the RPC recovers.
  4. _get_or_create_token (assets/utils.py#L459) then either raises NotERC20Conformant or, if the caller passed all three fallback_* values, stores those.

Consequences per caller

Caller Result
No fallbacks (generic ERC20 transfer decoder, Uniswap V2 swap, Curve LP) NotERC20Conformant is swallowed by the decoder and the event is dropped. The transaction is still marked TX_DECODED (chain/decoding/decoder.py#L619), so the transfer stays missing from history until the user manually redecodes. The user is not told.
With fallbacks (Velodrome/Aerodrome pool, Curve gauge) The fallback name and symbol are written to the global DB permanently and look the same as real metadata.
Balancer pool cache Name and symbol come from the API, so only decimals is queried. A failure stores 18 silently (correct for BPTs by luck).

Related issues in the same path

  • A partial failure looks complete. If only decimals() fails or reverts while name and symbol succeed, nothing counts as missing and the token is stored with 18 decimals without a warning. Amounts for that token are then wrong.
  • Fallbacks replace everything. When any one field is missing, all three fallbacks are used, overwriting fields the chain did return.
  • The repair path can blank metadata. edit_token_and_clean_cache (assets/utils.py#L135) re-queries a token whose name equals its identifier. If that query also fails it writes name=None, symbol=None. That is recoverable, since a NULL name loads as the identifier, but any symbol the row had is cleared and decimals are reset to 18.

Why it never heals

  • No periodic task re-queries token metadata. Data-issue remediation and the other scheduled tasks do not touch it.
  • The only re-query is lazy: a token whose name equals its identifier (NULL in the DB) is queried again the next time some code calls get_or_create_evm_token for it with an inquirer. Nothing goes looking for these tokens.
  • Fallback-filled tokens are never re-queried because their name is not the identifier, and nothing records that the values were fallbacks.
  • Decimals defaulted to 18 are stored as a real value and never revisited.
  • Dropped events need a manual redecode, because the transaction is marked decoded regardless of the outcome.

Manual workaround for now

A token left without metadata can be fixed by hand from the asset manager:

  1. Open Manage Assets > Assets, find the token and choose Edit.
  2. Click the refresh button at the end of the Address field. This queries the chain again (/blockchains/evm/erc20details) and fills in name, symbol and decimals.
  3. Check the values and click Save.
  4. Redecode the transactions that involve the token, so any events dropped during the original decode are recreated.

Example with wUSD+ on Optimism (eip155:10/erc20:0xA348700745D249c3b49D2c2AcAC9A5AE8155F826). Before the refresh, the name is the identifier, the symbol is empty and decimals shows 18:

Image

After the refresh it shows Wrapped USD+, wUSD+ and 6 decimals. The token was showing 18 decimals but the real value is 6, so amounts were off by a factor of 10^12:

Image

Limits of the workaround:

  • The refresh goes through the same get_erc20_contract_info and the same session cache. If the original lookup failed in the current session, the refresh returns the cached empty result. Restart rotki first.
  • Users have to notice the broken token on their own. Nothing in the UI flags tokens with missing or fallback metadata.
  • Tokens stored with fallback values look normal in the form, so it is hard to tell which ones need a refresh.

Limitations to keep in mind for a fix

  • A transport failure cannot always be told apart from a non-conformant contract. multicall_2 is called with require_success=False, so a single reverting method and a dead node both arrive as (False, b'') unless RemoteError is surfaced before the conversion.
  • Some tokens really lack name/symbol, or return bytes32 (UNI V1 LP, old WETH). Those must keep being cached as non-conformant, otherwise every decode re-queries them.
  • Leaving a transaction undecoded on transport failure needs a decoder-level signal. Today possible_decoding_exceptions swallows NotERC20Conformant per rule and the tx is marked decoded at the end. Re-raising would also affect the other events of that transaction.
  • There is no field for "metadata came from a fallback". Marking these tokens needs either a schema change in the global DB or a convention such as storing NULL name/symbol and keeping the fallback only for display.
  • A bulk re-query costs RPC calls for every flagged token on every chain, so a repair task needs batching and a cooldown, and must not run during history fetching.
  • The global DB is shared across users on the machine and also receives packaged asset updates, so a repair must not overwrite metadata that came from an asset update or a user edit.

Possible direction

  1. In get_erc20_contract_info, let a transport failure raise (or return a distinct "query failed" result) and never cache it. Only cache a successful response that is missing methods.
  2. In the decoder, treat a transport failure as "retry later": do not mark the tx decoded, so the normal decode pass retries it.
  3. Use fallbacks only for the fields that are missing.
  4. Record fallback-filled or defaulted fields so a periodic, batched task can re-query them, alongside tokens with a NULL name or symbol.
  5. Never write None over existing metadata in the repair path.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions