---
id: 27
title: "L'ABI en détail : sélecteur, paramètres statiques et types dynamiques"
slug: 0-15-abi-internals
date: 2026/10/02
summary: "En partant du calldata brut, cet article décompose l'ABI : le sélecteur de 4 octets provient de la troncature de la signature canonique, les types statiques occupent directement un mot entier, les types dynamiques reposent sur un adressage à deux niveaux, par décalage et par longueur ; les événements écrivent les paramètres indexed dans les topics, en échange d'une capacité de recherche. L'ABI n'est pas auto-descriptive, ce qui contraint aussi bien le décodage que les mises à niveau."
keywords: encodage ABI,sélecteur de fonction,décodage calldata,journaux d'événements,topics
heroImage: /images/articles/photos/0-15-abi-internals.jpg
---

Le calldata d'un virement ERC-20 commence généralement par `0xa9059cbb`, suivi de deux mots de 32 octets : le premier est l'adresse du destinataire, le second le montant. Aucun champ on-chain n'indique que cette suite d'octets correspond à `transfer(address,uint256)`, et l'EVM (Ethereum Virtual Machine, machine virtuelle Ethereum) ne connaît pas non plus les noms de fonctions. Elle se contente d'appliquer la logique de répartition compilée dans le bytecode au déploiement : comparer les 4 premiers octets du calldata, puis sauter dans le segment de code correspondant.

D'où viennent ces 4 octets, pourquoi certains paramètres sont placés directement au début tandis que d'autres doivent être cherchés plus loin, et pourquoi un calldata reste indéchiffrable sans définition d'interface : voilà trois questions à comprendre pour appréhender l'interaction avec les contrats. Cet article expose d'abord les règles d'encodage de l'ABI (Application Binary Interface, interface binaire d'application), puis déroule à la main le décodage d'un calldata d'exemple tiré du document de spécification, et explique enfin pourquoi les journaux d'événements répartissent les paramètres entre topics et data.

## Le sélecteur est la troncature du hachage de la signature canonique, pas le nom de la fonction

La spécification ABI de Solidity donne une définition très brève du sélecteur de fonction (function selector) : les 4 premiers octets du calldata, tirés des 4 octets de poids fort du hachage Keccak-256 de la signature de la fonction. La signature retenue ici est la forme canonique, qui n'équivaut pas à l'écriture dans le code source : le nom de la fonction suivi de la liste des types de paramètres entre parenthèses, les types séparés par une seule virgule, sans espace, sans nom de paramètre et sans modificateur d'emplacement de données tel que `memory`, `calldata` ou `storage`.

La forme canonique procède aussi à une normalisation des types. `uint` et `int` doivent s'écrire `uint256` et `int256`, `address payable` et les types de contrat s'écrivent `address`, une énumération (enum) s'écrit `uint8`, et un type de valeur défini par l'utilisateur s'écrit avec son type sous-jacent. Un struct s'écrit comme la liste de ses types de composants entre parenthèses, c'est-à-dire un tuple : si `S` possède deux champs `uint256`, la signature de `function f(S memory s)` est `f((uint256,uint256))` et le sélecteur provient des 4 premiers octets de `keccak256("f((uint256,uint256))")`. La valeur de retour ne participe pas à la signature, ce qui est cohérent avec la résolution de surcharge de Solidity : l'appelant ne distingue la fonction cible que par les paramètres, une différence de valeur de retour ne pouvant servir à lever l'ambiguïté. Chaque caractère de la signature compte : une espace en trop ou un `uint` écrit `uint256` donnent un sélecteur totalement différent.

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract SelectorDemo {
    function selectorOfTransfer() external pure returns (bytes4) {
        // Entre guillemets, la signature canonique : sans espace, sans nom de paramètre, uint normalisé en uint256
        return bytes4(keccak256("transfer(address,uint256)"));
    }
}
```

La conséquence directe de cet espace de 32 bits est que les collisions ne relèvent pas de l'hypothèse théorique. En septembre 2026, sur 4byte.directory, le seul sélecteur `0xa9059cbb` répertorie 6 signatures candidates ; outre `transfer(address,uint256)`, on y trouve `many_msg_babbage(bytes1)`, `workMyDirefulOwner(uint256,uint256)` et des noms de remplissage tels que `func_2093253501(bytes)`. Les entrées de cette base sont librement soumises, leur nombre et leur contenu évoluent avec le temps. Quand deux fonctions distinctes partagent un sélecteur, le préfixe du calldata ne permet pas à lui seul de distinguer l'intention d'appel ; il faut s'appuyer sur le code du contrat cible ou sur sa définition d'interface.

La documentation officielle de Solidity ne consacre pas de section à cette limitation, mais solc vérifie au niveau de l'implémentation les doublons de sélecteur au sein d'un même contrat (chaîne d'héritage comprise) : lorsque deux fonctions ont le même sélecteur, la compilation échoue avec `Function signature hash collision` (reproductible avec un contrat minimal) ; entre contrats et entre versions, ce contrôle n'existe pas. Autrement dit, retrouver un nom de fonction par une base de type 4byte n'est pas fiable : la base fournit un ensemble de candidats et, en cas de collision, aucune réponse unique ne peut être établie.

## Les types statiques occupent directement un mot entier

L'encodage commence au 5e octet. La règle répartit d'abord les paramètres en deux catégories, statiques et dynamiques : les types statiques sont encodés sur place et écrits directement dans le flux d'octets des paramètres ; les types dynamiques placent leur contenu réel plus loin et ne laissent à leur emplacement qu'un décalage.

Le mot de l'EVM fait 32 octets, et les types statiques occupent toujours un mot entier. `uint<M>` et `int<M>` sont écrits en big-endian avec remplissage des poids forts : les nombres non signés sont complétés par des zéros, les nombres signés par extension de signe avec des `0xff` ou des `0x00`. `bool` équivaut à `uint8`, `true` s'encode 1 et `false` 0. `address` équivaut à `uint160` : une adresse de 20 octets occupe donc les poids faibles d'un mot de 32 octets, précédée de 12 octets nuls. Pour les types d'octets de taille fixe `bytes<M>`, le remplissage va dans l'autre sens : la valeur est alignée à gauche, complétée par des zéros à droite jusqu'à 32 octets. Bien que tous deux statiques, `uint32(1)` et `bytes4(0x00000001)` occupent le même mot mais à des positions différentes : le premier dans les 4 derniers octets, le second dans les 4 premiers — c'est l'erreur de lecture la plus fréquente sur un vidage hexadécimal.

Un tableau de taille fixe `<type>[M]` est lui aussi classé statique lorsque le type de ses éléments est statique ; il est alors encodé comme un tuple, ses M éléments disposés à la suite sans écrire de longueur. Les structs et les tuples se déploient de la même manière, récursivement. La spécification définit les types statiques comme « tous les types sauf `bytes`, `string`, `T[]`, `T[k]` dont les éléments sont dynamiques, et les tuples contenant des membres dynamiques » ; cette définition est le seul critère pour décider entre « lire la valeur directement » et « suivre le pointeur ».

Le calldata a lui aussi un coût en gaz, et celui-ci ne coïncide pas avec l'alignement sur 32 octets de l'ABI. L'EIP-2028 a ramené le coût d'un octet non nul dans les données de transaction de 68 gas à 16 gas, l'octet nul restant à 4 gas ; ce coût relève du gaz intrinsèque de la transaction. Un `uint256` transmettant une très petite valeur occupe malgré tout 32 octets, dont l'immense majorité sont des octets nuls : le coût est faible, mais non nul. À l'inverse, empaqueter plusieurs petits entiers en `bytes32` de façon compacte économise du gaz, au prix de la disparition on-chain de frontières de paramètres lisibles : le décodeur doit connaître cette disposition personnalisée.

## Les types dynamiques : un adressage à deux niveaux par décalage et préfixe de longueur

`bytes`, `string`, `T[]`, ainsi que les tableaux de taille fixe et les tuples dont les éléments sont dynamiques, relèvent tous des types dynamiques. Leur longueur dépend de la valeur à l'exécution et ne peut être fixée à la compilation : ils ne peuvent donc pas occuper une largeur fixe dans le flux d'octets des paramètres.

L'ABI adopte une disposition tête/queue (head/tail). Les « têtes » de tous les paramètres sont d'abord alignées : pour un type statique, la tête est son propre encodage ; pour un type dynamique, c'est un décalage de 32 octets. Ce décalage se réfère au début du bloc de paramètres, c'est-à-dire au premier octet qui suit le sélecteur, et non au début du calldata entier — c'est la source de décalage la plus fréquente lors d'un décodage manuel. La longueur de la tête dépend uniquement du type du paramètre, pas de sa valeur. Après les têtes viennent les « queues » des différents paramètres dynamiques ; le premier élément d'une queue est la longueur, dont l'unité dépend du type : un nombre d'octets pour `bytes` et `string`, un nombre d'éléments pour un tableau, le contenu venant ensuite.

Pour `string`, la longueur indiquée est le nombre d'octets après encodage UTF-8, et non le nombre de caractères : les caractères chinois et les emoji occupent donc plusieurs octets. Le contenu est ensuite complété par des zéros jusqu'à un multiple de 32 octets. Les éléments d'un tableau suivent récursivement les mêmes règles ; dans un tableau imbriqué (par exemple `uint256[][]`), chaque niveau possède sa propre longueur et son propre décalage, le décalage étant toujours relatif au début du bloc d'encodage de ce niveau. La spécification énonce clairement l'objectif de cette conception : dans le pire des cas, le nombre de lectures nécessaires pour atteindre une valeur ne dépasse pas sa profondeur d'imbrication dans la structure de paramètres, et les données de n'importe quel élément peuvent être déplacées en bloc sans devenir invalides, puisqu'elles ne dépendent que d'adresses relatives.

Un détail facile à négliger : rien n'exige qu'un décalage soit minimal, ni que les zones de données ne se recouvrent pas. Le mode d'encodage strict (strict encoding mode) défini par la spécification impose une tête compacte, sans trous ; l'encodeur Solidity produit toujours le mode strict, mais le décodeur ne le vérifie pas systématiquement. Autrement dit, un même appel, sémantiquement identique, peut admettre plusieurs dispositions d'octets : une implémentation de décodage doit soit accepter des décalages comportant des trous, soit les refuser explicitement — deux choix qui produisent des divergences lors des comparaisons entre clients.

## Décoder un calldata à la main

L'exemple de décodage est tiré directement de la section Examples de la « Contract ABI Specification » : l'appel `sam(bytes,bool,uint256[])`, avec les paramètres `"dave"`, `true` et `[1,2,3]`, soit 292 octets, découpés toutes les 32 octets comme suit.

```text
0xa5643bf2
0000000000000000000000000000000000000000000000000000000000000060
0000000000000000000000000000000000000000000000000000000000000001
00000000000000000000000000000000000000000000000000000000000000a0
0000000000000000000000000000000000000000000000000000000000000004
6461766500000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000003
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000002
0000000000000000000000000000000000000000000000000000000000000003
```

Les étapes de décodage peuvent être reproduites en suivant strictement la définition de la spécification :

1. Les 4 premiers octets `0xa5643bf2` constituent le sélecteur, qui correspond à la signature `sam(bytes,bool,uint256[])`. Noter que le `uint` de la signature doit s'écrire `uint256`.
2. Le bloc de paramètres comporte 3 têtes de 32 octets chacune. Le 1er mot est `0x60`, soit 96, le décalage du 1er paramètre `bytes` ; le 2e mot est `0x01`, le `bool` valant `true` ; le 3e mot est `0xa0`, soit 160, qui pointe vers le 3e paramètre `uint256[]`.
3. Les têtes totalisent 96 octets : la queue du 1er paramètre commence donc au 96e octet du bloc de paramètres, ce qui rend `0x60` cohérent. Le 1er mot de cette queue est `0x04`, indiquant que `bytes` fait 4 octets ; les 4 octets suivants sont `64617665`, qui se lisent `dave` en ASCII ; le tout est complété par des zéros jusqu'à 32 octets.
4. La queue du 1er paramètre occupe deux mots, soit 64 octets, de 96 à 160 : le décalage du 2e paramètre dynamique vaut donc `0xa0`. À cet endroit, le 1er mot est `0x03`, indiquant que le tableau compte 3 éléments ; les 3 mots suivants valent respectivement 1, 2 et 3.

Le même processus peut s'écrire sous la forme d'un décodeur minimal. Le code Python ci-dessous ne traite que la signature ci-dessus ; il sert à montrer que l'unité d'un décalage est l'octet et où se situe le préfixe de longueur.

```python
# Entrée : le bloc de paramètres qui suit le sélecteur, découpé en mots de 32 octets
def word(data, i):
    return int.from_bytes(data[i * 32:(i + 1) * 32], "big")

def decode_sam(args: bytes):
    bytes_off = word(args, 0)          # 1er mot : décalage en octets du paramètre bytes par rapport au début du bloc de paramètres
    flag = word(args, 1) != 0          # 2e mot : bool
    arr_off = word(args, 2)            # 3e mot : décalage de uint256[]

    n = word(args, bytes_off // 32)    # une fois le décalage converti en index de mot, lire le préfixe de longueur
    s = args[bytes_off + 32: bytes_off + 32 + n].decode()

    k = word(args, arr_off // 32)      # pour un tableau aussi, lire d'abord le nombre d'éléments
    items = [word(args, arr_off // 32 + 1 + j) for j in range(k)]
    return s, flag, items              # ("dave", True, [1, 2, 3])
```

## L'ABI n'est pas auto-descriptive : le schéma est indispensable hors chaîne

La spécification l'indique dès son introduction : cet encodage n'est pas auto-descriptif et le décodage exige un schéma. La portée opérationnelle de cette phrase dépasse ce qu'elle laisse paraître à la lecture.

L'EVM ne reçoit que des octets : noms de fonctions, noms de paramètres et informations de type disparaissent tous à la compilation. La répartition repose sur la comparaison du sélecteur de 4 octets, la cible du saut étant un switch généré par le compilateur ; les données de retour ne sont elles aussi qu'un flux d'octets et, pour lire une valeur de retour de longueur dynamique, l'appelant a besoin d'opérations telles que `RETURNDATASIZE` et `RETURNDATACOPY` (proposées par l'EIP-211, activées par la mise à niveau Byzantium). Aucun emplacement on-chain ne stocke le JSON d'ABI : celui-ci ne peut être publié qu'avec le code source, en tant que produit de compilation. Il en découle plusieurs contraintes concrètes.

Pour afficher « quelle fonction a été appelée et avec quels paramètres », un explorateur de blocs doit d'abord disposer du code source vérifié et de l'ABI. Un contrat non vérifié ne peut montrer que le calldata brut ou, à défaut, recourir à une base de type 4byte dont la recherche ne renvoie qu'un ensemble de signatures candidates, sans réponse certaine en cas de collision. Les indexeurs et les pipelines de données doivent connaître l'ABI au moment du déploiement, faute de quoi ils ne peuvent indexer ni les événements ni les appels. L'évolution de l'interface ne dispose pas non plus d'un mécanisme de compatibilité tel qu'une numérotation de champs : ajouter, supprimer ou réordonner les paramètres d'une fonction change le sélecteur, ce qui constitue une rupture d'interface totale pour l'appelant ; une mise à niveau peut donc généralement seulement ajouter des fonctions, et non modifier les anciennes. Un contrat proxy peut ajouter des fonctions au niveau de l'implémentation, mais les sélecteurs doivent rester stables : toute « refonte » modifiant une signature fait retomber les appels existants dans la branche fallback, laquelle ne reçoit que le calldata brut et ne peut pas restituer l'intention de l'appelant.

Deux autres points sont souvent mal compris. Un sélecteur identique ne signifie pas que les encodages sont interchangeables : `transfer(address,uint256)` et `many_msg_babbage(bytes1)` partagent `0xa9059cbb`, mais leurs longueurs de paramètres et leurs sémantiques diffèrent totalement ; en cas de collision, un outil doit croiser le code du contrat ou la longueur du calldata pour trancher. Le sélecteur d'une erreur personnalisée (custom error) provient lui aussi des 4 premiers octets de la signature d'erreur : n'importe quel contrat peut renvoyer des données correspondant à la signature d'une erreur donnée. La spécification avertit donc explicitement les appelants de ne pas traiter ces données d'erreur comme une information de source fiable ; elles ne valent que comme indice.

## Les événements placent la capacité de recherche dans les topics et la lisibilité dans data

Les règles d'encodage des événements ont la même origine que celles des appels de fonctions, mais leur point d'application diffère. Un journal se compose de l'adresse du contrat, d'au plus 4 sujets (topics) et d'un segment de données de longueur arbitraire. Pour un événement non anonyme, `topics[0]` est toujours le hachage Keccak-256 de la signature de l'événement, sans troncature à 4 octets : c'est pourquoi, lors du filtrage d'événements avec `eth_getLogs`, la condition porte sur le hachage de signature et non sur le nom de l'événement. La signature d'événement emploie elle aussi la forme canonique : `uint` est normalisé en `uint256`.

Les autres paramètres se répartissent en deux voies selon qu'ils sont `indexed` ou non. Un paramètre `indexed` de type valeur est encodé directement dans `topics[1]` à `topics[3]` : remplissage des poids forts pour les entiers, remplissage à droite pour les octets de taille fixe, et les 20 octets de poids faible pour une adresse. Un événement non anonyme admet au plus 3 paramètres indexed, ce qui, avec le sujet de signature, en remplit exactement 4 ; un événement déclaré `anonymous` n'écrit pas de sujet de signature et admet jusqu'à 4 paramètres indexed, au prix de la perte du filtrage par signature. Les paramètres non `indexed` sont encodés dans `data` selon les règles de l'ABI ; ces données présentent elles aussi une structure tête/queue, et les paramètres dynamiques y sont toujours adressés par décalage.

Les différences se concentrent sur les types complexes marqués indexed. Dès qu'un tableau, un `string`, un `bytes` ou un struct est marqué `indexed`, c'est le hachage Keccak-256 de cet encodage qui est écrit dans le topic. Un hachage étant irréversible, ces paramètres peuvent être recherchés avec précision : en calculant à l'avance le hachage de la valeur cible, on peut l'utiliser comme condition de filtre pour retrouver le journal ; en revanche, il est impossible de reconstituer la valeur d'origine depuis le journal, seulement de confirmer ou d'infirmer la correspondance. Le remède préconisé par la spécification consiste à déclarer deux paramètres portant la même valeur, l'un indexed pour la recherche, l'autre non indexed pour la lecture, au prix d'un volume de journal et d'un coût en gaz plus élevés. Les règles d'encodage du hachage laissent elles-mêmes une ambiguïté : lorsqu'un struct contient plusieurs tableaux dynamiques, la séquence d'octets concaténée peut ne pas être unique ; la spécification recommande de ne pas conclure au sens d'un événement sur la seule base du résultat de recherche d'un paramètre indexed.

Il faut enfin distinguer « recherchable » et « décodable ». Un paramètre indexed de type valeur se lit directement ; un paramètre indexed de type dynamique peut seulement être retrouvé par correspondance, pas lu ; un paramètre non indexed se lit mais ne se recherche pas par valeur. En séparant topics et data, le journal réalise un compromis entre volume du journal, capacité de recherche et capacité de décodage : aucune catégorie de paramètre ne réunit simultanément toutes ces propriétés.

## Sources

- Documentation Solidity « Contract ABI Specification », sélecteur de fonction, disposition tête/queue, mode d'encodage strict, spécification d'encodage des événements et des paramètres indexed, exemple de décodage `sam` : https://docs.soliditylang.org/en/latest/abi-spec.html
- Liste des signatures candidates pour `0xa9059cbb` sur 4byte.directory (consultée en septembre 2026, entrées librement soumises) : https://www.4byte.directory/signatures/?bytes4_signature=0xa9059cbb
- EIP-609 « Hardfork Meta: Byzantium », méta-proposition inscrivant les EIP-211 et EIP-214 dans la liste d'inclusion de Byzantium : https://eips.ethereum.org/EIPS/eip-609
- EIP-2028 « Transaction data gas cost reduction », octet de calldata non nul ramené de 68 gas à 16 gas : https://eips.ethereum.org/EIPS/eip-2028
- EIP-211 « New opcodes: RETURNDATASIZE and RETURNDATACOPY », lecture de données de retour dynamiques et `BYZANTIUM_FORK_BLKNUM` : https://eips.ethereum.org/EIPS/eip-211
- Ethereum Yellow Paper, `G_txdatazero` à 4 gas et `G_txdatanonzero` à 16 gas dans la table des frais de l'annexe G : https://ethereum.github.io/yellowpaper/paper.pdf
- Référence des opcodes EVM d'ethereum.org et table dynamique des coûts en gaz de wolflo/evm-opcodes, tarification des octets nuls et non nuls du gaz intrinsèque de transaction : https://ethereum.org/en/developers/docs/evm/opcodes/ et https://github.com/wolflo/evm-opcodes/blob/main/gas.md

## Pour aller plus loin

- [« Ce que signifie réellement la compatibilité EVM : bytecode, précompilations, JSON-RPC et outillage »](/fr/blog/evm-compatibility-explained)
- [« Parallélisation optimiste de Bitroot : détection, réexécution et déterminisme »](/fr/blog/bitrootevm-)

Pour la relation entre les instructions d'appel et le contexte, voir 0.17 « Comparatif des instructions d'appel : CALL, CALLCODE, DELEGATECALL et STATICCALL ».
