Repository: rotki/rotki
Pull request #9970: Re-enable onchain curve pool query
Author: login: nicholasyoder
Labels: name: ready for final review
description: Backend PR ready to be reviewed by great Lefteris
Created: 2025-05-17T01:01:02Z | Merged: 2025-05-19T21:28:39Z

Closes https://github.com/orgs/rotki/projects/11?pane=issue&itemId=111026071

* Queries only new pools, rather than trying to query all pools and will only process a maximum of 50 new pools per chain at a time.
* Runs the onchain pool query in a separate greenlet in order to not block the decoding.
* Saves the pools and tokens as it goes so if it is stopped partway through the progress is saved. In relation to this change, a large portion of code is moved from `_ensure_curve_tokens_existence` into `_ensure_single_pool_curve_tokens_existence` for more easily saving the tokens for a single pool at a time.
* Improves handling of pool names - on more recent pools the metaregistry `get_pool_name` function fails. Now we construct the pool name from the underlying tokens if the name is not available from the metaregistry.


Diff:
diff --git a/rotkehlchen/chain/evm/decoding/curve/constants.py b/rotkehlchen/chain/evm/decoding/curve/constants.py
index c3d63131fa3..d2e64de0311 100644
--- a/rotkehlchen/chain/evm/decoding/curve/constants.py
+++ b/rotkehlchen/chain/evm/decoding/curve/constants.py
@@ -79,3 +79,7 @@
 CURVE_SWAP_ROUTER_NG: Final = string_to_evm_address('0xF0d4c12A5768D806021F80a262B4d39d26C58b8D')
 DEPOSIT_AND_STAKE_ZAP: Final = string_to_evm_address('0x37c5ab57AF7100Bdc9B668d766e193CCbF6614FD')
 CHILD_LIQUIDITY_GAUGE_FACTORY: Final = string_to_evm_address('0xabC000d88f23Bb45525E447528DBF656A9D55bf5')  # noqa: E501
+# Maximum number of pools to query from the onchain metaregistry. Querying too many pools onchain
+# takes a very long time.  When testing this ~500 pools took ~10 minutes. Setting the max to 50
+# here so it should only spend about 1 minute on each chain.
+MAX_ONCHAIN_POOLS_QUERY: Final = 50
diff --git a/rotkehlchen/chain/evm/decoding/curve/curve_cache.py b/rotkehlchen/chain/evm/decoding/curve/curve_cache.py
index c6c09c74579..5f8cd1347ea 100644
--- a/rotkehlchen/chain/evm/decoding/curve/curve_cache.py
+++ b/rotkehlchen/chain/evm/decoding/curve/curve_cache.py
@@ -1,5 +1,6 @@
 import logging
-from typing import TYPE_CHECKING, Literal, NamedTuple
+from dataclasses import dataclass
+from typing import TYPE_CHECKING, Any, Literal
 
 from rotkehlchen.assets.asset import UnderlyingToken
 from rotkehlchen.assets.utils import TokenEncounterInfo, get_or_create_evm_token
@@ -12,12 +13,10 @@
     CURVE_CHAIN_ID,
     CURVE_METAREGISTRY_METHODS,
     IGNORED_CURVE_POOLS,
+    MAX_ONCHAIN_POOLS_QUERY,
 )
 from rotkehlchen.chain.evm.types import string_to_evm_address
-from rotkehlchen.chain.evm.utils import (
-    maybe_notify_cache_query_status,
-    maybe_notify_new_pools_status,
-)
+from rotkehlchen.chain.evm.utils import maybe_notify_cache_query_status
 from rotkehlchen.constants import ONE
 from rotkehlchen.db.addressbook import DBAddressbook
 from rotkehlchen.errors.misc import (
@@ -52,19 +51,21 @@
 if TYPE_CHECKING:
 
     from rotkehlchen.chain.evm.node_inquirer import EvmNodeInquirer
-    from rotkehlchen.db.dbhandler import DBHandler
     from rotkehlchen.user_messages import MessagesAggregator
 
 logger = logging.getLogger(__name__)
 log = RotkehlchenLogsAdapter(logger)
 
 
-class CurvePoolData(NamedTuple):
+@dataclass
+class CurvePoolData:
     pool_address: ChecksumEvmAddress
-    pool_name: str
+    pool_name: str | None
     lp_token_address: ChecksumEvmAddress
     gauge_address: ChecksumEvmAddress | None
+    # Coins in the pool. Has the LP token of the base pool for metapools.
     coins: list[ChecksumEvmAddress]
+    # All underlying coins in a meta-pool, including the coins from the base pool. None for non-metapools.  # noqa: E501
     underlying_coins: list[ChecksumEvmAddress] | None
 
 
@@ -106,6 +107,155 @@ def read_curve_pools_and_gauges(chain_id: ChainID) -> tuple[dict[ChecksumEvmAddr
     return curve_pools, curve_gauges
 
 
+def _ensure_single_pool_curve_tokens_existence(
+        evm_inquirer: 'EvmNodeInquirer',
+        pool: CurvePoolData,
+) -> CurvePoolData | None:
+    encounter = TokenEncounterInfo(
+        description=f'Querying curve pools for {evm_inquirer.chain_id.to_name()}',
+        should_notify=False,
+    )
+    # Ensure pool coins exist in the globaldb.
+    # We have to create underlying tokens only if pool utilizes them.
+    underlying_token_names = []
+    if pool.underlying_coins is None:
+        # If underlying coins is None, it means that pool doesn't utilize them.
+        # If underlying coins list is equal to coins then there are no underlying coins as well.  # noqa: E501
+        for token_address in pool.coins:
+            if token_address == ETH_SPECIAL_ADDRESS:
+                underlying_token_names.append('ETH')
+                continue
+
+            try:  # ensure token exists
+                underlying_token_names.append(get_or_create_evm_token(
+                    userdb=evm_inquirer.database,
+                    evm_address=token_address,
+                    chain_id=evm_inquirer.chain_id,
+                    evm_inquirer=evm_inquirer,
+                    encounter=encounter,
+                ).symbol)
+            except NotERC20Conformant as e:
+                log.error(
+                    f'Skipping pool {pool} because {token_address} is not a '
+                    f'valid ERC20 token. {e}',
+                )
+                continue
+
+    elif len(pool.coins) == len(pool.underlying_coins):
+        # Coins and underlying coins lists represent a
+        # mapping of coin -> underlying coin (each coin always has one underlying coin).
+        for token_address, underlying_token_address in zip(pool.coins, pool.underlying_coins, strict=True):  # noqa: E501
+            if token_address == ETH_SPECIAL_ADDRESS:
+                underlying_token_names.append('ETH')
+                continue
+
+            try:  # ensure underlying token exists
+                get_or_create_evm_token(
+                    userdb=evm_inquirer.database,
+                    evm_address=underlying_token_address,
+                    chain_id=evm_inquirer.chain_id,
+                    evm_inquirer=evm_inquirer,
+                    encounter=encounter,
+                )
+            except NotERC20Conformant as e:
+                log.error(
+                    f'Skipping pool {pool} because {underlying_token_address} is not a '
+                    f'valid ERC20 token. {e}',
+                )
+                continue
+
+            if underlying_token_address == token_address:  # they are the same token. Stop here
+                continue  # can happen for a pool to have token's underlying token as itself
+
+            try:  # and ensure token exists
+                underlying_token_names.append(get_or_create_evm_token(
+                    userdb=evm_inquirer.database,
+                    evm_address=token_address,
+                    chain_id=evm_inquirer.chain_id,
+                    evm_inquirer=evm_inquirer,
+                    underlying_tokens=[UnderlyingToken(
+                        address=underlying_token_address,
+                        token_kind=EvmTokenKind.ERC20,
+                        weight=ONE,
+                    )],
+                    encounter=encounter,
+                ).symbol)
+            except NotERC20Conformant as e:
+                log.error(
+                    f'Skipping pool {pool} because {token_address} is not a '
+                    f'valid ERC20 token. {e}',
+                )
+                continue
+
+    else:  # Otherwise just ensure that coins and underlying coins exist in our assets database  # noqa: E501
+        for token_address in set(pool.coins + pool.underlying_coins):
+            if token_address == ETH_SPECIAL_ADDRESS:
+                underlying_token_names.append('ETH')
+                continue
+
+            try:
+                underlying_token_names.append(get_or_create_evm_token(
+                    userdb=evm_inquirer.database,
+                    evm_address=token_address,
+                    chain_id=evm_inquirer.chain_id,
+                    evm_inquirer=evm_inquirer,
+                    encounter=encounter,
+                ).symbol)
+            except NotERC20Conformant as e:
+                log.error(
+                    f'Skipping coin {token_address} because it is not a '
+                    f'valid ERC20 token. {e}',
+                )
+                continue
+
+    if pool.pool_name is None:
+        pool.pool_name = '/'.join(underlying_token_names)
+
+    # finally ensure lp token and gauge token exists in the globaldb. Since they are created
+    # by curve, they should always be an ERC20
+    try:
+        pool_lp_token = get_or_create_evm_token(
+            userdb=evm_inquirer.database,
+            evm_address=pool.lp_token_address,
+            chain_id=evm_inquirer.chain_id,
+            evm_inquirer=evm_inquirer,
+            protocol=CURVE_POOL_PROTOCOL,
+            encounter=encounter,
+        )
+    except NotERC20Conformant as e:
+        log.error(  # should only fail if the node is not up to date
+            f'Failed to check integrity of the curve pool {pool.lp_token_address} '
+            f'due to {e}. Skipping',
+        )
+        return None
+
+    if pool.gauge_address is not None:
+        try:
+            # Old gauges don't have the name/symbol/decimals methods. They use 18 decimals but
+            # the name and symbol differ and in the latest versions it is configurable. To be
+            # in sync with the onchain name we use the contract values and only
+            # if they are missing we resort to the fallback name and symbol.
+            get_or_create_evm_token(
+                userdb=evm_inquirer.database,
+                evm_address=pool.gauge_address,
+                chain_id=evm_inquirer.chain_id,
+                evm_inquirer=evm_inquirer,
+                encounter=encounter,
+                underlying_tokens=[UnderlyingToken(
+                    address=pool.lp_token_address,
+                    token_kind=EvmTokenKind.ERC20,
+                    weight=ONE,
+                )],
+                fallback_decimals=18,  # all gauges have 18 decimals https://t.me/curvefi/654915  # noqa: E501
+                fallback_name=f'{pool_lp_token.name} Gauge Deposit',
+                fallback_symbol=f'{pool_lp_token.symbol}-gauge',
+            )
+        except NotERC20Conformant:
+            log.warning(f'Curve gauge {pool.gauge_address} is not a valid ERC20 token.')
+
+    return pool
+
+
 def _ensure_curve_tokens_existence(
         evm_inquirer: 'EvmNodeInquirer',
         all_pools: list[CurvePoolData],
@@ -118,10 +268,7 @@ def _ensure_curve_tokens_existence(
     and return it.
     """
     verified_pools, last_notified_ts, all_pools_length = [], Timestamp(0), len(all_pools)
-    encounter = TokenEncounterInfo(
-        description=f'Querying curve pools for {evm_inquirer.chain_id.to_name()}',
-        should_notify=False,
-    )
+
     for idx, pool in enumerate(all_pools):
         last_notified_ts = maybe_notify_cache_query_status(
             msg_aggregator=msg_aggregator,
@@ -131,165 +278,40 @@ def _ensure_curve_tokens_existence(
             processed=idx + 1,
             total=all_pools_length,
         )
+        if (verified_pool := _ensure_single_pool_curve_tokens_existence(
+            evm_inquirer=evm_inquirer,
+            pool=pool,
+        )) is not None:
+            verified_pools.append(verified_pool)
 
-        # Ensure pool coins exist in the globaldb.
-        # We have to create underlying tokens only if pool utilizes them.
-        if pool.underlying_coins is None or pool.underlying_coins == pool.coins:
-            # If underlying coins is None, it means that pool doesn't utilize them.
-            # If underlying coins list is equal to coins then there are no underlying coins as well.  # noqa: E501
-            for token_address in pool.coins:
-                if token_address == ETH_SPECIAL_ADDRESS:
-                    continue
-                # ensure token exists
-                try:
-                    get_or_create_evm_token(
-                        userdb=evm_inquirer.database,
-                        evm_address=token_address,
-                        chain_id=evm_inquirer.chain_id,
-                        evm_inquirer=evm_inquirer,
-                        encounter=encounter,
-                    )
-                except NotERC20Conformant as e:
-                    log.error(
-                        f'Skipping pool {pool} because {token_address} is not a '
-                        f'valid ERC20 token. {e}',
-                    )
-                    continue
-        elif len(pool.coins) == len(pool.underlying_coins):
-            # Coins and underlying coins lists represent a
-            # mapping of coin -> underlying coin (each coin always has one underlying coin).
-            for token_address, underlying_token_address in zip(pool.coins, pool.underlying_coins, strict=True):  # noqa: E501
-                if token_address == ETH_SPECIAL_ADDRESS:
-                    continue
-                # ensure underlying token exists
-                try:
-                    get_or_create_evm_token(
-                        userdb=evm_inquirer.database,
-                        evm_address=underlying_token_address,
-                        chain_id=evm_inquirer.chain_id,
-                        evm_inquirer=evm_inquirer,
-                        encounter=encounter,
-                    )
-                except NotERC20Conformant as e:
-                    log.error(
-                        f'Skipping pool {pool} because {underlying_token_address} is not a '
-                        f'valid ERC20 token. {e}',
-                    )
-                    continue
-
-                if underlying_token_address == token_address:  # they are the same token. Stop here
-                    continue  # can happen for a pool to have token's underlying token as itself
-
-                try:
-                    # and ensure token exists
-                    get_or_create_evm_token(
-                        userdb=evm_inquirer.database,
-                        evm_address=token_address,
-                        chain_id=evm_inquirer.chain_id,
-                        evm_inquirer=evm_inquirer,
-                        underlying_tokens=[UnderlyingToken(
-                            address=underlying_token_address,
-                            token_kind=EvmTokenKind.ERC20,
-                            weight=ONE,
-                        )],
-                        encounter=encounter,
-                    )
-                except NotERC20Conformant as e:
-                    log.error(
-                        f'Skipping pool {pool} because {token_address} is not a '
-                        f'valid ERC20 token. {e}',
-                    )
-                    continue
-        else:
-            # Otherwise just ensure that coins and underlying coins exist in our assets database  # noqa: E501
-            for token_address in set(pool.coins + pool.underlying_coins):
-                if token_address == ETH_SPECIAL_ADDRESS:
-                    continue
-                try:
-                    get_or_create_evm_token(
-                        userdb=evm_inquirer.database,
-                        evm_address=token_address,
-                        chain_id=evm_inquirer.chain_id,
-                        evm_inquirer=evm_inquirer,
-                        encounter=encounter,
-                    )
-                except NotERC20Conformant as e:
-                    log.error(
-                        f'Skipping coin {token_address} because it is not a '
-                        f'valid ERC20 token. {e}',
-                    )
-                    continue
-
-        # finally ensure lp token and gauge token exists in the globaldb. Since they are created
-        # by curve, they should always be an ERC20
-        try:
-            pool_lp_token = get_or_create_evm_token(
-                userdb=evm_inquirer.database,
-                evm_address=pool.lp_token_address,
-                chain_id=evm_inquirer.chain_id,
-                evm_inquirer=evm_inquirer,
-                protocol=CURVE_POOL_PROTOCOL,
-                encounter=encounter,
-            )
-        except NotERC20Conformant as e:
-            log.error(  # should only fail if the node is not up to date
-                f'Failed to check integrity of the curve pool {pool.lp_token_address} '
-                f'due to {e}. Skipping',
-            )
-            continue
-
-        if pool.gauge_address is not None:
-            try:
-                # Old gauges don't have the name/symbol/decimals methods. They use 18 decimals but
-                # the name and symbol differ and in the latest versions it is configurable. To be
-                # in sync with the onchain name we use the contract values and only
-                # if they are missing we resort to the fallback name and symbol.
-                get_or_create_evm_token(
-                    userdb=evm_inquirer.database,
-                    evm_address=pool.gauge_address,
-                    chain_id=evm_inquirer.chain_id,
-                    evm_inquirer=evm_inquirer,
-                    encounter=encounter,
-                    underlying_tokens=[UnderlyingToken(
-                        address=pool.lp_token_address,
-                        token_kind=EvmTokenKind.ERC20,
-                        weight=ONE,
-                    )],
-                    fallback_decimals=18,  # all gauges have 18 decimals https://t.me/curvefi/654915  # noqa: E501
-                    fallback_name=f'{pool_lp_token.name} Gauge Deposit',
-                    fallback_symbol=f'{pool_lp_token.symbol}-gauge',
-                )
-            except NotERC20Conformant:
-                log.warning(f'Curve gauge {pool.gauge_address} is not a valid ERC20 token.')
-
-        verified_pools.append(pool)
     return verified_pools
 
 
 def _save_curve_data_to_cache(
-        database: 'DBHandler',
+        evm_inquirer: 'EvmNodeInquirer',
         new_data: list[CurvePoolData],
-        chain_id: ChainID,
 ) -> None:
     """Stores data received about curve pools and gauges in the cache"""
-    db_addressbook = DBAddressbook(db_handler=database)
-    chain_id_str = str(chain_id.serialize_for_db())
+    db_addressbook = DBAddressbook(db_handler=evm_inquirer.database)
+    chain_id_str = str(evm_inquirer.chain_id.serialize_for_db())
     for pool in new_data:
-        addresbook_entries = [AddressbookEntry(
-            address=pool.pool_address,
-            name=pool.pool_name,
-            blockchain=chain_id.to_blockchain(),
-        )]
-        if pool.gauge_address is not None:
-            addresbook_entries.append(AddressbookEntry(
-                address=pool.gauge_address,
-                name=f'Curve gauge for {pool.pool_name}',
-                blockchain=chain_id.to_blockchain(),
-            ))
+        addressbook_entries = []
+        if pool.pool_name is not None:
+            addressbook_entries = [AddressbookEntry(
+                address=pool.pool_address,
+                name=pool.pool_name,
+                blockchain=evm_inquirer.blockchain,
+            )]
+            if pool.gauge_address is not None:
+                addressbook_entries.append(AddressbookEntry(
+                    address=pool.gauge_address,
+                    name=f'Curve gauge for {pool.pool_name}',
+                    blockchain=evm_inquirer.blockchain,
+                ))
         with GlobalDBHandler().conn.write_ctx() as write_cursor:
             db_addressbook.add_or_update_addressbook_entries(
                 write_cursor=write_cursor,
-                entries=addresbook_entries,
+                entries=addressbook_entries,
             )
             globaldb_set_general_cache_values(
                 write_cursor=write_cursor,
@@ -350,8 +372,14 @@ def _query_curve_data_from_api(
 
             coins = [deserialize_evm_address(x['address']) for x in api_pool_data['coins']]
             underlying_coins = None
-            if 'underlyingCoins' in api_pool_data:
-                underlying_coins = [deserialize_evm_address(x['address']) for x in api_pool_data['underlyingCoins']]  # noqa: E501
+            if (
+                'underlyingCoins' in api_pool_data and
+                (u_coins := [
+                    deserialize_evm_address(x['address'])
+                    for x in api_pool_data['underlyingCoins']
+                ]) != coins
+            ):
+                underlying_coins = u_coins
 
             processed_new_pools.append(CurvePoolData(
                 pool_address=pool_address,
@@ -376,7 +404,7 @@ def _query_curve_data_from_chain(
         evm_inquirer: 'EvmNodeInquirer',
         existing_pools: set[ChecksumEvmAddress],
         msg_aggregator: 'MessagesAggregator',
-) -> list[CurvePoolData]:
+) -> None:
     """
     Query all curve information(lp tokens, pools, gagues, pool coins) from metaregistry.
 
@@ -390,85 +418,123 @@ def _query_curve_data_from_chain(
             method_name='get_address',
             arguments=[7],
         ))
-    except DeserializationError as e:
-        log.error(f'Curve address provider returned an invalid address for metaregistry. {e}')
-        return []
+    except (RemoteError, DeserializationError) as e:
+        log.error(
+            f'Failed to retrieve metaregistry address from the Curve '
+            f'address provider on {evm_inquirer.chain_name} due to {e!s}',
+        )
+        return
 
     metaregistry = EvmContract(
         address=metaregistry_address,
         abi=evm_inquirer.contracts.abi('CURVE_METAREGISTRY'),  # type: ignore[call-overload]  # for some reason mypy doesn't properly see argument type
         deployed_block=0,  # deployment_block is not used and the contract is dynamic
     )
-    pool_count = metaregistry.call(node_inquirer=evm_inquirer, method_name='pool_count')
-    new_pools: list[CurvePoolData] = []
+    try:
+        pool_count = metaregistry.call(node_inquirer=evm_inquirer, method_name='pool_count')
+    except RemoteError as e:
+        log.error(
+            'Failed to retrieve Curve pool count from the metaregistry '
+            f'on {evm_inquirer.chain_name} due to {e!s}',
+        )
+        return
+
+    if (existing_pool_count := len(existing_pools)) == pool_count:
+        return
+    if (new_pool_count := pool_count - existing_pool_count) > MAX_ONCHAIN_POOLS_QUERY:
+        pool_count = existing_pool_count + MAX_ONCHAIN_POOLS_QUERY
+        new_pool_count = MAX_ONCHAIN_POOLS_QUERY
+        log.info(
+            f'Tried to query {new_pool_count} {evm_inquirer.chain_name} Curve pools. '
+            f'Too many pools to query onchain. Only querying {new_pool_count}.',
+        )
+
     last_notified_ts = Timestamp(0)
-    for pool_index in range(pool_count):
-        last_notified_ts = maybe_notify_new_pools_status(
+    pools_to_skip = IGNORED_CURVE_POOLS | existing_pools
+    for pool_index in range((start_idx := pool_count - new_pool_count), pool_count):
+        last_notified_ts = maybe_notify_cache_query_status(
             msg_aggregator=msg_aggregator,
             last_notified_ts=last_notified_ts,
             protocol=CPT_CURVE,
             chain=evm_inquirer.chain_id,
-            get_new_pools_count=lambda: len(new_pools),
+            processed=(processed := pool_index - start_idx),
+            total=new_pool_count,
         )
 
-        raw_address = metaregistry.call(
-            node_inquirer=evm_inquirer,
-            method_name='pool_list',
-            arguments=[pool_index],
-        )
         try:
-            pool_address = deserialize_evm_address(raw_address)
-        except DeserializationError as e:
-            log.error(f'Could not deserialize curve pool address {raw_address}. {e}')
-            continue
+            if (pool_address := deserialize_evm_address(metaregistry.call(
+                node_inquirer=evm_inquirer,
+                method_name='pool_list',
+                arguments=[pool_index],
+            ))) in pools_to_skip:
+                continue
 
-        if pool_address in IGNORED_CURVE_POOLS or pool_address in existing_pools:
+            log.debug(
+                f'Processing Curve pool {processed}/{new_pool_count} {pool_address} '
+                f'on {evm_inquirer.chain_name}.',
+            )
+            raw_pool_properties = evm_inquirer.multicall_2(
+                calls=[(
+                    metaregistry_address,
+                    metaregistry.encode(method_name=method_name, arguments=[pool_address]),
+                ) for method_name in CURVE_METAREGISTRY_METHODS],
+                require_success=False,
+            )
+        except (RemoteError, DeserializationError) as e:
+            log.error(
+                f'Failed to retrieve Curve pool address for index {pool_index} '
+                f'from the metaregistry on {evm_inquirer.chain_name} due to {e!s}',
+            )
             continue
 
-        calls = [
-            (
-                metaregistry_address,
-                metaregistry.encode(method_name=method_name, arguments=[pool_address]),
-            )
-            for method_name in CURVE_METAREGISTRY_METHODS
-        ]
-        raw_pool_properties = evm_inquirer.multicall_2(calls=calls, require_success=True)
-        decoded_pool_properties = [
-            metaregistry.decode(result=result[1], method_name=method_name, arguments=[pool_address])  # noqa: E501
-            for result, method_name in zip(raw_pool_properties, CURVE_METAREGISTRY_METHODS, strict=True)  # length should be same due to the call # noqa: E501
-        ]
-        try:
-            pool_name: str = decoded_pool_properties[0][0]
-            gauge_address = deserialize_evm_address(decoded_pool_properties[1][0])
-            lp_token_address = deserialize_evm_address(decoded_pool_properties[2][0])
-            coins_raw = decoded_pool_properties[3][0]
-            underlying_coins_raw = decoded_pool_properties[4][0]
-            coins = []
-            for raw_coin_address in coins_raw:
-                if raw_coin_address == ZERO_ADDRESS:
-                    break
-                coins.append(deserialize_evm_address(raw_coin_address))
-            underlying_coins = []
-            for raw_underlying_coin_address in underlying_coins_raw:
-                if raw_underlying_coin_address == ZERO_ADDRESS:
+        decoded_pool_properties: list[Any] = []
+        for (success, result), method_name in zip(raw_pool_properties, CURVE_METAREGISTRY_METHODS, strict=True):  # length should be same due to the call  # noqa: E501
+            if success is False:
+                if method_name == 'get_pool_name':  # There are a number of pools (especially later ones) where the pool name query fails  # noqa: E501
+                    decoded_pool_properties.append(None)  # Pool name will be constructed from the underlying tokens later instead  # noqa: E501
+                    continue
+                else:
                     break
-                underlying_coins.append(deserialize_evm_address(raw_underlying_coin_address))
 
-            new_pools.append(CurvePoolData(
-                pool_address=pool_address,
-                pool_name=pool_name,
-                lp_token_address=lp_token_address,
-                gauge_address=gauge_address if gauge_address != ZERO_ADDRESS else None,
-                coins=coins,
-                underlying_coins=underlying_coins if len(underlying_coins) > 0 else None,
-            ))
+            decoded_pool_properties.append(metaregistry.decode(
+                result=result,
+                method_name=method_name,
+                arguments=[pool_address],
+            )[0])
+
+        if len(decoded_pool_properties) != len(CURVE_METAREGISTRY_METHODS):
+            log.error(
+                f'Failed to query properties of curve pool {pool_address} '
+                f'on {evm_inquirer.chain_name}. Skipping.',
+            )
+            continue
+
+        try:
+            pool_name, gauge_address, lp_token_address, coins_raw, underlying_coins_raw = decoded_pool_properties  # noqa: E501
+            gauge_address = deserialize_evm_address(gauge_address)
+            lp_token_address = deserialize_evm_address(lp_token_address)
+            coins = [deserialize_evm_address(x) for x in coins_raw if x != ZERO_ADDRESS]
+            u_coins = [deserialize_evm_address(x) for x in underlying_coins_raw if x != ZERO_ADDRESS]  # noqa: E501
+            underlying_coins = None if u_coins == coins or len(u_coins) == 0 else u_coins
         except DeserializationError as e:
             log.error(
                 f'Could not deserialize evm address while decoding curve pool {pool_address} '
                 f'information from metaregistry: {e}',
             )
+            continue
 
-    return new_pools
+        if (pool := _ensure_single_pool_curve_tokens_existence(
+            evm_inquirer=evm_inquirer,
+            pool=CurvePoolData(
+                pool_address=pool_address,
+                pool_name=pool_name,
+                lp_token_address=lp_token_address,
+                gauge_address=gauge_address if gauge_address != ZERO_ADDRESS else None,
+                coins=coins,
+                underlying_coins=underlying_coins,
+            ),
+        )) is not None:
+            _save_curve_data_to_cache(evm_inquirer=evm_inquirer, new_data=[pool])
 
 
 def query_curve_data(
@@ -486,18 +552,31 @@ def query_curve_data(
             string_to_evm_address(address[0])
             for address in cursor.execute(
                 'SELECT value FROM unique_cache WHERE key LIKE ?',
-                (f'{compute_cache_key((CacheType.CURVE_POOL_ADDRESS, str(inquirer.chain_id.serialize_for_db())))}%',),  # noqa: E501
+                (compute_cache_key((
+                    CacheType.CURVE_POOL_ADDRESS,
+                    str(inquirer.chain_id.serialize_for_db()),
+                    '0x%',  # Include the beginning `0x` of the address to avoid matching chain id 10 when using chain id 1  # noqa: E501
+                )),),
             )
         }
 
+    pools_data: list[CurvePoolData] = []
     try:
-        pools_data: list[CurvePoolData] = _query_curve_data_from_api(
+        pools_data = _query_curve_data_from_api(
             chain_id=inquirer.chain_id,
             existing_pools=existing_pools,
         )
     except (RemoteError, UnableToDecryptRemoteData) as e:
-        log.error(f'Could not query curve api due to: {e}.')
-        return None
+        log.error(f'Could not query curve api due to: {e}. Will query the metaregistry on chain')
+        inquirer.greenlet_manager.spawn_and_track(
+            after_seconds=None,
+            task_name='Query Curve pools from the onchain metaregistry',
+            exception_is_error=False,
+            method=_query_curve_data_from_chain,
+            evm_inquirer=inquirer,
+            existing_pools=existing_pools,
+            msg_aggregator=msg_aggregator,
+        )
 
     if len(pools_data) == 0:
         # if no new pools, update the last_queried_ts of db entries
@@ -514,12 +593,7 @@ def query_curve_data(
         all_pools=pools_data,
         msg_aggregator=msg_aggregator,
     )
-
-    _save_curve_data_to_cache(
-        database=inquirer.database,
-        new_data=verified_pools,
-        chain_id=inquirer.chain_id,
-    )
+    _save_curve_data_to_cache(evm_inquirer=inquirer, new_data=verified_pools)
     return verified_pools
 
 
diff --git a/rotkehlchen/tests/unit/globaldb/test_globaldb_cache.py b/rotkehlchen/tests/unit/globaldb/test_globaldb_cache.py
index c424ccba53f..3702f278271 100644
--- a/rotkehlchen/tests/unit/globaldb/test_globaldb_cache.py
+++ b/rotkehlchen/tests/unit/globaldb/test_globaldb_cache.py
@@ -4,6 +4,7 @@
 from typing import TYPE_CHECKING, cast
 from unittest.mock import MagicMock, _Call, call, patch
 
+import gevent
 import pytest
 import requests
 from freezegun import freeze_time
@@ -322,18 +323,23 @@ def test_convex_cache(ethereum_inquirer):
 
 @pytest.mark.vcr(filter_query_parameters=['apikey'])
 @pytest.mark.parametrize('have_decoders', [True])
-@pytest.mark.parametrize('use_curve_api', [True])  # TODO: re-enable query using onchain data. Part of https://github.com/orgs/rotki/projects/11?pane=issue&itemId=111026071  Essentially add a False here again by undoing what happened in https://github.com/rotki/rotki/pull/9963/files#diff-098f0ac6382a14207e3774683524c1ecdf2e15742e39d0925b64ca8fe2520348 # noqa: E501
+@pytest.mark.parametrize('use_curve_api', [True, False])
 def test_curve_cache(rotkehlchen_instance, use_curve_api, globaldb):
     global_cursor = globaldb.conn.cursor()
     """Test curve pools fetching mechanism"""
     # Set initial cache data to check that it is gone after the cache update
     with GlobalDBHandler().conn.write_ctx() as write_cursor:
-        # make sure that curve cache is clear of expected pools and addressbook entries
-        for lp_token in CURVE_EXPECTED_LP_TOKENS_TO_POOLS:
-            write_cursor.execute(f"DELETE FROM unique_cache WHERE key LIKE '%{lp_token}%'")
+        # Make sure that curve cache is clear of ethereum pools and expected addressbook entries
+        write_cursor.execute("DELETE FROM unique_cache WHERE key LIKE 'CURVE_POOL_ADDRESS10x%'")
         for entry in CURVE_EXPECTED_ADDRESBOOK_ENTRIES_FROM_CHAIN:
             write_cursor.execute('DELETE FROM address_book WHERE address=?', (entry.address,))
 
+        # Ensure addresses for optimism (chain id 10 + 0x of address) are present. Regression test
+        # for a problem where the 0x wasn't included and chain id 1 would also match chain id 10.
+        assert write_cursor.execute(
+            "SELECT COUNT(*) FROM unique_cache WHERE key LIKE 'CURVE_POOL_ADDRESS100x%'",
+        ).fetchone()[0] > 0
+
     # delete one of the tokens to check that it is created during the update
     token_id = evm_address_to_identifier(
         address='0x3Ed3B47Dd13EC9a98b44e6204A523E766B225811',  # aUSDT
@@ -407,6 +413,8 @@ def mock_requests_get(url, timeout):  # pylint: disable=unused-argument
             node_inquirer=ethereum_inquirer,
             transactions_decoder=rotkehlchen_instance.chains_aggregator.ethereum.transactions_decoder,
         )
+        # the onchain pool queries run in a separate greenlet
+        gevent.joinall(rotkehlchen_instance.greenlet_manager.greenlets)
 
     pools, gauges = read_curve_pools_and_gauges(chain_id=ChainID.ETHEREUM)
     for pool in CURVE_EXPECTED_LP_TOKENS_TO_POOLS.values():


Reviews:
user: login: yabirgb
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-19T07:25:32Z
user: login: nicholasyoder
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T14:39:19Z
user: login: nicholasyoder
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T14:39:50Z
user: login: nicholasyoder
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T14:40:18Z
user: login: nicholasyoder
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T14:41:10Z
user: login: nicholasyoder
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T14:52:38Z
user: login: nicholasyoder
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T14:52:50Z
user: login: nicholasyoder
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T14:52:57Z
user: login: nicholasyoder
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T14:53:39Z
user: login: yabirgb
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-19T15:17:28Z
user: login: nicholasyoder
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T15:36:10Z
user: login: LefterisJP
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-19T16:38:44Z
user: login: nicholasyoder
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T17:49:07Z
user: login: nicholasyoder
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T17:52:12Z
user: login: nicholasyoder
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T17:55:12Z
user: login: LefterisJP
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-19T19:59:49Z
user: login: nicholasyoder
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T20:58:07Z
user: login: LefterisJP
body: LGTM
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-19T21:28:33Z

Review comments:
diff_hunk: @@ -79,3 +79,5 @@
 CURVE_SWAP_ROUTER_NG: Final = string_to_evm_address('0xF0d4c12A5768D806021F80a262B4d39d26C58b8D')
 DEPOSIT_AND_STAKE_ZAP: Final = string_to_evm_address('0x37c5ab57AF7100Bdc9B668d766e193CCbF6614FD')
 CHILD_LIQUIDITY_GAUGE_FACTORY: Final = string_to_evm_address('0xabC000d88f23Bb45525E447528DBF656A9D55bf5')  # noqa: E501
+# Maximum number of pools to query from the onchain metaregistry. Querying too many pools onchain takes a very long time.  # noqa: E501
+MAX_ONCHAIN_POOLS_QUERY: Final = 500
path: rotkehlchen/chain/evm/decoding/curve/constants.py
user: login: yabirgb
body: why 500 instead of 250 or another number? Do you have the number of how many chain how many pools there are per evm chain? My problem with 500 is that it might be still too much
created_at: 2025-05-19T06:54:52Z
author_association: MEMBER
diff_hunk: @@ -390,59 +402,99 @@ def _query_curve_data_from_chain(
             method_name='get_address',
             arguments=[7],
         ))
-    except DeserializationError as e:
-        log.error(f'Curve address provider returned an invalid address for metaregistry. {e}')
-        return []
+    except (RemoteError, DeserializationError) as e:
+        log.error(
+            f'Failed to retrieve metaregistry address from the Curve '
+            f'address provider on {evm_inquirer.chain_name} due to {e!s}',
+        )
+        return
 
     metaregistry = EvmContract(
         address=metaregistry_address,
         abi=evm_inquirer.contracts.abi('CURVE_METAREGISTRY'),  # type: ignore[call-overload]  # for some reason mypy doesn't properly see argument type
         deployed_block=0,  # deployment_block is not used and the contract is dynamic
     )
-    pool_count = metaregistry.call(node_inquirer=evm_inquirer, method_name='pool_count')
-    new_pools: list[CurvePoolData] = []
+    try:
+        pool_count = metaregistry.call(node_inquirer=evm_inquirer, method_name='pool_count')
+    except RemoteError as e:
+        log.error(
+            'Failed to retrieve Curve pool count from the metaregistry '
+            f'on {evm_inquirer.chain_name} due to {e!s}',
+        )
+        return
+
+    if (existing_pool_count := len(existing_pools)) == pool_count:
+        return
+    if (new_pool_count := pool_count - existing_pool_count) > MAX_ONCHAIN_POOLS_QUERY:
+        log.error(
+            f'Skipping onchain query of {new_pool_count} {evm_inquirer.chain_name} '
+            f'Curve pools. Too many pools to query onchain.',
+        )
+        return
path: rotkehlchen/chain/evm/decoding/curve/curve_cache.py
user: login: yabirgb
body: we said to query a few of them didn't we?
created_at: 2025-05-19T07:11:32Z
author_association: MEMBER
diff_hunk: @@ -390,59 +402,99 @@ def _query_curve_data_from_chain(
             method_name='get_address',
             arguments=[7],
         ))
-    except DeserializationError as e:
-        log.error(f'Curve address provider returned an invalid address for metaregistry. {e}')
-        return []
+    except (RemoteError, DeserializationError) as e:
+        log.error(
+            f'Failed to retrieve metaregistry address from the Curve '
+            f'address provider on {evm_inquirer.chain_name} due to {e!s}',
+        )
+        return
 
     metaregistry = EvmContract(
         address=metaregistry_address,
         abi=evm_inquirer.contracts.abi('CURVE_METAREGISTRY'),  # type: ignore[call-overload]  # for some reason mypy doesn't properly see argument type
         deployed_block=0,  # deployment_block is not used and the contract is dynamic
     )
-    pool_count = metaregistry.call(node_inquirer=evm_inquirer, method_name='pool_count')
-    new_pools: list[CurvePoolData] = []
+    try:
+        pool_count = metaregistry.call(node_inquirer=evm_inquirer, method_name='pool_count')
+    except RemoteError as e:
+        log.error(
+            'Failed to retrieve Curve pool count from the metaregistry '
+            f'on {evm_inquirer.chain_name} due to {e!s}',
+        )
+        return
+
+    if (existing_pool_count := len(existing_pools)) == pool_count:
+        return
+    if (new_pool_count := pool_count - existing_pool_count) > MAX_ONCHAIN_POOLS_QUERY:
+        log.error(
+            f'Skipping onchain query of {new_pool_count} {evm_inquirer.chain_name} '
+            f'Curve pools. Too many pools to query onchain.',
+        )
+        return
+
     last_notified_ts = Timestamp(0)
-    for pool_index in range(pool_count):
-        last_notified_ts = maybe_notify_new_pools_status(
+    for pool_index in range((start_idx := pool_count - new_pool_count), pool_count):
+        last_notified_ts = maybe_notify_cache_query_status(
             msg_aggregator=msg_aggregator,
             last_notified_ts=last_notified_ts,
             protocol=CPT_CURVE,
             chain=evm_inquirer.chain_id,
-            get_new_pools_count=lambda: len(new_pools),
+            processed=(processed := pool_index - start_idx),
+            total=new_pool_count,
         )
 
-        raw_address = metaregistry.call(
-            node_inquirer=evm_inquirer,
-            method_name='pool_list',
-            arguments=[pool_index],
-        )
         try:
-            pool_address = deserialize_evm_address(raw_address)
-        except DeserializationError as e:
-            log.error(f'Could not deserialize curve pool address {raw_address}. {e}')
-            continue
+            if (pool_address := deserialize_evm_address(metaregistry.call(
+                node_inquirer=evm_inquirer,
+                method_name='pool_list',
+                arguments=[pool_index],
+            ))) in IGNORED_CURVE_POOLS | existing_pools:
+                continue
 
-        if pool_address in IGNORED_CURVE_POOLS or pool_address in existing_pools:
+            log.debug(
+                f'Processing Curve pool {processed}/{new_pool_count} {pool_address} '
+                f'on {evm_inquirer.chain_name}.',
+            )
+            raw_pool_properties = evm_inquirer.multicall_2(
+                calls=[(
+                    metaregistry_address,
+                    metaregistry.encode(method_name=method_name, arguments=[pool_address]),
+                ) for method_name in CURVE_METAREGISTRY_METHODS],
+                require_success=False,
+            )
+        except (RemoteError, DeserializationError) as e:
+            log.error(
+                f'Failed to retrieve Curve pool address for index {pool_index} '
+                f'from the metaregistry on {evm_inquirer.chain_name} due to {e!s}',
+            )
             continue
 
-        calls = [
-            (
-                metaregistry_address,
-                metaregistry.encode(method_name=method_name, arguments=[pool_address]),
+        decoded_pool_properties: list[Any] = []
+        for (success, result), method_name in zip(raw_pool_properties, CURVE_METAREGISTRY_METHODS, strict=True):  # length should be same due to the call  # noqa: E501
+            if success is False:
+                if method_name == 'get_pool_name':  # There are a number of pools (especially later ones) where the pool name query fails  # noqa: E501
+                    decoded_pool_properties.append(None)
+                    continue
path: rotkehlchen/chain/evm/decoding/curve/curve_cache.py
user: login: yabirgb
body: why does it fail? They don't have this method? Is there a new method?
created_at: 2025-05-19T07:14:09Z
author_association: MEMBER
diff_hunk: @@ -106,6 +104,147 @@ def read_curve_pools_and_gauges(chain_id: ChainID) -> tuple[dict[ChecksumEvmAddr
     return curve_pools, curve_gauges
 
 
+def _ensure_single_pool_curve_tokens_existence(
+        evm_inquirer: 'EvmNodeInquirer',
+        pool: CurvePoolData,
+) -> CurvePoolData | None:
+    encounter = TokenEncounterInfo(
+        description=f'Querying curve pools for {evm_inquirer.chain_id.to_name()}',
+        should_notify=False,
+    )
+    # Ensure pool coins exist in the globaldb.
+    # We have to create underlying tokens only if pool utilizes them.
+    if pool.underlying_coins is None or pool.underlying_coins == pool.coins:
+        # If underlying coins is None, it means that pool doesn't utilize them.
path: rotkehlchen/chain/evm/decoding/curve/curve_cache.py
user: login: yabirgb
body: I have forgotten what is the difference between `underlying_coins` and coins. Could you document it in the data structure?
created_at: 2025-05-19T07:16:32Z
author_association: MEMBER
diff_hunk: @@ -275,21 +285,23 @@ def _save_curve_data_to_cache(
     db_addressbook = DBAddressbook(db_handler=database)
     chain_id_str = str(chain_id.serialize_for_db())
     for pool in new_data:
-        addresbook_entries = [AddressbookEntry(
-            address=pool.pool_address,
-            name=pool.pool_name,
-            blockchain=chain_id.to_blockchain(),
-        )]
-        if pool.gauge_address is not None:
-            addresbook_entries.append(AddressbookEntry(
-                address=pool.gauge_address,
-                name=f'Curve gauge for {pool.pool_name}',
+        addressbook_entries = []
+        if pool.pool_name is not None:
+            addressbook_entries = [AddressbookEntry(
+                address=pool.pool_address,
+                name=pool.pool_name,
                 blockchain=chain_id.to_blockchain(),
-            ))
+            )]
+            if pool.gauge_address is not None:
+                addressbook_entries.append(AddressbookEntry(
+                    address=pool.gauge_address,
+                    name=f'Curve gauge for {pool.pool_name}',
+                    blockchain=chain_id.to_blockchain(),
path: rotkehlchen/chain/evm/decoding/curve/curve_cache.py
user: login: yabirgb
body: you can avoid calling twice this method in a for loop if you do it outside but event better I believe you can pass directly the chain id and the blockchain from the evm_inquirer as argument to this function and avoid calling any function at all
created_at: 2025-05-19T07:22:14Z
author_association: MEMBER
diff_hunk: @@ -390,59 +402,99 @@ def _query_curve_data_from_chain(
             method_name='get_address',
             arguments=[7],
         ))
-    except DeserializationError as e:
-        log.error(f'Curve address provider returned an invalid address for metaregistry. {e}')
-        return []
+    except (RemoteError, DeserializationError) as e:
+        log.error(
+            f'Failed to retrieve metaregistry address from the Curve '
+            f'address provider on {evm_inquirer.chain_name} due to {e!s}',
+        )
+        return
 
     metaregistry = EvmContract(
         address=metaregistry_address,
         abi=evm_inquirer.contracts.abi('CURVE_METAREGISTRY'),  # type: ignore[call-overload]  # for some reason mypy doesn't properly see argument type
         deployed_block=0,  # deployment_block is not used and the contract is dynamic
     )
-    pool_count = metaregistry.call(node_inquirer=evm_inquirer, method_name='pool_count')
-    new_pools: list[CurvePoolData] = []
+    try:
+        pool_count = metaregistry.call(node_inquirer=evm_inquirer, method_name='pool_count')
+    except RemoteError as e:
+        log.error(
+            'Failed to retrieve Curve pool count from the metaregistry '
+            f'on {evm_inquirer.chain_name} due to {e!s}',
+        )
+        return
+
+    if (existing_pool_count := len(existing_pools)) == pool_count:
+        return
path: rotkehlchen/chain/evm/decoding/curve/curve_cache.py
user: login: yabirgb
body: shouldn't you update in the db the last time this logic executed? Otherwise it will be requeried? Or that happens in another place. 
created_at: 2025-05-19T07:22:59Z
author_association: MEMBER
diff_hunk: @@ -486,18 +545,31 @@ def query_curve_data(
             string_to_evm_address(address[0])
             for address in cursor.execute(
                 'SELECT value FROM unique_cache WHERE key LIKE ?',
-                (f'{compute_cache_key((CacheType.CURVE_POOL_ADDRESS, str(inquirer.chain_id.serialize_for_db())))}%',),  # noqa: E501
+                (compute_cache_key((
+                    CacheType.CURVE_POOL_ADDRESS,
+                    str(inquirer.chain_id.serialize_for_db()),
+                    '0x%',  # Include the beginning `0x` of the address to avoid matching chain id 10 when using chain id 1  # noqa: E501
path: rotkehlchen/chain/evm/decoding/curve/curve_cache.py
user: login: yabirgb
body: oh this was a bug we had?
created_at: 2025-05-19T07:24:35Z
author_association: MEMBER
diff_hunk: @@ -79,3 +79,5 @@
 CURVE_SWAP_ROUTER_NG: Final = string_to_evm_address('0xF0d4c12A5768D806021F80a262B4d39d26C58b8D')
 DEPOSIT_AND_STAKE_ZAP: Final = string_to_evm_address('0x37c5ab57AF7100Bdc9B668d766e193CCbF6614FD')
 CHILD_LIQUIDITY_GAUGE_FACTORY: Final = string_to_evm_address('0xabC000d88f23Bb45525E447528DBF656A9D55bf5')  # noqa: E501
+# Maximum number of pools to query from the onchain metaregistry. Querying too many pools onchain takes a very long time.  # noqa: E501
+MAX_ONCHAIN_POOLS_QUERY: Final = 500
path: rotkehlchen/chain/evm/decoding/curve/constants.py
user: login: nicholasyoder
body: > Do you have the number of how many chain how many pools there are per evm chain?

it varies per chain. ethereum has the most ~1580, most of the other ones have several hundred to a thousand.

> problem with 500 is that it might be still too much

yeah, probably so. Doing 500 on a single chain took a bit less than 10 mins. But that does start to add up for when you have all the chains. Changed it to 250. Could go even lower if you think we should - but since this is in a separate greenlet now, it just runs in the background and saves its progress as it goes, so its not going to hurt anything too much for it to run for a little while.
created_at: 2025-05-19T14:39:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -390,59 +402,99 @@ def _query_curve_data_from_chain(
             method_name='get_address',
             arguments=[7],
         ))
-    except DeserializationError as e:
-        log.error(f'Curve address provider returned an invalid address for metaregistry. {e}')
-        return []
+    except (RemoteError, DeserializationError) as e:
+        log.error(
+            f'Failed to retrieve metaregistry address from the Curve '
+            f'address provider on {evm_inquirer.chain_name} due to {e!s}',
+        )
+        return
 
     metaregistry = EvmContract(
         address=metaregistry_address,
         abi=evm_inquirer.contracts.abi('CURVE_METAREGISTRY'),  # type: ignore[call-overload]  # for some reason mypy doesn't properly see argument type
         deployed_block=0,  # deployment_block is not used and the contract is dynamic
     )
-    pool_count = metaregistry.call(node_inquirer=evm_inquirer, method_name='pool_count')
-    new_pools: list[CurvePoolData] = []
+    try:
+        pool_count = metaregistry.call(node_inquirer=evm_inquirer, method_name='pool_count')
+    except RemoteError as e:
+        log.error(
+            'Failed to retrieve Curve pool count from the metaregistry '
+            f'on {evm_inquirer.chain_name} due to {e!s}',
+        )
+        return
+
+    if (existing_pool_count := len(existing_pools)) == pool_count:
+        return
+    if (new_pool_count := pool_count - existing_pool_count) > MAX_ONCHAIN_POOLS_QUERY:
+        log.error(
+            f'Skipping onchain query of {new_pool_count} {evm_inquirer.chain_name} '
+            f'Curve pools. Too many pools to query onchain.',
+        )
+        return
path: rotkehlchen/chain/evm/decoding/curve/curve_cache.py
user: login: nicholasyoder
body: I had written down during our call the other day that if it was more than the max, to just skip, but perhaps I misunderstood something. It probably would make more sense to go ahead and query some instead of skipping completely. Changed this.
created_at: 2025-05-19T14:39:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -390,59 +402,99 @@ def _query_curve_data_from_chain(
             method_name='get_address',
             arguments=[7],
         ))
-    except DeserializationError as e:
-        log.error(f'Curve address provider returned an invalid address for metaregistry. {e}')
-        return []
+    except (RemoteError, DeserializationError) as e:
+        log.error(
+            f'Failed to retrieve metaregistry address from the Curve '
+            f'address provider on {evm_inquirer.chain_name} due to {e!s}',
+        )
+        return
 
     metaregistry = EvmContract(
         address=metaregistry_address,
         abi=evm_inquirer.contracts.abi('CURVE_METAREGISTRY'),  # type: ignore[call-overload]  # for some reason mypy doesn't properly see argument type
         deployed_block=0,  # deployment_block is not used and the contract is dynamic
     )
-    pool_count = metaregistry.call(node_inquirer=evm_inquirer, method_name='pool_count')
-    new_pools: list[CurvePoolData] = []
+    try:
+        pool_count = metaregistry.call(node_inquirer=evm_inquirer, method_name='pool_count')
+    except RemoteError as e:
+        log.error(
+            'Failed to retrieve Curve pool count from the metaregistry '
+            f'on {evm_inquirer.chain_name} due to {e!s}',
+        )
+        return
+
+    if (existing_pool_count := len(existing_pools)) == pool_count:
+        return
+    if (new_pool_count := pool_count - existing_pool_count) > MAX_ONCHAIN_POOLS_QUERY:
+        log.error(
+            f'Skipping onchain query of {new_pool_count} {evm_inquirer.chain_name} '
+            f'Curve pools. Too many pools to query onchain.',
+        )
+        return
+
     last_notified_ts = Timestamp(0)
-    for pool_index in range(pool_count):
-        last_notified_ts = maybe_notify_new_pools_status(
+    for pool_index in range((start_idx := pool_count - new_pool_count), pool_count):
+        last_notified_ts = maybe_notify_cache_query_status(
             msg_aggregator=msg_aggregator,
             last_notified_ts=last_notified_ts,
             protocol=CPT_CURVE,
             chain=evm_inquirer.chain_id,
-            get_new_pools_count=lambda: len(new_pools),
+            processed=(processed := pool_index - start_idx),
+            total=new_pool_count,
         )
 
-        raw_address = metaregistry.call(
-            node_inquirer=evm_inquirer,
-            method_name='pool_list',
-            arguments=[pool_index],
-        )
         try:
-            pool_address = deserialize_evm_address(raw_address)
-        except DeserializationError as e:
-            log.error(f'Could not deserialize curve pool address {raw_address}. {e}')
-            continue
+            if (pool_address := deserialize_evm_address(metaregistry.call(
+                node_inquirer=evm_inquirer,
+                method_name='pool_list',
+                arguments=[pool_index],
+            ))) in IGNORED_CURVE_POOLS | existing_pools:
+                continue
 
-        if pool_address in IGNORED_CURVE_POOLS or pool_address in existing_pools:
+            log.debug(
+                f'Processing Curve pool {processed}/{new_pool_count} {pool_address} '
+                f'on {evm_inquirer.chain_name}.',
+            )
+            raw_pool_properties = evm_inquirer.multicall_2(
+                calls=[(
+                    metaregistry_address,
+                    metaregistry.encode(method_name=method_name, arguments=[pool_address]),
+                ) for method_name in CURVE_METAREGISTRY_METHODS],
+                require_success=False,
+            )
+        except (RemoteError, DeserializationError) as e:
+            log.error(
+                f'Failed to retrieve Curve pool address for index {pool_index} '
+                f'from the metaregistry on {evm_inquirer.chain_name} due to {e!s}',
+            )
             continue
 
-        calls = [
-            (
-                metaregistry_address,
-                metaregistry.encode(method_name=method_name, arguments=[pool_address]),
+        decoded_pool_properties: list[Any] = []
+        for (success, result), method_name in zip(raw_pool_properties, CURVE_METAREGISTRY_METHODS, strict=True):  # length should be same due to the call  # noqa: E501
+            if success is False:
+                if method_name == 'get_pool_name':  # There are a number of pools (especially later ones) where the pool name query fails  # noqa: E501
+                    decoded_pool_properties.append(None)
+                    continue
path: rotkehlchen/chain/evm/decoding/curve/curve_cache.py
user: login: nicholasyoder
body: As discussed in [discord](https://discord.com/channels/657906918408585217/1080783409640648765/1374020462979317770) when this fails the pool name is now constructed from the underlying tokens in `_ensure_single_pool_curve_tokens_existence`.  Also had to change `CurvePoolData` into a dataclass to make it mutable so the pool name can be edited.

created_at: 2025-05-19T14:40:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -106,6 +104,147 @@ def read_curve_pools_and_gauges(chain_id: ChainID) -> tuple[dict[ChecksumEvmAddr
     return curve_pools, curve_gauges
 
 
+def _ensure_single_pool_curve_tokens_existence(
+        evm_inquirer: 'EvmNodeInquirer',
+        pool: CurvePoolData,
+) -> CurvePoolData | None:
+    encounter = TokenEncounterInfo(
+        description=f'Querying curve pools for {evm_inquirer.chain_id.to_name()}',
+        should_notify=False,
+    )
+    # Ensure pool coins exist in the globaldb.
+    # We have to create underlying tokens only if pool utilizes them.
+    if pool.underlying_coins is None or pool.underlying_coins == pool.coins:
+        # If underlying coins is None, it means that pool doesn't utilize them.
path: rotkehlchen/chain/evm/decoding/curve/curve_cache.py
user: login: nicholasyoder
body: Added comments to CurvePoolData.
created_at: 2025-05-19T14:41:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -275,21 +285,23 @@ def _save_curve_data_to_cache(
     db_addressbook = DBAddressbook(db_handler=database)
     chain_id_str = str(chain_id.serialize_for_db())
     for pool in new_data:
-        addresbook_entries = [AddressbookEntry(
-            address=pool.pool_address,
-            name=pool.pool_name,
-            blockchain=chain_id.to_blockchain(),
-        )]
-        if pool.gauge_address is not None:
-            addresbook_entries.append(AddressbookEntry(
-                address=pool.gauge_address,
-                name=f'Curve gauge for {pool.pool_name}',
+        addressbook_entries = []
+        if pool.pool_name is not None:
+            addressbook_entries = [AddressbookEntry(
+                address=pool.pool_address,
+                name=pool.pool_name,
                 blockchain=chain_id.to_blockchain(),
-            ))
+            )]
+            if pool.gauge_address is not None:
+                addressbook_entries.append(AddressbookEntry(
+                    address=pool.gauge_address,
+                    name=f'Curve gauge for {pool.pool_name}',
+                    blockchain=chain_id.to_blockchain(),
path: rotkehlchen/chain/evm/decoding/curve/curve_cache.py
user: login: nicholasyoder
body: Actually changed this to just pass the evm_inquirer itself since the database, chainid, and blockchain all come directly from the inquirer.
created_at: 2025-05-19T14:52:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -390,59 +402,99 @@ def _query_curve_data_from_chain(
             method_name='get_address',
             arguments=[7],
         ))
-    except DeserializationError as e:
-        log.error(f'Curve address provider returned an invalid address for metaregistry. {e}')
-        return []
+    except (RemoteError, DeserializationError) as e:
+        log.error(
+            f'Failed to retrieve metaregistry address from the Curve '
+            f'address provider on {evm_inquirer.chain_name} due to {e!s}',
+        )
+        return
 
     metaregistry = EvmContract(
         address=metaregistry_address,
         abi=evm_inquirer.contracts.abi('CURVE_METAREGISTRY'),  # type: ignore[call-overload]  # for some reason mypy doesn't properly see argument type
         deployed_block=0,  # deployment_block is not used and the contract is dynamic
     )
-    pool_count = metaregistry.call(node_inquirer=evm_inquirer, method_name='pool_count')
-    new_pools: list[CurvePoolData] = []
+    try:
+        pool_count = metaregistry.call(node_inquirer=evm_inquirer, method_name='pool_count')
+    except RemoteError as e:
+        log.error(
+            'Failed to retrieve Curve pool count from the metaregistry '
+            f'on {evm_inquirer.chain_name} due to {e!s}',
+        )
+        return
+
+    if (existing_pool_count := len(existing_pools)) == pool_count:
+        return
path: rotkehlchen/chain/evm/decoding/curve/curve_cache.py
user: login: nicholasyoder
body: it happens in `query_curve_data` - after starting the greenlet to query onchain, it does `if len(pools_data) == 0:` and updates the last cache ts.
created_at: 2025-05-19T14:52:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -486,18 +545,31 @@ def query_curve_data(
             string_to_evm_address(address[0])
             for address in cursor.execute(
                 'SELECT value FROM unique_cache WHERE key LIKE ?',
-                (f'{compute_cache_key((CacheType.CURVE_POOL_ADDRESS, str(inquirer.chain_id.serialize_for_db())))}%',),  # noqa: E501
+                (compute_cache_key((
+                    CacheType.CURVE_POOL_ADDRESS,
+                    str(inquirer.chain_id.serialize_for_db()),
+                    '0x%',  # Include the beginning `0x` of the address to avoid matching chain id 10 when using chain id 1  # noqa: E501
path: rotkehlchen/chain/evm/decoding/curve/curve_cache.py
user: login: nicholasyoder
body: yep, for ethereum (`1`) it was getting all the ones in optimism (`10`) as well.
created_at: 2025-05-19T14:52:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -79,3 +79,5 @@
 CURVE_SWAP_ROUTER_NG: Final = string_to_evm_address('0xF0d4c12A5768D806021F80a262B4d39d26C58b8D')
 DEPOSIT_AND_STAKE_ZAP: Final = string_to_evm_address('0x37c5ab57AF7100Bdc9B668d766e193CCbF6614FD')
 CHILD_LIQUIDITY_GAUGE_FACTORY: Final = string_to_evm_address('0xabC000d88f23Bb45525E447528DBF656A9D55bf5')  # noqa: E501
+# Maximum number of pools to query from the onchain metaregistry. Querying too many pools onchain takes a very long time.  # noqa: E501
+MAX_ONCHAIN_POOLS_QUERY: Final = 500
path: rotkehlchen/chain/evm/decoding/curve/constants.py
user: login: nicholasyoder
body: Changed this to only 50 as discussed in [discord](https://discord.com/channels/657906918408585217/1374023232927305800/1374035082729488494)
created_at: 2025-05-19T14:53:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -390,59 +410,100 @@ def _query_curve_data_from_chain(
             method_name='get_address',
             arguments=[7],
         ))
-    except DeserializationError as e:
-        log.error(f'Curve address provider returned an invalid address for metaregistry. {e}')
-        return []
+    except (RemoteError, DeserializationError) as e:
+        log.error(
+            f'Failed to retrieve metaregistry address from the Curve '
+            f'address provider on {evm_inquirer.chain_name} due to {e!s}',
+        )
+        return
 
     metaregistry = EvmContract(
         address=metaregistry_address,
         abi=evm_inquirer.contracts.abi('CURVE_METAREGISTRY'),  # type: ignore[call-overload]  # for some reason mypy doesn't properly see argument type
         deployed_block=0,  # deployment_block is not used and the contract is dynamic
     )
-    pool_count = metaregistry.call(node_inquirer=evm_inquirer, method_name='pool_count')
-    new_pools: list[CurvePoolData] = []
+    try:
+        pool_count = metaregistry.call(node_inquirer=evm_inquirer, method_name='pool_count')
+    except RemoteError as e:
+        log.error(
+            'Failed to retrieve Curve pool count from the metaregistry '
+            f'on {evm_inquirer.chain_name} due to {e!s}',
+        )
+        return
+
+    if (existing_pool_count := len(existing_pools)) == pool_count:
+        return
+    if (new_pool_count := pool_count - existing_pool_count) > MAX_ONCHAIN_POOLS_QUERY:
+        pool_count = existing_pool_count + MAX_ONCHAIN_POOLS_QUERY
+        new_pool_count = MAX_ONCHAIN_POOLS_QUERY
+        log.error(
+            f'Tried to query {new_pool_count} {evm_inquirer.chain_name} Curve pools. '
+            f'Too many pools to query onchain. Only querying {new_pool_count}.',
+        )
+
     last_notified_ts = Timestamp(0)
-    for pool_index in range(pool_count):
-        last_notified_ts = maybe_notify_new_pools_status(
+    for pool_index in range((start_idx := pool_count - new_pool_count), pool_count):
+        last_notified_ts = maybe_notify_cache_query_status(
             msg_aggregator=msg_aggregator,
             last_notified_ts=last_notified_ts,
             protocol=CPT_CURVE,
             chain=evm_inquirer.chain_id,
-            get_new_pools_count=lambda: len(new_pools),
+            processed=(processed := pool_index - start_idx),
+            total=new_pool_count,
         )
 
-        raw_address = metaregistry.call(
-            node_inquirer=evm_inquirer,
-            method_name='pool_list',
-            arguments=[pool_index],
-        )
         try:
-            pool_address = deserialize_evm_address(raw_address)
-        except DeserializationError as e:
-            log.error(f'Could not deserialize curve pool address {raw_address}. {e}')
-            continue
+            if (pool_address := deserialize_evm_address(metaregistry.call(
+                node_inquirer=evm_inquirer,
+                method_name='pool_list',
+                arguments=[pool_index],
+            ))) in IGNORED_CURVE_POOLS | existing_pools:
+                continue
path: rotkehlchen/chain/evm/decoding/curve/curve_cache.py
user: login: yabirgb
body: mmmm  this union of sets you don't need to do it in every loop. Do it once outside
created_at: 2025-05-19T15:11:31Z
author_association: MEMBER
diff_hunk: @@ -106,6 +108,152 @@ def read_curve_pools_and_gauges(chain_id: ChainID) -> tuple[dict[ChecksumEvmAddr
     return curve_pools, curve_gauges
 
 
+def _ensure_single_pool_curve_tokens_existence(
+        evm_inquirer: 'EvmNodeInquirer',
+        pool: CurvePoolData,
+) -> CurvePoolData | None:
+    encounter = TokenEncounterInfo(
+        description=f'Querying curve pools for {evm_inquirer.chain_id.to_name()}',
+        should_notify=False,
path: rotkehlchen/chain/evm/decoding/curve/curve_cache.py
user: login: yabirgb
body: this function is mostly moving the one from the other place right?
created_at: 2025-05-19T15:12:55Z
author_association: MEMBER
diff_hunk: @@ -106,6 +108,152 @@ def read_curve_pools_and_gauges(chain_id: ChainID) -> tuple[dict[ChecksumEvmAddr
     return curve_pools, curve_gauges
 
 
+def _ensure_single_pool_curve_tokens_existence(
+        evm_inquirer: 'EvmNodeInquirer',
+        pool: CurvePoolData,
+) -> CurvePoolData | None:
+    encounter = TokenEncounterInfo(
+        description=f'Querying curve pools for {evm_inquirer.chain_id.to_name()}',
+        should_notify=False,
path: rotkehlchen/chain/evm/decoding/curve/curve_cache.py
user: login: nicholasyoder
body: yep just moved it from `_ensure_curve_tokens_existence` so we can add the tokens one pool at a time for the onchain query.
created_at: 2025-05-19T15:36:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -52,19 +51,22 @@
 if TYPE_CHECKING:
 
     from rotkehlchen.chain.evm.node_inquirer import EvmNodeInquirer
-    from rotkehlchen.db.dbhandler import DBHandler
     from rotkehlchen.user_messages import MessagesAggregator
 
 logger = logging.getLogger(__name__)
 log = RotkehlchenLogsAdapter(logger)
 
 
-class CurvePoolData(NamedTuple):
+@dataclass
+class CurvePoolData:
     pool_address: ChecksumEvmAddress
-    pool_name: str
+    pool_name: str | None
     lp_token_address: ChecksumEvmAddress
     gauge_address: ChecksumEvmAddress | None
+    # Coins in the pool. Has the LP token of the base pool for metapools.
     coins: list[ChecksumEvmAddress]
+    # All underlying coins in a pool. Includes the coins from the base pool for metapools.
+    # Same as `coins` for non-metapools.
path: rotkehlchen/chain/evm/decoding/curve/curve_cache.py
user: login: LefterisJP
body: And if it's a non-metapool that's when this is None? Would not make sense to duplicate data right?
created_at: 2025-05-19T16:13:07Z
author_association: MEMBER
diff_hunk: @@ -106,6 +108,152 @@ def read_curve_pools_and_gauges(chain_id: ChainID) -> tuple[dict[ChecksumEvmAddr
     return curve_pools, curve_gauges
 
 
+def _ensure_single_pool_curve_tokens_existence(
+        evm_inquirer: 'EvmNodeInquirer',
+        pool: CurvePoolData,
+) -> CurvePoolData | None:
+    encounter = TokenEncounterInfo(
+        description=f'Querying curve pools for {evm_inquirer.chain_id.to_name()}',
+        should_notify=False,
+    )
+    # Ensure pool coins exist in the globaldb.
+    # We have to create underlying tokens only if pool utilizes them.
+    underlying_token_names = []
+    if pool.underlying_coins is None or pool.underlying_coins == pool.coins:
+        # If underlying coins is None, it means that pool doesn't utilize them.
+        # If underlying coins list is equal to coins then there are no underlying coins as well.  # noqa: E501
+        for token_address in pool.coins:
+            if token_address == ETH_SPECIAL_ADDRESS:
+                underlying_token_names.append('ETH')
+                continue
+            # ensure token exists
path: rotkehlchen/chain/evm/decoding/curve/curve_cache.py
user: login: LefterisJP
body: ```suggestion

            # ensure token exists
```
created_at: 2025-05-19T16:16:46Z
author_association: MEMBER
diff_hunk: @@ -106,6 +108,152 @@ def read_curve_pools_and_gauges(chain_id: ChainID) -> tuple[dict[ChecksumEvmAddr
     return curve_pools, curve_gauges
 
 
+def _ensure_single_pool_curve_tokens_existence(
+        evm_inquirer: 'EvmNodeInquirer',
+        pool: CurvePoolData,
+) -> CurvePoolData | None:
+    encounter = TokenEncounterInfo(
+        description=f'Querying curve pools for {evm_inquirer.chain_id.to_name()}',
+        should_notify=False,
+    )
+    # Ensure pool coins exist in the globaldb.
+    # We have to create underlying tokens only if pool utilizes them.
+    underlying_token_names = []
+    if pool.underlying_coins is None or pool.underlying_coins == pool.coins:
+        # If underlying coins is None, it means that pool doesn't utilize them.
+        # If underlying coins list is equal to coins then there are no underlying coins as well.  # noqa: E501
+        for token_address in pool.coins:
+            if token_address == ETH_SPECIAL_ADDRESS:
+                underlying_token_names.append('ETH')
+                continue
+            # ensure token exists
+            try:
+                underlying_token_names.append(get_or_create_evm_token(
+                    userdb=evm_inquirer.database,
+                    evm_address=token_address,
+                    chain_id=evm_inquirer.chain_id,
+                    evm_inquirer=evm_inquirer,
+                    encounter=encounter,
+                ).symbol)
+            except NotERC20Conformant as e:
+                log.error(
+                    f'Skipping pool {pool} because {token_address} is not a '
+                    f'valid ERC20 token. {e}',
+                )
+                continue
+    elif len(pool.coins) == len(pool.underlying_coins):
+        # Coins and underlying coins lists represent a
+        # mapping of coin -> underlying coin (each coin always has one underlying coin).
+        for token_address, underlying_token_address in zip(pool.coins, pool.underlying_coins, strict=True):  # noqa: E501
+            if token_address == ETH_SPECIAL_ADDRESS:
+                continue
+            # ensure underlying token exists
path: rotkehlchen/chain/evm/decoding/curve/curve_cache.py
user: login: LefterisJP
body: ```suggestion

            # ensure underlying token exists
```
created_at: 2025-05-19T16:17:53Z
author_association: MEMBER
diff_hunk: @@ -106,6 +108,152 @@ def read_curve_pools_and_gauges(chain_id: ChainID) -> tuple[dict[ChecksumEvmAddr
     return curve_pools, curve_gauges
 
 
+def _ensure_single_pool_curve_tokens_existence(
+        evm_inquirer: 'EvmNodeInquirer',
+        pool: CurvePoolData,
+) -> CurvePoolData | None:
+    encounter = TokenEncounterInfo(
+        description=f'Querying curve pools for {evm_inquirer.chain_id.to_name()}',
+        should_notify=False,
+    )
+    # Ensure pool coins exist in the globaldb.
+    # We have to create underlying tokens only if pool utilizes them.
+    underlying_token_names = []
+    if pool.underlying_coins is None or pool.underlying_coins == pool.coins:
+        # If underlying coins is None, it means that pool doesn't utilize them.
+        # If underlying coins list is equal to coins then there are no underlying coins as well.  # noqa: E501
+        for token_address in pool.coins:
+            if token_address == ETH_SPECIAL_ADDRESS:
+                underlying_token_names.append('ETH')
+                continue
+            # ensure token exists
+            try:
+                underlying_token_names.append(get_or_create_evm_token(
+                    userdb=evm_inquirer.database,
+                    evm_address=token_address,
+                    chain_id=evm_inquirer.chain_id,
+                    evm_inquirer=evm_inquirer,
+                    encounter=encounter,
+                ).symbol)
+            except NotERC20Conformant as e:
+                log.error(
+                    f'Skipping pool {pool} because {token_address} is not a '
+                    f'valid ERC20 token. {e}',
+                )
+                continue
+    elif len(pool.coins) == len(pool.underlying_coins):
+        # Coins and underlying coins lists represent a
+        # mapping of coin -> underlying coin (each coin always has one underlying coin).
+        for token_address, underlying_token_address in zip(pool.coins, pool.underlying_coins, strict=True):  # noqa: E501
+            if token_address == ETH_SPECIAL_ADDRESS:
+                continue
+            # ensure underlying token exists
+            try:
+                get_or_create_evm_token(
+                    userdb=evm_inquirer.database,
+                    evm_address=underlying_token_address,
+                    chain_id=evm_inquirer.chain_id,
+                    evm_inquirer=evm_inquirer,
+                    encounter=encounter,
+                )
+            except NotERC20Conformant as e:
+                log.error(
+                    f'Skipping pool {pool} because {underlying_token_address} is not a '
+                    f'valid ERC20 token. {e}',
+                )
+                continue
+
+            if underlying_token_address == token_address:  # they are the same token. Stop here
+                continue  # can happen for a pool to have token's underlying token as itself
+
+            try:
+                # and ensure token exists
path: rotkehlchen/chain/evm/decoding/curve/curve_cache.py
user: login: LefterisJP
body: ```suggestion
            try:  # and ensure token exists
```
created_at: 2025-05-19T16:18:14Z
author_association: MEMBER
diff_hunk: @@ -106,6 +108,152 @@ def read_curve_pools_and_gauges(chain_id: ChainID) -> tuple[dict[ChecksumEvmAddr
     return curve_pools, curve_gauges
 
 
+def _ensure_single_pool_curve_tokens_existence(
+        evm_inquirer: 'EvmNodeInquirer',
+        pool: CurvePoolData,
+) -> CurvePoolData | None:
+    encounter = TokenEncounterInfo(
+        description=f'Querying curve pools for {evm_inquirer.chain_id.to_name()}',
+        should_notify=False,
+    )
+    # Ensure pool coins exist in the globaldb.
+    # We have to create underlying tokens only if pool utilizes them.
+    underlying_token_names = []
+    if pool.underlying_coins is None or pool.underlying_coins == pool.coins:
+        # If underlying coins is None, it means that pool doesn't utilize them.
+        # If underlying coins list is equal to coins then there are no underlying coins as well.  # noqa: E501
+        for token_address in pool.coins:
+            if token_address == ETH_SPECIAL_ADDRESS:
+                underlying_token_names.append('ETH')
+                continue
+            # ensure token exists
+            try:
+                underlying_token_names.append(get_or_create_evm_token(
+                    userdb=evm_inquirer.database,
+                    evm_address=token_address,
+                    chain_id=evm_inquirer.chain_id,
+                    evm_inquirer=evm_inquirer,
+                    encounter=encounter,
+                ).symbol)
+            except NotERC20Conformant as e:
+                log.error(
+                    f'Skipping pool {pool} because {token_address} is not a '
+                    f'valid ERC20 token. {e}',
+                )
+                continue
+    elif len(pool.coins) == len(pool.underlying_coins):
+        # Coins and underlying coins lists represent a
+        # mapping of coin -> underlying coin (each coin always has one underlying coin).
+        for token_address, underlying_token_address in zip(pool.coins, pool.underlying_coins, strict=True):  # noqa: E501
+            if token_address == ETH_SPECIAL_ADDRESS:
+                continue
+            # ensure underlying token exists
+            try:
+                get_or_create_evm_token(
+                    userdb=evm_inquirer.database,
+                    evm_address=underlying_token_address,
+                    chain_id=evm_inquirer.chain_id,
+                    evm_inquirer=evm_inquirer,
+                    encounter=encounter,
+                )
+            except NotERC20Conformant as e:
+                log.error(
+                    f'Skipping pool {pool} because {underlying_token_address} is not a '
+                    f'valid ERC20 token. {e}',
+                )
+                continue
+
+            if underlying_token_address == token_address:  # they are the same token. Stop here
+                continue  # can happen for a pool to have token's underlying token as itself
+
+            try:
+                # and ensure token exists
+                underlying_token_names.append(get_or_create_evm_token(
+                    userdb=evm_inquirer.database,
+                    evm_address=token_address,
+                    chain_id=evm_inquirer.chain_id,
+                    evm_inquirer=evm_inquirer,
+                    underlying_tokens=[UnderlyingToken(
+                        address=underlying_token_address,
+                        token_kind=EvmTokenKind.ERC20,
+                        weight=ONE,
+                    )],
+                    encounter=encounter,
+                ).symbol)
+            except NotERC20Conformant as e:
+                log.error(
+                    f'Skipping pool {pool} because {token_address} is not a '
+                    f'valid ERC20 token. {e}',
+                )
+                continue
+    else:
+        # Otherwise just ensure that coins and underlying coins exist in our assets database  # noqa: E501
path: rotkehlchen/chain/evm/decoding/curve/curve_cache.py
user: login: LefterisJP
body: ```suggestion
    else:  # Otherwise just ensure that coins and underlying coins exist in our assets database  # noqa: E501
```
created_at: 2025-05-19T16:18:30Z
author_association: MEMBER
diff_hunk: @@ -106,6 +108,152 @@ def read_curve_pools_and_gauges(chain_id: ChainID) -> tuple[dict[ChecksumEvmAddr
     return curve_pools, curve_gauges
 
 
+def _ensure_single_pool_curve_tokens_existence(
+        evm_inquirer: 'EvmNodeInquirer',
+        pool: CurvePoolData,
+) -> CurvePoolData | None:
+    encounter = TokenEncounterInfo(
+        description=f'Querying curve pools for {evm_inquirer.chain_id.to_name()}',
+        should_notify=False,
+    )
+    # Ensure pool coins exist in the globaldb.
+    # We have to create underlying tokens only if pool utilizes them.
+    underlying_token_names = []
+    if pool.underlying_coins is None or pool.underlying_coins == pool.coins:
+        # If underlying coins is None, it means that pool doesn't utilize them.
+        # If underlying coins list is equal to coins then there are no underlying coins as well.  # noqa: E501
+        for token_address in pool.coins:
+            if token_address == ETH_SPECIAL_ADDRESS:
+                underlying_token_names.append('ETH')
+                continue
+            # ensure token exists
+            try:
+                underlying_token_names.append(get_or_create_evm_token(
+                    userdb=evm_inquirer.database,
+                    evm_address=token_address,
+                    chain_id=evm_inquirer.chain_id,
+                    evm_inquirer=evm_inquirer,
+                    encounter=encounter,
+                ).symbol)
+            except NotERC20Conformant as e:
+                log.error(
+                    f'Skipping pool {pool} because {token_address} is not a '
+                    f'valid ERC20 token. {e}',
+                )
+                continue
+    elif len(pool.coins) == len(pool.underlying_coins):
+        # Coins and underlying coins lists represent a
+        # mapping of coin -> underlying coin (each coin always has one underlying coin).
+        for token_address, underlying_token_address in zip(pool.coins, pool.underlying_coins, strict=True):  # noqa: E501
+            if token_address == ETH_SPECIAL_ADDRESS:
+                continue
+            # ensure underlying token exists
+            try:
+                get_or_create_evm_token(
+                    userdb=evm_inquirer.database,
+                    evm_address=underlying_token_address,
+                    chain_id=evm_inquirer.chain_id,
+                    evm_inquirer=evm_inquirer,
+                    encounter=encounter,
+                )
+            except NotERC20Conformant as e:
+                log.error(
+                    f'Skipping pool {pool} because {underlying_token_address} is not a '
+                    f'valid ERC20 token. {e}',
+                )
+                continue
+
+            if underlying_token_address == token_address:  # they are the same token. Stop here
+                continue  # can happen for a pool to have token's underlying token as itself
+
+            try:
+                # and ensure token exists
+                underlying_token_names.append(get_or_create_evm_token(
+                    userdb=evm_inquirer.database,
+                    evm_address=token_address,
+                    chain_id=evm_inquirer.chain_id,
+                    evm_inquirer=evm_inquirer,
+                    underlying_tokens=[UnderlyingToken(
+                        address=underlying_token_address,
+                        token_kind=EvmTokenKind.ERC20,
+                        weight=ONE,
+                    )],
+                    encounter=encounter,
+                ).symbol)
+            except NotERC20Conformant as e:
+                log.error(
+                    f'Skipping pool {pool} because {token_address} is not a '
+                    f'valid ERC20 token. {e}',
+                )
+                continue
+    else:
+        # Otherwise just ensure that coins and underlying coins exist in our assets database  # noqa: E501
+        for token_address in set(pool.coins + pool.underlying_coins):
+            if token_address == ETH_SPECIAL_ADDRESS:
+                continue
path: rotkehlchen/chain/evm/decoding/curve/curve_cache.py
user: login: LefterisJP
body: ```suggestion
                continue

```
created_at: 2025-05-19T16:18:43Z
author_association: MEMBER
diff_hunk: @@ -390,59 +410,101 @@ def _query_curve_data_from_chain(
             method_name='get_address',
             arguments=[7],
         ))
-    except DeserializationError as e:
-        log.error(f'Curve address provider returned an invalid address for metaregistry. {e}')
-        return []
+    except (RemoteError, DeserializationError) as e:
+        log.error(
+            f'Failed to retrieve metaregistry address from the Curve '
+            f'address provider on {evm_inquirer.chain_name} due to {e!s}',
+        )
+        return
 
     metaregistry = EvmContract(
         address=metaregistry_address,
         abi=evm_inquirer.contracts.abi('CURVE_METAREGISTRY'),  # type: ignore[call-overload]  # for some reason mypy doesn't properly see argument type
         deployed_block=0,  # deployment_block is not used and the contract is dynamic
     )
-    pool_count = metaregistry.call(node_inquirer=evm_inquirer, method_name='pool_count')
-    new_pools: list[CurvePoolData] = []
+    try:
+        pool_count = metaregistry.call(node_inquirer=evm_inquirer, method_name='pool_count')
+    except RemoteError as e:
+        log.error(
+            'Failed to retrieve Curve pool count from the metaregistry '
+            f'on {evm_inquirer.chain_name} due to {e!s}',
+        )
+        return
+
+    if (existing_pool_count := len(existing_pools)) == pool_count:
+        return
+    if (new_pool_count := pool_count - existing_pool_count) > MAX_ONCHAIN_POOLS_QUERY:
+        pool_count = existing_pool_count + MAX_ONCHAIN_POOLS_QUERY
+        new_pool_count = MAX_ONCHAIN_POOLS_QUERY
+        log.error(
path: rotkehlchen/chain/evm/decoding/curve/curve_cache.py
user: login: LefterisJP
body: This is not an error.

```suggestion
        log.info(
```
created_at: 2025-05-19T16:32:35Z
author_association: MEMBER
diff_hunk: @@ -390,59 +410,101 @@ def _query_curve_data_from_chain(
             method_name='get_address',
             arguments=[7],
         ))
-    except DeserializationError as e:
-        log.error(f'Curve address provider returned an invalid address for metaregistry. {e}')
-        return []
+    except (RemoteError, DeserializationError) as e:
+        log.error(
+            f'Failed to retrieve metaregistry address from the Curve '
+            f'address provider on {evm_inquirer.chain_name} due to {e!s}',
+        )
+        return
 
     metaregistry = EvmContract(
         address=metaregistry_address,
         abi=evm_inquirer.contracts.abi('CURVE_METAREGISTRY'),  # type: ignore[call-overload]  # for some reason mypy doesn't properly see argument type
         deployed_block=0,  # deployment_block is not used and the contract is dynamic
     )
-    pool_count = metaregistry.call(node_inquirer=evm_inquirer, method_name='pool_count')
-    new_pools: list[CurvePoolData] = []
+    try:
+        pool_count = metaregistry.call(node_inquirer=evm_inquirer, method_name='pool_count')
+    except RemoteError as e:
+        log.error(
+            'Failed to retrieve Curve pool count from the metaregistry '
+            f'on {evm_inquirer.chain_name} due to {e!s}',
+        )
+        return
+
+    if (existing_pool_count := len(existing_pools)) == pool_count:
+        return
+    if (new_pool_count := pool_count - existing_pool_count) > MAX_ONCHAIN_POOLS_QUERY:
+        pool_count = existing_pool_count + MAX_ONCHAIN_POOLS_QUERY
+        new_pool_count = MAX_ONCHAIN_POOLS_QUERY
+        log.error(
+            f'Tried to query {new_pool_count} {evm_inquirer.chain_name} Curve pools. '
+            f'Too many pools to query onchain. Only querying {new_pool_count}.',
+        )
+
     last_notified_ts = Timestamp(0)
-    for pool_index in range(pool_count):
-        last_notified_ts = maybe_notify_new_pools_status(
+    pools_to_skip = IGNORED_CURVE_POOLS | existing_pools
+    for pool_index in range((start_idx := pool_count - new_pool_count), pool_count):
+        last_notified_ts = maybe_notify_cache_query_status(
             msg_aggregator=msg_aggregator,
             last_notified_ts=last_notified_ts,
             protocol=CPT_CURVE,
             chain=evm_inquirer.chain_id,
-            get_new_pools_count=lambda: len(new_pools),
+            processed=(processed := pool_index - start_idx),
+            total=new_pool_count,
         )
 
-        raw_address = metaregistry.call(
-            node_inquirer=evm_inquirer,
-            method_name='pool_list',
-            arguments=[pool_index],
-        )
         try:
-            pool_address = deserialize_evm_address(raw_address)
-        except DeserializationError as e:
-            log.error(f'Could not deserialize curve pool address {raw_address}. {e}')
-            continue
+            if (pool_address := deserialize_evm_address(metaregistry.call(
+                node_inquirer=evm_inquirer,
+                method_name='pool_list',
+                arguments=[pool_index],
+            ))) in pools_to_skip:
+                continue
 
-        if pool_address in IGNORED_CURVE_POOLS or pool_address in existing_pools:
+            log.debug(
+                f'Processing Curve pool {processed}/{new_pool_count} {pool_address} '
+                f'on {evm_inquirer.chain_name}.',
+            )
+            raw_pool_properties = evm_inquirer.multicall_2(
+                calls=[(
+                    metaregistry_address,
+                    metaregistry.encode(method_name=method_name, arguments=[pool_address]),
+                ) for method_name in CURVE_METAREGISTRY_METHODS],
+                require_success=False,
+            )
+        except (RemoteError, DeserializationError) as e:
+            log.error(
+                f'Failed to retrieve Curve pool address for index {pool_index} '
+                f'from the metaregistry on {evm_inquirer.chain_name} due to {e!s}',
+            )
             continue
 
-        calls = [
-            (
-                metaregistry_address,
-                metaregistry.encode(method_name=method_name, arguments=[pool_address]),
+        decoded_pool_properties: list[Any] = []
+        for (success, result), method_name in zip(raw_pool_properties, CURVE_METAREGISTRY_METHODS, strict=True):  # length should be same due to the call  # noqa: E501
+            if success is False:
+                if method_name == 'get_pool_name':  # There are a number of pools (especially later ones) where the pool name query fails  # noqa: E501
+                    decoded_pool_properties.append(None)  # Pool name will be constructed from the underlying tokens later instead  # noqa: E501
+                    continue
+                else:
+                    break
+
+            decoded_pool_properties.append(metaregistry.decode(
+                result=result,
+                method_name=method_name,
+                arguments=[pool_address],
+            )[0])
+
+        if len(decoded_pool_properties) < 5:
path: rotkehlchen/chain/evm/decoding/curve/curve_cache.py
user: login: LefterisJP
body: What is this 5 here? You need to explain it in a comment. Also shouldn't it be exactly 5? You are just unfolding it below.
created_at: 2025-05-19T16:34:39Z
author_association: MEMBER
diff_hunk: @@ -486,18 +545,31 @@ def query_curve_data(
             string_to_evm_address(address[0])
             for address in cursor.execute(
                 'SELECT value FROM unique_cache WHERE key LIKE ?',
-                (f'{compute_cache_key((CacheType.CURVE_POOL_ADDRESS, str(inquirer.chain_id.serialize_for_db())))}%',),  # noqa: E501
+                (compute_cache_key((
+                    CacheType.CURVE_POOL_ADDRESS,
+                    str(inquirer.chain_id.serialize_for_db()),
+                    '0x%',  # Include the beginning `0x` of the address to avoid matching chain id 10 when using chain id 1  # noqa: E501
path: rotkehlchen/chain/evm/decoding/curve/curve_cache.py
user: login: LefterisJP
body: Do you have a regression test for this bug? If not can you add?
created_at: 2025-05-19T16:37:01Z
author_association: MEMBER
diff_hunk: @@ -486,18 +551,31 @@ def query_curve_data(
             string_to_evm_address(address[0])
             for address in cursor.execute(
                 'SELECT value FROM unique_cache WHERE key LIKE ?',
-                (f'{compute_cache_key((CacheType.CURVE_POOL_ADDRESS, str(inquirer.chain_id.serialize_for_db())))}%',),  # noqa: E501
+                (compute_cache_key((
+                    CacheType.CURVE_POOL_ADDRESS,
+                    str(inquirer.chain_id.serialize_for_db()),
+                    '0x%',  # Include the beginning `0x` of the address to avoid matching chain id 10 when using chain id 1  # noqa: E501
+                )),),
             )
         }
 
+    pools_data: list[CurvePoolData] = []
     try:
-        pools_data: list[CurvePoolData] = _query_curve_data_from_api(
+        pools_data = _query_curve_data_from_api(
             chain_id=inquirer.chain_id,
             existing_pools=existing_pools,
         )
     except (RemoteError, UnableToDecryptRemoteData) as e:
-        log.error(f'Could not query curve api due to: {e}.')
-        return None
+        log.error(f'Could not query curve api due to: {e}. Will query metaregistry on chain')
path: rotkehlchen/chain/evm/decoding/curve/curve_cache.py
user: login: LefterisJP
body: ```suggestion
        log.error(f'Could not query curve api due to: {e}. Will query the metaregistry on chain')
```
created_at: 2025-05-19T16:37:54Z
author_association: MEMBER
diff_hunk: @@ -486,18 +551,31 @@ def query_curve_data(
             string_to_evm_address(address[0])
             for address in cursor.execute(
                 'SELECT value FROM unique_cache WHERE key LIKE ?',
-                (f'{compute_cache_key((CacheType.CURVE_POOL_ADDRESS, str(inquirer.chain_id.serialize_for_db())))}%',),  # noqa: E501
+                (compute_cache_key((
+                    CacheType.CURVE_POOL_ADDRESS,
+                    str(inquirer.chain_id.serialize_for_db()),
+                    '0x%',  # Include the beginning `0x` of the address to avoid matching chain id 10 when using chain id 1  # noqa: E501
+                )),),
             )
         }
 
+    pools_data: list[CurvePoolData] = []
     try:
-        pools_data: list[CurvePoolData] = _query_curve_data_from_api(
+        pools_data = _query_curve_data_from_api(
             chain_id=inquirer.chain_id,
             existing_pools=existing_pools,
         )
     except (RemoteError, UnableToDecryptRemoteData) as e:
-        log.error(f'Could not query curve api due to: {e}.')
-        return None
+        log.error(f'Could not query curve api due to: {e}. Will query metaregistry on chain')
+        inquirer.greenlet_manager.spawn_and_track(
+            after_seconds=None,
+            task_name='Query Curve pools from onchain metaregistry',
path: rotkehlchen/chain/evm/decoding/curve/curve_cache.py
user: login: LefterisJP
body: ```suggestion
            task_name='Query Curve pools from the onchain metaregistry',
```
created_at: 2025-05-19T16:38:03Z
author_association: MEMBER
diff_hunk: @@ -52,19 +51,22 @@
 if TYPE_CHECKING:
 
     from rotkehlchen.chain.evm.node_inquirer import EvmNodeInquirer
-    from rotkehlchen.db.dbhandler import DBHandler
     from rotkehlchen.user_messages import MessagesAggregator
 
 logger = logging.getLogger(__name__)
 log = RotkehlchenLogsAdapter(logger)
 
 
-class CurvePoolData(NamedTuple):
+@dataclass
+class CurvePoolData:
     pool_address: ChecksumEvmAddress
-    pool_name: str
+    pool_name: str | None
     lp_token_address: ChecksumEvmAddress
     gauge_address: ChecksumEvmAddress | None
+    # Coins in the pool. Has the LP token of the base pool for metapools.
     coins: list[ChecksumEvmAddress]
+    # All underlying coins in a pool. Includes the coins from the base pool for metapools.
+    # Same as `coins` for non-metapools.
path: rotkehlchen/chain/evm/decoding/curve/curve_cache.py
user: login: nicholasyoder
body: > And if it's a non-metapool that's when this is None?

mmm... yes sometimes.

This is just a data structure to hold the pool properties between where its queried from the api or chain and where its processed in `_ensure_single_pool_curve_tokens_existence`. One of the checks when processing it is `pool.underlying_coins is None or pool.underlying_coins == pool.coins`. When querying from the api this is set to None for non-metapools since the `underlyingCoins` key is not present, but when querying onchain, `get_underlying_coins` will return the same value as `get_coins` for at least some types of non-metapools.

Added to the comment here to also mention that is can be None for non-metapools.
created_at: 2025-05-19T17:49:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -390,59 +410,101 @@ def _query_curve_data_from_chain(
             method_name='get_address',
             arguments=[7],
         ))
-    except DeserializationError as e:
-        log.error(f'Curve address provider returned an invalid address for metaregistry. {e}')
-        return []
+    except (RemoteError, DeserializationError) as e:
+        log.error(
+            f'Failed to retrieve metaregistry address from the Curve '
+            f'address provider on {evm_inquirer.chain_name} due to {e!s}',
+        )
+        return
 
     metaregistry = EvmContract(
         address=metaregistry_address,
         abi=evm_inquirer.contracts.abi('CURVE_METAREGISTRY'),  # type: ignore[call-overload]  # for some reason mypy doesn't properly see argument type
         deployed_block=0,  # deployment_block is not used and the contract is dynamic
     )
-    pool_count = metaregistry.call(node_inquirer=evm_inquirer, method_name='pool_count')
-    new_pools: list[CurvePoolData] = []
+    try:
+        pool_count = metaregistry.call(node_inquirer=evm_inquirer, method_name='pool_count')
+    except RemoteError as e:
+        log.error(
+            'Failed to retrieve Curve pool count from the metaregistry '
+            f'on {evm_inquirer.chain_name} due to {e!s}',
+        )
+        return
+
+    if (existing_pool_count := len(existing_pools)) == pool_count:
+        return
+    if (new_pool_count := pool_count - existing_pool_count) > MAX_ONCHAIN_POOLS_QUERY:
+        pool_count = existing_pool_count + MAX_ONCHAIN_POOLS_QUERY
+        new_pool_count = MAX_ONCHAIN_POOLS_QUERY
+        log.error(
+            f'Tried to query {new_pool_count} {evm_inquirer.chain_name} Curve pools. '
+            f'Too many pools to query onchain. Only querying {new_pool_count}.',
+        )
+
     last_notified_ts = Timestamp(0)
-    for pool_index in range(pool_count):
-        last_notified_ts = maybe_notify_new_pools_status(
+    pools_to_skip = IGNORED_CURVE_POOLS | existing_pools
+    for pool_index in range((start_idx := pool_count - new_pool_count), pool_count):
+        last_notified_ts = maybe_notify_cache_query_status(
             msg_aggregator=msg_aggregator,
             last_notified_ts=last_notified_ts,
             protocol=CPT_CURVE,
             chain=evm_inquirer.chain_id,
-            get_new_pools_count=lambda: len(new_pools),
+            processed=(processed := pool_index - start_idx),
+            total=new_pool_count,
         )
 
-        raw_address = metaregistry.call(
-            node_inquirer=evm_inquirer,
-            method_name='pool_list',
-            arguments=[pool_index],
-        )
         try:
-            pool_address = deserialize_evm_address(raw_address)
-        except DeserializationError as e:
-            log.error(f'Could not deserialize curve pool address {raw_address}. {e}')
-            continue
+            if (pool_address := deserialize_evm_address(metaregistry.call(
+                node_inquirer=evm_inquirer,
+                method_name='pool_list',
+                arguments=[pool_index],
+            ))) in pools_to_skip:
+                continue
 
-        if pool_address in IGNORED_CURVE_POOLS or pool_address in existing_pools:
+            log.debug(
+                f'Processing Curve pool {processed}/{new_pool_count} {pool_address} '
+                f'on {evm_inquirer.chain_name}.',
+            )
+            raw_pool_properties = evm_inquirer.multicall_2(
+                calls=[(
+                    metaregistry_address,
+                    metaregistry.encode(method_name=method_name, arguments=[pool_address]),
+                ) for method_name in CURVE_METAREGISTRY_METHODS],
+                require_success=False,
+            )
+        except (RemoteError, DeserializationError) as e:
+            log.error(
+                f'Failed to retrieve Curve pool address for index {pool_index} '
+                f'from the metaregistry on {evm_inquirer.chain_name} due to {e!s}',
+            )
             continue
 
-        calls = [
-            (
-                metaregistry_address,
-                metaregistry.encode(method_name=method_name, arguments=[pool_address]),
+        decoded_pool_properties: list[Any] = []
+        for (success, result), method_name in zip(raw_pool_properties, CURVE_METAREGISTRY_METHODS, strict=True):  # length should be same due to the call  # noqa: E501
+            if success is False:
+                if method_name == 'get_pool_name':  # There are a number of pools (especially later ones) where the pool name query fails  # noqa: E501
+                    decoded_pool_properties.append(None)  # Pool name will be constructed from the underlying tokens later instead  # noqa: E501
+                    continue
+                else:
+                    break
+
+            decoded_pool_properties.append(metaregistry.decode(
+                result=result,
+                method_name=method_name,
+                arguments=[pool_address],
+            )[0])
+
+        if len(decoded_pool_properties) < 5:
path: rotkehlchen/chain/evm/decoding/curve/curve_cache.py
user: login: nicholasyoder
body: Replaced it with ` != len(CURVE_METAREGISTRY_METHODS)` as that both makes it clear what its checking right in the code and also will still work if the number of methods its calling changes at some point.
created_at: 2025-05-19T17:52:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -486,18 +545,31 @@ def query_curve_data(
             string_to_evm_address(address[0])
             for address in cursor.execute(
                 'SELECT value FROM unique_cache WHERE key LIKE ?',
-                (f'{compute_cache_key((CacheType.CURVE_POOL_ADDRESS, str(inquirer.chain_id.serialize_for_db())))}%',),  # noqa: E501
+                (compute_cache_key((
+                    CacheType.CURVE_POOL_ADDRESS,
+                    str(inquirer.chain_id.serialize_for_db()),
+                    '0x%',  # Include the beginning `0x` of the address to avoid matching chain id 10 when using chain id 1  # noqa: E501
path: rotkehlchen/chain/evm/decoding/curve/curve_cache.py
user: login: nicholasyoder
body: Changed the test to only remove the cached pool addresses from ethereum, and to assert that some exist in optimism with a comment about the problem. The test now fails if this is reverted to just `%` instead of `0x%`.
created_at: 2025-05-19T17:55:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -52,19 +51,22 @@
 if TYPE_CHECKING:
 
     from rotkehlchen.chain.evm.node_inquirer import EvmNodeInquirer
-    from rotkehlchen.db.dbhandler import DBHandler
     from rotkehlchen.user_messages import MessagesAggregator
 
 logger = logging.getLogger(__name__)
 log = RotkehlchenLogsAdapter(logger)
 
 
-class CurvePoolData(NamedTuple):
+@dataclass
+class CurvePoolData:
     pool_address: ChecksumEvmAddress
-    pool_name: str
+    pool_name: str | None
     lp_token_address: ChecksumEvmAddress
     gauge_address: ChecksumEvmAddress | None
+    # Coins in the pool. Has the LP token of the base pool for metapools.
     coins: list[ChecksumEvmAddress]
+    # All underlying coins in a pool. Includes the coins from the base pool for metapools.
+    # Same as `coins` or None for non-metapools.
path: rotkehlchen/chain/evm/decoding/curve/curve_cache.py
user: login: LefterisJP
body: >  Same as `coins` or None for non-metapools.

Wouldn't it be better to be consistent here? If it's a non metapool make it None. Being consistent is important and easier to reason about it and have less conditionals.
created_at: 2025-05-19T19:54:52Z
author_association: MEMBER
diff_hunk: @@ -106,6 +108,153 @@ def read_curve_pools_and_gauges(chain_id: ChainID) -> tuple[dict[ChecksumEvmAddr
     return curve_pools, curve_gauges
 
 
+def _ensure_single_pool_curve_tokens_existence(
+        evm_inquirer: 'EvmNodeInquirer',
+        pool: CurvePoolData,
+) -> CurvePoolData | None:
+    encounter = TokenEncounterInfo(
+        description=f'Querying curve pools for {evm_inquirer.chain_id.to_name()}',
+        should_notify=False,
+    )
+    # Ensure pool coins exist in the globaldb.
+    # We have to create underlying tokens only if pool utilizes them.
+    underlying_token_names = []
+    if pool.underlying_coins is None or pool.underlying_coins == pool.coins:
+        # If underlying coins is None, it means that pool doesn't utilize them.
+        # If underlying coins list is equal to coins then there are no underlying coins as well.  # noqa: E501
+        for token_address in pool.coins:
+            if token_address == ETH_SPECIAL_ADDRESS:
+                underlying_token_names.append('ETH')
+                continue
+
+            try:  # ensure token exists
+                underlying_token_names.append(get_or_create_evm_token(
+                    userdb=evm_inquirer.database,
+                    evm_address=token_address,
+                    chain_id=evm_inquirer.chain_id,
+                    evm_inquirer=evm_inquirer,
+                    encounter=encounter,
+                ).symbol)
+            except NotERC20Conformant as e:
+                log.error(
+                    f'Skipping pool {pool} because {token_address} is not a '
+                    f'valid ERC20 token. {e}',
+                )
+                continue
path: rotkehlchen/chain/evm/decoding/curve/curve_cache.py
user: login: LefterisJP
body: ```suggestion
                continue

```
created_at: 2025-05-19T19:55:23Z
author_association: MEMBER
diff_hunk: @@ -106,6 +108,153 @@ def read_curve_pools_and_gauges(chain_id: ChainID) -> tuple[dict[ChecksumEvmAddr
     return curve_pools, curve_gauges
 
 
+def _ensure_single_pool_curve_tokens_existence(
+        evm_inquirer: 'EvmNodeInquirer',
+        pool: CurvePoolData,
+) -> CurvePoolData | None:
+    encounter = TokenEncounterInfo(
+        description=f'Querying curve pools for {evm_inquirer.chain_id.to_name()}',
+        should_notify=False,
+    )
+    # Ensure pool coins exist in the globaldb.
+    # We have to create underlying tokens only if pool utilizes them.
+    underlying_token_names = []
+    if pool.underlying_coins is None or pool.underlying_coins == pool.coins:
+        # If underlying coins is None, it means that pool doesn't utilize them.
+        # If underlying coins list is equal to coins then there are no underlying coins as well.  # noqa: E501
+        for token_address in pool.coins:
+            if token_address == ETH_SPECIAL_ADDRESS:
+                underlying_token_names.append('ETH')
+                continue
+
+            try:  # ensure token exists
+                underlying_token_names.append(get_or_create_evm_token(
+                    userdb=evm_inquirer.database,
+                    evm_address=token_address,
+                    chain_id=evm_inquirer.chain_id,
+                    evm_inquirer=evm_inquirer,
+                    encounter=encounter,
+                ).symbol)
+            except NotERC20Conformant as e:
+                log.error(
+                    f'Skipping pool {pool} because {token_address} is not a '
+                    f'valid ERC20 token. {e}',
+                )
+                continue
+    elif len(pool.coins) == len(pool.underlying_coins):
+        # Coins and underlying coins lists represent a
+        # mapping of coin -> underlying coin (each coin always has one underlying coin).
+        for token_address, underlying_token_address in zip(pool.coins, pool.underlying_coins, strict=True):  # noqa: E501
+            if token_address == ETH_SPECIAL_ADDRESS:
+                underlying_token_names.append('ETH')
+                continue
+
+            try:  # ensure underlying token exists
+                get_or_create_evm_token(
+                    userdb=evm_inquirer.database,
+                    evm_address=underlying_token_address,
+                    chain_id=evm_inquirer.chain_id,
+                    evm_inquirer=evm_inquirer,
+                    encounter=encounter,
+                )
+            except NotERC20Conformant as e:
+                log.error(
+                    f'Skipping pool {pool} because {underlying_token_address} is not a '
+                    f'valid ERC20 token. {e}',
+                )
+                continue
+
+            if underlying_token_address == token_address:  # they are the same token. Stop here
+                continue  # can happen for a pool to have token's underlying token as itself
+
+            try:  # and ensure token exists
+                underlying_token_names.append(get_or_create_evm_token(
+                    userdb=evm_inquirer.database,
+                    evm_address=token_address,
+                    chain_id=evm_inquirer.chain_id,
+                    evm_inquirer=evm_inquirer,
+                    underlying_tokens=[UnderlyingToken(
+                        address=underlying_token_address,
+                        token_kind=EvmTokenKind.ERC20,
+                        weight=ONE,
+                    )],
+                    encounter=encounter,
+                ).symbol)
+            except NotERC20Conformant as e:
+                log.error(
+                    f'Skipping pool {pool} because {token_address} is not a '
+                    f'valid ERC20 token. {e}',
+                )
+                continue
path: rotkehlchen/chain/evm/decoding/curve/curve_cache.py
user: login: LefterisJP
body: ```suggestion
                continue

```
created_at: 2025-05-19T19:55:31Z
author_association: MEMBER
diff_hunk: @@ -52,19 +51,22 @@
 if TYPE_CHECKING:
 
     from rotkehlchen.chain.evm.node_inquirer import EvmNodeInquirer
-    from rotkehlchen.db.dbhandler import DBHandler
     from rotkehlchen.user_messages import MessagesAggregator
 
 logger = logging.getLogger(__name__)
 log = RotkehlchenLogsAdapter(logger)
 
 
-class CurvePoolData(NamedTuple):
+@dataclass
+class CurvePoolData:
     pool_address: ChecksumEvmAddress
-    pool_name: str
+    pool_name: str | None
     lp_token_address: ChecksumEvmAddress
     gauge_address: ChecksumEvmAddress | None
+    # Coins in the pool. Has the LP token of the base pool for metapools.
     coins: list[ChecksumEvmAddress]
+    # All underlying coins in a pool. Includes the coins from the base pool for metapools.
+    # Same as `coins` or None for non-metapools.
path: rotkehlchen/chain/evm/decoding/curve/curve_cache.py
user: login: nicholasyoder
body: yeah, that probably would be better. Changed it.
created_at: 2025-05-19T20:58:06Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov[bot]
created_at: 2025-05-17T01:12:24Z
body: ## [Codecov](https://app.codecov.io/gh/rotki/rotki/pull/9970?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rotki) Report
Attention: Patch coverage is `46.61017%` with `63 lines` in your changes missing coverage. Please review.
> Project coverage is 55.30%. Comparing base [(`e4987e9`)](https://app.codecov.io/gh/rotki/rotki/commit/e4987e98c7c220834320207ef9bb133bb08067a5?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rotki) to head [(`0673fc7`)](https://app.codecov.io/gh/rotki/rotki/commit/0673fc78f9fd96849fa71df0ba6b7848c0b11edf?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rotki).
> Report is 3 commits behind head on develop.

| [Files with missing lines](https://app.codecov.io/gh/rotki/rotki/pull/9970?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rotki) | Patch % | Lines |
|---|---|---|
| [...otkehlchen/chain/evm/decoding/curve/curve\_cache.py](https://app.codecov.io/gh/rotki/rotki/pull/9970?src=pr&el=tree&filepath=rotkehlchen%2Fchain%2Fevm%2Fdecoding%2Fcurve%2Fcurve_cache.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rotki#diff-cm90a2VobGNoZW4vY2hhaW4vZXZtL2RlY29kaW5nL2N1cnZlL2N1cnZlX2NhY2hlLnB5) | 46.15% | [48 Missing and 15 partials :warning: ](https://app.codecov.io/gh/rotki/rotki/pull/9970?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rotki) |

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##           develop    #9970      +/-   ##
===========================================
+ Coverage    49.74%   55.30%   +5.56%     
===========================================
  Files         1999     1999              
  Lines       145621   145621              
  Branches     13466    13469       +3     
===========================================
+ Hits         72435    80536    +8101     
+ Misses       70559    62202    -8357     
- Partials      2627     2883     +256     
```

| [Flag](https://app.codecov.io/gh/rotki/rotki/pull/9970/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rotki) | Coverage Δ | |
|---|---|---|
| [backend](https://app.codecov.io/gh/rotki/rotki/pull/9970/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rotki) | `81.06% <46.61%> (+12.62%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rotki#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/rotki/rotki/pull/9970?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rotki).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rotki).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: rotkibot
created_at: 2025-05-19T21:28:51Z
body: rotki/test-caching#166 was successfully merged
author_association: NONE