diff --git a/requirements.txt b/requirements.txt index 4c4eceb..a3cf8a4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,5 @@ requests==2.31.0 networkx==3.3 types_networkx==3.2.1.20240425 # TODO: use specific commit -traces_parser @ git+https://github.com/TOD-theses/traces_parser \ No newline at end of file +traces_parser @ git+https://github.com/TOD-theses/traces_parser +eth_abi==5.1.0 \ No newline at end of file diff --git a/tests/analysis/snapshots/snap_test_currency_changes.py b/tests/analysis/snapshots/snap_test_currency_changes.py index ecd1907..ae10911 100644 --- a/tests/analysis/snapshots/snap_test_currency_changes.py +++ b/tests/analysis/snapshots/snap_test_currency_changes.py @@ -12,8 +12,8 @@ GenericRepr(""), { "change": -10, + "currency_identifier": "Wei", "owner": "0x000000000000000000000000000000000000aaaa", - "token_address": None, "type": "ETHER", }, ), @@ -21,8 +21,8 @@ GenericRepr(""), { "change": 10, + "currency_identifier": "Wei", "owner": "0x000000000000000000000000000000000000bbbb", - "token_address": None, "type": "ETHER", }, ), @@ -30,8 +30,8 @@ GenericRepr(""), { "change": -20, + "currency_identifier": "Wei", "owner": "0x000000000000000000000000000000000000aaaa", - "token_address": None, "type": "ETHER", }, ), @@ -39,8 +39,8 @@ GenericRepr(""), { "change": 20, + "currency_identifier": "Wei", "owner": "0x000000000000000000000000000000000000cccc", - "token_address": None, "type": "ETHER", }, ), @@ -48,8 +48,8 @@ GenericRepr(""), { "change": -10, + "currency_identifier": "Wei", "owner": "0x000000000000000000000000000000000000bbbb", - "token_address": None, "type": "ETHER", }, ), @@ -57,8 +57,8 @@ GenericRepr(""), { "change": 10, + "currency_identifier": "Wei", "owner": "0x000000000000000000000000000000000000cccc", - "token_address": None, "type": "ETHER", }, ), diff --git a/tests/evaluation/snapshots/snap_test_financial_gain_loss.py b/tests/evaluation/snapshots/snap_test_financial_gain_loss.py index e1e7724..e532377 100644 --- a/tests/evaluation/snapshots/snap_test_financial_gain_loss.py +++ b/tests/evaluation/snapshots/snap_test_financial_gain_loss.py @@ -12,28 +12,28 @@ "report": { "gains": { "0x000000000000000000000000000000000000cccc": { - "ETHER": { + "ETHER-Wei": { "change": 10, + "currency_identifier": "Wei", "owner": "0x000000000000000000000000000000000000cccc", - "token_address": None, "type": "ETHER", } } }, "losses": { "0x000000000000000000000000000000000000aaaa": { - "ETHER": { + "ETHER-Wei": { "change": -8, + "currency_identifier": "Wei", "owner": "0x000000000000000000000000000000000000aaaa", - "token_address": None, "type": "ETHER", } }, "0x000000000000000000000000000000000000bbbb": { - "ETHER": { + "ETHER-Wei": { "change": -2, + "currency_identifier": "Wei", "owner": "0x000000000000000000000000000000000000bbbb", - "token_address": None, "type": "ETHER", } }, @@ -45,7 +45,7 @@ "test_financial_gain_loss_evaluation evaluation_str" ] = """=== Evaluation: Financial gains and losses === Losses in normal compared to reverse scenario: -> 0x000000000000000000000000000000000000cccc lost 10 ETHER (in Wei) +> 0x000000000000000000000000000000000000cccc lost 10 ETHER Wei """ diff --git a/tests/utils/events/snapshots/__init__.py b/tests/utils/events/snapshots/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/utils/events/snapshots/snap_test_events_decoder.py b/tests/utils/events/snapshots/snap_test_events_decoder.py new file mode 100644 index 0000000..2baae2e --- /dev/null +++ b/tests/utils/events/snapshots/snap_test_events_decoder.py @@ -0,0 +1,161 @@ +# -*- coding: utf-8 -*- +# snapshottest: v1 - https://goo.gl/zC4yUc +from __future__ import unicode_literals + +from snapshottest import Snapshot + + +snapshots = Snapshot() + +snapshots["test_events_decoder_erc1155_burn_batch currency_changes"] = [ + { + "change": -4369, + "currency_identifier": "0x000000000000000000000000000000000000abcd-0xabababababababababababababababababababababababababababababababab", + "owner": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "type": "ERC-1155", + }, + { + "change": -8738, + "currency_identifier": "0x000000000000000000000000000000000000abcd-0xcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", + "owner": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "type": "ERC-1155", + }, +] + +snapshots["test_events_decoder_erc1155_burn_single currency_changes"] = [ + { + "change": -855236050549443759, + "currency_identifier": "0x000000000000000000000000000000000000abcd-0xabababababababababababababababababababababababababababababababab", + "owner": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "type": "ERC-1155", + } +] + +snapshots["test_events_decoder_erc1155_mint_batch currency_changes"] = [ + { + "change": 4369, + "currency_identifier": "0x000000000000000000000000000000000000abcd-0xabababababababababababababababababababababababababababababababab", + "owner": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "type": "ERC-1155", + }, + { + "change": 8738, + "currency_identifier": "0x000000000000000000000000000000000000abcd-0xcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", + "owner": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "type": "ERC-1155", + }, +] + +snapshots["test_events_decoder_erc1155_mint_single currency_changes"] = [ + { + "change": 855236050549443759, + "currency_identifier": "0x000000000000000000000000000000000000abcd-0xabababababababababababababababababababababababababababababababab", + "owner": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "type": "ERC-1155", + } +] + +snapshots["test_events_decoder_erc1155_transfer_batch currency_changes"] = [ + { + "change": -4369, + "currency_identifier": "0x000000000000000000000000000000000000abcd-0xabababababababababababababababababababababababababababababababab", + "owner": "0xffffffffffffffffffffffffffffffffffffffff", + "type": "ERC-1155", + }, + { + "change": 4369, + "currency_identifier": "0x000000000000000000000000000000000000abcd-0xabababababababababababababababababababababababababababababababab", + "owner": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "type": "ERC-1155", + }, + { + "change": -8738, + "currency_identifier": "0x000000000000000000000000000000000000abcd-0xcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", + "owner": "0xffffffffffffffffffffffffffffffffffffffff", + "type": "ERC-1155", + }, + { + "change": 8738, + "currency_identifier": "0x000000000000000000000000000000000000abcd-0xcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", + "owner": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "type": "ERC-1155", + }, +] + +snapshots["test_events_decoder_erc1155_transfer_single currency_changes"] = [ + { + "change": -855236050549443759, + "currency_identifier": "0x000000000000000000000000000000000000abcd-0xabababababababababababababababababababababababababababababababab", + "owner": "0xffffffffffffffffffffffffffffffffffffffff", + "type": "ERC-1155", + }, + { + "change": 855236050549443759, + "currency_identifier": "0x000000000000000000000000000000000000abcd-0xabababababababababababababababababababababababababababababababab", + "owner": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "type": "ERC-1155", + }, +] + +snapshots["test_events_decoder_erc20_transfer currency_changes"] = [ + { + "change": -855236050549443759, + "currency_identifier": "0x000000000000000000000000000000000000abcd", + "owner": "0x916b2aff900d06c526b4935f999462b65f1a24fe", + "type": "ERC-20", + }, + { + "change": 855236050549443759, + "currency_identifier": "0x000000000000000000000000000000000000abcd", + "owner": "0xd68060e9b273492d643a8eca70ad18c9ce2fb378", + "type": "ERC-20", + }, +] + +snapshots["test_events_decoder_erc721_transfer currency_changes"] = [ + { + "change": -1, + "currency_identifier": "0x000000000000000000000000000000000000abcd-0x0000000000000000000000000000000000000000000000000bde68a8201b8caf", + "owner": "0x916b2aff900d06c526b4935f999462b65f1a24fe", + "type": "ERC-721", + }, + { + "change": 1, + "currency_identifier": "0x000000000000000000000000000000000000abcd-0x0000000000000000000000000000000000000000000000000bde68a8201b8caf", + "owner": "0xd68060e9b273492d643a8eca70ad18c9ce2fb378", + "type": "ERC-721", + }, +] + +snapshots["test_events_decoder_erc777_burned currency_changes"] = [ + { + "change": -855236050549443759, + "currency_identifier": "0x000000000000000000000000000000000000abcd", + "owner": "0xd68060e9b273492d643a8eca70ad18c9ce2fb378", + "type": "ERC-777", + } +] + +snapshots["test_events_decoder_erc777_mint currency_changes"] = [ + { + "change": 855236050549443759, + "currency_identifier": "0x000000000000000000000000000000000000abcd", + "owner": "0xd68060e9b273492d643a8eca70ad18c9ce2fb378", + "type": "ERC-777", + } +] + +snapshots["test_events_decoder_erc777_sent currency_changes"] = [ + { + "change": -855236050549443759, + "currency_identifier": "0x000000000000000000000000000000000000abcd", + "owner": "0xffffffffffffffffffffffffffffffffffffffff", + "type": "ERC-777", + }, + { + "change": 855236050549443759, + "currency_identifier": "0x000000000000000000000000000000000000abcd", + "owner": "0xd68060e9b273492d643a8eca70ad18c9ce2fb378", + "type": "ERC-777", + }, +] diff --git a/tests/utils/events/test_events_decoder.py b/tests/utils/events/test_events_decoder.py new file mode 100644 index 0000000..4966f74 --- /dev/null +++ b/tests/utils/events/test_events_decoder.py @@ -0,0 +1,301 @@ +from traces_parser.datatypes.hexstring import HexString +from traces_analyzer.utils.events.events_decoder import EventsDecoder +from traces_analyzer.utils.events.tokens.erc_1155 import ( + ERC1155TransferBatchEvent, + ERC1155TransferSingleEvent, +) +from traces_analyzer.utils.events.tokens.erc_20 import ERC20TransferEvent +from traces_analyzer.utils.events.tokens.erc_721 import ERC721TransferEvent + +from tests.test_utils.test_utils import _test_addr + +from snapshottest.pytest import PyTestSnapshotTest + +from traces_analyzer.utils.events.tokens.erc_777 import ( + ERC777BurnedEvent, + ERC777MintedEvent, + ERC777SentEvent, +) +from eth_abi.abi import encode + + +def get_events_decoder(): + return EventsDecoder( + [ + ERC20TransferEvent, + ERC721TransferEvent, + ERC777MintedEvent, + ERC777SentEvent, + ERC777BurnedEvent, + ERC1155TransferSingleEvent, + ERC1155TransferBatchEvent, + ] + ) + + +_token_address = _test_addr("0xabcd") + + +def test_events_decoder_erc20_transfer(snapshot: PyTestSnapshotTest): + sender = HexString( + "000000000000000000000000916b2aff900d06c526b4935f999462b65f1a24fe" + ) + to = HexString("000000000000000000000000d68060e9b273492d643a8eca70ad18c9ce2fb378") + value = HexString( + "0000000000000000000000000000000000000000000000000bde68a8201b8caf" + ) + topics = [ERC20TransferEvent.signature(), sender, to] + data = value + + decoder = get_events_decoder() + event = decoder.decode_event(topics, data, _token_address) + + assert isinstance(event, ERC20TransferEvent) + snapshot.assert_match(event.get_currency_changes(), "currency_changes") + + +def test_events_decoder_erc721_transfer(snapshot: PyTestSnapshotTest): + sender = HexString( + "000000000000000000000000916b2aff900d06c526b4935f999462b65f1a24fe" + ) + to = HexString("000000000000000000000000d68060e9b273492d643a8eca70ad18c9ce2fb378") + token_id = HexString( + "0000000000000000000000000000000000000000000000000bde68a8201b8caf" + ) + topics = [ERC721TransferEvent.signature(), sender, to, token_id] + data = HexString("") + + decoder = get_events_decoder() + event = decoder.decode_event(topics, data, _token_address) + + assert isinstance(event, ERC721TransferEvent) + snapshot.assert_match(event.get_currency_changes(), "currency_changes") + + +def test_events_decoder_erc777_mint(snapshot: PyTestSnapshotTest): + operator = HexString( + "000000000000000000000000916b2aff900d06c526b4935f999462b65f1a24fe" + ) + to = HexString("000000000000000000000000d68060e9b273492d643a8eca70ad18c9ce2fb378") + value = HexString( + "0000000000000000000000000000000000000000000000000bde68a8201b8caf" + ) + topics = [ERC777MintedEvent.signature(), operator, to] + data = value + HexString.zeros(32 * 2) + + decoder = get_events_decoder() + event = decoder.decode_event(topics, data, _token_address) + + assert isinstance(event, ERC777MintedEvent) + snapshot.assert_match(event.get_currency_changes(), "currency_changes") + + +def test_events_decoder_erc777_sent(snapshot: PyTestSnapshotTest): + operator = HexString( + "000000000000000000000000916b2aff900d06c526b4935f999462b65f1a24fe" + ) + sender = HexString( + "000000000000000000000000ffffffffffffffffffffffffffffffffffffffff" + ) + to = HexString("000000000000000000000000d68060e9b273492d643a8eca70ad18c9ce2fb378") + value = HexString( + "0000000000000000000000000000000000000000000000000bde68a8201b8caf" + ) + topics = [ERC777SentEvent.signature(), operator, sender, to] + data = value + HexString.zeros(32 * 2) + + decoder = get_events_decoder() + event = decoder.decode_event(topics, data, _token_address) + + assert isinstance(event, ERC777SentEvent) + snapshot.assert_match(event.get_currency_changes(), "currency_changes") + + +def test_events_decoder_erc777_burned(snapshot: PyTestSnapshotTest): + operator = HexString( + "000000000000000000000000916b2aff900d06c526b4935f999462b65f1a24fe" + ) + holder = HexString( + "000000000000000000000000d68060e9b273492d643a8eca70ad18c9ce2fb378" + ) + value = HexString( + "0000000000000000000000000000000000000000000000000bde68a8201b8caf" + ) + topics = [ERC777BurnedEvent.signature(), operator, holder] + data = value + HexString.zeros(32 * 2) + + decoder = get_events_decoder() + event = decoder.decode_event(topics, data, _token_address) + + assert isinstance(event, ERC777BurnedEvent) + snapshot.assert_match(event.get_currency_changes(), "currency_changes") + + +def test_events_decoder_erc1155_mint_single(snapshot: PyTestSnapshotTest): + operator = HexString( + "000000000000000000000000916b2aff900d06c526b4935f999462b65f1a24fe" + ) + sender = HexString.zeros(32) + to = HexString("000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee") + token_id = HexString( + "abababababababababababababababababababababababababababababababab" + ) + value = HexString( + "0000000000000000000000000000000000000000000000000bde68a8201b8caf" + ) + topics = [ERC1155TransferSingleEvent.signature(), operator, sender, to] + data = token_id + value + + decoder = get_events_decoder() + event = decoder.decode_event(topics, data, _token_address) + + assert isinstance(event, ERC1155TransferSingleEvent) + snapshot.assert_match(event.get_currency_changes(), "currency_changes") + + +def test_events_decoder_erc1155_transfer_single(snapshot: PyTestSnapshotTest): + operator = HexString( + "000000000000000000000000916b2aff900d06c526b4935f999462b65f1a24fe" + ) + sender = HexString( + "000000000000000000000000ffffffffffffffffffffffffffffffffffffffff" + ) + to = HexString("000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee") + token_id = HexString( + "abababababababababababababababababababababababababababababababab" + ) + value = HexString( + "0000000000000000000000000000000000000000000000000bde68a8201b8caf" + ) + topics = [ERC1155TransferSingleEvent.signature(), operator, sender, to] + data = token_id + value + + decoder = get_events_decoder() + event = decoder.decode_event(topics, data, _token_address) + + assert isinstance(event, ERC1155TransferSingleEvent) + snapshot.assert_match(event.get_currency_changes(), "currency_changes") + + +def test_events_decoder_erc1155_burn_single(snapshot: PyTestSnapshotTest): + operator = HexString( + "000000000000000000000000916b2aff900d06c526b4935f999462b65f1a24fe" + ) + sender = HexString( + "000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + ) + to = HexString.zeros(32) + token_id = HexString( + "abababababababababababababababababababababababababababababababab" + ) + value = HexString( + "0000000000000000000000000000000000000000000000000bde68a8201b8caf" + ) + topics = [ERC1155TransferSingleEvent.signature(), operator, sender, to] + data = token_id + value + + decoder = get_events_decoder() + event = decoder.decode_event(topics, data, _token_address) + + assert isinstance(event, ERC1155TransferSingleEvent) + snapshot.assert_match(event.get_currency_changes(), "currency_changes") + + +def test_events_decoder_erc1155_mint_batch(snapshot: PyTestSnapshotTest): + operator = HexString( + "000000000000000000000000916b2aff900d06c526b4935f999462b65f1a24fe" + ) + sender = HexString.zeros(32) + to = HexString("000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee") + token_ids = [ + HexString( + "abababababababababababababababababababababababababababababababab" + ).as_int(), + HexString( + "cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd" + ).as_int(), + ] + values = [ + HexString( + "0000000000000000000000000000000000000000000000000000000000001111" + ).as_int(), + HexString( + "0000000000000000000000000000000000000000000000000000000000002222" + ).as_int(), + ] + topics = [ERC1155TransferBatchEvent.signature(), operator, sender, to] + data = HexString(encode(["uint256[]", "uint256[]"], [token_ids, values]).hex()) + + decoder = get_events_decoder() + event = decoder.decode_event(topics, data, _token_address) + + assert isinstance(event, ERC1155TransferBatchEvent) + snapshot.assert_match(event.get_currency_changes(), "currency_changes") + + +def test_events_decoder_erc1155_transfer_batch(snapshot: PyTestSnapshotTest): + operator = HexString( + "000000000000000000000000916b2aff900d06c526b4935f999462b65f1a24fe" + ) + sender = HexString( + "000000000000000000000000ffffffffffffffffffffffffffffffffffffffff" + ) + to = HexString("000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee") + token_ids = [ + HexString( + "abababababababababababababababababababababababababababababababab" + ).as_int(), + HexString( + "cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd" + ).as_int(), + ] + values = [ + HexString( + "0000000000000000000000000000000000000000000000000000000000001111" + ).as_int(), + HexString( + "0000000000000000000000000000000000000000000000000000000000002222" + ).as_int(), + ] + topics = [ERC1155TransferBatchEvent.signature(), operator, sender, to] + data = HexString(encode(["uint256[]", "uint256[]"], [token_ids, values]).hex()) + + decoder = get_events_decoder() + event = decoder.decode_event(topics, data, _token_address) + + assert isinstance(event, ERC1155TransferBatchEvent) + snapshot.assert_match(event.get_currency_changes(), "currency_changes") + + +def test_events_decoder_erc1155_burn_batch(snapshot: PyTestSnapshotTest): + operator = HexString( + "000000000000000000000000916b2aff900d06c526b4935f999462b65f1a24fe" + ) + sender = HexString( + "000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + ) + to = HexString.zeros(32) + token_ids = [ + HexString( + "abababababababababababababababababababababababababababababababab" + ).as_int(), + HexString( + "cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd" + ).as_int(), + ] + values = [ + HexString( + "0000000000000000000000000000000000000000000000000000000000001111" + ).as_int(), + HexString( + "0000000000000000000000000000000000000000000000000000000000002222" + ).as_int(), + ] + topics = [ERC1155TransferBatchEvent.signature(), operator, sender, to] + data = HexString(encode(["uint256[]", "uint256[]"], [token_ids, values]).hex()) + + decoder = get_events_decoder() + event = decoder.decode_event(topics, data, _token_address) + + assert isinstance(event, ERC1155TransferBatchEvent) + snapshot.assert_match(event.get_currency_changes(), "currency_changes") diff --git a/traces_analyzer/evaluation/financial_gain_loss_evaluation.py b/traces_analyzer/evaluation/financial_gain_loss_evaluation.py index 6963acb..a37a4f9 100644 --- a/traces_analyzer/evaluation/financial_gain_loss_evaluation.py +++ b/traces_analyzer/evaluation/financial_gain_loss_evaluation.py @@ -46,11 +46,11 @@ def _cli_report(self) -> str: s = "Gains in normal compared to reverse scenario:\n" for addr, gains in self._gains_and_losses["gains"].items(): for change in gains.values(): - s += f'> {addr} gained {change["change"]} {change["type"]} {change["token_address"] or "(in Wei)"}\n' + s += f'> {addr} gained {change["change"]} {change["type"]} {change["currency_identifier"]}\n' s = "Losses in normal compared to reverse scenario:\n" for addr, gains in self._gains_and_losses["gains"].items(): for change in gains.values(): - s += f'> {addr} lost {change["change"]} {change["type"]} {change["token_address"] or "(in Wei)"}\n' + s += f'> {addr} lost {change["change"]} {change["type"]} {change["currency_identifier"]}\n' return s @@ -59,6 +59,7 @@ def compute_gains_and_losses( changes_normal: Sequence[tuple[Instruction, CurrencyChange]], changes_reverse: Sequence[tuple[Instruction, CurrencyChange]], ) -> GainsAndLosses: + # TODO: we should add T_A and T_B together and then compare them grouped_normal = group_by_address(changes_normal) grouped_reverse = group_by_address(changes_reverse) @@ -86,7 +87,7 @@ def group_by_address( for _, change in changes: addr = change["owner"] - key = change["type"] + (change["token_address"] or "") + key = f'{change["type"]}-{change["currency_identifier"]}' if key not in groups[addr]: groups[addr][key] = deepcopy(change) else: diff --git a/traces_analyzer/features/extractors/currency_changes.py b/traces_analyzer/features/extractors/currency_changes.py index b51a304..aacb379 100644 --- a/traces_analyzer/features/extractors/currency_changes.py +++ b/traces_analyzer/features/extractors/currency_changes.py @@ -1,5 +1,3 @@ -from typing import TypedDict - from typing_extensions import override from traces_analyzer.features.feature_extractor import SingleInstructionFeatureExtractor @@ -14,20 +12,20 @@ LOG4, ) - -class CURRENCY: - ETHER = "ETHER" - - -class CurrencyChange(TypedDict): - type: str - """Type of the currency, e.g. ETHER or ERC-20, ...""" - token_address: str | None - """ID for the currency. For Ether this is None, for tokens this is the storage address that emitted the LOG""" - owner: str - """Address for which a change occurred""" - change: int - """Positive or negative change""" +from traces_analyzer.types.currency_change import CURRENCY_TYPE, CurrencyChange +from traces_analyzer.utils.events.event import CurrencyChangeEvent +from traces_analyzer.utils.events.events_decoder import EventsDecoder +from traces_analyzer.utils.events.tokens.erc_1155 import ( + ERC1155TransferBatchEvent, + ERC1155TransferSingleEvent, +) +from traces_analyzer.utils.events.tokens.erc_20 import ERC20TransferEvent +from traces_analyzer.utils.events.tokens.erc_721 import ERC721TransferEvent +from traces_analyzer.utils.events.tokens.erc_777 import ( + ERC777BurnedEvent, + ERC777MintedEvent, + ERC777SentEvent, +) class CurrencyChangesFeatureExtractor(SingleInstructionFeatureExtractor): @@ -35,6 +33,17 @@ class CurrencyChangesFeatureExtractor(SingleInstructionFeatureExtractor): def __init__(self) -> None: super().__init__() + self.event_decoder = EventsDecoder( + [ + ERC20TransferEvent, + ERC721TransferEvent, + ERC777MintedEvent, + ERC777SentEvent, + ERC777BurnedEvent, + ERC1155TransferSingleEvent, + ERC1155TransferBatchEvent, + ] + ) self.currency_changes: list[tuple[Instruction, CurrencyChange]] = [] @override @@ -50,8 +59,8 @@ def on_instruction(self, instruction: Instruction): ( instruction, { - "type": CURRENCY.ETHER, - "token_address": None, + "type": CURRENCY_TYPE.ETHER, + "currency_identifier": "Wei", "owner": sender.with_prefix(), "change": -value, }, @@ -61,8 +70,8 @@ def on_instruction(self, instruction: Instruction): ( instruction, { - "type": CURRENCY.ETHER, - "token_address": None, + "type": CURRENCY_TYPE.ETHER, + "currency_identifier": "Wei", "owner": receiver.with_prefix(), "change": value, }, @@ -70,5 +79,17 @@ def on_instruction(self, instruction: Instruction): ) if isinstance(instruction, (LOG0, LOG1, LOG2, LOG3, LOG4)): - # TODO - pass + accesses = instruction.get_accesses() + topics = [access.value.get_hexstring() for access in accesses.stack[2:]] + data = accesses.memory[0].value.get_hexstring() + if not topics: + return + + event = self.event_decoder.decode_event( + topics, data, instruction.call_context.storage_address + ) + if event: + assert isinstance(event, CurrencyChangeEvent), f"Invalid event: {event}" + self.currency_changes.extend( + [(instruction, c) for c in event.get_currency_changes()] + ) diff --git a/traces_analyzer/types/currency_change.py b/traces_analyzer/types/currency_change.py new file mode 100644 index 0000000..b681420 --- /dev/null +++ b/traces_analyzer/types/currency_change.py @@ -0,0 +1,20 @@ +from typing import TypedDict + + +class CURRENCY_TYPE: + ETHER = "ETHER" + ERC20 = "ERC-20" + ERC721 = "ERC-721" + ERC777 = "ERC-777" + ERC1155 = "ERC-1155" + + +class CurrencyChange(TypedDict): + type: str + """Type of the currency, e.g. ETHER or ERC-20, ...""" + currency_identifier: str + """ID for the currency. For Ether this is None, for tokens this is the storage address that emitted the LOG and potentially a token id""" + owner: str + """Address for which a change occurred""" + change: int + """Positive or negative change""" diff --git a/traces_analyzer/utils/events/__init__.py b/traces_analyzer/utils/events/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/traces_analyzer/utils/events/event.py b/traces_analyzer/utils/events/event.py new file mode 100644 index 0000000..fe0f962 --- /dev/null +++ b/traces_analyzer/utils/events/event.py @@ -0,0 +1,31 @@ +from typing import Sequence +from typing_extensions import Self +from traces_parser.datatypes.hexstring import HexString +from abc import abstractmethod + +from traces_analyzer.features.extractors.currency_changes import CurrencyChange + + +class Event: + @staticmethod + @abstractmethod + def signature() -> HexString: + pass + + @classmethod + @abstractmethod + def can_decode(cls, topics: Sequence[HexString], data: HexString) -> bool: + pass + + @classmethod + @abstractmethod + def decode( + cls, topics: Sequence[HexString], data: HexString, storage_address: HexString + ) -> Self: + pass + + +class CurrencyChangeEvent(Event): + @abstractmethod + def get_currency_changes(self) -> Sequence[CurrencyChange]: + pass diff --git a/traces_analyzer/utils/events/events_decoder.py b/traces_analyzer/utils/events/events_decoder.py new file mode 100644 index 0000000..61f34b9 --- /dev/null +++ b/traces_analyzer/utils/events/events_decoder.py @@ -0,0 +1,23 @@ +from typing import Sequence + +from traces_parser.datatypes.hexstring import HexString + +from traces_analyzer.utils.events.event import Event + + +class EventDecodingException(Exception): + pass + + +class EventsDecoder: + def __init__(self, events: Sequence[type[Event]]) -> None: + self._events = events + + def decode_event( + self, topics: Sequence[HexString], data: HexString, storage_address: HexString + ): + if not topics: + raise EventDecodingException("Can not decode event without any topic") + for event in self._events: + if event.can_decode(topics, data): + return event.decode(topics, data, storage_address) diff --git a/traces_analyzer/utils/events/tokens/__init__.py b/traces_analyzer/utils/events/tokens/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/traces_analyzer/utils/events/tokens/erc_1155.py b/traces_analyzer/utils/events/tokens/erc_1155.py new file mode 100644 index 0000000..f5035eb --- /dev/null +++ b/traces_analyzer/utils/events/tokens/erc_1155.py @@ -0,0 +1,148 @@ +from typing import Sequence +from traces_parser.datatypes.hexstring import HexString +from typing_extensions import override, Self +from traces_analyzer.features.extractors.currency_changes import ( + CURRENCY_TYPE, + CurrencyChange, +) +from traces_analyzer.utils.events.event import CurrencyChangeEvent +from eth_abi.abi import decode + + +class ERC1155TransferSingleEvent(CurrencyChangeEvent): + def __init__( + self, + sender: HexString, + to: HexString, + value: HexString, + token_id: HexString, + token_address: HexString, + ) -> None: + super().__init__() + self.sender = sender + self.to = to + self.value = value.as_int() + self.token_id = token_id + self.token_address = token_address + + @override + @staticmethod + def signature() -> HexString: + # TransferSingle(address indexed _operator, address indexed _from, address indexed _to, uint256 _id, uint256 _value) + # https://www.4byte.directory/event-signatures/?bytes_signature=0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62 + return HexString( + "0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62" + ) + + @override + @classmethod + def can_decode(cls, topics: Sequence[HexString], data: HexString) -> bool: + return len(topics) == 4 and topics[0] == cls.signature() + + @override + @classmethod + def decode( + cls, topics: Sequence[HexString], data: HexString, storage_address: HexString + ) -> Self: + id, value = decode(["uint256", "uint256"], bytes.fromhex(data.without_prefix())) + return cls( + topics[2].as_address(), + topics[3].as_address(), + HexString.from_int(value), + HexString.from_int(id), + storage_address, + ) + + @override + def get_currency_changes(self) -> Sequence[CurrencyChange]: + id = f"{self.token_address.with_prefix()}-{self.token_id.with_prefix()}" + changes = [] + if self.sender.as_int() != 0: + changes.append( + CurrencyChange( + type=CURRENCY_TYPE.ERC1155, + currency_identifier=id, + owner=self.sender.with_prefix(), + change=-self.value, + ) + ) + if self.to.as_int() != 0: + changes.append( + CurrencyChange( + type=CURRENCY_TYPE.ERC1155, + currency_identifier=id, + owner=self.to.with_prefix(), + change=self.value, + ) + ) + return changes + + +class ERC1155TransferBatchEvent(CurrencyChangeEvent): + def __init__( + self, + sender: HexString, + to: HexString, + values: Sequence[HexString], + token_ids: Sequence[HexString], + token_address: HexString, + ) -> None: + super().__init__() + self.sender = sender + self.to = to + self.values = [v.as_int() for v in values] + self.token_ids = token_ids + self.token_address = token_address + + @override + @staticmethod + def signature() -> HexString: + # TransferBatch(address indexed _operator, address indexed _from, address indexed _to, uint256[] _ids, uint256[] _values) + # https://www.4byte.directory/event-signatures/?bytes_signature=0x4a39dc06d4c0dbc64b70af90fd698a233a518aa5d07e595d983b8c0526c8f7fb + return HexString( + "0x4a39dc06d4c0dbc64b70af90fd698a233a518aa5d07e595d983b8c0526c8f7fb" + ) + + @override + @classmethod + def can_decode(cls, topics: Sequence[HexString], data: HexString) -> bool: + return len(topics) == 4 and topics[0] == cls.signature() + + @override + @classmethod + def decode( + cls, topics: Sequence[HexString], data: HexString, storage_address: HexString + ) -> Self: + ids, values = decode( + ["uint256[]", "uint256[]"], bytes.fromhex(data.without_prefix()) + ) + ids = [HexString.from_int(id) for id in ids] + values = [HexString.from_int(value) for value in values] + return cls( + topics[2].as_address(), topics[3].as_address(), values, ids, storage_address + ) + + @override + def get_currency_changes(self) -> Sequence[CurrencyChange]: + changes = [] + for value, token_id in zip(self.values, self.token_ids): + id = f"{self.token_address.with_prefix()}-{token_id.with_prefix()}" + if self.sender.as_int() != 0: + changes.append( + CurrencyChange( + type=CURRENCY_TYPE.ERC1155, + currency_identifier=id, + owner=self.sender.with_prefix(), + change=-value, + ) + ) + if self.to.as_int() != 0: + changes.append( + CurrencyChange( + type=CURRENCY_TYPE.ERC1155, + currency_identifier=id, + owner=self.to.with_prefix(), + change=value, + ) + ) + return changes diff --git a/traces_analyzer/utils/events/tokens/erc_20.py b/traces_analyzer/utils/events/tokens/erc_20.py new file mode 100644 index 0000000..f791c1c --- /dev/null +++ b/traces_analyzer/utils/events/tokens/erc_20.py @@ -0,0 +1,63 @@ +from typing import Sequence +from traces_parser.datatypes.hexstring import HexString +from typing_extensions import override, Self +from traces_analyzer.features.extractors.currency_changes import ( + CURRENCY_TYPE, + CurrencyChange, +) +from traces_analyzer.utils.events.event import CurrencyChangeEvent + + +class ERC20TransferEvent(CurrencyChangeEvent): + def __init__( + self, + sender: HexString, + to: HexString, + value: HexString, + token_address: HexString, + ) -> None: + super().__init__() + self.sender = sender + self.to = to + self.value = value.as_int() + self.token_address = token_address + + @override + @staticmethod + def signature() -> HexString: + # Transfer(address indexed _from, address indexed _to, uint256 _value) + # https://www.4byte.directory/event-signatures/?bytes_signature=0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef + return HexString( + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" + ) + + @override + @classmethod + def can_decode(cls, topics: Sequence[HexString], data: HexString) -> bool: + return len(topics) == 3 and topics[0] == cls.signature() + + @override + @classmethod + def decode( + cls, topics: Sequence[HexString], data: HexString, storage_address: HexString + ) -> Self: + return cls( + topics[1].as_address(), topics[2].as_address(), data, storage_address + ) + + @override + def get_currency_changes(self) -> Sequence[CurrencyChange]: + return [ + CurrencyChange( + type=CURRENCY_TYPE.ERC20, + currency_identifier=self.token_address.with_prefix(), + owner=self.sender.with_prefix(), + change=-self.value, + ), + CurrencyChange( + type=CURRENCY_TYPE.ERC20, + currency_identifier=self.token_address.with_prefix(), + owner=self.to.with_prefix(), + change=self.value, + ), + ] diff --git a/traces_analyzer/utils/events/tokens/erc_721.py b/traces_analyzer/utils/events/tokens/erc_721.py new file mode 100644 index 0000000..fd21ca1 --- /dev/null +++ b/traces_analyzer/utils/events/tokens/erc_721.py @@ -0,0 +1,64 @@ +from typing import Sequence +from traces_parser.datatypes.hexstring import HexString +from typing_extensions import override, Self +from traces_analyzer.features.extractors.currency_changes import ( + CURRENCY_TYPE, + CurrencyChange, +) +from traces_analyzer.utils.events.event import CurrencyChangeEvent + + +class ERC721TransferEvent(CurrencyChangeEvent): + def __init__( + self, + sender: HexString, + to: HexString, + token_id: HexString, + token_address: HexString, + ) -> None: + super().__init__() + self.sender = sender + self.to = to + self.token_id = token_id + self.token_address = token_address + + @override + @staticmethod + def signature() -> HexString: + # Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId) + # https://www.4byte.directory/event-signatures/?bytes_signature=0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef + return HexString( + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" + ) + + @override + @classmethod + def can_decode(cls, topics: Sequence[HexString], data: HexString) -> bool: + return len(topics) == 4 and topics[0] == cls.signature() + + @override + @classmethod + def decode( + cls, topics: Sequence[HexString], data: HexString, storage_address: HexString + ) -> Self: + return cls( + topics[1].as_address(), topics[2].as_address(), topics[3], storage_address + ) + + @override + def get_currency_changes(self) -> Sequence[CurrencyChange]: + id = f"{self.token_address.with_prefix()}-{self.token_id.with_prefix()}" + return [ + CurrencyChange( + type=CURRENCY_TYPE.ERC721, + currency_identifier=id, + owner=self.sender.with_prefix(), + change=-1, + ), + CurrencyChange( + type=CURRENCY_TYPE.ERC721, + currency_identifier=id, + owner=self.to.with_prefix(), + change=1, + ), + ] diff --git a/traces_analyzer/utils/events/tokens/erc_777.py b/traces_analyzer/utils/events/tokens/erc_777.py new file mode 100644 index 0000000..080bbe3 --- /dev/null +++ b/traces_analyzer/utils/events/tokens/erc_777.py @@ -0,0 +1,153 @@ +from typing import Sequence +from traces_parser.datatypes.hexstring import HexString +from typing_extensions import override, Self +from traces_analyzer.features.extractors.currency_changes import ( + CURRENCY_TYPE, + CurrencyChange, +) +from traces_analyzer.utils.events.event import CurrencyChangeEvent + + +class ERC777SentEvent(CurrencyChangeEvent): + def __init__( + self, + sender: HexString, + to: HexString, + amount: HexString, + token_address: HexString, + ) -> None: + super().__init__() + self.sender = sender + self.to = to + self.value = amount.as_int() + self.token_address = token_address + + @override + @staticmethod + def signature() -> HexString: + # Sent(address indexed operator,address indexed from,address indexed to,uint256 amount,bytes data,bytes operatorData) + # https://www.4byte.directory/event-signatures/?bytes_signature=0x06b541ddaa720db2b10a4d0cdac39b8d360425fc073085fac19bc82614677987 + return HexString( + "0x06b541ddaa720db2b10a4d0cdac39b8d360425fc073085fac19bc82614677987" + ) + + @override + @classmethod + def can_decode(cls, topics: Sequence[HexString], data: HexString) -> bool: + return len(topics) == 4 and topics[0] == cls.signature() + + @override + @classmethod + def decode( + cls, topics: Sequence[HexString], data: HexString, storage_address: HexString + ) -> Self: + return cls( + topics[2].as_address(), topics[3].as_address(), data[:64], storage_address + ) + + @override + def get_currency_changes(self) -> Sequence[CurrencyChange]: + return [ + CurrencyChange( + type=CURRENCY_TYPE.ERC777, + currency_identifier=self.token_address.with_prefix(), + owner=self.sender.with_prefix(), + change=-self.value, + ), + CurrencyChange( + type=CURRENCY_TYPE.ERC777, + currency_identifier=self.token_address.with_prefix(), + owner=self.to.with_prefix(), + change=self.value, + ), + ] + + +class ERC777MintedEvent(CurrencyChangeEvent): + def __init__( + self, + to: HexString, + amount: HexString, + token_address: HexString, + ) -> None: + super().__init__() + self.to = to + self.value = amount.as_int() + self.token_address = token_address + + @override + @staticmethod + def signature() -> HexString: + # Minted(address indexed operator, address indexed to, uint256 amount, bytes data, bytes operatorData) + # https://www.4byte.directory/event-signatures/?bytes_signature=0x2fe5be0146f74c5bce36c0b80911af6c7d86ff27e89d5cfa61fc681327954e5d + return HexString( + "0x2fe5be0146f74c5bce36c0b80911af6c7d86ff27e89d5cfa61fc681327954e5d" + ) + + @override + @classmethod + def can_decode(cls, topics: Sequence[HexString], data: HexString) -> bool: + return len(topics) == 3 and topics[0] == cls.signature() + + @override + @classmethod + def decode( + cls, topics: Sequence[HexString], data: HexString, storage_address: HexString + ) -> Self: + return cls(topics[2].as_address(), data[:64], storage_address) + + @override + def get_currency_changes(self) -> Sequence[CurrencyChange]: + return [ + CurrencyChange( + type=CURRENCY_TYPE.ERC777, + currency_identifier=self.token_address.with_prefix(), + owner=self.to.with_prefix(), + change=self.value, + ), + ] + + +class ERC777BurnedEvent(CurrencyChangeEvent): + def __init__( + self, + to: HexString, + amount: HexString, + token_address: HexString, + ) -> None: + super().__init__() + self.holder = to + self.value = amount.as_int() + self.token_address = token_address + + @override + @staticmethod + def signature() -> HexString: + # Minted(address indexed operator, address indexed from, uint256 amount, bytes data, bytes operatorData) + # https://www.4byte.directory/event-signatures/?bytes_signature=0xa78a9be3a7b862d26933ad85fb11d80ef66b8f972d7cbba06621d583943a4098 + return HexString( + "0xa78a9be3a7b862d26933ad85fb11d80ef66b8f972d7cbba06621d583943a4098" + ) + + @override + @classmethod + def can_decode(cls, topics: Sequence[HexString], data: HexString) -> bool: + return len(topics) == 3 and topics[0] == cls.signature() + + @override + @classmethod + def decode( + cls, topics: Sequence[HexString], data: HexString, storage_address: HexString + ) -> Self: + return cls(topics[2].as_address(), data[:64], storage_address) + + @override + def get_currency_changes(self) -> Sequence[CurrencyChange]: + return [ + CurrencyChange( + type=CURRENCY_TYPE.ERC777, + currency_identifier=self.token_address.with_prefix(), + owner=self.holder.with_prefix(), + change=-self.value, + ), + ]