Interchaînes, DeFi et contrats intelligents · Leçon 1 sur 6
Décoder les interactions avec les contrats intelligents
9 min de lecture
Toute interaction avec un contrat intelligent se présente sous forme d'octets : une chaîne de données d'appel (calldata) dans la transaction, ainsi que des topics et des données dans les journaux. Transformer ces octets pour déterminer « qui a appelé quoi, avec quels arguments et ce qui s'est passé » constitue la compétence centrale de l'analyse du modèle à comptes. Cela suppose d'obtenir la bonne ABI, pour le bon code, au bon bloc, et de savoir d'où proviennent les hachages qui identifient les fonctions et les événements.
L'ABI comme clé de décodage
L'interface binaire d'application (ABI) est une description JSON des fonctions, événements et erreurs d'un contrat : leurs noms, les types des paramètres, les paramètres d'événement qui sont indexés et les types de retour. Les compilateurs la produisent à partir du code source, et les explorateurs la publient pour les contrats vérifiés. Grâce à l'ABI, un décodeur sait qu'un préfixe donné de 4 octets désigne transfer, que les 32 octets suivants sont une adresse et que les 32 octets qui suivent sont un montant.
Le décodage utilise la signature canonique : le nom suivi des types des paramètres entre parenthèses, séparés par des virgules, sans espaces ni noms de paramètres. Les alias sont développés (uint devient uint256) et les structures sont écrites sous forme de tuples, par exemple aggregate3((address,bool,bytes)[]). Un seul espace supplémentaire ou un alias non développé produit un hachage complètement différent, ce qui constitue la raison la plus fréquente pour laquelle un décodeur construit manuellement ne trouve rien.
Sélecteurs de fonction
Un sélecteur de fonction correspond aux 4 premiers octets du hachage keccak-256 de la signature canonique de la fonction. keccak256("transfer(address,uint256)") commence par 0xa9059cbb ; chaque appel à la fonction transfer d'un ERC-20 commence donc par ces 4 octets. Après le sélecteur, les arguments sont encodés selon l'ABI en mots de 32 octets : les types statiques (address, uint256, bool) occupent directement leur emplacement, avec un remplissage à gauche ; les types dynamiques (bytes, string, tableaux) laissent un décalage à leur emplacement et placent leur longueur et leur contenu à la fin.
a9059cbb sélecteur : transfer(address,uint256)
000000000000000000000000b0b5e6f7…41aa arg 1 : adresse du destinataire
0000000000000000000000000000000000000000000000000000000077359400 arg 2 : 2 000 000 000 unités de base
Ces données d'appel envoient 2 000 000 000 unités de base, soit 2 000 jetons si le jeton possède 6 décimales, au destinataire. Notez que le destinataire ne correspond pas au champ to de la transaction, qui désigne le contrat du jeton.
Quatre octets permettent environ 4,3 milliards de valeurs, et des collisions de sélecteurs existent : burn(uint256) et collate_propagate_storage(bytes16) produisent tous deux le hachage 0x42966c68. Les bases de signatures ouvertes (comme l'annuaire 4byte et le service de recherche de signatures Sourcify/OpenChain) associent les sélecteurs à des signatures textuelles, mais elles peuvent renvoyer plusieurs candidats pour un même sélecteur, et n'importe qui peut soumettre des entrées. Privilégiez l'ABI vérifiée du contrat que vous décodez ; utilisez les bases de données pour les contrats non vérifiés et départagez les candidats en vérifiant que les arguments se décodent correctement. Un sélecteur que le contrat ne reconnaît pas est dirigé vers sa fonction de repli ; les appels avec un « sélecteur inconnu » peuvent donc tout de même avoir un effet.
Le même mécanisme identifie les données de réversion, qui expliquent pourquoi une transaction a échoué. Un simple message require est encodé sous la forme Error(string), avec le sélecteur 0x08c379a0, suivi du texte encodé selon l'ABI ; les vérifications insérées par le compilateur, comme celles des dépassements arithmétiques, utilisent Panic(uint256), avec le sélecteur 0x4e487b71 et un code numérique ; les contrats peuvent également définir des erreurs personnalisées, dont les sélecteurs sont dérivés exactement comme les sélecteurs de fonction et figurent dans l'ABI. Décoder les motifs de réversion des tentatives infructueuses d'un attaquant avant une exploitation réussie révèle souvent ce qu'il cherchait à tester.
Signatures d'événement et topics
Les événements utilisent le hachage complet de 32 octets de leur signature canonique comme topic0. Un événement non anonyme peut comporter jusqu'à trois paramètres indexés dans topic1 à topic3. Les événements anonymes omettent topic0, ce qui libère un quatrième emplacement indexé, mais les rend invisibles aux filtres qui recherchent par signature. Les paramètres indexés de type dynamique (string, bytes, tableaux) sont stockés sous forme de hachage keccak-256 de la valeur ; la valeur d'origine ne peut donc pas être récupérée à partir du topic.
Un ensemble de référence de valeurs topic0, vérifiées par hachage des signatures :
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
Les deux lignes Swap correspondent aux événements de paire Uniswap v2 et de pool Uniswap v3 (voir la leçon Échanges sur les DEX, échanges instantanés et flux de liquidité). Deux enseignements se dégagent du tableau. Premièrement, topic0 vous indique le type d'événement et le champ address du journal vous indique quel contrat l'a émis ; un filtre portant uniquement sur topic0 renvoie les événements de tous les contrats qui utilisent cette signature, y compris les faux. Deuxièmement, des événements ayant les mêmes types mais une indexation différente partagent le même topic0, comme les événements Transfer des ERC-20 et ERC-721 ; vérifiez donc le nombre de topics avant de décoder.
Contrats proxy et recherche de l'implémentation
De nombreux contrats sont des proxys : un petit contrat qui conserve le stockage et les soldes et transmet chaque appel, avec delegatecall, à un contrat d'implémentation qui contient la logique. Une mise à niveau consiste à faire pointer le proxy vers une nouvelle logique. Pour un analyste, cela a trois conséquences :
- L'ABI dont vous avez besoin est celle de l'implémentation, pas celle du proxy.
- Les journaux sont émis avec l'adresse du proxy, car le code s'exécute dans le contexte du proxy. L'identité d'un jeton reste l'adresse du proxy au fil des mises à niveau.
- L'implémentation peut changer au fil du temps ; décodez donc avec l'implémentation qui était active au bloc de la transaction.
EIP-1967 a normalisé l'emplacement où les proxys stockent ces pointeurs, afin que les outils puissent les trouver. Chaque emplacement correspond au hachage keccak-256 d'une chaîne fixe, moins 1 :
| Usage | Chaîne hachée | Emplacement |
|---|---|---|
| Implémentation | eip1967.proxy.implementation | 0x3608…2bbc |
| Administrateur | eip1967.proxy.admin | 0xb531…6103 |
| Balise | eip1967.proxy.beacon | 0xa3f0…3d50 |
La lecture de l'emplacement d'implémentation à un bloc donné nécessite un appel JSON-RPC :
{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_getStorageAt",
"params": [
"0x9c4e…1d27",
"0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc",
"0x1406f40"
]
}
La réponse est un mot de 32 octets, par exemple 0x000000000000000000000000e5a1…7c30 ; les 20 derniers octets constituent l'adresse d'implémentation au bloc 21 000 000. Les lectures historiques nécessitent un nœud d'archive (voir la leçon Blocs, en-têtes et API des nœuds). Le proxy émet également Upgraded(address indexed implementation) à chaque changement ; récupérer ces journaux fournit donc la chronologie complète des mises à niveau :
{
"address": "0x9c4e…1d27",
"topics": [
"0xbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b",
"0x000000000000000000000000e5a1…7c30"
],
"data": "0x",
"blockNumber": "0x13d6200",
"decoded": {"event": "Upgraded", "proxy": "0x9c4e…1d27", "implementation": "0xe5a1…7c30", "block": 20800000}
}
Le champ address du journal désigne le proxy, et son seul paramètre indexé est la nouvelle implémentation. Ici, l'implémentation lue au bloc 21 000 000 a été installée au bloc 20 800 000 ; les transactions antérieures à ce bloc doivent être décodées avec l'ABI de l'implémentation précédente.
D'autres modèles nécessitent d'autres recherches :
- Les proxys transparents et UUPS (de type EIP-1822, tels qu'implémentés par OpenZeppelin) utilisent tous deux l'emplacement d'implémentation EIP-1967 ; ils diffèrent par l'endroit où se trouve la fonction de mise à niveau.
- Les proxys à balise stockent une adresse de balise ; appelez
implementation()sur la balise. - Les proxys minimaux (clones EIP-1167) codent en dur l'adresse d'implémentation dans leur bytecode d'exécution de 45 octets, entre le préfixe
363d3d373d3d3d363d73et le suffixe5af43d82803e903d91602b57fd5bf3. - Les proxys multisignatures Safe conservent leur implémentation (le singleton, historiquement appelé
masterCopy) à l'emplacement de stockage 0, et non dans un emplacement EIP-1967. Le vol de Bybit de février 2025 a écrasé précisément cet emplacement (voir la leçon Analyser les exploitations de vulnérabilités et les piratages). - Les diamants (EIP-2535) dirigent chaque sélecteur vers un contrat de facette différent ; vous avez besoin de la table d'association entre sélecteurs et facettes au bloc concerné.
Code source vérifié
Un explorateur ou le dépôt ouvert Sourcify marque un contrat comme vérifié lorsque la compilation du code source publié avec les paramètres indiqués reproduit le bytecode déployé. Sourcify distingue une correspondance complète (exacte), où même le hachage des métadonnées intégrées correspond, d'une correspondance partielle, où seul le code exécutable correspond. La vérification prouve que le code source correspond au bytecode. Elle ne prouve pas que le code est sûr, audité ou honnête, et le nom du contrat dans le code source vérifié est choisi par la personne qui le déploie. Pour les contrats non vérifiés, les analystes combinent les recherches de sélecteurs, la sortie du décompilateur et le comportement observé dans les traces, et réduisent leur niveau de confiance en conséquence.
Multicall et appels groupés
Le regroupement masque des actions derrière un appel externe unique. Formes courantes :
- Les contrats Multicall, comme
aggregate3((address,bool,bytes)[])de Multicall3 (sélecteur 0x82ad56cb), qui exécute une liste d'appels. - Les fonctions de routeur
multicall(bytes[])(0xac9650d8) etmulticall(uint256,bytes[])(0x5ae401dc, avec une échéance), où chaque élément contient les données d'appel d'une autre fonction du même routeur. - Les lots d'appels multisignatures (MultiSend de Safe) et les lots d'appels de comptes intelligents (ERC-4337
handleOps, comptes délégués EIP-7702).
Le sélecteur externe indique uniquement « lot d'appels ». Décodez de manière récursive : décodez l'appel externe, prenez chaque élément bytes interne, lisez son sélecteur et décodez-le avec l'ABI de la cible. Confirmez ensuite à l'aide de la trace, l'arbre d'appels qui indique la cible, la valeur et la réussite de chaque appel interne, car un lot peut autoriser l'échec d'appels individuels. Les journaux de tous les appels internes figurent dans le même reçu, classés par logIndex, ce qui permet de les rattacher aux étapes. La leçon Interroger les données UTXO ainsi que les journaux et les traces EVM montre comment procéder à grande échelle.
Points clés pour l'examen : signatures canoniques (sans espaces, uint256, tuples) ; dérivation d'un sélecteur de fonction (les 4 premiers octets de keccak-256, par exemple 0xa9059cbb pour transfer) et lecture des mots des données d'appel ; collisions de sélecteurs et manière de départager les candidats d'une base de données ; topic0 comme hachage complet de la signature d'événement, limites des paramètres indexés, événements anonymes et topics dynamiques hachés ; proxys : les journaux portent l'adresse du proxy, l'emplacement d'implémentation EIP-1967 (keccak-256 de "eip1967.proxy.implementation" moins 1), eth_getStorageAt à un bloc donné, événements Upgraded, modèles à balise, minimaux et Safe à l'emplacement 0 ; ce que la vérification prouve et ne prouve pas ; décodage récursif des lots multicall et confirmation à l'aide des traces.
Questions d’entraînement
Vérifiez votre compréhension. Ces questions ne comptent pas pour l’examen.
Connectez-vous pour enregistrer votre progression et reprendre là où vous vous êtes arrêté. C’est gratuit. Se connecter