Repository: rotki/rotki

Issue:
Provide remote updates for airdrops
## Abstract

At the moment we need to create a release to provide users with new airdrops listing but sometimes we are not fast enough or we can't do a release in time before the claim period expires. Airdrops should be populated with remote updates as we do for other parts of the app like notifications, spam assets...

## Specification

The schema should include at least:

- csv url with the addresses receiving the airdrop
- expiration date of the airdrop
- asset metadata of the airdrop if we need to add a new one



Discussion:

Resolving patch:
diff --git a/rotkehlchen/api/rest.py b/rotkehlchen/api/rest.py
--- a/rotkehlchen/api/rest.py
+++ b/rotkehlchen/api/rest.py
@@ -63,7 +63,7 @@
 )
 from rotkehlchen.chain.accounts import SingleBlockchainAccountData
 from rotkehlchen.chain.bitcoin.xpub import XpubManager
-from rotkehlchen.chain.ethereum.airdrops import AIRDROPS, check_airdrops
+from rotkehlchen.chain.ethereum.airdrops import check_airdrops, fetch_airdrops_metadata
 from rotkehlchen.chain.ethereum.defi.protocols import DEFI_PROTOCOLS
 from rotkehlchen.chain.ethereum.modules.convex.convex_cache import (
     query_convex_data,
@@ -168,7 +168,6 @@
     RemoteError,
     SystemPermissionError,
     TagConstraintError,
-    UnableToDecryptRemoteData,
 )
 from rotkehlchen.errors.price import NoPriceForGivenTimestamp
 from rotkehlchen.errors.serialization import DeserializationError
@@ -2267,7 +2266,7 @@ def get_ethereum_airdrops(self) -> dict[str, Any]:
                 database=self.rotkehlchen.data.db,
                 tolerance_for_amount_check=FVal('0.00000000000001000'),
             )
-        except (RemoteError, UnableToDecryptRemoteData) as e:
+        except RemoteError as e:
             return wrap_in_fail_result(str(e), status_code=HTTPStatus.BAD_GATEWAY)
         except OSError as e:
             return wrap_in_fail_result(str(e), status_code=HTTPStatus.INSUFFICIENT_STORAGE)
@@ -4385,12 +4384,15 @@ def refresh_general_cache(self) -> dict[str, Any]:
     def get_airdrops_metadata(self) -> Response:
         """Returns a list of airdrops metadata"""
         result = []
-        for identifier, airdrop in AIRDROPS.items():
-            result.append({
-                'identifier': identifier,
-                'name': airdrop.name,
-                'icon': airdrop.icon,
-            })
+        try:
+            for identifier, airdrop in fetch_airdrops_metadata(self.rotkehlchen.data.db)[0].items():  # noqa: E501
+                result.append({
+                    'identifier': identifier,
+                    'name': airdrop.name,
+                    'icon': airdrop.icon,
+                })
+        except RemoteError as e:
+            return api_response(wrap_in_fail_result(str(e)), status_code=HTTPStatus.BAD_GATEWAY)
         return api_response(result=_wrap_in_ok_result(result=result))
 
     def get_defi_metadata(self) -> Response:
diff --git a/rotkehlchen/chain/ethereum/airdrops.py b/rotkehlchen/chain/ethereum/airdrops.py
--- a/rotkehlchen/chain/ethereum/airdrops.py
+++ b/rotkehlchen/chain/ethereum/airdrops.py
@@ -1,50 +1,45 @@
 import csv
+import json
 import logging
 from collections import defaultdict
 from collections.abc import Iterator, Sequence
 from json.decoder import JSONDecodeError
 from pathlib import Path
-from typing import Any, NamedTuple
+from typing import Any, Final, NamedTuple
 
 import requests
 
-from rotkehlchen.assets.asset import Asset
+from rotkehlchen.assets.asset import Asset, CryptoAsset
+from rotkehlchen.assets.types import AssetType
+from rotkehlchen.assets.utils import get_or_create_evm_token
 from rotkehlchen.chain.ethereum.utils import token_normalized_value_decimals
 from rotkehlchen.chain.evm.types import string_to_evm_address
 from rotkehlchen.constants import ZERO
-from rotkehlchen.constants.assets import (
-    A_1INCH,
-    A_COMBO,
-    A_CORN,
-    A_CRV,
-    A_CVX,
-    A_DIVA,
-    A_ENS,
-    A_FOX,
-    A_GNOSIS_VCOW,
-    A_GRAIN,
-    A_LDO,
-    A_PSP,
-    A_SDL,
-    A_SHU,
-    A_TORN,
-    A_UNI,
-    A_VCOW,
-)
+from rotkehlchen.constants.timing import HOUR_IN_SECONDS
 from rotkehlchen.db.dbhandler import DBHandler
 from rotkehlchen.db.settings import CachedSettings
-from rotkehlchen.errors.misc import RemoteError, UnableToDecryptRemoteData
+from rotkehlchen.errors.asset import UnknownAsset
+from rotkehlchen.errors.misc import RemoteError
+from rotkehlchen.errors.serialization import DeserializationError
 from rotkehlchen.fval import FVal
+from rotkehlchen.globaldb.cache import (
+    globaldb_get_unique_cache_last_queried_ts_by_key,
+    globaldb_get_unique_cache_value,
+    globaldb_set_unique_cache_value,
+)
+from rotkehlchen.globaldb.handler import GlobalDBHandler
 from rotkehlchen.history.events.structures.types import HistoryEventSubType, HistoryEventType
 from rotkehlchen.logging import RotkehlchenLogsAdapter
-from rotkehlchen.types import ChecksumEvmAddress, FValWithTolerance, Timestamp
-from rotkehlchen.utils.misc import ts_now
+from rotkehlchen.types import CacheType, ChainID, ChecksumEvmAddress, FValWithTolerance, Timestamp
+from rotkehlchen.utils.misc import is_production, ts_now
 from rotkehlchen.utils.serialization import jsonloads_dict, rlk_jsondumps
 
 logger = logging.getLogger(__name__)
 log = RotkehlchenLogsAdapter(logger)
 
-SMALLEST_AIRDROP_SIZE = 20900
+SMALLEST_AIRDROP_SIZE: Final = 20900
+AIRDROPS_REPO_BASE: Final = f'https://raw.githubusercontent.com/rotki/data/{"main" if is_production() else "develop"}'  # noqa: E501
+AIRDROPS_INDEX: Final = f'{AIRDROPS_REPO_BASE}/airdrops/index_v1.json'
 
 
 class Airdrop(NamedTuple):
@@ -54,253 +49,120 @@ class Airdrop(NamedTuple):
     individual airdrops along with their associated data, such as URL, token address,
     website URL, name, and icon filename.
     """
-    csv_url: str
-    asset: Asset
+    csv_path: str
+    asset: CryptoAsset
     url: str
     name: str
     icon: str
     cutoff_time: Timestamp | None = None
 
 
-AIRDROPS: dict[str, Airdrop] = {
-    'uniswap': Airdrop(
-        # is checksummed
-        csv_url='https://gist.githubusercontent.com/LefterisJP/d883cb7187a7c4fcf98c7a62f45568e7/raw/3718c95d572a29b9c3906d7c64726d3bd7524bfd/uniswap.csv',
-        asset=A_UNI,
-        url='https://app.uniswap.org/',
-        name='Uniswap',
-        icon='uniswap.svg',
-    ),
-    '1inch': Airdrop(
-        # is checksummed
-        csv_url='https://gist.githubusercontent.com/LefterisJP/8f41d1511bf354d7e56810188116a410/raw/87d967e86e1435aa3a9ddb97ce20531e4e52dbad/1inch.csv',
-        asset=A_1INCH,
-        url='https://1inch.exchange/',
-        name='1inch',
-        icon='1inch.svg',
-    ),
-    'tornado': Airdrop(
-        # is checksummed
-        csv_url='https://raw.githubusercontent.com/rotki/data/main/airdrops/tornado.csv',
-        asset=A_TORN,
-        url='https://tornado.cash/',
-        name='Tornado Cash',
-        icon='tornado.svg',
-    ),
-    'cornichon': Airdrop(
-        # is checksummed
-        csv_url='https://gist.githubusercontent.com/LefterisJP/5199d8bc6caa3253c343cd5084489088/raw/7e9ca4c4772fc50780bfe9997e1c43525e1b7445/cornichon_airdrop.csv',
-        asset=A_CORN,
-        url='https://cornichon.ape.tax/',
-        name='Cornichon',
-        icon='cornichon.svg',
-    ),
-    'grain': Airdrop(
-        # is checksummed
-        csv_url='https://gist.githubusercontent.com/LefterisJP/08d7a5b28876741b300c944650c89280/raw/987ab4a92d5363fdbe262f639565732bd1fd3921/grain_iou.csv',
-        asset=A_GRAIN,
-        url='https://claim.harvest.finance/',
-        name='Grain',
-        icon='grain.png',
-    ),
-    'furucombo': Airdrop(
-        # is checksummed
-        csv_url='https://gist.githubusercontent.com/LefterisJP/69612e155e8063fd6b3422d4efbf22a3/raw/b9023960ab1c478ee2620c456e208e5124115c19/furucombo_airdrop.csv',
-        asset=A_COMBO,
-        url='https://furucombo.app/',
-        name='Furucombo',
-        icon='furucombo.svg',
-    ),
-    'lido': Airdrop(
-        # is checksummed
-        csv_url='https://gist.githubusercontent.com/LefterisJP/57a8d65280a482fed6f3e2cc00c0e540/raw/e6ebac56c438cc8a882585c5f5bfba64eb57c424/lido_airdrop.csv',
-        asset=A_LDO,
-        url='https://lido.fi/',
-        name='Lido',
-        icon='lido.svg',
-    ),
-    'curve': Airdrop(
-        # is checksummed
-        csv_url='https://gist.githubusercontent.com/LefterisJP/9a37e5342ddb6219a805a82bcd3d63fe/raw/71e89f0e95ea8ef5503fb1ac569447fea63f1ede/curve_airdrop.csv',
-        asset=A_CRV,
-        url='https://www.curve.fi/',
-        name='Curve Finance',
-        icon='curve.png',
-    ),
-    'convex': Airdrop(
-        csv_url='https://gist.githubusercontent.com/LefterisJP/fd0ebccbc645f7de2b142907bd207363/raw/0613689dd5212b81788ed1a108c751b29b2ce93a/convex_airdrop.csv',
-        asset=A_CVX,
-        url='https://www.convexfinance.com/',
-        name='Convex',
-        icon='convex.jpeg',
-    ),
-    'shapeshift': Airdrop(
-        csv_url='https://raw.githubusercontent.com/rotki/data/main/airdrops/shapeshift.csv',
-        asset=A_FOX,
-        url='https://shapeshift.com/shapeshift-decentralize-airdrop',
-        name='ShapeShift',
-        icon='shapeshift.svg',
-    ),
-    'ens': Airdrop(
-        csv_url='https://raw.githubusercontent.com/rotki/data/main/airdrops/ens.csv',
-        asset=A_ENS,
-        url='https://claim.ens.domains/',
-        name='ENS',
-        icon='ens.svg',
-    ),
-    'psp': Airdrop(
-        csv_url='https://raw.githubusercontent.com/rotki/data/main/airdrops/psp.csv',
-        asset=A_PSP,
-        url='https://paraswap.io/',
-        name='ParaSwap',
-        icon='paraswap.svg',
-    ),
-    'sdl': Airdrop(
-        csv_url='https://raw.githubusercontent.com/rotki/data/main/airdrops/saddle_finance.csv',
-        asset=A_SDL,
-        url='https://saddle.exchange/#/',
-        name='SaddleFinance',
-        icon='saddle-finance.svg',
-    ),
-    'cow_mainnet': Airdrop(
-        csv_url='https://raw.githubusercontent.com/rotki/data/main/airdrops/cow_mainnet.csv',
-        asset=A_VCOW,
-        url='https://cowswap.exchange/#/claim',
-        name='COW (ethereum)',
-        icon='cow.svg',
-    ),
-    'cow_gnosis': Airdrop(
-        csv_url='https://raw.githubusercontent.com/rotki/data/main/airdrops/cow_gnosis.csv',
-        asset=A_GNOSIS_VCOW,
-        url='https://cowswap.exchange/#/claim',
-        name='COW (gnosis chain)',
-        icon='cow.svg',
-    ),
-    'diva': Airdrop(
-        csv_url='https://raw.githubusercontent.com/rotki/data/main/airdrops/diva.csv',
-        asset=A_DIVA,
-        url='https://claim.diva.community/',
-        name='DIVA',
-        icon='diva.svg',
-    ),
-    'shutter': Airdrop(
-        csv_url='https://raw.githubusercontent.com/rotki/data/main/airdrops/shutter.csv',
-        asset=A_SHU,
-        url='https://claim.shutter.network/',
-        name='SHU',
-        icon='shutter.png',
-        cutoff_time=Timestamp(1721000000),  # Sun Jul 14 2024 23:33:20 UTC
-    ),
-}
-
-POAP_AIRDROPS = {
-    'aave_v2_pioneers': (
-        # is checksummed
-        'https://gist.githubusercontent.com/LefterisJP/569992ba1536474f97f7c74104e66354/raw/a8c5bfb91c8328f8a9d2b6f853a0a55328458ed7/poap_aave_v2_pioneers.json',
-        'https://poap.delivery/aave-v2-pioneers',
-        'AAVE V2 Pioneers',
-    ),
-    'beacon_chain_first_1024': (
-        # is checksummed
-        'https://gist.githubusercontent.com/LefterisJP/73469098efe0b12965e5752899be00fe/raw/2ee02c22b68b90333359e2f1d24ff5d460dba092/poap_beacon_chain_first_1024.json',
-        'https://poap.delivery/beacon-chain-first-1024',
-        'Beacon Chain First 1024 Depositors and Proposers',
-    ),
-    'beacon_chain_first_32769': (
-        # is checksummed
-        'https://gist.githubusercontent.com/LefterisJP/6302f4e6da6c1488427fbb8b6207222e/raw/1e2e6ebc8c29ba75c2189a5780c132da4ed8530c/poap_beacon_chain_first_32769.json',
-        'https://poap.delivery/beacon-chain-first-32769',
-        'Beacon Chain First 32,769 Block Validators',
-    ),
-    'coingecko_yield_farming': (
-        # is checksummed
-        'https://gist.githubusercontent.com/LefterisJP/58d23332afc6e9fe701ecc80fcc864f6/raw/25ee4153498e9a4c709542d6f541cc9ab76997d8/poap_coingecko_yield_farming.json',
-        'https://poap.delivery/coin-gecko',
-        'Coin Gecko Yield Farming',
-    ),
-    'eth2_genesis': (
-        # is checksummed
-        'https://gist.githubusercontent.com/LefterisJP/7ce2c343de427c9fe6f54dc9bd6d1a0c/raw/e55baebe6657c11c73b6b808cb269fab31c02da8/poap_eth2_genesis.json',
-        'https://poap.delivery/eth2-genesis/',
-        'Beacon Chain Genesis Depositor',
-    ),
-    'half_rekt': (
-        # is checksummed
-        'https://gist.githubusercontent.com/LefterisJP/429e9c9b3948499cfe793cea75a3b0d6/raw/a0a09372d5bf01285490108661a7c223c1a7de8d/poap_half_rekt.json',
-        'https://poap.delivery/half-rekt',
-        'Half Rekt',
-    ),
-    'keep_stakers': (
-        # is checksummed
-        'https://gist.githubusercontent.com/LefterisJP/b794526fb996cb85dfb825ee5f814e4f/raw/69ed0700a9f7432d783148c89872b86f1d0ee3dd/poap_keep_stakers.json',
-        'https://poap.delivery/keep-stakers',
-        'KEEP Network Mainnet Stakers',
-    ),
-    'lumberjackers': (
-        # is checksummed
-        'https://gist.githubusercontent.com/LefterisJP/802359b6825472ee0081580dbe1a1c82/raw/a456fcd1eb1ddedac1cf4b6dd4bbb2d57371f028/poap_lumberjackers.json',
-        'https://poap.delivery/lumberjackers',
-        'False Start Lumberjackers',
-    ),
-    'medalla': (
-        # is checksummed
-        'https://gist.githubusercontent.com/LefterisJP/1a293bf46b388f709df84ff98c5c5cc6/raw/196ccb50451d908d71cca4bc43731d6547b2276b/poap_medalla.json',
-        'https://poap.delivery/medalla',
-        'Medalla Validator',
-    ),
-    'muir_glacier': (
-        # is checksummed
-        'https://gist.githubusercontent.com/LefterisJP/d135745cf9f4f3143555e0f6a8f0d804/raw/d34c93087100168cac0dfd0ab46254b4a82ff0b8/poap_muir_glacier.json',
-        'https://poap.delivery/muir-glacier',
-        'Muir Glacier',
-    ),
-    'proof_of_gucci': (
-        # is checksummed
-        'https://gist.githubusercontent.com/LefterisJP/43b2c4bb73923d7bb3eaf3b329f7f7a1/raw/e5adb753a3a86f0a50bf93137e2c4adc61548293/poap_proof_of_gucci.json',
-        'https://poap.delivery/proof-of-gucci',
-        'YFI - Proof of Gucci',
-    ),
-    'proof_of_gucci_design_competition': (
-        # is checksummed
-        'https://gist.githubusercontent.com/LefterisJP/40c6b2a94d4b8d7f442522b099e5e258/raw/a0851ce92ca2f668e17a97c9df982bc3e12f9bb3/poap_proof_of_gucci_design_competition.json',
-        'https://poap.delivery/proof-of-gucci-design',
-        'YFI - Proof of Gucci Design Competition',
-    ),
-    'resuscitators': (
-        # is checksummed
-        'https://gist.githubusercontent.com/LefterisJP/0ac0216f82f16453b74a40529384a152/raw/f003083090efd834bcee1d0d1fe4e218380aa0cf/poap_resucitators.json',
-        'https://poap.delivery/medalla-resuscitator',
-        'Medalla Resuscitators',
-    ),
-    'yam': (
-        # is checksummed
-        'https://gist.githubusercontent.com/LefterisJP/d676e1f3db8df96928c2501c6be434ac/raw/fa0a603117f6e3e807a49491924aaaa2bc89179a/poap_yam.json',
-        'https://poap.delivery/yam',
-        'Yam Heroes',
-    ),
-    'ycover': (
-        # is checksummed
-        'https://gist.githubusercontent.com/LefterisJP/727c05f3a9cab79059258528595c102e/raw/59ff367bd532f632ce94e69900709155a55de82a/poap_ycover.json',
-        'https://poap.delivery/ycover',
-        'A New Face For yCover',
-    ),
-    'yfi_og': (
-        # is checksummed
-        'https://gist.githubusercontent.com/LefterisJP/58862ec2b398c770d60c24e50e18e50c/raw/3292995f530baedcdacde02526c6b2bd1de0e110/poap_yfi_og.json',
-        'https://poap.delivery/yfi-og',
-        'I Played 4 YFI',
-    ),
-}
-
-
-def get_airdrop_data(name: str, data_dir: Path) -> Iterator[list[str]]:
-    airdrops_dir = data_dir / 'airdrops'
+def _parse_airdrops(database: 'DBHandler', airdrops_data: dict[str, Any]) -> dict[str, Airdrop]:
+    """Parses the airdrops' data from airdrops metadata index. Also, creates the new token if
+    it's not present in the DB.
+
+    May raise - RemoteError if the metadata is invalid.
+    """
+    airdrops: dict[str, Airdrop] = {}
+    for protocol_name, airdrop_data in airdrops_data.items():
+        try:
+            if (new_asset_data := airdrop_data.get('new_asset_data')) is not None:
+                try:
+                    if (chain_id_raw := new_asset_data.get('chain_id')) is not None:
+                        chain_id = ChainID.deserialize(chain_id_raw)
+                    new_asset_type = AssetType.deserialize(new_asset_data['asset_type'])
+                except DeserializationError as e:
+                    log.error(f'Airdrops Index contains an invalid ChainID or AssetType of a token for {protocol_name}. {e!s}')  # noqa: E501
+                    continue
+
+                if new_asset_type == AssetType.EVM_TOKEN:
+                    crypto_asset: CryptoAsset = get_or_create_evm_token(
+                        userdb=database,
+                        evm_address=new_asset_data['address'],
+                        chain_id=chain_id,
+                        decimals=new_asset_data['decimals'],
+                        name=new_asset_data['name'],
+                        symbol=new_asset_data['symbol'],
+                    )
+                elif (asset := Asset(airdrop_data['asset_identifier'])).exists() is True:
+                    crypto_asset = asset.resolve_to_crypto_asset()  # use the local values of user, if it pre-existed  # noqa: E501
+                else:
+                    crypto_asset = CryptoAsset.initialize(
+                        identifier=airdrop_data['asset_identifier'],
+                        asset_type=new_asset_type,
+                        name=new_asset_data['name'],
+                        symbol=new_asset_data['symbol'],
+                        coingecko=new_asset_data.get('coingecko'),
+                        cryptocompare=new_asset_data.get('cryptocompare'),
+                    )
+                    GlobalDBHandler.add_asset(crypto_asset)
+            else:
+                try:
+                    crypto_asset = Asset(airdrop_data['asset_identifier']).resolve_to_crypto_asset()  # noqa: E501
+                except UnknownAsset as e:
+                    log.error(f"Airdrops Index did not provide any data for {airdrop_data['asset_identifier']}. {e!s}")  # noqa: E501
+                    continue
+
+            airdrops[protocol_name] = Airdrop(
+                asset=crypto_asset,
+                csv_path=f"{AIRDROPS_REPO_BASE}/{airdrop_data['csv_path']}",
+                url=airdrop_data['url'],
+                name=airdrop_data['name'],
+                icon=airdrop_data['icon'],
+                cutoff_time=airdrop_data.get('cutoff_time'),
+            )
+        except KeyError as e:
+            log.error(f'Airdrops Index does not contain a valid key for {protocol_name}. {e!s}')
+    return airdrops
+
+
+def fetch_airdrops_metadata(database: 'DBHandler') -> tuple[dict[str, Airdrop], dict[str, list[str]]]:  # noqa: E501
+    """Fetches airdrop metadata from the rotki/data repository. If it's not cached, parses them,
+    and returns them in two parts: a dict of Airdrop instances and a dict of POAP airdrops data.
+
+    May raise - RemoteError if the request fails or metadata is invalid.
+    """
+    airdrops_data = None
+    with GlobalDBHandler().conn.read_ctx() as cursor:
+        airdrops_metadata_cached_at = globaldb_get_unique_cache_last_queried_ts_by_key(
+            cursor=cursor,
+            key_parts=(CacheType.AIRDROPS_METADATA,),
+        )
+        if ts_now() - airdrops_metadata_cached_at < HOUR_IN_SECONDS * 12:
+            airdrops_metadata_cache = globaldb_get_unique_cache_value(
+                cursor=cursor,
+                key_parts=(CacheType.AIRDROPS_METADATA,),
+            )
+            if airdrops_metadata_cache is not None:
+                airdrops_data = jsonloads_dict(airdrops_metadata_cache)  # get cached response
+
+    if airdrops_data is None:  # query fresh data
+        try:
+            airdrops_data = requests.get(url=AIRDROPS_INDEX, timeout=CachedSettings().get_timeout_tuple()).json()  # noqa: E501
+        except requests.exceptions.RequestException as e:
+            raise RemoteError(f'Airdrops Index request failed due to {e!s}') from e
+
+        with GlobalDBHandler().conn.write_ctx() as write_cursor:
+            globaldb_set_unique_cache_value(
+                write_cursor=write_cursor,
+                key_parts=(CacheType.AIRDROPS_METADATA,),
+                value=json.dumps(airdrops_data),
+            )
+
+    return (
+        _parse_airdrops(database=database, airdrops_data=airdrops_data['airdrops']),
+        airdrops_data['poap_airdrops'],
+    )
+
+
+def get_airdrop_data(airdrop_csv_path: str, name: str, parent_data_dir: Path) -> Iterator[list[str]]:  # noqa: E501
+    airdrops_dir = parent_data_dir / 'airdrops'
     airdrops_dir.mkdir(parents=True, exist_ok=True)
     filename = airdrops_dir / f'{name}.csv'
     if not filename.is_file():
         # if not cached, get it from the gist
         try:
-            response = requests.get(url=AIRDROPS[name][0], timeout=(30, 100))  # a large read timeout is necessary because the queried data is quite large  # noqa: E501
+            response = requests.get(url=airdrop_csv_path, timeout=(30, 100))  # a large read timeout is necessary because the queried data is quite large  # noqa: E501
         except requests.exceptions.RequestException as e:
             raise RemoteError(f'Airdrops Gist request failed due to {e!s}') from e
         content = response.text
@@ -314,7 +176,7 @@ def get_airdrop_data(name: str, data_dir: Path) -> Iterator[list[str]]:
                 f.write(content)
         except csv.Error as e:
             log.debug(f'airdrop file {filename} contains invalid data {content}')
-            raise UnableToDecryptRemoteData(
+            raise RemoteError(
                 f'File {filename} contains invalid data. Check logs.',
             ) from e
     # Verify the CSV file
@@ -324,14 +186,14 @@ def get_airdrop_data(name: str, data_dir: Path) -> Iterator[list[str]]:
         yield from iterator
 
 
-def get_poap_airdrop_data(name: str, data_dir: Path) -> dict[str, Any]:
+def get_poap_airdrop_data(poap_airdrop_path: str, name: str, data_dir: Path) -> dict[str, Any]:
     airdrops_dir = data_dir / 'airdrops_poap'
     airdrops_dir.mkdir(parents=True, exist_ok=True)
     filename = airdrops_dir / f'{name}.json'
     if not filename.is_file():
         # if not cached, get it from the gist
         try:
-            request = requests.get(url=POAP_AIRDROPS[name][0], timeout=CachedSettings().get_timeout_tuple())  # noqa: E501
+            request = requests.get(url=f'{AIRDROPS_REPO_BASE}/{poap_airdrop_path}', timeout=CachedSettings().get_timeout_tuple())  # noqa: E501
         except requests.exceptions.RequestException as e:
             raise RemoteError(f'POAP airdrops Gist request failed due to {e!s}') from e
 
@@ -347,9 +209,9 @@ def get_poap_airdrop_data(name: str, data_dir: Path) -> dict[str, Any]:
 
 
 def calculate_claimed_airdrops(
-        airdrop_data: list[tuple[ChecksumEvmAddress, Asset, FValWithTolerance]],
+        airdrop_data: Sequence[tuple[ChecksumEvmAddress, CryptoAsset, FValWithTolerance]],
         database: DBHandler,
-) -> list[tuple[ChecksumEvmAddress, Asset, FVal]]:
+) -> Sequence[tuple[ChecksumEvmAddress, str, FVal]]:
     """Calculates which of the given airdrops have been claimed.
     It does so by checking if there is a claim history event in the database
     that matches the airdrop data (address, asset and amount). It returns a list
@@ -368,7 +230,7 @@ def calculate_claimed_airdrops(
 
     query_part = ' OR '.join(query_parts)
     with database.conn.read_ctx() as cursor:
-        claim_events = cursor.execute(
+        return cursor.execute(
             'SELECT events.location_label, events.asset, events.amount '
             'FROM history_events AS events '
             'WHERE events.type=? AND events.subtype=? '
@@ -376,8 +238,6 @@ def calculate_claimed_airdrops(
             tuple(bindings),
         ).fetchall()
 
-    return [(event[0], event[1], event[2]) for event in claim_events]
-
 
 def check_airdrops(
         addresses: Sequence[ChecksumEvmAddress],
@@ -390,17 +250,19 @@ def check_airdrops(
         - RemoteError if the remote request fails
     """
     found_data: dict[ChecksumEvmAddress, dict] = defaultdict(lambda: defaultdict(dict))
-    data_dir = database.user_data_dir.parent
+    parent_data_dir = database.user_data_dir.parent
     airdrop_tuples = []
     current_time = ts_now()
-    for protocol_name, airdrop_data in AIRDROPS.items():
+    airdrops, poap_airdrops = fetch_airdrops_metadata(database=database)
+
+    for protocol_name, airdrop_data in airdrops.items():
         if airdrop_data.cutoff_time is not None and current_time > airdrop_data.cutoff_time:
             log.debug(f'Skipping {protocol_name} airdrop since it is not claimable after {airdrop_data.cutoff_time}')  # noqa: E501
             continue
 
-        for row in get_airdrop_data(protocol_name, data_dir):
+        for row in get_airdrop_data(airdrop_data.csv_path, protocol_name, parent_data_dir):
             if len(row) < 2:
-                raise UnableToDecryptRemoteData(
+                raise RemoteError(
                     f'Airdrop CSV for {protocol_name} contains an invalid row: {row}',
                 )
             addr, amount, *_ = row
@@ -414,27 +276,25 @@ def check_airdrops(
                 'sdl',
                 'cow_mainnet',
                 'cow_gnosis',
-                'shutter',
             }:
                 amount = token_normalized_value_decimals(int(amount), 18)  # type: ignore
             if addr in addresses:
-                asset = airdrop_data[1]
                 found_data[addr][protocol_name] = {  # type: ignore
                     'amount': str(amount),
-                    'asset': asset,
+                    'asset': airdrop_data.asset,
                     'link': airdrop_data[2],
                     'claimed': False,
                 }
                 airdrop_tuples.append((
                     string_to_evm_address(addr),
-                    asset,
+                    airdrop_data.asset,
                     FValWithTolerance(
                         value=FVal(amount),
                         tolerance=tolerance_for_amount_check,
                     ),
                 ))
 
-    asset_to_protocol = {item[1]: protocol for protocol, item in AIRDROPS.items()}
+    asset_to_protocol = {item[1].identifier: protocol for protocol, item in airdrops.items()}
     claim_events_tuple = calculate_claimed_airdrops(
         airdrop_data=airdrop_tuples,
         database=database,
@@ -442,8 +302,8 @@ def check_airdrops(
     for event_tuple in claim_events_tuple:
         found_data[event_tuple[0]][asset_to_protocol[event_tuple[1]]]['claimed'] = True
 
-    for protocol_name, poap_airdrop_data in POAP_AIRDROPS.items():
-        data_dict = get_poap_airdrop_data(protocol_name, data_dir)
+    for protocol_name, poap_airdrop_data in poap_airdrops.items():
+        data_dict = get_poap_airdrop_data(poap_airdrop_data[0], protocol_name, parent_data_dir)
         for addr, assets in data_dict.items():
             # not doing to_checksum_address() here since the file addresses are checksummed
             # and doing to_checksum_address() so many times hits performance
diff --git a/rotkehlchen/types.py b/rotkehlchen/types.py
--- a/rotkehlchen/types.py
+++ b/rotkehlchen/types.py
@@ -947,6 +947,7 @@ class CacheType(Enum):
     CONVEX_POOL_NAME = auto()  # map convex pool rewards address -> pool name
     SPAM_ASSET_FALSE_POSITIVE = auto()  # assets that shouldn't be marked as spam automatically
     COINLIST = auto()  # coinlist / all coins cache for various oracles
+    AIRDROPS_METADATA = auto()  # airdrops index fetched from rotki/data repo
 
     def serialize(self) -> str:
         # Using custom serialize method instead of SerializableEnumMixin since mixin replaces
@@ -967,6 +968,7 @@ def serialize(self) -> str:
     CacheType.ENS_LABELHASH,
     CacheType.CONVEX_POOL_NAME,
     CacheType.COINLIST,
+    CacheType.AIRDROPS_METADATA,
 ]
 
 UNIQUE_CACHE_KEYS: tuple[UniqueCacheType, ...] = typing.get_args(UniqueCacheType)


Test patch:
diff --git a/rotkehlchen/tests/api/test_metadata.py b/rotkehlchen/tests/api/test_metadata.py
--- a/rotkehlchen/tests/api/test_metadata.py
+++ b/rotkehlchen/tests/api/test_metadata.py
@@ -1,8 +1,9 @@
 from typing import TYPE_CHECKING
+import pytest
 
 import requests
 
-from rotkehlchen.chain.ethereum.airdrops import AIRDROPS
+from rotkehlchen.chain.ethereum.airdrops import fetch_airdrops_metadata
 from rotkehlchen.chain.ethereum.defi.protocols import DEFI_PROTOCOLS
 from rotkehlchen.tests.utils.api import api_url_for, assert_proper_response_with_result
 from rotkehlchen.types import SupportedBlockchain
@@ -11,6 +12,7 @@
     from rotkehlchen.api.server import APIServer
 
 
+@pytest.mark.vcr()
 def test_metadata_endpoint(rotkehlchen_api_server: 'APIServer') -> None:
     """Test that all the endpoints that query mappings or metadata from the backend work fine"""
 
@@ -21,7 +23,9 @@ def test_metadata_endpoint(rotkehlchen_api_server: 'APIServer') -> None:
             'name': airdrop.name,
             'icon': airdrop.icon,
         }
-        for identifier, airdrop in AIRDROPS.items()
+        for identifier, airdrop in fetch_airdrops_metadata(
+            database=rotkehlchen_api_server.rest_api.rotkehlchen.data.db,
+        )[0].items()
     ]
     airdrops_response = requests.get(
         api_url_for(rotkehlchen_api_server, 'airdropsmetadataresource'),
diff --git a/rotkehlchen/tests/fixtures/globaldb.py b/rotkehlchen/tests/fixtures/globaldb.py
--- a/rotkehlchen/tests/fixtures/globaldb.py
+++ b/rotkehlchen/tests/fixtures/globaldb.py
@@ -68,6 +68,12 @@ def fixture_empty_global_addressbook() -> bool:
     return False
 
 
+@pytest.fixture(name='remove_global_assets')
+def fixture_remove_global_assets() -> list[str]:
+    """Asset identifiers to remove from the globalDB"""
+    return []
+
+
 def create_globaldb(
         data_directory,
         sql_vm_instructions_cb,
@@ -93,6 +99,7 @@ def _initialize_fixture_globaldb(
         globaldb_migrations,
         run_globaldb_migrations,
         empty_global_addressbook,
+        remove_global_assets,
 ) -> GlobalDBHandler:
     # clean the previous resolver memory cache, as it
     # may have cached results from a discarded database
@@ -131,6 +138,13 @@ def _initialize_fixture_globaldb(
         with globaldb.conn.write_ctx() as cursor:
             cursor.execute('DELETE FROM address_book')
 
+    if len(remove_global_assets) > 0:
+        with globaldb.conn.write_ctx() as cursor:
+            cursor.executemany(
+                'DELETE FROM assets WHERE identifier=?;',
+                [(asset,) for asset in remove_global_assets],
+            )
+
     return globaldb
 
 
@@ -145,6 +159,7 @@ def fixture_globaldb(
         globaldb_migrations,
         run_globaldb_migrations,
         empty_global_addressbook,
+        remove_global_assets,
 ):
     globaldb = _initialize_fixture_globaldb(
         custom_globaldb=custom_globaldb,
@@ -156,6 +171,7 @@ def fixture_globaldb(
         globaldb_migrations=globaldb_migrations,
         run_globaldb_migrations=run_globaldb_migrations,
         empty_global_addressbook=empty_global_addressbook,
+        remove_global_assets=remove_global_assets,
     )
     yield globaldb
 
diff --git a/rotkehlchen/tests/unit/test_ethereum_airdrops.py b/rotkehlchen/tests/unit/test_ethereum_airdrops.py
--- a/rotkehlchen/tests/unit/test_ethereum_airdrops.py
+++ b/rotkehlchen/tests/unit/test_ethereum_airdrops.py
@@ -4,44 +4,123 @@
 import pytest
 
 from rotkehlchen.accounting.structures.balance import Balance
-from rotkehlchen.chain.ethereum.airdrops import AIRDROPS, POAP_AIRDROPS, Airdrop, check_airdrops
+from rotkehlchen.assets.resolver import AssetResolver
+from rotkehlchen.chain.ethereum.airdrops import AIRDROPS_INDEX, AIRDROPS_REPO_BASE, check_airdrops
 from rotkehlchen.chain.evm.types import string_to_evm_address
 from rotkehlchen.constants.assets import A_1INCH, A_GRAIN, A_SHU, A_UNI
+from rotkehlchen.constants.timing import HOUR_IN_SECONDS
 from rotkehlchen.db.history_events import DBHistoryEvents
-from rotkehlchen.errors.misc import UnableToDecryptRemoteData
+from rotkehlchen.errors.misc import RemoteError
 from rotkehlchen.fval import FVal
+from rotkehlchen.globaldb.cache import globaldb_get_unique_cache_value
+from rotkehlchen.globaldb.handler import GlobalDBHandler
 from rotkehlchen.history.events.structures.evm_event import EvmEvent
 from rotkehlchen.history.events.structures.types import HistoryEventSubType, HistoryEventType
 from rotkehlchen.tests.utils.factories import make_evm_tx_hash
-from rotkehlchen.types import Location, TimestampMS
+from rotkehlchen.types import CacheType, Location, TimestampMS
+from rotkehlchen.utils.serialization import rlk_jsondumps
 
 TEST_ADDR1 = string_to_evm_address('0x2B888954421b424C5D3D9Ce9bB67c9bD47537d12')
 TEST_ADDR2 = string_to_evm_address('0x51985CE8BB9AB1708746b24e22e37CD7A980Ec24')
+TEST_POAP1 = string_to_evm_address('0x043e2a6047e50710e0f5189DBA7623C4A183F871')
 NOT_CSV_WEBPAGE = {
-    'test': Airdrop(
-        csv_url='https://github.com/rotki/yabirgb',
-        asset=A_UNI,
-        url='https://github.com',
-        name='Yabirgb',
-        icon='yabirgb.png',
-        cutoff_time=None,
-    ),
+    'airdrops': {
+        'test': {
+            'csv_path': 'notavalidpath/yabirgb',
+            'asset_identifier': 'eip155:1/erc20:0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984',
+            'url': 'https://github.com',
+            'name': 'Yabirgb',
+            'icon': 'yabirgb.png',
+        },
+    }, 'poap_airdrops': {},
 }
+MOCK_AIRDROP_INDEX = {'airdrops': {
+    'uniswap': {
+        'csv_path': 'airdrops/uniswap.csv',
+        'asset_identifier': 'eip155:1/erc20:0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984',
+        'url': 'https://app.uniswap.org/',
+        'name': 'Uniswap',
+        'icon': 'uniswap.svg',
+    },
+    '1inch': {
+        'csv_path': 'airdrops/1inch.csv',
+        'asset_identifier': 'eip155:1/erc20:0x111111111117dC0aa78b770fA6A738034120C302',
+        'url': 'https://1inch.exchange/',
+        'name': '1inch',
+        'icon': '1inch.svg',
+    },
+    'grain': {
+        'csv_path': 'airdrops/grain_iou.csv',
+        'asset_identifier': 'eip155:1/erc20:0x6589fe1271A0F29346796C6bAf0cdF619e25e58e',
+        'url': 'https://claim.harvest.finance/',
+        'name': 'Grain',
+        'icon': 'grain.png',
+    },
+    'shapeshift': {
+        'csv_path': 'airdrops/shapeshift.csv',
+        'asset_identifier': 'eip155:1/erc20:0xc770EEfAd204B5180dF6a14Ee197D99d808ee52d',
+        'url': 'https://shapeshift.com/shapeshift-decentralize-airdrop',
+        'name': 'ShapeShift',
+        'icon': 'shapeshift.svg',
+    },
+    'cow_gnosis': {
+        'csv_path': 'airdrops/cow_gnosis.csv',
+        'asset_identifier': 'eip155:100/erc20:0xc20C9C13E853fc64d054b73fF21d3636B2d97eaB',
+        'url': 'https://cowswap.exchange/#/claim',
+        'name': 'COW (gnosis chain)',
+        'icon': 'cow.svg',
+    },
+    'diva': {
+        'csv_path': 'airdrops/diva.csv',
+        'asset_identifier': 'eip155:1/erc20:0xBFAbdE619ed5C4311811cF422562709710DB587d',
+        'url': 'https://claim.diva.community/',
+        'name': 'DIVA',
+        'icon': 'diva.svg',
+    },
+    'shutter': {
+        'csv_path': 'airdrops/shutter.csv',
+        'asset_identifier': 'eip155:1/erc20:0xe485E2f1bab389C08721B291f6b59780feC83Fd7',
+        'url': 'https://claim.shutter.network/',
+        'name': 'SHU',
+        'icon': 'shutter.png',
+        'cutoff_time': 1721000000,
+    },
+}, 'poap_airdrops': {
+    'aave_v2_pioneers': [
+        'airdrops/poap/poap_aave_v2_pioneers.json',
+        'https://poap.delivery/aave-v2-pioneers',
+        'AAVE V2 Pioneers',
+    ],
+}}
 
 
-@pytest.fixture(name='mock_airdrop_list')
-def _fixture_airdrop_list(airdrop_list):
-    with patch(
-        'rotkehlchen.chain.ethereum.airdrops.AIRDROPS',
-        new=airdrop_list,
-    ):
-        yield
+def _mock_airdrop_list(url: str, timeout: int = 0):  # pylint: disable=unused-argument
+    mock_response = Mock()
+    if url == AIRDROPS_INDEX:
+        mock_response.json = lambda: NOT_CSV_WEBPAGE
+        return mock_response
+    else:  # when CSV is queried, return invalid payload
+        mock_response.text = mock_response.content = '<>invalid CSV<>'
+        return mock_response
 
 
+@pytest.mark.freeze_time()
 @pytest.mark.parametrize('number_of_eth_accounts', [2])
 @pytest.mark.parametrize('use_clean_caching_directory', [True])
-@pytest.mark.freeze_time()
-def test_check_airdrops(ethereum_accounts, database, freezer):
+@pytest.mark.parametrize('new_asset_data', [{
+    'asset_type': 'EVM_TOKEN',  # test with EVM token
+    'address': '0xe485E2f1bab389C08721B291f6b59780feC83Fd7',
+    'name': 'Shutter',
+    'symbol': 'SHU',
+    'chain_id': 1,
+    'decimals': 18,
+}, {
+    'asset_type': 'SOLANA_TOKEN',  # test with non EVM token
+    'name': 'Some Non EVM Token',
+    'symbol': 'NONEVM',
+}])
+@pytest.mark.parametrize('remove_global_assets', [['eip155:1/erc20:0xe485E2f1bab389C08721B291f6b59780feC83Fd7']])  # noqa: E501
+def test_check_airdrops(freezer, ethereum_accounts, database, new_asset_data):
     # create airdrop claim events to test the claimed attribute
     tolerance_for_amount_check = FVal('0.1')
     claim_events = [
@@ -67,53 +146,73 @@ def test_check_airdrops(ethereum_accounts, database, freezer):
             location_label=string_to_evm_address(TEST_ADDR1),
         ),
     ]
+    MOCK_AIRDROP_INDEX['airdrops']['shutter']['new_asset_data'] = new_asset_data
+
+    new_asset_identifier = MOCK_AIRDROP_INDEX['airdrops']['shutter']['asset_identifier']
+    AssetResolver.assets_cache.clear()  # remove new asset from cache
 
     events_db = DBHistoryEvents(database)
     with database.conn.write_ctx() as write_cursor:
         events_db.add_history_events(write_cursor, claim_events)
 
-    def mock_requests_get(url: str, timeout: int):  # pylint: disable=unused-argument
+    def mock_requests_get(url: str, timeout: int = 0):  # pylint: disable=unused-argument
         """Mocking the airdrop data is very convenient here because the airdrop data is quite large
         and read timeout errors can happen even with 90secs threshold. Vcr-ing it is not possible
-        because the vcr yaml file is above the github limit of 100MB."""
+        because the vcr yaml file is above the github limit of 100MB. The schema of AIRDROPS_INDEX
+        is checked in the rotki/data repo."""
         url_to_data_map = {
-            AIRDROPS['uniswap'][0]:
+            AIRDROPS_INDEX: MOCK_AIRDROP_INDEX,
+            f'{AIRDROPS_REPO_BASE}/airdrops/uniswap.csv':
                 f'address,uni,is_lp,is_user,is_socks\n{TEST_ADDR1},400,False,True,False\n{TEST_ADDR2},400.050642,True,True,False\n',
-            AIRDROPS['1inch'][0]:
+            f'{AIRDROPS_REPO_BASE}/airdrops/1inch.csv':
                 f'address,tokens\n{TEST_ADDR1},630.374421472277638654\n',
-            AIRDROPS['shapeshift'][0]:
+            f'{AIRDROPS_REPO_BASE}/airdrops/shapeshift.csv':
                 f'address,tokens\n{TEST_ADDR1},200\n',
-            AIRDROPS['cow_gnosis'][0]:
+            f'{AIRDROPS_REPO_BASE}/airdrops/cow_gnosis.csv':
                 f'address,tokens\n{TEST_ADDR1},99807039723201809834\n',
-            AIRDROPS['diva'][0]:
+            f'{AIRDROPS_REPO_BASE}/airdrops/diva.csv':
                 f'address,tokens\n{TEST_ADDR1},84000\n',
-            AIRDROPS['grain'][0]:
+            f'{AIRDROPS_REPO_BASE}/airdrops/grain_iou.csv':
                 f'address,tokens\n{TEST_ADDR2},16301717650649890035791\n',
-            AIRDROPS['shutter'][0]:
-                f'address,tokens\n{TEST_ADDR2},394857029384576349787465\n',
+            f'{AIRDROPS_REPO_BASE}/airdrops/shutter.csv':
+                f'address,tokens\n{TEST_ADDR2},394857.029384576349787465\n',
+            f'{AIRDROPS_REPO_BASE}/airdrops/poap/poap_aave_v2_pioneers.json':
+                f'{{"{TEST_POAP1}": [\n566\n]}}',
         }
         mock_response = Mock()
-        mock_response.text = url_to_data_map.get(url, 'address,tokens\n')  # Return the data from the dictionary or just a header if 'url' is not found  # noqa: E501
-        if url in [poap_url[0] for poap_url in POAP_AIRDROPS.values()]:
-            mock_response.text = '{}'  # any valid json response will do because tested addresses do not have poap airdrops  # noqa: E501
-        mock_response.content = mock_response.text.encode('utf-8')
+        if url == AIRDROPS_INDEX:
+            mock_response.json = lambda: url_to_data_map[AIRDROPS_INDEX]
+        else:
+            mock_response.text = url_to_data_map.get(url, 'address,tokens\n')  # Return the data from the dictionary or just a header if 'url' is not found  # noqa: E501
+            assert isinstance(mock_response.text, str)
+            mock_response.content = mock_response.text.encode('utf-8')
         return mock_response
 
-    freezer.move_to(datetime.datetime.fromtimestamp(  # testing just on the cutoff time of shutter
-        AIRDROPS['shutter'].cutoff_time, tz=datetime.UTC),
-    )
+    # testing just on the cutoff time of shutter
+    freezer.move_to(datetime.datetime.fromtimestamp(1721000000, tz=datetime.UTC))
     with (
         patch('rotkehlchen.chain.ethereum.airdrops.SMALLEST_AIRDROP_SIZE', 1),
         patch('rotkehlchen.chain.ethereum.airdrops.requests.get', side_effect=mock_requests_get),
+        patch('rotkehlchen.globaldb.handler.GlobalDBHandler.packaged_db_conn', side_effect=lambda: GlobalDBHandler().conn),  # not using packaged DB to ensure that new tokens are created  # noqa: E501
     ):
+        with GlobalDBHandler().conn.read_ctx() as cursor:
+            assert cursor.execute(
+                'SELECT COUNT(*) FROM assets WHERE identifier=?',
+                (new_asset_identifier,),
+            ).fetchone()[0] == 0  # asset not present before
         data = check_airdrops(
-            addresses=ethereum_accounts + [TEST_ADDR1, TEST_ADDR2],
+            addresses=ethereum_accounts + [TEST_ADDR1, TEST_ADDR2, TEST_POAP1],
             database=database,
             tolerance_for_amount_check=tolerance_for_amount_check,
         )
+        with GlobalDBHandler().conn.read_ctx() as cursor:
+            assert cursor.execute(
+                'SELECT COUNT(*) FROM assets WHERE identifier=?',
+                (new_asset_identifier,),
+            ).fetchone()[0] == 1  # asset present after
 
     # Test data is returned for the address correctly
-    assert len(data) == 2
+    assert len(data) == 3
     assert len(data[TEST_ADDR1]) == 5
     assert data[TEST_ADDR1]['uniswap'] == {
         'amount': '400',
@@ -147,30 +246,57 @@ def mock_requests_get(url: str, timeout: int):  # pylint: disable=unused-argumen
         'link': 'https://claim.shutter.network/',
         'claimed': False,
     }
+    assert len(data[TEST_POAP1]) == 1
+    assert data[TEST_POAP1]['poap'] == [{
+        'event': 'aave_v2_pioneers',
+        'assets': [566],
+        'link': 'https://poap.delivery/aave-v2-pioneers',
+        'name': 'AAVE V2 Pioneers',
+    }]
 
-    freezer.move_to(datetime.datetime.fromtimestamp(  # after cutoff time of shutter
-        AIRDROPS['shutter'].cutoff_time + 1, tz=datetime.UTC),
-    )
+    # after cutoff time of shutter
+    freezer.move_to(datetime.datetime.fromtimestamp(1721000001, tz=datetime.UTC))
     with (
         patch('rotkehlchen.chain.ethereum.airdrops.SMALLEST_AIRDROP_SIZE', 1),
-        patch('rotkehlchen.chain.ethereum.airdrops.requests.get', side_effect=mock_requests_get),
+        patch('rotkehlchen.chain.ethereum.airdrops.requests.get') as mock_get,
     ):
         data = check_airdrops(
             addresses=[TEST_ADDR2],
             database=database,
             tolerance_for_amount_check=tolerance_for_amount_check,
         )
+        assert mock_get.call_count == 0  # not queried again because it was cached a second ago
     assert len(data[TEST_ADDR2]) == 2
     assert 'shutter' not in data[TEST_ADDR2]
 
-    # Test cache files are created
-    for protocol_name in AIRDROPS:
+    freezer.move_to(datetime.datetime.fromtimestamp(1721000001 + 12 * HOUR_IN_SECONDS, tz=datetime.UTC))  # noqa: E501
+    with (
+        patch('rotkehlchen.chain.ethereum.airdrops.SMALLEST_AIRDROP_SIZE', 1),
+        patch('rotkehlchen.chain.ethereum.airdrops.requests.get', side_effect=mock_requests_get) as mock_get,  # noqa: E501
+    ):
+        data = check_airdrops(
+            addresses=[TEST_ADDR2],
+            database=database,
+            tolerance_for_amount_check=tolerance_for_amount_check,
+        )
+        assert mock_get.call_count == 1  # queried again because it was cached 12 hours ago
+
+    # Test cache file and row is created
+    for protocol_name in MOCK_AIRDROP_INDEX['airdrops']:
         assert (database.user_data_dir.parent / 'airdrops' / f'{protocol_name}.csv').is_file()
+    with GlobalDBHandler().conn.read_ctx() as cursor:
+        assert globaldb_get_unique_cache_value(
+            cursor=cursor,
+            key_parts=(CacheType.AIRDROPS_METADATA,),
+        ) == rlk_jsondumps(MOCK_AIRDROP_INDEX)
 
 
-@pytest.mark.parametrize('airdrop_list', [NOT_CSV_WEBPAGE])
-def test_airdrop_fail(mock_airdrop_list, database):  # pylint: disable=unused-argument
-    with pytest.raises(UnableToDecryptRemoteData):
+@pytest.mark.parametrize('use_clean_caching_directory', [True])
+def test_airdrop_fail(database):
+    with (
+        patch('rotkehlchen.chain.ethereum.airdrops.requests.get', side_effect=_mock_airdrop_list),
+        pytest.raises(RemoteError),
+    ):
         check_airdrops(
             addresses=[TEST_ADDR1],
             database=database,