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
_query_token_contract (node_inquirer.py#L1416) catches RemoteError and returns (False, b'') for each call, as if every method reverted.
_process_contract_info turns that into decimals=18 (DEFAULT_TOKEN_DECIMALS), name=None, symbol=None.
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.
_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:
- Open Manage Assets > Assets, find the token and choose Edit.
- 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.
- Check the values and click Save.
- 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:
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:
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
- 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.
- In the decoder, treat a transport failure as "retry later": do not mark the tx decoded, so the normal decode pass retries it.
- Use fallbacks only for the fields that are missing.
- Record fallback-filled or defaulted fields so a periodic, batched task can re-query them, alongside tokens with a
NULL name or symbol.
- Never write
None over existing metadata in the repair path.
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,symbolandnamefrom 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
_query_token_contract(node_inquirer.py#L1416) catchesRemoteErrorand returns(False, b'')for each call, as if every method reverted._process_contract_infoturns that intodecimals=18(DEFAULT_TOKEN_DECIMALS),name=None,symbol=None.get_erc20_contract_info(node_inquirer.py#L1431) stores the result incontract_info_erc20_cachewithout 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._get_or_create_token(assets/utils.py#L459) then either raisesNotERC20Conformantor, if the caller passed all threefallback_*values, stores those.Consequences per caller
NotERC20Conformantis swallowed by the decoder and the event is dropped. The transaction is still markedTX_DECODED(chain/decoding/decoder.py#L619), so the transfer stays missing from history until the user manually redecodes. The user is not told.Related issues in the same path
decimals()fails or reverts whilenameandsymbolsucceed, nothing counts as missing and the token is stored with 18 decimals without a warning. Amounts for that token are then wrong.edit_token_and_clean_cache(assets/utils.py#L135) re-queries a token whose name equals its identifier. If that query also fails it writesname=None, symbol=None. That is recoverable, since aNULLname loads as the identifier, but any symbol the row had is cleared and decimals are reset to 18.Why it never heals
NULLin the DB) is queried again the next time some code callsget_or_create_evm_tokenfor it with an inquirer. Nothing goes looking for these tokens.Manual workaround for now
A token left without metadata can be fixed by hand from the asset manager:
/blockchains/evm/erc20details) and fills in name, symbol and decimals.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: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:Limits of the workaround:
get_erc20_contract_infoand the same session cache. If the original lookup failed in the current session, the refresh returns the cached empty result. Restart rotki first.Limitations to keep in mind for a fix
multicall_2is called withrequire_success=False, so a single reverting method and a dead node both arrive as(False, b'')unlessRemoteErroris surfaced before the conversion.name/symbol, or returnbytes32(UNI V1 LP, old WETH). Those must keep being cached as non-conformant, otherwise every decode re-queries them.possible_decoding_exceptionsswallowsNotERC20Conformantper rule and the tx is marked decoded at the end. Re-raising would also affect the other events of that transaction.NULLname/symbol and keeping the fallback only for display.Possible direction
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.NULLname or symbol.Noneover existing metadata in the repair path.