Repository: rotki/rotki

Issue:
Work with new ENS contracts
## Abstract

ENS upgrades some of their contracts, and thus we should update our code. Related tweet: https://twitter.com/nicksdjohnson/status/1640650011502419971

## Specification

- [ ] One place that will definitely need updating is the ENS decoder https://github.com/rotki/rotki/blob/7d65cb6925c558c962b651d7263ea50506b63553/rotkehlchen/chain/ethereum/modules/ens/decoder.py#L37 . We should support both old an new registar controllers.
- [ ] Check if any other place will need to be touched, but probably decoder is the only place.


Discussion:
Addresses are here. Any changesshould also go in 1.28: https://github.com/ensdomains/ens-contracts/pull/236

Resolving patch:
diff --git a/rotkehlchen/chain/ethereum/modules/ens/decoder.py b/rotkehlchen/chain/ethereum/modules/ens/decoder.py
--- a/rotkehlchen/chain/ethereum/modules/ens/decoder.py
+++ b/rotkehlchen/chain/ethereum/modules/ens/decoder.py
@@ -1,6 +1,6 @@
 import logging
 from multiprocessing.managers import RemoteError
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, Any, Optional
 
 from eth_utils import to_checksum_address
 
@@ -35,17 +35,23 @@
 log = RotkehlchenLogsAdapter(logger)
 
 
-ENS_REGISTRAR_CONTROLLER = string_to_evm_address('0x283Af0B28c62C092C9727F1Ee09c02CA627EB7F5')
+ENS_REGISTRAR_CONTROLLER_1 = string_to_evm_address('0x283Af0B28c62C092C9727F1Ee09c02CA627EB7F5')
+ENS_REGISTRAR_CONTROLLER_2 = string_to_evm_address('0x253553366Da8546fC250F225fe3d25d0C782303b')
 ENS_REGISTRY_WITH_FALLBACK = string_to_evm_address('0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e')
 ENS_PUBLIC_RESOLVER_2_ADDRESS = string_to_evm_address('0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41')
+ENS_PUBLIC_RESOLVER_3_ADDRESS = string_to_evm_address('0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63')
 
-NAME_REGISTERED = b'\xcaj\xbb\xe9\xd7\xf1\x14"\xcbl\xa7b\x9f\xbfo\xe9\xef\xb1\xc6!\xf7\x1c\xe8\xf0+\x9f*#\x00\x97@O'  # noqa: E501
-NAME_REGISTERED_ABI = '{"anonymous":false,"inputs":[{"indexed":false,"internalType":"string","name":"name","type":"string"},{"indexed":true,"internalType":"bytes32","name":"label","type":"bytes32"},{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":false,"internalType":"uint256","name":"cost","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"expires","type":"uint256"}],"name":"NameRegistered","type":"event"}'  # noqa: E501
 NAME_RENEWED = b'=\xa2L\x02E\x82\x93\x1c\xfa\xf8&}\x8e\xd2M\x13\xa8*\x80h\xd5\xbd3}0\xecE\xce\xa4\xe5\x06\xae'  # noqa: E501
 NAME_RENEWED_ABI = '{"anonymous":false,"inputs":[{"indexed":false,"internalType":"string","name":"name","type":"string"},{"indexed":true,"internalType":"bytes32","name":"label","type":"bytes32"},{"indexed":false,"internalType":"uint256","name":"cost","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"expires","type":"uint256"}],"name":"NameRenewed","type":"event"}'  # noqa: E501
 NEW_RESOLVER = b'3W!\xb0\x18f\xdc#\xfb\xee\x8bk,{\x1e\x14\xd6\xf0\\(\xcd5\xa2\xc94#\x9f\x94\tV\x02\xa0'  # noqa: E501
-TEXT_CHANGED = b'\xd8\xc93K\x1a\x9c/\x9d\xa3B\xa0\xa2\xb3&)\xc1\xa2)\xb6D]\xadx\x94\x7fgKDDJuP'
-TEXT_CHANGED_ABI = '{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":true,"internalType":"string","name":"indexedKey","type":"string"},{"indexed":false,"internalType":"string","name":"key","type":"string"}],"name":"TextChanged","type":"event"}'  # noqa: E501
+NAME_REGISTERED_SINGLE_COST = b'\xcaj\xbb\xe9\xd7\xf1\x14"\xcbl\xa7b\x9f\xbfo\xe9\xef\xb1\xc6!\xf7\x1c\xe8\xf0+\x9f*#\x00\x97@O'  # noqa: E501
+NAME_REGISTERED_SINGLE_COST_ABI = '{"anonymous":false,"inputs":[{"indexed":false,"internalType":"string","name":"name","type":"string"},{"indexed":true,"internalType":"bytes32","name":"label","type":"bytes32"},{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":false,"internalType":"uint256","name":"cost","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"expires","type":"uint256"}],"name":"NameRegistered","type":"event"}'  # noqa: E501
+NAME_REGISTERED_BASE_COST_AND_PREMIUM = b"i\xe3\x7f\x15\x1e\xb9\x8a\ta\x8d\xda\xa8\x0c\x8c\xfa\xf1\xceY\x96\x86|H\x9fE\xb5U\xb4\x12'\x1e\xbf'"  # noqa: E501
+NAME_REGISTERED_BASE_COST_AND_PREMIUM_ABI = '{"anonymous":false,"inputs":[{"indexed":false,"internalType":"string","name":"name","type":"string"},{"indexed":true,"internalType":"bytes32","name":"label","type":"bytes32"},{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":false,"internalType":"uint256","name":"baseCost","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"premium","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"expires","type":"uint256"}],"name":"NameRegistered","type":"event"}'  # noqa: E501
+TEXT_CHANGED_KEY_ONLY = b'\xd8\xc93K\x1a\x9c/\x9d\xa3B\xa0\xa2\xb3&)\xc1\xa2)\xb6D]\xadx\x94\x7fgKDDJuP'  # noqa: E501
+TEXT_CHANGED_KEY_ONLY_ABI = '{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":true,"internalType":"string","name":"indexedKey","type":"string"},{"indexed":false,"internalType":"string","name":"key","type":"string"}],"name":"TextChanged","type":"event"}'  # noqa: E501
+TEXT_CHANGED_KEY_AND_VALUE = b'D\x8b\xc0\x14\xf1Sg&\xcf\x8dT\xff=d\x81\xed<\xbch<%\x91\xca Bt\x00\x9a\xfa\t\xb1\xa1'  # noqa: E501
+TEXT_CHANGED_KEY_AND_VALUE_ABI = '{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":true,"internalType":"string","name":"indexedKey","type":"string"},{"indexed":false,"internalType":"string","name":"key","type":"string"},{"indexed":false,"internalType":"string","name":"value","type":"string"}],"name":"TextChanged","type":"event"}'  # noqa: E501
 
 
 class EnsDecoder(DecoderInterface, CustomizableDateMixin):
@@ -61,7 +67,10 @@ def __init__(  # pylint: disable=super-init-not-called
         self.eth = A_ETH.resolve_to_crypto_asset()
 
     def _decode_ens_registrar_event(self, context: DecoderContext) -> DecodingOutput:
-        if context.tx_log.topics[0] == NAME_REGISTERED:
+        if context.tx_log.topics[0] in (
+            NAME_REGISTERED_SINGLE_COST,
+            NAME_REGISTERED_BASE_COST_AND_PREMIUM,
+        ):
             return self._decode_name_registered(context=context)
 
         if context.tx_log.topics[0] == NAME_RENEWED:
@@ -71,19 +80,27 @@ def _decode_ens_registrar_event(self, context: DecoderContext) -> DecodingOutput
 
     def _decode_name_registered(self, context: DecoderContext) -> DecodingOutput:
         try:
-            _, decoded_data = decode_event_data_abi_str(context.tx_log, NAME_REGISTERED_ABI)
+            _, decoded_data = decode_event_data_abi_str(
+                context.tx_log,
+                NAME_REGISTERED_SINGLE_COST_ABI if context.tx_log.topics[0] == NAME_REGISTERED_SINGLE_COST else NAME_REGISTERED_BASE_COST_AND_PREMIUM_ABI,  # noqa: E501
+            )
         except DeserializationError as e:
             log.debug(f'Failed to decode ENS name registered event due to {str(e)}')
             return DEFAULT_DECODING_OUTPUT
 
         name = decoded_data[0]
-        amount = from_wei(decoded_data[1])
-        expires = decoded_data[2]
+        if context.tx_log.topics[0] == NAME_REGISTERED_SINGLE_COST:
+            amount = from_wei(decoded_data[1])
+            expires = decoded_data[2]
+        else:
+            amount = from_wei(decoded_data[1] + decoded_data[2])  # In the new version the amount is baseCost + premium  # noqa: E501
+            expires = decoded_data[3]
 
         refund_from_registrar = None
         to_remove_indices = []
+        new_event = None
         for event_idx, event in enumerate(context.decoded_events):
-            if event.event_type == HistoryEventType.RECEIVE and event.asset == A_ETH and event.address == ENS_REGISTRAR_CONTROLLER:  # noqa: E501
+            if event.event_type == HistoryEventType.RECEIVE and event.asset == A_ETH and event.address == ENS_REGISTRAR_CONTROLLER_1:  # noqa: E501
                 # remove ETH refund event
                 refund_from_registrar = event.balance.amount
                 to_remove_indices.append(event_idx)
@@ -112,13 +129,13 @@ def _decode_name_registered(self, context: DecoderContext) -> DecodingOutput:
         for index in to_remove_indices:
             del context.decoded_events[index]
 
-        return DEFAULT_DECODING_OUTPUT
+        return DecodingOutput(event=new_event)
 
     def _decode_name_renewed(self, context: DecoderContext) -> DecodingOutput:
         try:
             _, decoded_data = decode_event_data_abi_str(context.tx_log, NAME_RENEWED_ABI)
         except DeserializationError as e:
-            log.debug(f'Failed to decode ENS name renewed event due to {str(e)}')
+            log.error(f'Failed to decode ENS name renewed event due to {str(e)}')
             return DEFAULT_DECODING_OUTPUT
 
         name = decoded_data[0]
@@ -170,52 +187,65 @@ def _decode_ens_registry_with_fallback_event(self, context: DecoderContext) -> D
             ))
         return DEFAULT_DECODING_OUTPUT
 
-    def _decode_ens_public_resolver_2_events(self, context: DecoderContext) -> DecodingOutput:
+    def _decode_ens_public_resolver_events(self, context: DecoderContext) -> DecodingOutput:
         """Decode event where a text property (discord, telegram, etc.) is set for an ENS name."""
-        if context.tx_log.topics[0] == TEXT_CHANGED:
-            try:
-                _, decoded_data = decode_event_data_abi_str(context.tx_log, TEXT_CHANGED_ABI)
-                changed_key = decoded_data[0]
-            except (DeserializationError, KeyError) as e:
-                msg = str(e)
-                if isinstance(e, KeyError):
-                    msg = f'missing key {msg}'
-
-                log.debug(f'Failed to decode ENS set-text event due to {msg}')
-                return DEFAULT_DECODING_OUTPUT
+        if context.tx_log.topics[0] not in (TEXT_CHANGED_KEY_ONLY, TEXT_CHANGED_KEY_AND_VALUE):
+            return DEFAULT_DECODING_OUTPUT
 
-            node = context.tx_log.topics[1]
+        try:
+            _, decoded_data = decode_event_data_abi_str(
+                context.tx_log,
+                TEXT_CHANGED_KEY_ONLY_ABI if context.tx_log.topics[0] == TEXT_CHANGED_KEY_ONLY else TEXT_CHANGED_KEY_AND_VALUE_ABI,  # noqa: E501
+            )
+        except DeserializationError as e:
+            log.error(f'Failed to decode ENS set-text event due to {str(e)}')
+            return DEFAULT_DECODING_OUTPUT
+
+        changed_key = decoded_data[0]
+        new_value = decoded_data[1] if context.tx_log.topics[0] == TEXT_CHANGED_KEY_AND_VALUE else None  # noqa: E501
+        node = context.tx_log.topics[1]  # node is a hash of the name used by ens internals
+        contract = self.ethereum.contracts.contract_by_address(address=context.tx_log.address)  # Is either public resolver v2 or v3  # noqa: E501
+
+        name_to_show: Optional[str] = None
+        if contract is None:
+            self.msg_aggregator.add_error(
+                f'Failed to find ENS public resolver contract with address '
+                f'{context.tx_log.address}. This should never happen. Please, '
+                f'open an issue in rotki\'s github repository.',
+            )
+        else:
             try:
-                address = self.ethereum.contracts.contract(string_to_evm_address('0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41')).call(  # noqa: E501
+                address = contract.call(
                     node_inquirer=self.ethereum,
                     method_name='addr',
                     arguments=[node],
                 )
+            except RemoteError as e:
+                log.debug(f'Failed to query ENS name of node {node.hex()} due to: {str(e)}')
+            else:
                 address = to_checksum_address(address)
                 ens_mapping = find_ens_mappings(
                     ethereum_inquirer=self.ethereum,
                     addresses=[address],
                     ignore_cache=False,
                 )
-            except RemoteError as e:
-                log.debug(f'Failed to decode ENS set-text event due to {str(e)}')
-                return DEFAULT_DECODING_OUTPUT
-
-            name_to_show = ens_mapping.get(address, address)
-
-            notes = f'Set ENS {changed_key} attribute for {name_to_show}'
-            context.decoded_events.append(self.base.make_event_from_transaction(
-                transaction=context.transaction,
-                tx_log=context.tx_log,
-                event_type=HistoryEventType.INFORMATIONAL,
-                event_subtype=HistoryEventSubType.NONE,
-                asset=A_ETH,
-                balance=Balance(),
-                location_label=context.transaction.from_address,
-                notes=notes,
-                counterparty=CPT_ENS,
-                address=context.transaction.to_address,
-            ))
+                name_to_show = ens_mapping.get(address, address)
+
+        notes = f'Set ENS {changed_key} {f"to {new_value} " if new_value else ""}attribute'
+        if name_to_show is not None:
+            notes += f' for {name_to_show}'
+        context.decoded_events.append(self.base.make_event_from_transaction(
+            transaction=context.transaction,
+            tx_log=context.tx_log,
+            event_type=HistoryEventType.INFORMATIONAL,
+            event_subtype=HistoryEventSubType.NONE,
+            asset=A_ETH,
+            balance=Balance(),
+            location_label=context.transaction.from_address,
+            notes=notes,
+            counterparty=CPT_ENS,
+            address=context.transaction.to_address,
+        ))
         return DEFAULT_DECODING_OUTPUT
 
     # -- DecoderInterface methods
@@ -236,9 +266,11 @@ def possible_events(self) -> DecoderEventMappingType:
 
     def addresses_to_decoders(self) -> dict[ChecksumEvmAddress, tuple[Any, ...]]:
         return {
-            ENS_REGISTRAR_CONTROLLER: (self._decode_ens_registrar_event,),
+            ENS_REGISTRAR_CONTROLLER_1: (self._decode_ens_registrar_event,),
+            ENS_REGISTRAR_CONTROLLER_2: (self._decode_ens_registrar_event,),
             ENS_REGISTRY_WITH_FALLBACK: (self._decode_ens_registry_with_fallback_event,),
-            ENS_PUBLIC_RESOLVER_2_ADDRESS: (self._decode_ens_public_resolver_2_events,),
+            ENS_PUBLIC_RESOLVER_2_ADDRESS: (self._decode_ens_public_resolver_events,),
+            ENS_PUBLIC_RESOLVER_3_ADDRESS: (self._decode_ens_public_resolver_events,),
         }
 
     def counterparties(self) -> list[CounterpartyDetails]:


Test patch:
diff --git a/rotkehlchen/tests/unit/decoders/test_ens.py b/rotkehlchen/tests/unit/decoders/test_ens.py
--- a/rotkehlchen/tests/unit/decoders/test_ens.py
+++ b/rotkehlchen/tests/unit/decoders/test_ens.py
@@ -18,6 +18,8 @@
     EvmTokenKind,
     EvmTransaction,
     Location,
+    Timestamp,
+    TimestampMS,
     deserialize_evm_tx_hash,
 )
 from rotkehlchen.utils.hexbytes import hexstring_to_bytes
@@ -294,3 +296,92 @@ def test_set_resolver(ethereum_transaction_decoder, ethereum_accounts):
         ),
     ]
     assert events == expected_events
+
+
+@pytest.mark.vcr()
+@pytest.mark.parametrize('ethereum_accounts', [['0xbc2E9Df6281a8e853121dc52dBc8BCc8bBE3ed0e']])
+def test_set_attribute_v2(database, ethereum_inquirer, ethereum_accounts):
+    """Test that setting ens text attribute using public resolver deployed in March 2023 works"""
+    tx_hex = deserialize_evm_tx_hash('0x6b354e4da21cfb06a8eb4cb5b7efd20558ae3be6a7a7c563f318e041fb3bfdd9')  # noqa: E501
+    evmhash = deserialize_evm_tx_hash(tx_hex)
+    user_address = ethereum_accounts[0]
+    events, _ = get_decoded_events_of_transaction(
+        evm_inquirer=ethereum_inquirer,
+        database=database,
+        tx_hash=tx_hex,
+    )
+    expected_events = [
+        EvmEvent(
+            event_identifier=evmhash,
+            sequence_index=0,
+            timestamp=TimestampMS(1681296527000),
+            location=Location.ETHEREUM,
+            event_type=HistoryEventType.SPEND,
+            event_subtype=HistoryEventSubType.FEE,
+            asset=A_ETH,
+            balance=Balance(amount=FVal('0.0013186458834505')),
+            location_label=user_address,
+            notes='Burned 0.0013186458834505 ETH for gas',
+            counterparty=CPT_GAS,
+            address=None,
+        ), EvmEvent(
+            event_identifier=evmhash,
+            sequence_index=192,
+            timestamp=TimestampMS(1681296527000),
+            location=Location.ETHEREUM,
+            event_type=HistoryEventType.INFORMATIONAL,
+            event_subtype=HistoryEventSubType.NONE,
+            asset=A_ETH,
+            balance=Balance(),
+            location_label=user_address,
+            notes='Set ENS url to https://mercury.foundation attribute for alextatarsky.eth',  # noqa: E501
+            counterparty=CPT_ENS,
+            address=string_to_evm_address('0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63'),
+        ),
+    ]
+    assert events == expected_events
+
+
+@pytest.mark.vcr()
+@pytest.mark.parametrize('ethereum_accounts', [['0xA3B9E4b2C18eFB1C767542e8eb9419B840881467']])
+def test_register_v2(database, ethereum_inquirer, ethereum_accounts):
+    """Test that registering an ens name using eth registar deployed in March 2023 works"""
+    tx_hex = deserialize_evm_tx_hash('0x5150f6e1c76b74fa914e06df9e56577cdeec0faea11f9949ff529daeb16b1c76')  # noqa: E501
+    evmhash = deserialize_evm_tx_hash(tx_hex)
+    user_address = ethereum_accounts[0]
+    events, decoder = get_decoded_events_of_transaction(
+        evm_inquirer=ethereum_inquirer,
+        database=database,
+        tx_hash=tx_hex,
+    )
+    expires_timestamp = Timestamp(1712756435)
+    expected_events = [
+        EvmEvent(
+            event_identifier=evmhash,
+            sequence_index=0,
+            timestamp=TimestampMS(1681220435000),
+            location=Location.ETHEREUM,
+            event_type=HistoryEventType.SPEND,
+            event_subtype=HistoryEventSubType.FEE,
+            asset=A_ETH,
+            balance=Balance(amount=FVal('0.00670203024617044')),
+            location_label=user_address,
+            notes='Burned 0.00670203024617044 ETH for gas',
+            counterparty=CPT_GAS,
+            address=None,
+        ), EvmEvent(
+            event_identifier=evmhash,
+            sequence_index=1,
+            timestamp=TimestampMS(1681220435000),
+            location=Location.ETHEREUM,
+            event_type=HistoryEventType.TRADE,
+            event_subtype=HistoryEventSubType.SPEND,
+            asset=A_ETH,
+            balance=Balance(amount=FVal('0.002609751671170445')),
+            location_label=user_address,
+            notes=f'Register ENS name ens2qr.eth for 0.002609751671170445 ETH until {decoder.decoders["Ens"].timestamp_to_date(expires_timestamp)}',  # noqa: E501
+            counterparty=CPT_ENS,
+            address=string_to_evm_address('0x253553366Da8546fC250F225fe3d25d0C782303b'),
+        ),
+    ]
+    assert events == expected_events