Skip to content

Commit

Permalink
Merge pull request #7933 from LefterisJP/eigenlayer_airdrop
Browse files Browse the repository at this point in the history
Decode eigenlayer airdrop claim event
  • Loading branch information
yabirgb committed May 14, 2024
2 parents e88c530 + a6d69c4 commit 9b46244
Show file tree
Hide file tree
Showing 9 changed files with 172 additions and 26 deletions.
1 change: 1 addition & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ Changelog
=========

* :feature:`7570` Users can choose whether to automatically force-push when a time discrepancy warning occurs during automatic database sync.
* :feature:`-` The eigenlayer airdrop claim event should now be properly decoded in the history events view.

* :release:`1.33.0 <2024-05-08>`
* :feature:`7798` rotki now accurately decodes transactions on the Kyber swap aggregator across all supported chains.
Expand Down
2 changes: 2 additions & 0 deletions rotkehlchen/chain/ethereum/modules/eigenlayer/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@

CPT_EIGENLAYER: Final = 'eigenlayer'
EIGENLAYER_STRATEGY_MANAGER: Final = string_to_evm_address('0x858646372CC42E1A627fcE94aa7A7033e7CF075A') # noqa: E501
EIGENLAYER_AIRDROP_DISTRIBUTOR: Final = string_to_evm_address('0x035bdAeaB85E47710C27EdA7FD754bA80aD4ad02') # noqa: E501
DEPOSIT_TOPIC: Final = b'|\xff\xf9\x08\xa4\xb5\x83\xf3d0\xb2]u\x96LE\x8d\x8e\xde\x8a\x99\xbda\xbeu\x0e\x97\xee\x1b/:\x96' # noqa: E501
WITHDRAWAL_COMPLETE_TOPIC: Final = b"\xe7\xeb\x0c\xa1\x1b\x83tN\xce=x\xe9\xbe\x01\xb9\x13B_\xba\xe7\x0c2\xce'rm\x0e\xcd\xe9.\xf8\xd2" # noqa: E501
EIGENLAYER_CPT_DETAILS: Final = CounterpartyDetails(
identifier=CPT_EIGENLAYER,
label='EigenLayer',
image='eigenlayer.png',
)
EIGEN_TOKEN_ID: Final = 'eip155:1/erc20:0xec53bF9167f50cDEB3Ae105f56099aaaB9061F83'
35 changes: 31 additions & 4 deletions rotkehlchen/chain/ethereum/modules/eigenlayer/decoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
from rotkehlchen.chain.ethereum.modules.eigenlayer.constants import (
CPT_EIGENLAYER,
DEPOSIT_TOPIC,
EIGEN_TOKEN_ID,
EIGENLAYER_AIRDROP_DISTRIBUTOR,
EIGENLAYER_CPT_DETAILS,
EIGENLAYER_STRATEGY_MANAGER,
WITHDRAWAL_COMPLETE_TOPIC,
)
from rotkehlchen.chain.evm.decoding.interfaces import DecoderInterface
from rotkehlchen.chain.evm.decoding.clique.decoder import CliqueAirdropDecoderInterface
from rotkehlchen.chain.evm.decoding.structures import (
DEFAULT_DECODING_OUTPUT,
DecoderContext,
Expand All @@ -22,12 +24,11 @@
from rotkehlchen.types import ChecksumEvmAddress
from rotkehlchen.utils.misc import hex_or_bytes_to_address


logger = logging.getLogger(__name__)
log = RotkehlchenLogsAdapter(logger)


class EigenlayerDecoder(DecoderInterface):
class EigenlayerDecoder(CliqueAirdropDecoderInterface):

def _decode_deposit(self, context: DecoderContext) -> DecodingOutput:
depositor = hex_or_bytes_to_address(context.tx_log.data[0:32])
Expand Down Expand Up @@ -96,10 +97,36 @@ def decode_event(self, context: DecoderContext) -> DecodingOutput:

return DEFAULT_DECODING_OUTPUT

def decode_airdrop(self, context: DecoderContext) -> DecodingOutput:
if not (decode_result := self._decode_claim(context)):
return DEFAULT_DECODING_OUTPUT

claiming_address, claimed_amount = decode_result
notes = f'Claim {claimed_amount} EIGEN from the Eigenlayer airdrop'
for event in context.decoded_events:
if (
event.event_type == HistoryEventType.RECEIVE and
event.location_label == claiming_address and
event.asset.identifier == EIGEN_TOKEN_ID and
event.balance.amount == claimed_amount
):
event.event_type = HistoryEventType.RECEIVE
event.event_subtype = HistoryEventSubType.AIRDROP
event.counterparty = CPT_EIGENLAYER
event.notes = notes
break
else:
log.error(f'Could not match eigenlayer airdrop receive event in {context.transaction.tx_hash.hex()}') # noqa: E501

return DEFAULT_DECODING_OUTPUT

# -- DecoderInterface methods

def addresses_to_decoders(self) -> dict[ChecksumEvmAddress, tuple[Any, ...]]:
return {EIGENLAYER_STRATEGY_MANAGER: (self.decode_event,)}
return {
EIGENLAYER_STRATEGY_MANAGER: (self.decode_event,),
EIGENLAYER_AIRDROP_DISTRIBUTOR: (self.decode_airdrop,),
}

@staticmethod
def counterparties() -> tuple[CounterpartyDetails, ...]:
Expand Down
1 change: 0 additions & 1 deletion rotkehlchen/chain/ethereum/modules/omni/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from rotkehlchen.chain.evm.types import string_to_evm_address

CLAIMED: Final = b'\xd8\x13\x8f\x8a?7|RY\xcaT\x8ep\xe4\xc2\xde\x94\xf1)\xf5\xa1\x106\xa1[iQ<\xba+Bj' # noqa: E501
OMNI_AIDROP_CONTRACT: Final = string_to_evm_address('0xD0c155595929FD6bE034c3848C00DAeBC6D330F6')
OMNI_STAKING_CONTRACT: Final = string_to_evm_address('0xD2639676dA3dEA5491d27DA19340556b3a7d58B8')
OMNI_TOKEN_ID: Final = 'eip155:1/erc20:0x36E66fbBce51e4cD5bd3C62B637Eb411b18949D4'
Expand Down
24 changes: 6 additions & 18 deletions rotkehlchen/chain/ethereum/modules/omni/decoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
from rotkehlchen.accounting.structures.balance import Balance
from rotkehlchen.assets.asset import Asset
from rotkehlchen.chain.ethereum.utils import token_normalized_value_decimals
from rotkehlchen.chain.evm.decoding.clique.decoder import CliqueAirdropDecoderInterface
from rotkehlchen.chain.evm.decoding.constants import STAKED
from rotkehlchen.chain.evm.decoding.interfaces import DecoderInterface
from rotkehlchen.chain.evm.decoding.structures import (
DEFAULT_DECODING_OUTPUT,
DecoderContext,
Expand All @@ -20,32 +20,20 @@
from rotkehlchen.types import ChecksumEvmAddress
from rotkehlchen.utils.misc import hex_or_bytes_to_address, hex_or_bytes_to_int

from .constants import (
CLAIMED,
CPT_OMNI,
OMNI_AIDROP_CONTRACT,
OMNI_STAKING_CONTRACT,
OMNI_TOKEN_ID,
)
from .constants import CPT_OMNI, OMNI_AIDROP_CONTRACT, OMNI_STAKING_CONTRACT, OMNI_TOKEN_ID

logger = logging.getLogger(__name__)
log = RotkehlchenLogsAdapter(logger)


class OmniDecoder(DecoderInterface):
class OmniDecoder(CliqueAirdropDecoderInterface):

def _decode_omni_claim(self, context: DecoderContext) -> DecodingOutput:
if context.tx_log.topics[0] != CLAIMED:
return DEFAULT_DECODING_OUTPUT

if not self.base.is_tracked(claiming_address := hex_or_bytes_to_address(context.tx_log.topics[1])): # noqa: E501
transfer_found = False
if not (decode_result := self._decode_claim(context)):
return DEFAULT_DECODING_OUTPUT

claimed_amount = token_normalized_value_decimals(
token_amount=hex_or_bytes_to_int(context.tx_log.data),
token_decimals=18, # omni has 18 decimals
)
transfer_found = False
claiming_address, claimed_amount = decode_result
notes = f'Claim {claimed_amount} OMNI from the Omni genesis airdrop'
for event in context.decoded_events:
if (
Expand Down
Empty file.
3 changes: 3 additions & 0 deletions rotkehlchen/chain/evm/decoding/clique/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from typing import Final

CLIQUE_CLAIMED: Final = b'\xd8\x13\x8f\x8a?7|RY\xcaT\x8ep\xe4\xc2\xde\x94\xf1)\xf5\xa1\x106\xa1[iQ<\xba+Bj' # noqa: E501
27 changes: 27 additions & 0 deletions rotkehlchen/chain/evm/decoding/clique/decoder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from abc import ABC

from rotkehlchen.chain.ethereum.utils import token_normalized_value_decimals
from rotkehlchen.chain.evm.decoding.clique.constants import CLIQUE_CLAIMED
from rotkehlchen.chain.evm.decoding.interfaces import DecoderInterface
from rotkehlchen.chain.evm.decoding.structures import DecoderContext
from rotkehlchen.fval import FVal
from rotkehlchen.types import ChecksumEvmAddress
from rotkehlchen.utils.misc import hex_or_bytes_to_address, hex_or_bytes_to_int


class CliqueAirdropDecoderInterface(DecoderInterface, ABC):
"""Decoders of protocols using clique airdrop claim"""

def _decode_claim(self, context: DecoderContext) -> tuple[ChecksumEvmAddress, FVal] | None:
"""Just decode the claim part of a clique claim and return the amount and address"""
if context.tx_log.topics[0] != CLIQUE_CLAIMED:
return None

if not self.base.is_tracked(claiming_address := hex_or_bytes_to_address(context.tx_log.topics[1])): # noqa: E501
return None

claimed_amount = token_normalized_value_decimals(
token_amount=hex_or_bytes_to_int(context.tx_log.data),
token_decimals=18, # both omni and eigen have 18 decimals
)
return claiming_address, claimed_amount
105 changes: 102 additions & 3 deletions rotkehlchen/tests/unit/decoders/test_eigenlayer.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import pytest

from rotkehlchen.accounting.structures.balance import Balance
from rotkehlchen.assets.asset import Asset
from rotkehlchen.chain.ethereum.modules.eigenlayer.constants import CPT_EIGENLAYER
from rotkehlchen.chain.ethereum.modules.eigenlayer.constants import (
CPT_EIGENLAYER,
EIGEN_TOKEN_ID,
EIGENLAYER_AIRDROP_DISTRIBUTOR,
EIGENLAYER_STRATEGY_MANAGER,
)
from rotkehlchen.chain.evm.decoding.constants import CPT_GAS
from rotkehlchen.chain.evm.types import string_to_evm_address
from rotkehlchen.constants.assets import A_ETH
Expand Down Expand Up @@ -48,8 +54,8 @@ def test_deposit_token(database, ethereum_inquirer, ethereum_accounts):
asset=a_sweth,
balance=Balance(amount=ZERO),
location_label=ethereum_accounts[0],
notes=f'Revoke swETH spending approval of {ethereum_accounts[0]} by 0x858646372CC42E1A627fcE94aa7A7033e7CF075A', # noqa: E501
address=string_to_evm_address('0x858646372CC42E1A627fcE94aa7A7033e7CF075A'),
notes=f'Revoke swETH spending approval of {ethereum_accounts[0]} by {EIGENLAYER_STRATEGY_MANAGER}', # noqa: E501
address=EIGENLAYER_STRATEGY_MANAGER,
), EvmEvent(
tx_hash=tx_hash,
sequence_index=113,
Expand Down Expand Up @@ -109,3 +115,96 @@ def test_withdraw(database, ethereum_inquirer, ethereum_accounts):
address=strategy_addr,
)]
assert events == expected_events


@pytest.mark.vcr(filter_query_parameters=['apikey'])
@pytest.mark.parametrize('ethereum_accounts', [['0xabe8430e3f0BeCa32915dA84E530f81A01379953']])
def test_airdrop_claim(database, ethereum_inquirer, ethereum_accounts):
tx_hash = deserialize_evm_tx_hash('0x0a72c7bf0fe1808035f8df466a70453f29c6b57d0bec46913d993a19ef72265c') # noqa: E501
events, _ = get_decoded_events_of_transaction(
evm_inquirer=ethereum_inquirer,
database=database,
tx_hash=tx_hash,
)
timestamp, gas_amount, claim_amount = TimestampMS(1715680739000), '0.00074757962836592', '110'
expected_events = [EvmEvent(
tx_hash=tx_hash,
sequence_index=0,
timestamp=timestamp,
location=Location.ETHEREUM,
event_type=HistoryEventType.SPEND,
event_subtype=HistoryEventSubType.FEE,
asset=A_ETH,
balance=Balance(amount=FVal(gas_amount)),
location_label=ethereum_accounts[0],
notes=f'Burned {gas_amount} ETH for gas',
counterparty=CPT_GAS,
), EvmEvent(
tx_hash=tx_hash,
sequence_index=253,
timestamp=timestamp,
location=Location.ETHEREUM,
event_type=HistoryEventType.RECEIVE,
event_subtype=HistoryEventSubType.AIRDROP,
asset=Asset(EIGEN_TOKEN_ID),
balance=Balance(amount=FVal(claim_amount)),
location_label=ethereum_accounts[0],
notes='Claim 110 EIGEN from the Eigenlayer airdrop',
counterparty=CPT_EIGENLAYER,
address=EIGENLAYER_AIRDROP_DISTRIBUTOR,
)]
assert events == expected_events


@pytest.mark.vcr(filter_query_parameters=['apikey'])
@pytest.mark.parametrize('ethereum_accounts', [['0x65151A6343b16c38286f31fcC93e20246629cF8c']])
def test_stake_eigen(database, ethereum_inquirer, ethereum_accounts):
tx_hash = deserialize_evm_tx_hash('0x4da63226965a8b0584f137efa934106cd0cb7a15b536d6f659945cfd4c260b4e') # noqa: E501
events, _ = get_decoded_events_of_transaction(
evm_inquirer=ethereum_inquirer,
database=database,
tx_hash=tx_hash,
)
timestamp, gas_amount, staked_amount = TimestampMS(1715684387000), '0.00141296787957222', '110'
a_eigen, strategy_addr = Asset(EIGEN_TOKEN_ID), '0xaCB55C530Acdb2849e6d4f36992Cd8c9D50ED8F7'
expected_events = [EvmEvent(
tx_hash=tx_hash,
sequence_index=0,
timestamp=timestamp,
location=Location.ETHEREUM,
event_type=HistoryEventType.SPEND,
event_subtype=HistoryEventSubType.FEE,
asset=A_ETH,
balance=Balance(amount=FVal(gas_amount)),
location_label=ethereum_accounts[0],
notes=f'Burned {gas_amount} ETH for gas',
counterparty=CPT_GAS,
), EvmEvent(
tx_hash=tx_hash,
sequence_index=206,
timestamp=timestamp,
location=Location.ETHEREUM,
event_type=HistoryEventType.INFORMATIONAL,
event_subtype=HistoryEventSubType.APPROVE,
asset=a_eigen,
balance=Balance(amount=ZERO),
location_label=ethereum_accounts[0],
notes=f'Revoke EIGEN spending approval of {ethereum_accounts[0]} by {EIGENLAYER_STRATEGY_MANAGER}', # noqa: E501
address=EIGENLAYER_STRATEGY_MANAGER,
), EvmEvent(
tx_hash=tx_hash,
sequence_index=207,
timestamp=timestamp,
location=Location.ETHEREUM,
event_type=HistoryEventType.STAKING,
event_subtype=HistoryEventSubType.DEPOSIT_ASSET,
asset=a_eigen,
balance=Balance(amount=FVal(staked_amount)),
location_label=ethereum_accounts[0],
notes=f'Deposit {staked_amount} EIGEN in EigenLayer',
counterparty=CPT_EIGENLAYER,
extra_data={'strategy': strategy_addr},
product=EvmProduct.STAKING,
address=strategy_addr,
)]
assert events == expected_events

0 comments on commit 9b46244

Please sign in to comment.