Traçage et graphes de transactions · Leçon 5 sur 6
Traçage sur les chaînes à modèle de comptes, des jetons et des NFT
17 min de lecture
Sur Ethereum et les autres chaînes à modèle de comptes, une grande partie de la valeur impliquée dans une fraude ou un vol typique n’apparaît jamais sous la forme d’un simple transfert entre deux adresses. Elle circule à l’intérieur d’appels de contrats, sous forme d’événements de jetons dont le contrat lui-même a choisi le contenu à émettre, ou au moyen d’une signature que la victime a fournie plusieurs jours auparavant. Un examinateur qui ne suit que l’onglet « transactions » d’un explorateur manquera des étapes, attribuera à tort les mouvements de fonds et présentera des chiffres que la partie adverse pourra réfuter à partir de la même chaîne, parfois avec un écart d’un facteur de mille milliards.
Quatre canaux de circulation de la valeur
Chaque canal est enregistré à un endroit différent ; une fiche d’étape doit donc préciser la provenance de ses éléments de preuve.
| Canal | Lieu d’enregistrement | Piège pour l’analyse forensique |
|---|---|---|
| Valeur de premier niveau : ETH natif envoyé par une transaction | La transaction elle-même | Une transaction échouée ne déplace aucune valeur, mais paie tout de même du gaz |
| Transferts internes : ETH envoyé par un contrat pendant l’exécution | Uniquement dans les traces d’exécution | Absents des reçus et des journaux ; certains explorateurs les affichent, d’autres non |
| Transferts de jetons : ERC-20, ERC-721, ERC-1155 | Événements Transfer dans les journaux du reçu | L’adresse du contrat de l’événement identifie le jeton, et non son symbole |
| Modifications d’état sans événements standard : rebasage, frais sur transfert, jetons non standard | Stockage du contrat | Les sommes des événements ne correspondent pas aux soldes ; effectuer un rapprochement au moyen de requêtes d’état à un bloc donné |
Les transferts internes sont la source la plus fréquente d’étapes manquées : un échange qui verse de l’ETH natif, un contrat de réacheminement ou un contrat de portefeuille regroupant des opérations déplace de l’ETH par des appels qui ne laissent aucun journal. Leur obtention nécessite un nœud capable de produire des traces, généralement un nœud d’archive, et les noms des méthodes diffèrent selon les clients (debug_traceTransaction avec un traceur d’appels chez certains, trace_transaction chez d’autres). Consignez le client, sa version et la méthode à l’origine de chaque trace, car deux traceurs peuvent présenter différemment la même exécution (les leçons 2.4, 2.9 et 5.1 couvrent le décodage).
Lire les transferts ERC-20
Un jeton ERC-20 (EIP-20) est un contrat qui conserve une correspondance entre adresses et soldes. Lorsqu’une personne « envoie des USDC », le champ to de la transaction désigne le contrat du jeton (ou un routeur), sa valeur native value est généralement de 0, et le destinataire n’apparaît que dans les données d’appel et les journaux. Les modifications de solde émettent Transfer(address indexed _from, address indexed _to, uint256 _value), avec topic0 égal à 0xddf252ad…b3ef sur toutes les chaînes EVM, l’expéditeur et le destinataire dans les topics 1 et 2, et le montant brut dans data.
L’identité du jeton est l’adresse du contrat qui émet le journal, jamais son nom ou son symbole : n’importe qui peut déployer un contrat appelé « USDC » qui émet des événements Transfer parfaitement formés. Les événements sont également écrits par le propre code du contrat ; un contrat malveillant peut donc émettre des événements Transfer qui ne correspondent à aucune modification de solde. Filtrez ou signalez les transferts de valeur nulle et les transferts de jetons non répertoriés avant l’analyse.
Décimales et unités
Le journal stocke un entier en unités de base. Le nombre de décimales qui permet de le convertir en unités lisibles n’est pas inclus dans l’événement ; lisez-le une fois par contrat avec decimals() et conservez-le avec sa source.
| Jeton (réseau principal Ethereum) | Décimales | Valeur brute pour 1 unité |
|---|---|---|
| USDC, USDT | 6 | 1 000 000 |
| WBTC | 8 | 100 000 000 |
| DAI, WETH, plupart des jetons | 18 | 1 000 000 000 000 000 000 |
Appliquer 18 décimales à un jeton qui en utilise 6 sous-estime les montants d’un facteur de 10^12 ; l’inverse les surestime du même facteur. Un même actif peut utiliser un nombre différent de décimales selon la chaîne, car ses versions transférées par pont et réémises sont des contrats distincts. Les valeurs brutes sont des entiers de 256 bits (jusqu’à 78 chiffres) : stockez-les donc sous forme de chaînes de caractères ou de nombres décimaux de haute précision et n’effectuez la division qu’au moment de la présentation.
Créations, destructions et jetons qui dérogent au modèle
Les créations sont des événements Transfer provenant de l’adresse zéro, et la plupart des destructions sont des événements Transfer à destination de celle-ci ; envoyer des jetons à une adresse « morte » telle que 0x…dEaD n’est pas une destruction, car le solde existe toujours. Certains jetons dérogent à cette convention avec des événements Mint et Burn personnalisés, et le contrat WETH d’origine (WETH9) émet Deposit et Withdrawal sans Transfer : un solde WETH reconstitué uniquement à partir des journaux Transfer est donc erroné pour toute adresse qui effectue un enveloppement ou un désenveloppement.
La règle « solde = valeurs Transfer entrantes moins valeurs Transfer sortantes » échoue également pour les modèles suivants :
| Modèle | Ce qui se produit | Mesure à prendre |
|---|---|---|
| Frais sur transfert | Un transfert de 100 délivre, par exemple, 98 ; 2 vont à un portefeuille de frais ou sont détruits, souvent sous la forme d’un second événement Transfer | Utiliser le montant du Transfer vers le destinataire, et non le montant des données d’appel |
| Rebasage | Les soldes changent sans aucun événement Transfer | Lire balanceOf à différentes hauteurs de bloc ; modéliser le rebasage |
| Fondé sur des parts | Le contrat suit des parts ; les soldes affichés sont arrondis à la baisse de quelques unités de base | Suivre les événements relatifs aux parts lorsqu’ils existent |
| Gelable ou suspendable | L’émetteur peut bloquer une adresse ou détruire son solde au moyen de fonctions personnalisées | Décoder également les événements d’administration de l’émetteur |
Le stETH de Lido est un exemple bien connu de rebasage : les soldes augmentent lorsque l’oracle du protocole signale des récompenses, sans émission de Transfer, et les transferts peuvent laisser 1 à 2 wei en raison de l’arrondi des parts. Sa version enveloppée, wstETH, se comporte comme un ERC-20 ordinaire.
Avant de présenter les flux de jetons, effectuez un rapprochement : identifiez le jeton par l’adresse du contrat et l’identifiant de chaîne ; reconstituez les soldes pertinents à partir des journaux Transfer jusqu’au bloc de clôture ; comparez-les avec balanceOf, et comparez les créations moins les destructions avec totalSupply(), à ce bloc ; expliquez chaque différence ou indiquez-la comme une limite. Les événements de jetons sont des affirmations produites par le code du contrat ; les requêtes d’état permettent de les vérifier.
Autorisations, transferFrom et draineurs
ERC-20 permet à un propriétaire d’autoriser un dépensier avec approve(spender, amount), qui émet Approval(owner, spender, value). Le dépensier, généralement un routeur DEX ou un pool de prêt, appelle ensuite transferFrom(owner, recipient, amount) ; dans le Transfer qui en résulte, from est le propriétaire dont les jetons ont été déplacés, et non le compte qui a envoyé la transaction. Les autorisations illimitées (2^256 − 1, soit 64 chiffres hexadécimaux f) permettent au dépensier de déplacer à tout moment la totalité du solde du propriétaire. Les plafonds autorisés ne peuvent pas toujours être reconstitués à partir des événements (la bibliothèque largement copiée d’OpenZeppelin a cessé, dans sa version 5.0, d’émettre un nouvel Approval lorsque transferFrom consomme un plafond autorisé) : lisez donc allowance(owner, spender) au bloc qui vous intéresse.
Un draineur est un kit d’hameçonnage qui persuade une victime d’accorder un droit de dépense, puis l’utilise. L’acte de la victime prend l’une des formes suivantes :
- une transaction d’autorisation sur chaîne, souvent illimitée, signée et envoyée par la victime ;
- une signature permit hors chaîne (EIP-2612), que l’attaquant soumet en payant le gaz ; la spécification permet à toute adresse de soumettre un permit valide, et sa réussite définit le plafond autorisé et émet un événement
Approvaldans une transaction que la victime n’a jamais envoyée ; - une signature destinée à un contrat d’autorisation partagé que la victime avait déjà autorisé, lequel autorise ensuite le transfert.
Le contrat draineur appelle ensuite transferFrom et répartit les fonds obtenus. Une étude de He et de ses collègues de Zhejiang University et de Mohamed bin Zayed University of Artificial Intelligence (Démasquer l’économie de l’ombre : analyse approfondie de l’hameçonnage par draineur en tant que service sur Ethereum, 2025), portant sur la période de mars 2023 à avril 2025, a constaté que la répartition la plus courante était de 20 % pour l’opérateur du kit et de 80 % pour l’affilié ayant mené la campagne d’hameçonnage. Une répartition fixe répétée chez de nombreuses victimes constitue un schéma significatif, mais elle reste une inférence sur les rôles tant que d’autres éléments de preuve ne la confirment pas.
Une partie des éléments de preuve se trouve donc hors chaîne. Dans un cadre légalement autorisé, l’appareil de la victime peut contenir l’historique de navigation du site d’hameçonnage et l’enregistrement des demandes de signature du portefeuille, qui permettent ensemble de dater l’acte de la victime (leçon 7.3). Les signatures hors chaîne ne laissent aucune trace sur chaîne avant leur utilisation ; le délai entre la signature et le drainage peut donc être long.
Exemple de traçage : un draineur utilisant permit
Une victime signale la perte de 25 000 USDC. L’examinateur décode les transactions pertinentes à partir d’un nœud de traçage :
tx 0x3b9e…a417 bloc 24,598,331 statut 1 expéditeur 0xD7c4…19
appel 0xD7c4…19 -> 0x8f2A…c3 (contrat, non vérifié)
journal 41 jeton 0xA0b8…eB48 Approval propriétaire 0x5e1C…2e dépensier 0x8f2A…c3 valeur 25000000000
journal 42 jeton 0xA0b8…eB48 Transfer de 0x5e1C…2e vers 0x0dE3…7a valeur 5000000000
journal 43 jeton 0xA0b8…eB48 Transfer de 0x5e1C…2e vers 0xAf10…d0 valeur 20000000000
tx 0x77c0…5b12 bloc 24,598,402 statut 1 expéditeur 0xAf10…d0
journal 7 jeton 0xA0b8…eB48 Transfer de 0xAf10…d0 vers 0x9c3B…e1 (pool) valeur 20000000000
journal 9 jeton WETH Transfer de 0x9c3B…e1 vers 0x4d1F…80 (routeur) valeur 7.9e18
trace [0,3] CALL 0x4d1F…80 -> 0xF5a9…31 valeur 7.9 ETH
tx 0x1e44…c9a0 bloc 24,598,460 statut 1 expéditeur 0xF5a9…31
valeur de premier niveau 0xF5a9…31 -> 0x2bD1…f6 7.89 ETH
La première transaction n’a pas été envoyée par la victime. Son expéditeur a appelé un contrat non vérifié, qui a soumis le permit de la victime (journal 41, un plafond autorisé de 25 000 USDC avec 6 décimales) et a sorti 5 000 et 20 000 USDC de l’adresse de la victime dans la même transaction (journaux 42 et 43). Les montants concordent : 5 000 + 20 000 = 25 000, soit une répartition 20/80. Un rapport indiquant « la victime a envoyé 25 000 USDC » décrirait de manière erronée un événement que la clé de la victime n’a jamais signé. Dans la deuxième transaction, le destinataire de la part la plus importante a échangé ses 20 000 USDC par l’intermédiaire d’un pool ; le routeur a désenveloppé 7,9 WETH et envoyé 7,9 ETH à une nouvelle adresse. Cette étape n’apparaît que dans la trace à la position [0,3] : un examinateur ne lisant que les journaux perdrait ici la piste des fonds. Dans la troisième transaction, la nouvelle adresse a envoyé 7,89 ETH, conservant environ 0,01 ETH sur lesquels elle a payé le gaz, à 0x2bD1…f6, qui réachemine ses dépôts vers un portefeuille à chaud d’une plateforme d’échange.
Chaque flux est ici de niveau A : enregistré sur chaîne et vérifiable à partir d’un nœud. Les rôles d’opérateur et d’affilié sont des inférences tirées de la répartition. L’étiquette de dépôt sur une plateforme d’échange ne repose que sur une seule heuristique tant que la plateforme d’échange ne la confirme pas ; à partir de là, le traçage se poursuit comme décrit dans la leçon 3.4.
Consigner une étape passant par un contrat
La fiche d’étape de la leçon 3.2 nécessite des champs supplémentaires sur les chaînes à modèle de comptes : l’index du journal ou la position dans la trace, car une transaction peut contenir des dizaines de transferts ; l’adresse du contrat du jeton, son nombre de décimales et la valeur entière brute ; l’expéditeur de la transaction ainsi que les champs from et to du transfert, car il s’agit souvent de parties différentes ; et le statut de la transaction, puisque les transactions annulées laissent des traces, mais aucun transfert durable.
Deux pièges d’identification sont propres aux jetons. Les jetons contrefaits copient le nom et le symbole d’un véritable stablecoin ; seule l’adresse du contrat permet de déterminer lequel est authentique. L’empoisonnement d’adresse envoie des transferts de valeur nulle, infime ou fictive depuis une adresse ressemblant à une adresse utilisée par la victime, dans l’espoir qu’elle la copie depuis l’historique. Comparez toujours les adresses complètes, jamais uniquement les premiers et les derniers caractères.
Stablecoins et visibilité de l’émetteur
Les transferts de stablecoins sont des événements de jetons ordinaires et se tracent de manière déterministe, avec deux caractéristiques supplémentaires. Premièrement, les principaux contrats de stablecoins adossés à des monnaies fiduciaires donnent à l’émetteur une fonction de gel (liste de blocage) : un solde qui n’a pas bougé peut donc parfois être immobilisé plus rapidement que par une décision de justice (leçon 10.1). Deuxièmement, l’émetteur voit la même chaîne que tout le monde et ne détient des dossiers d’identité que pour ses clients directs, généralement ceux qui créent et rachètent les jetons : il peut confirmer les adresses qu’il a gelées et les personnes qui ont créé ou racheté des jetons, mais pas qui contrôle l’adresse d’un détenteur ordinaire. Consignez la chaîne et l’adresse du contrat pour chaque étape en stablecoin : un même symbole existe sur de nombreuses chaînes, et une version transférée par pont est un contrat distinct que le gel de l’émetteur d’origine peut ne pas atteindre. Les flux importants de stablecoins sur Tron sont abordés dans la leçon 5.4.
NFT et standards multijetons
Un jeton ERC-721 attribue exactement un propriétaire à chaque identifiant de jeton. Son Transfer(address,address,uint256) possède le même topic0 qu’ERC-20, car indexed ne fait pas partie de la signature, mais l’identifiant du jeton est indexé : un Transfer ERC-721 possède quatre topics et un champ data vide, tandis qu’un Transfer ERC-20 possède trois topics avec le montant dans data. Un décodeur fondé uniquement sur topic0 interprète les identifiants de jetons comme des montants. ApprovalForAll permet à un opérateur, généralement un contrat de place de marché, de déplacer tous les jetons que le propriétaire détient dans une collection, et un hameçonnage qui l’obtient peut vider une collection en une seule transaction : c’est le premier événement à vérifier lorsqu’une victime signale le vol de NFT. Pour confirmer le standard qu’un contrat déclare prendre en charge, appelez supportsInterface (ERC-165) : 0x80ac58cd pour ERC-721, 0xd9b67a26 pour ERC-1155, 0x2a55205a pour le standard de redevances ERC-2981. Les premières collections peuvent être antérieures aux standards : les CryptoPunks (2017) utilisent des événements personnalisés tels que PunkTransfer et PunkBought, invisibles pour un décodeur limité à ERC-721.
ERC-1155 regroupe de nombreux types de jetons dans un seul contrat, chacun avec un id et une quantité par détenteur. Ses événements TransferSingle (topic0 0xc3d58168…0f62) et TransferBatch (0x4a39dc06…f7fb) comportent un opérateur, le compte qui effectue le transfert : une vente sur une place de marché affiche donc la place de marché comme opérateur et le vendeur comme from. Les créations et les destructions utilisent l’adresse zéro. Les événements par lots contiennent des tableaux d’identifiants et de valeurs ; un TransferBatch doit être converti en une ligne par identifiant, faute de quoi la majeure partie du volume d’un contrat utilisant intensivement les lots disparaît sans être signalée.
Ventes, redevances et flux de transactions fictives
Un événement Transfer ne contient aucun prix : les propriétaires déplacent des NFT entre leurs propres portefeuilles, les prêtent, les distribuent gratuitement et les vendent contre des paiements hors chaîne. Une vente est une inférence tirée d’un paiement dans la même transaction et, idéalement, de l’événement de règlement de la place de marché. Seaport, utilisé par OpenSea et d’autres, émet OrderFulfilled avec une offre (ce que donne le créateur de l’ordre) et une contrepartie (ce que reçoivent le créateur et les bénéficiaires des frais) : pour une annonce vendue à 10 ETH, la contrepartie verse 9,45 ETH au vendeur, 0,05 ETH (0,5 %) à une adresse de frais et 0,5 ETH (5 %) au créateur. Lorsqu’un vendeur accepte une offre d’achat, l’auteur de l’offre est l’acheteur, qui propose du WETH. Le prix par élément d’un lot est une ventilation et doit être présenté comme tel. Les redevances ERC-2981 sont informatives (OpenSea, par exemple, a rendu les frais de créateur facultatifs pour les nouvelles collections à compter du 31 août 2023) : mesurez donc les redevances réellement payées, et non le taux annoncé.
Les NFT peuvent déguiser un transfert de valeur en vente : une personne qui blanchit des fonds achète, avec des fonds illicites, un NFT à un portefeuille qu’elle contrôle également, et le produit de la vente ressemble à un revenu de négociation. Le volet paiement porte la valeur : suivez l’ETH ou le stablecoin versé au vendeur et traitez le mouvement du NFT comme un élément de contexte. Les signaux de transactions fictives comprennent des cycles fermés (A → B → A), une source de financement commune à l’acheteur et au vendeur, une sortie commune pour les fonds obtenus, une position combinée sans risque, un autofinancement par prêts flash ou par paiements dans la même transaction, et des prix nettement supérieurs à la fourchette de la collection. CryptoPunk 9998 est le cas célèbre : le 28 octobre 2021, il a été « vendu » pour environ 124 457 ETH (environ 532 millions de dollars) alors que, comme l’ont expliqué les créateurs de la collection, l’acheteur se l’était acheté à lui-même avec de l’argent emprunté, remboursé dans la même transaction. Une étude évaluée par les pairs (La Morgia et al., IEEE ICDCS 2023) a constaté que les transactions fictives représentaient environ 85 % du volume sur LooksRare, qui récompensait le volume de négociation par des jetons. Une requête recherchant les jetons rachetés par leur ancien vendeur fournit des pistes, pas des constatations ; le niveau de confiance augmente lorsque des signaux indépendants concordent (leçon 6.5).
Points clés pour l’examen : les quatre canaux de circulation de la valeur sur les chaînes à modèle de comptes et le lieu d’enregistrement de chacun ; pourquoi les transferts internes nécessitent un nœud capable de produire des traces et pourquoi le client, sa version et la méthode sont consignés ; l’identification des jetons par l’adresse du contrat, la conversion avec le bon nombre de décimales et les pièges des jetons contrefaits et de l’empoisonnement d’adresse ; les conventions de création et de destruction et les jetons qui y dérogent (WETH, frais sur transfert, rebasage), avec rapprochement par rapport àbalanceOfettotalSupply; approve,transferFrom, permit EIP-2612 et les répartitions des draineurs comme inférence sur les rôles ; la lecture et le rapprochement d’une trace décodée de draineur ; les champs supplémentaires de la fiche d’étape ; les gels de stablecoins et les limites de la visibilité de l’émetteur ; les topics ERC-721 par rapport à ERC-20, ApprovalForAll, l’opérateur et les lots ERC-1155 ; le fait qu’un transfert n’a pas de prix ; le suivi du volet paiement dans les flux de transactions fictives sur NFT.