Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for decoding Lido contracts events on ethereum network - Part 1 - Submit topic #7949

Merged
merged 3 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
11 changes: 11 additions & 0 deletions rotkehlchen/chain/ethereum/modules/lido/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from typing import Final

from rotkehlchen.fval import FVal

CPT_LIDO: Final = 'lido'

LIDO_STETH_SUBMITTED: Final = b'\x96\xa2\\\x8c\xe0\xba\xab\xc1\xfd\xef\xd9>\x9e\xd2]\x8e\t*32\xf3\xaa\x9aAr+V\x97#\x1d\x1d\x1a' # noqa: E501
LIDO_STETH_TRANSFER_SHARES: Final = b'\x9d\x9c\x90\x92\x96\xd9\xc6tE\x1c\x0c$\xf0,\xb6I\x81\xeb;r\x7f\x99\x86Y9\x19/\x88\nu]\xcb' # noqa: E501

# https://github.com/lidofinance/lido-dao/issues/442 . TODO: find constant we should use in the project. Temp 8 # noqa: E501
STETH_MAX_ROUND_ERROR_WEI: Final = FVal(8)
120 changes: 120 additions & 0 deletions rotkehlchen/chain/ethereum/modules/lido/decoder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import logging
from typing import TYPE_CHECKING, Any

from rotkehlchen.chain.ethereum.utils import token_normalized_value_decimals
from rotkehlchen.chain.evm.decoding.interfaces import DecoderInterface
from rotkehlchen.chain.evm.decoding.structures import (
DEFAULT_DECODING_OUTPUT,
ActionItem,
DecoderContext,
DecodingOutput,
)
from rotkehlchen.chain.evm.decoding.types import CounterpartyDetails
from rotkehlchen.constants.assets import A_ETH, A_STETH
from rotkehlchen.history.events.structures.types import HistoryEventSubType, HistoryEventType
from rotkehlchen.logging import RotkehlchenLogsAdapter
from rotkehlchen.types import ChecksumEvmAddress
from rotkehlchen.utils.misc import from_wei, hex_or_bytes_to_address, hex_or_bytes_to_int

from .constants import CPT_LIDO, LIDO_STETH_SUBMITTED, STETH_MAX_ROUND_ERROR_WEI

if TYPE_CHECKING:
from rotkehlchen.chain.evm.decoding.base import BaseDecoderTools
from rotkehlchen.chain.evm.node_inquirer import EvmNodeInquirer
from rotkehlchen.user_messages import MessagesAggregator

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


class LidoDecoder(DecoderInterface):

def __init__(
self,
evm_inquirer: 'EvmNodeInquirer',
base_tools: 'BaseDecoderTools',
msg_aggregator: 'MessagesAggregator',
) -> None:
super().__init__(
evm_inquirer=evm_inquirer,
base_tools=base_tools,
msg_aggregator=msg_aggregator,
)
self.steth_evm_address = A_STETH.resolve_to_evm_token().evm_address

gianluca-pub-dev marked this conversation as resolved.
Show resolved Hide resolved
def _decode_lido_staking_in_steth(self, context: DecoderContext, sender: ChecksumEvmAddress) -> DecodingOutput: # noqa: E501
"""Decode the submit of eth to lido contract for obtaining steth in return"""
amount_raw = hex_or_bytes_to_int(context.tx_log.data[:32])
collateral_amount = token_normalized_value_decimals(
token_amount=amount_raw,
token_decimals=18,
)

# Searching for the already decoded event,
# containing the ETH transfer of the submit transaction
paired_event = None
action_from_event_type = None
for event in context.decoded_events:
if (
event.address == self.steth_evm_address and
event.asset == A_ETH and
event.balance.amount == collateral_amount and
event.event_type == HistoryEventType.SPEND and
event.location_label == sender
):
event.event_type = HistoryEventType.DEPOSIT
event.event_subtype = HistoryEventSubType.DEPOSIT_ASSET
event.notes = f'Submit {collateral_amount} ETH to Lido'
event.counterparty = CPT_LIDO
# preparing next action to be processed when erc20 transfer will be decoded
# by rotki needed because submit event is emitted prior to the erc20 transfer
paired_event = event
action_from_event_type = HistoryEventType.RECEIVE
action_to_event_subtype = HistoryEventSubType.RECEIVE_WRAPPED
action_to_notes = 'Receive {amount} stETH in exchange for the deposited ETH' # {amount} to be set by transform ActionItem # noqa: E501
break
gianluca-pub-dev marked this conversation as resolved.
Show resolved Hide resolved
else: # did not find anything
log.error(
f'At lido steth submit decoding of tx {context.transaction.tx_hash.hex()}'
f' did not find the related ETH transfer',
)
return DEFAULT_DECODING_OUTPUT

action_items = [] # also create an action item for the reception of the stETH tokens
if paired_event is not None and action_from_event_type is not None:
action_items.append(ActionItem(
action='transform',
from_event_type=action_from_event_type,
from_event_subtype=HistoryEventSubType.NONE,
asset=A_STETH,
amount=collateral_amount,
amount_error_tolerance=from_wei(STETH_MAX_ROUND_ERROR_WEI), # passing the maximum rounding error for finding the related stETH transfer # noqa: E501
to_event_subtype=action_to_event_subtype,
to_notes=action_to_notes,
to_counterparty=CPT_LIDO,
paired_event_data=(paired_event, True),
extra_data={'staked_eth': str(collateral_amount)},
))

return DecodingOutput(action_items=action_items, matched_counterparty=CPT_LIDO)

def _decode_lido_eth_staking_contract(self, context: DecoderContext) -> DecodingOutput:
"""Decode interactions with stETH ans wstETH contracts"""
if (
context.tx_log.topics[0] == LIDO_STETH_SUBMITTED and
self.base.is_tracked(sender := hex_or_bytes_to_address(context.tx_log.topics[1]))
):
return self._decode_lido_staking_in_steth(context=context, sender=sender)
else:
return DEFAULT_DECODING_OUTPUT

# -- DecoderInterface methods

def addresses_to_decoders(self) -> dict[ChecksumEvmAddress, tuple[Any, ...]]:
return {
self.steth_evm_address: (self._decode_lido_eth_staking_contract,),
}

@staticmethod
def counterparties() -> tuple[CounterpartyDetails, ...]:
return (CounterpartyDetails(identifier=CPT_LIDO, label='Lido eth', image='lido.svg'),)
11 changes: 9 additions & 2 deletions rotkehlchen/chain/evm/decoding/decoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -903,7 +903,14 @@ def _maybe_decode_erc20_721_transfer(
action_item.asset == found_token and
action_item.from_event_type == transfer.event_type and
action_item.from_event_subtype == transfer.event_subtype and
(action_item.amount is None or action_item.amount == transfer.balance.amount) and # noqa: E501
(
(action_item.amount is None or action_item.amount == transfer.balance.amount) or # noqa: E501
(
action_item.amount_error_tolerance is not None and
action_item.amount is not None and
abs(action_item.amount - transfer.balance.amount) < action_item.amount_error_tolerance # noqa: E501
)
) and
(action_item.location_label is None or action_item.location_label == transfer.location_label) # noqa: E501
):
if action_item.action == 'skip':
Expand All @@ -920,7 +927,7 @@ def _maybe_decode_erc20_721_transfer(
if action_item.to_event_subtype is not None:
transfer.event_subtype = action_item.to_event_subtype
if action_item.to_notes is not None:
transfer.notes = action_item.to_notes
transfer.notes = action_item.to_notes if action_item.amount_error_tolerance is None else action_item.to_notes.format(amount=transfer.balance.amount) # noqa: E501
if action_item.to_counterparty is not None:
transfer.counterparty = action_item.to_counterparty
if action_item.extra_data is not None:
Expand Down
4 changes: 4 additions & 0 deletions rotkehlchen/chain/evm/decoding/structures.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ class ActionItem:
# Optional event data that pairs it with the event of the action item
# Contains a tuple with the paired event and whether it's an out event (True) or in event
paired_event_data: tuple['EvmEvent', bool] | None = None
# Error tolerance that can be used for protocols having rounding errors. Such as with stETH (https://github.com/lidofinance/lido-dao/issues/442)
# In those cases the notes should also be formatted to have an amount as format string since at
# action item matching this format will populate the note with the actual amount
amount_error_tolerance: Optional['FVal'] = None


@dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)
Expand Down
1 change: 1 addition & 0 deletions rotkehlchen/constants/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@
A_GNOSIS_COW = Asset('eip155:100/erc20:0x177127622c4A00F3d409B75571e12cB3c8973d3c')
A_FPIS = Asset('eip155:1/erc20:0xc2544A32872A91F4A553b404C6950e89De901fdb')
A_STETH = Asset('eip155:1/erc20:0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84')
A_WSTETH = Asset('eip155:1/erc20:0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0')
A_SAND = Asset('eip155:1/erc20:0x3845badAde8e6dFF049820680d1F14bD3903a5d0')
A_GLM = Asset('eip155:1/erc20:0x7DD9c5Cba05E151C895FDe1CF355C9A1D5DA6429')

Expand Down
68 changes: 68 additions & 0 deletions rotkehlchen/tests/unit/decoders/test_lido_eth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import pytest

from rotkehlchen.accounting.structures.balance import Balance
from rotkehlchen.chain.ethereum.modules.lido.constants import CPT_LIDO
from rotkehlchen.chain.evm.constants import ZERO_ADDRESS
from rotkehlchen.chain.evm.decoding.constants import CPT_GAS
from rotkehlchen.constants.assets import A_ETH, A_STETH
from rotkehlchen.fval import FVal
from rotkehlchen.history.events.structures.evm_event import EvmEvent
from rotkehlchen.history.events.structures.types import HistoryEventSubType, HistoryEventType
from rotkehlchen.tests.utils.ethereum import get_decoded_events_of_transaction
from rotkehlchen.types import Location, TimestampMS, deserialize_evm_tx_hash


@pytest.mark.vcr()
@pytest.mark.parametrize('ethereum_accounts', [['0x4C49d4Bd6a571827B4A556a0e1e3071DA6231B9D']])
def test_lido_steth_staking(database, ethereum_inquirer, ethereum_accounts):
tx_hex = deserialize_evm_tx_hash('0x23a3ee601475424e91bdc0999a780afe57bf37cbcce6d1c09a4dfaaae1765451') # noqa: E501
evmhash = deserialize_evm_tx_hash(tx_hex)
events, _ = get_decoded_events_of_transaction(
evm_inquirer=ethereum_inquirer,
database=database,
tx_hash=tx_hex,
)
timestamp, gas_str, amount_deposited, amount_minted = TimestampMS(1710486191000), '0.002846110430778206', '1.12137397', '1.121373969999999999' # noqa: E501
expected_events = [
EvmEvent(
tx_hash=evmhash,
sequence_index=0,
timestamp=timestamp,
location=Location.ETHEREUM,
event_type=HistoryEventType.SPEND,
event_subtype=HistoryEventSubType.FEE,
asset=A_ETH,
balance=Balance(amount=FVal(gas_str)),
location_label=ethereum_accounts[0],
notes=f'Burned {gas_str} ETH for gas',
counterparty=CPT_GAS,
), EvmEvent(
tx_hash=evmhash,
sequence_index=1,
timestamp=timestamp,
location=Location.ETHEREUM,
event_type=HistoryEventType.DEPOSIT,
event_subtype=HistoryEventSubType.DEPOSIT_ASSET,
asset=A_ETH,
balance=Balance(FVal(amount_deposited)),
location_label=ethereum_accounts[0],
notes=f'Submit {amount_deposited} {A_ETH.symbol_or_name()} to Lido',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is constant. No need to resolve (as resolution is DB roundtrip)

Suggested change
notes=f'Submit {amount_deposited} {A_ETH.symbol_or_name()} to Lido',
notes=f'Submit {amount_deposited} ETH to Lido',

counterparty=CPT_LIDO,
address=A_STETH.resolve_to_evm_token().evm_address,
), EvmEvent(
tx_hash=evmhash,
sequence_index=2,
timestamp=timestamp,
location=Location.ETHEREUM,
event_type=HistoryEventType.RECEIVE,
event_subtype=HistoryEventSubType.RECEIVE_WRAPPED,
asset=A_STETH,
balance=Balance(FVal(amount_minted)),
location_label=ethereum_accounts[0],
notes=f'Receive {amount_minted} {A_STETH.symbol_or_name()} in exchange for the deposited {A_ETH.symbol_or_name()}', # noqa: E501
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sam

Suggested change
notes=f'Receive {amount_minted} {A_STETH.symbol_or_name()} in exchange for the deposited {A_ETH.symbol_or_name()}', # noqa: E501
notes=f'Receive {amount_minted} stETH in exchange for the deposited ETH', # noqa: E501

counterparty=CPT_LIDO,
address=ZERO_ADDRESS,
extra_data={'staked_eth': str(amount_deposited)},
),
]
assert events == expected_events
2 changes: 2 additions & 0 deletions rotkehlchen/tests/unit/decoders/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ def test_decoders_initialization(ethereum_transaction_decoder: EthereumTransacti
'HarvestFinance',
'Juicebox',
'Kyber',
'Lido',
'Liquity',
'Lockedgno',
'Makerdao',
Expand Down Expand Up @@ -182,6 +183,7 @@ def test_decoders_initialization(ethereum_transaction_decoder: EthereumTransacti
'eigenlayer',
'omni',
'blur',
'lido',
}


Expand Down