Cross-Chain, DeFi und Smart Contracts · Lektion 1 von 6
Interaktionen mit Smart Contracts dekodieren
8 Min. Lesezeit
Jede Interaktion mit einem Smart Contract liegt zunächst als Bytes vor: als Calldata-Zeichenfolge in der Transaktion sowie als Topics und Daten in den Logs. Diese Bytes in die Aussage „Wer hat was mit welchen Argumenten aufgerufen, und was ist geschehen?“ zu übersetzen, ist die Kernkompetenz bei der Analyse des Kontenmodells. Sie setzt voraus, die richtige ABI für den richtigen Code am richtigen Block zu verwenden und zu wissen, woher die Hashes stammen, die Funktionen und Ereignisse kennzeichnen.
Die ABI als Dekodierungsschlüssel
Die Application Binary Interface (ABI) ist eine JSON-Beschreibung der Funktionen, Ereignisse und Fehler eines Vertrags: ihrer Namen, Parametertypen, der Angabe, welche Ereignisparameter indiziert sind, sowie der Rückgabetypen. Compiler erzeugen sie aus dem Quellcode, und Explorer veröffentlichen sie für verifizierte Verträge. Mit der ABI weiß ein Decoder, dass ein bestimmtes 4-Byte-Präfix transfer bedeutet, dass die nächsten 32 Bytes eine Adresse und die folgenden 32 Bytes einen Betrag darstellen.
Die Dekodierung verwendet die kanonische Signatur: den Namen, gefolgt von den Parametertypen in Klammern, durch Kommas getrennt, ohne Leerzeichen und ohne Parameternamen. Aliasse werden ausgeschrieben (uint wird zu uint256), und Structs werden als Tupel dargestellt, beispielsweise aggregate3((address,bool,bytes)[]). Ein einziges zusätzliches Leerzeichen oder ein nicht ausgeschriebener Alias erzeugt einen völlig anderen Hash. Dies ist der häufigste Grund, warum ein selbst erstellter Decoder nichts findet.
Funktionsselektoren
Ein Funktionsselektor besteht aus den ersten 4 Bytes des keccak-256-Hashes der kanonischen Funktionssignatur. keccak256("transfer(address,uint256)") beginnt mit 0xa9059cbb, daher beginnt jeder Aufruf von transfer nach ERC-20 mit diesen 4 Bytes. Nach dem Selektor werden Argumente in 32-Byte-Wörtern ABI-kodiert: Statische Typen (address, uint256, bool) stehen direkt an ihrer Position und werden links aufgefüllt; dynamische Typen (bytes, string, Arrays) hinterlassen an ihrer Position einen Offset und legen ihre Länge und ihren Inhalt am Ende ab.
a9059cbb Selektor: transfer(address,uint256)
000000000000000000000000b0b5e6f7…41aa Arg. 1: Empfängeradresse
0000000000000000000000000000000000000000000000000000000077359400 Arg. 2: 2.000.000.000 Basiseinheiten
Diese Calldata senden 2.000.000.000 Basiseinheiten, also 2.000 Token, wenn der Token 6 Dezimalstellen hat, an den Empfänger. Beachten Sie, dass der Empfänger nicht dem Feld to der Transaktion entspricht; dieses enthält den Token-Vertrag.
Vier Bytes ermöglichen etwa 4,3 Milliarden Werte, und es gibt Selektorkollisionen: burn(uint256) und collate_propagate_storage(bytes16) ergeben beide den Hash 0x42966c68. Offene Signaturdatenbanken (etwa das 4byte-Verzeichnis und die Signatursuche von Sourcify/OpenChain) ordnen Selektoren wieder textuelle Signaturen zu. Sie können jedoch für einen Selektor mehrere Kandidaten zurückgeben, und jeder kann Einträge einreichen. Bevorzugen Sie die verifizierte ABI des Vertrags, den Sie dekodieren; verwenden Sie Datenbanken für nicht verifizierte Verträge, und wählen Sie zwischen Kandidaten, indem Sie prüfen, ob sich die Argumente einwandfrei dekodieren lassen. Ein Selektor, den der Vertrag nicht erkennt, wird an dessen Fallback-Funktion weitergeleitet. Aufrufe mit einem „unbekannten Selektor“ können daher trotzdem etwas bewirken.
Dasselbe Schema kennzeichnet Revert-Daten, die erklären, warum eine Transaktion fehlgeschlagen ist. Eine einfache require-Meldung wird als Error(string) mit dem Selektor 0x08c379a0 kodiert, gefolgt vom ABI-kodierten Text; vom Compiler eingefügte Prüfungen, etwa auf arithmetischen Überlauf, verwenden Panic(uint256) mit dem Selektor 0x4e487b71 und einem numerischen Code. Außerdem können Verträge benutzerdefinierte Fehler definieren, deren Selektoren genauso wie Funktionsselektoren abgeleitet werden und in der ABI erscheinen. Die Dekodierung der Revert-Gründe fehlgeschlagener Versuche eines Angreifers vor einem erfolgreichen Exploit zeigt oft, was dieser getestet hat.
Ereignissignaturen und Topics
Ereignisse verwenden den vollständigen 32-Byte-Hash der kanonischen Ereignissignatur als topic0. Ein nicht anonymes Ereignis kann bis zu drei indizierte Parameter in topic1 bis topic3 enthalten. Anonyme Ereignisse lassen topic0 weg. Das schafft einen vierten indizierten Platz, macht sie aber für Filter unsichtbar, die nach Signatur suchen. Indizierte Parameter dynamischen Typs (string, bytes, Arrays) werden als keccak-256-Hash des Werts gespeichert, sodass der ursprüngliche Wert nicht aus dem Topic wiederhergestellt werden kann.
Ein Referenzsatz von topic0-Werten, verifiziert durch Hashen der Signaturen:
Transfer(address,address,uint256) 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
Approval(address,address,uint256) 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925
ApprovalForAll(address,address,bool) 0x17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c31
TransferSingle(address,address,address,uint256,uint256) 0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62
TransferBatch(address,address,address,uint256[],uint256[]) 0x4a39dc06d4c0dbc64b70af90fd698a233a518aa5d07e595d983b8c0526c8f7fb
Swap(address,uint256,uint256,uint256,uint256,address) 0xd78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822
Swap(address,address,int256,int256,uint160,uint128,int24) 0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67
Upgraded(address) 0xbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b
Die beiden Swap-Zeilen sind die Ereignisse eines Uniswap-v2-Paars und eines Uniswap-v3-Pools (siehe die Lektion DEX-Swaps, Sofortbörsen und Liquiditätsflüsse). Aus der Tabelle ergeben sich zwei Erkenntnisse. Erstens gibt topic0 die Art des Ereignisses an, und das Feld address des Logs zeigt, welcher Vertrag es ausgegeben hat; ein Filter allein auf topic0 liefert Ereignisse von jedem Vertrag zurück, der diese Signatur verwendet, einschließlich Fälschungen. Zweitens haben Ereignisse mit denselben Typen, aber unterschiedlicher Indizierung denselben topic0-Wert, wie etwa Transfer bei ERC-20 und ERC-721. Prüfen Sie daher vor der Dekodierung die Anzahl der Topics.
Proxy-Verträge und Ermittlung der Implementierung
Viele Verträge sind Proxys: kleine Verträge, die den Speicher und die Guthaben halten und jeden Aufruf mit delegatecall an einen Implementierungsvertrag weiterleiten, der die Logik enthält. Ein Upgrade bedeutet, den Proxy auf neue Logik zu verweisen. Für Analysten hat dies drei Konsequenzen:
- Sie benötigen die ABI der Implementierung, nicht die des Proxys.
- Logs werden mit der Adresse des Proxys ausgegeben, weil der Code im Kontext des Proxys ausgeführt wird. Die Identität eines Tokens bleibt auch über Upgrades hinweg die Proxy-Adresse.
- Die Implementierung kann sich im Laufe der Zeit ändern. Dekodieren Sie daher mit der Implementierung, die im Block der Transaktion aktiv war.
EIP-1967 hat standardisiert, wo Proxys diese Verweise speichern, damit Werkzeuge sie finden können. Jeder Slot ergibt sich aus keccak-256 einer festgelegten Zeichenfolge minus 1:
| Zweck | Gehashte Zeichenfolge | Slot |
|---|---|---|
| Implementierung | eip1967.proxy.implementation | 0x3608…2bbc |
| Administrator | eip1967.proxy.admin | 0xb531…6103 |
| Beacon | eip1967.proxy.beacon | 0xa3f0…3d50 |
Zum Auslesen des Implementierungsslots an einem bestimmten Block genügt ein JSON-RPC-Aufruf:
{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_getStorageAt",
"params": [
"0x9c4e…1d27",
"0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc",
"0x1406f40"
]
}
Die Antwort ist ein 32-Byte-Wort, beispielsweise 0x000000000000000000000000e5a1…7c30; die letzten 20 Bytes sind die Implementierungsadresse an Block 21.000.000. Historische Lesezugriffe erfordern einen Archivknoten (siehe die Lektion Blöcke, Header und Knoten-APIs). Der Proxy gibt außerdem bei jeder Änderung Upgraded(address indexed implementation) aus. Durch Abrufen dieser Logs erhalten Sie daher die vollständige Upgrade-Zeitleiste:
{
"address": "0x9c4e…1d27",
"topics": [
"0xbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b",
"0x000000000000000000000000e5a1…7c30"
],
"data": "0x",
"blockNumber": "0x13d6200",
"decoded": {"event": "Upgraded", "proxy": "0x9c4e…1d27", "implementation": "0xe5a1…7c30", "block": 20800000}
}
Das Feld address des Logs enthält den Proxy, und der einzige indizierte Parameter ist die neue Implementierung. Hier wurde die an Block 21.000.000 ausgelesene Implementierung an Block 20.800.000 installiert; Transaktionen vor diesem Block müssen mit der ABI der vorherigen Implementierung dekodiert werden.
Andere Muster erfordern andere Abfragen:
- Transparente und UUPS-Proxys (nach dem Muster von EIP-1822, wie von OpenZeppelin implementiert) verwenden beide den EIP-1967-Implementierungsslot; sie unterscheiden sich darin, wo die Upgrade-Funktion liegt.
- Beacon-Proxys speichern eine Beacon-Adresse; rufen Sie
implementation()auf dem Beacon auf. - Minimale Proxys (EIP-1167-Klone) kodieren die Implementierungsadresse fest in ihrem 45-Byte-Laufzeit-Bytecode, zwischen dem Präfix
363d3d373d3d3d363d73und dem Suffix5af43d82803e903d91602b57fd5bf3. - Safe-Multisig-Proxys halten ihre Implementierung (den Singleton, historisch
masterCopygenannt) in Speicherslot 0, nicht in einem EIP-1967-Slot. Beim Bybit-Diebstahl im Februar 2025 wurde genau dieser Slot überschrieben (siehe die Lektion Exploits und Hacks analysieren). - Diamonds (EIP-2535) leiten jeden Selektor an einen anderen Facet-Vertrag weiter; Sie benötigen die Zuordnungstabelle von Selektoren zu Facets für den betreffenden Block.
Verifizierter Quellcode
Ein Explorer oder das offene Sourcify-Repository kennzeichnet einen Vertrag als verifiziert, wenn die Kompilierung des veröffentlichten Quellcodes mit den angegebenen Einstellungen den bereitgestellten Bytecode reproduziert. Sourcify unterscheidet eine vollständige (exakte) Übereinstimmung, bei der sogar der eingebettete Metadaten-Hash übereinstimmt, von einer teilweisen Übereinstimmung, bei der nur der ausführbare Code übereinstimmt. Die Verifizierung belegt, dass der Quellcode dem Bytecode entspricht. Sie beweist nicht, dass der Code sicher, auditiert oder redlich ist, und der Vertragsname im verifizierten Quellcode wird vom Bereitsteller gewählt. Bei nicht verifizierten Verträgen kombinieren Analysten Selektorabfragen, Decompiler-Ausgaben und das in Traces beobachtete Verhalten und stufen ihre Aussagesicherheit entsprechend niedriger ein.
Multicall und gebündelte Aufrufe
Durch Bündelung verbergen sich Aktionen hinter einem einzigen äußeren Aufruf. Häufige Formen sind:
- Multicall-Verträge, etwa
aggregate3((address,bool,bytes)[])von Multicall3 (Selektor 0x82ad56cb), das eine Liste von Aufrufen ausführt. - Die Router-Funktionen
multicall(bytes[])(0xac9650d8) undmulticall(uint256,bytes[])(0x5ae401dc, mit einer Frist), bei denen jedes Element Calldata für eine andere Funktion desselben Routers enthält. - Multisig-Aufrufbündel (MultiSend von Safe) und Smart-Account-Aufrufbündel (
handleOpsnach ERC-4337, delegierte Konten nach EIP-7702).
Der äußere Selektor sagt nur „Aufrufbündel“. Dekodieren Sie rekursiv: Dekodieren Sie den äußeren Aufruf, nehmen Sie jedes innere bytes-Element, lesen Sie dessen Selektor und dekodieren Sie es mit der ABI des Ziels. Bestätigen Sie das Ergebnis anschließend anhand des Trace, des Aufrufbaums, der Ziel, Wert und Erfolg jedes inneren Aufrufs zeigt, denn ein Aufrufbündel kann das Fehlschlagen einzelner Aufrufe zulassen. Die Logs aller inneren Aufrufe erscheinen in einem einzigen Transaktionsbeleg, geordnet nach logIndex, wodurch sie den einzelnen Schritten zugeordnet werden können. Die Lektion UTXO-Daten sowie EVM-Logs und -Traces abfragen zeigt, wie Sie dies in großem Umfang durchführen.
Prüfungsschwerpunkt: kanonische Signaturen (keine Leerzeichen, uint256, Tupel); Ableitung eines Funktionsselektors (erste 4 Bytes von keccak-256, z. B. 0xa9059cbb für transfer) und Lesen von Calldata-Wörtern; Selektorkollisionen und Auswahl zwischen Datenbankkandidaten; topic0 als vollständiger Hash der Ereignissignatur, Grenzen für indizierte Parameter, anonyme Ereignisse und gehashte dynamische Topics; Proxys: Logs tragen die Proxy-Adresse, der EIP-1967-Implementierungsslot (keccak-256 von "eip1967.proxy.implementation" minus 1), eth_getStorageAt an einem Block, Upgraded-Ereignisse sowie Beacon-, Minimal-Proxy- und Safe-Slot-0-Muster; was Verifizierung beweist und was nicht; rekursive Dekodierung von Multicall-Aufrufbündeln und Bestätigung anhand von Traces.