Aller au contenu

Risque de stockage lié à delegatecall

Delegatecall exécute le code d'un autre contrat sur le stockage du contrat appelant. Découvrez comment les collisions de stockage, le pouvoir de mise à niveau et les cibles non fiables peuvent compromettre un proxy ou un portefeuille intelligent.

Mis à jour

À des fins éducatives uniquement ; ne constitue pas un conseil en investissement. Investir peut entraîner des pertes.

Réponse directe

delegatecall exécute le code d’un contrat cible dans le contexte du contrat appelant. L’appelant conserve son propre stockage, son solde et address(this), tandis que msg.sender et msg.value gardent les valeurs de l’appel initial.

Ce comportement permet de créer des proxys, des bibliothèques et des modules de portefeuille intelligent, mais il confère aussi au code délégué les pouvoirs effectifs de l’appelant. Toute écriture dans le stockage, tout transfert d’actifs, toute approbation ou tout appel externe est effectué en tant qu’appelant, et non en tant que cible ayant fourni le code.

Considérez chaque cible accessible par delegatecall comme du code privilégié. La sécurité dépend des règles de sélection de la cible, de la compatibilité des agencements de stockage, de l’état d’initialisation, des contrôles de mise à niveau et de l’implémentation exacte active pour la transaction.

Fonctionnement

Lors d’un appel externe ordinaire, le contrat appelé lit et écrit dans son propre stockage. Avec delegatecall, le bytecode de la cible s’exécute sur le stockage de l’appelant : une instruction SSTORE modifie un slot appartenant à l’appelant. Les noms de variables de la cible n’ont aucune importance à l’exécution ; seules les positions de slot calculées comptent.

Cela crée quatre limites à auditer :

  • Contrôle de la cible : déterminez si la destination est fixe, sélectionnée par un utilisateur, résolue par un registre ou modifiable par un administrateur.
  • Compatibilité du stockage : comparez l’ordre et les types des variables, l’héritage, les espaces réservés ainsi que les slots avec espace de noms ou standardisés dans toutes les versions de l’implémentation.
  • Initialisation et autorisation : vérifiez que les initialiseurs ne peuvent pas être rejoués et que les fonctions de mise à niveau ou de gestion des modules imposent l’appelant prévu et le délai de gouvernance défini.
  • Traitement des retours : vérifiez que les échecs sont propagés et que les données renvoyées sont décodées selon le type attendu ; les appels de bas niveau n’offrent pas les vérifications habituelles de Solidity sur le type du contrat.

ERC-1967 réduit les collisions de proxy en plaçant les adresses d’implémentation, de beacon et d’administrateur dans des slots standardisés, hors de l’allocation normale du compilateur. Il ne prouve pas qu’une implémentation est sûre ni qu’une mise à niveau autorisée est sans danger.

Exemple

Supposons qu’un portefeuille stocke owner dans slot 0. Un module d’extension compilé avec counter dans slot 0 incrémente le compteur lorsque le portefeuille l’appelle par delegatecall.

Cette écriture modifie la valeur owner du portefeuille, car le stockage appartient au portefeuille. Si le mot obtenu encode une adresse contrôlée par un attaquant, les contrôles d’autorisation ultérieurs peuvent reconnaître l’attaquant comme propriétaire, même si le module d’extension n’a jamais détenu les actifs du portefeuille.

Un reçu indiquant une réussite ne distingue pas les changements d’état voulus des changements nuisibles. La simulation de transaction doit donc examiner les différences de stockage, les variations d’actifs et d’approbations, les événements émis et les appels en aval à partir des adresses exactes du proxy et de l’implémentation.

Risques

  • Exécution sur une cible arbitraire : des cibles contrôlées par l’utilisateur ou insuffisamment validées peuvent exécuter du code malveillant avec les autorisations de l’appelant.
  • Collision de stockage : une implémentation peut écraser la propriété, les soldes, l’état de pause ou même le slot qui sélectionne l’implémentation suivante.
  • Mise à niveau dangereuse : un administrateur ou un processus de gouvernance compromis peut remplacer du code précédemment examiné après que les utilisateurs ont déposé des actifs ou accordé des approbations.
  • Échec de l’initialisation : un proxy ou une implémentation non initialisé peut permettre à un autre compte de s’attribuer des rôles privilégiés ou de configurer des dépendances dangereuses.
  • Inspection trompeuse : la vérification du seul code source du proxy, de l’implémentation actuelle ou de l’interface peut omettre un beacon, une mise à niveau en attente, un registre de modules ou un autre chemin d’exécution.

Avant de signer, déterminez l’implémentation à un bloc récent, vérifiez qui peut la modifier et sous quel délai, examinez le bytecode vérifié et l’agencement du stockage de la cible, simulez l’intégralité des calldata et comparez le stockage sensible ainsi que les approbations de jetons avant et après l’exécution. Pour un portefeuille intelligent, examinez également la façon dont les modules sont activés et désactivés, ainsi que les cibles qu’ils sont autorisés à choisir.

Idées reçues courantes

  • « La cible ne peut pas toucher aux actifs de l’appelant. » Le code délégué s’exécute en tant qu’appelant et peut invoquer des contrats externes, transférer des actifs ou créer des approbations si l’appelant dispose de ces capacités.
  • « Des noms de variables identiques empêchent les collisions. » L’EVM utilise les slots de stockage, et non les noms du code source. L’ordre de l’agencement, l’héritage et les types doivent rester compatibles.
  • « Un code de proxy vérifié signifie que le système est vérifié. » L’implémentation active, le beacon, l’administrateur des mises à niveau, l’état d’initialisation et les autorisations des modules constituent des parties distinctes du périmètre de confiance.

Sujets connexes

Sources

Navigation

Rechercher dans le wiki...