Analyse de sécurité des hooks Uniswap v4 : architecture, vulnérabilités et meilleures pratiques

icon MarsBit
Partager
AI summary iconRésumé

Depuis le déploiement d'Uniswap v4 sur la chaîne principale, le mécanisme Hook est devenu l'une des innovations les plus remarquées dans le domaine DeFi. La plateforme de lancement de memecoins sur la chaîne Base, Flaunch, utilise Hook pour implémenter un prix de prévente fixe et un mécanisme de liquidation automatique à l'entrée en bourse ; le protocole de liquidité Bunni v2 utilise Hook pour construire un modèle de liquidité programmable et de ré-encollatéralisation ; cette année, des tokens comme SATO, uPEG (Unipeg) et Slonks, basés sur des stratégies liées à Hook, ont connu des hausses de plusieurs dizaines de fois en peu de temps.

Dans le même temps où l'écosystème Hook prospère, les attaques exploitant les défauts d'implémentation de Hook augmentent considérablement. Cet article commence par le mécanisme Hook de Uniswap v4, puis analyse progressivement sa pile d'appels principale afin d'aider les projets à comprendre les vulnérabilités potentielles.

Sécurité du Hook Uniswap v4

1. Introduction

Le changement d'architecture le plus significatif d'Uniswap v4 par rapport à v3 est l'introduction du mécanisme de Hook : il permet aux développeurs de connecter des contrats personnalisés aux événements du cycle de vie des pools de liquidité, en injectant des logiques arbitraires lors des étapes de swap, d'ajout ou de retrait de liquidité, d'initialisation, etc.

Les principales modifications de la v4 sont les suivantes :

Modèle Singleton : l'état de tous les pools est géré de manière centralisée par un seul contrat PoolManager, sans déploiement de contrats indépendants pour chaque pool.

- Comptabilité flash : les variations de solde intermédiaires pendant le processus de transaction sont enregistrées uniquement dans le stockage temporaire et sont régularisées en une seule fois à la fin de la transaction.

Mécanisme Hook : chaque pool peut être lié à un contrat Hook, et PoolManager appellera ce contrat aux points clés (beforeInitialize, beforeSwap, afterAddLiquidity, etc.)

- Le Hook ne peut pas être remplacé : une fois l'initialisation du pool terminée, l'adresse Hook liée est définitivement fixée (l'adresse Hook liée au pool ne peut pas être modifiée, mais la possibilité de mettre à jour le contrat Hook dépend de sa mise en œuvre).

Image

Pendant la période v3, les développeurs n'avaient besoin de faire confiance qu'au protocole Uniswap lui-même ; pendant la période v4, la sécurité de chaque pool dépend de son Hook associé. Les Hooks ont transformé l'AMM d'un primitif financier fixe en une infrastructure financière programmable, mais le modèle de sécurité s'est fragmenté du niveau « protocole » au niveau « pool ».

2. Architecture Hook

2.1 PoolManager et modèle unlock/callback

Le contrat principal de la version v4 est le PoolManager en tant que singleton. Toute opération de modification d'état du pool (swap, ajout ou retrait de liquidité) doit d'abord appeler PoolManager.unlock() pour obtenir une autorisation de rappel unique, puis accomplir l'action spécifique dans unlockCallback(). À la fin du processus, PoolManager vérifie que le livre est équilibré :

Image

Revert directement lorsque NonzeroDeltaCount != 0 ; il s'agit de la contrainte fondamentale du flash accounting v4. Tout Hook peut temporairement déséquilibrer la comptabilité pendant son exécution, mais doit régler lui-même ce déséquilibre avant la fin de la transaction ; sinon, l'ensemble de la transaction est annulée.

Chaque pool est identifié de manière unique par la structure PoolKey, qui contient le champ hooks :

Image

PoolId est calculé à partir de keccak256(PoolKey), donc des adresses hooks différentes produisent des pools différents. Cela signifie également que PoolManager ne vérifie pas si une adresse hook a déjà été utilisée pour un autre pool ; un même contrat hook peut être lié simultanément à plusieurs pools.

Image

2.2 Codage des bits d'autorisation Hook dans l'adresse

Une conception contre-intuitive de v4 est que les autorisations des Hook ne sont pas déterminées par une variable interne au contrat, mais par l'adresse de déploiement du contrat Hook.

PoolManager vérifie les 14 bits les moins significatifs de l'adresse Hook pour déterminer si ce Hook doit être appelé à un point spécifique du cycle de vie :

Image

Par exemple, BEFORE_SWAP_FLAG = 1 << 7. Si le 7e bit de l'adresse Hook est à 1, PoolManager appelle la fonction beforeSwap() de ce Hook avant l'échange ; sinon, même si le contrat Hook implémente beforeSwap(), il ne sera jamais appelé par PoolManager.

Cela signifie que lors du déploiement de Hook, l'adresse doit être calculée à l'aide de CREATE2 + salt afin de construire une adresse dont les bits de poids faible correspondent exactement aux permissions cibles. Uniswap fournit officiellement l'outil HookMiner à cette fin :

Image

Lorsque les bits d'autorisation ne correspondent pas à l'implémentation de la fonction, deux types de problèmes se produisent :

(1) Une fonction hook a été implémentée, mais l'adresse n'est pas encodée avec les bits de permission correspondants — PoolManager n'appellera jamais cette fonction, ce qui rend la logique inopérante.

(2) L'adresse encode un bit de permission, mais le hook n'implémente pas la fonction correspondante — PoolManager peut subir un revert lors du rappel, entraînant un DOS ou un échec de validation de la valeur de retour, empêchant l'exécution de l'opération concernée.

Cela constitue également un obstacle naturel à la mise à niveau de Hook : si Hook peut être mis à niveau via un proxy, l'adresse de déploiement reste inchangée lors de la mise à niveau, ce qui signifie qu'après la mise à niveau, il n'est possible de modifier que l'implémentation des fonctions Hook existantes, et non d'ajouter de nouveaux types de Hook. Pour prévoir une capacité d'extension future, il faut préalablement réserver tous les bits de permissions susceptibles d'être utilisés lors du déploiement initial.

2.3 BaseHook et un piège de contrôle d'accès souvent négligé

Le contrat abstrait BaseHook fourni par la péripérie Uniswap v4 permet aux développeurs de l'hériter pour implémenter un Hook personnalisé. L'un des rôles importants de BaseHook est de fournir le modificateur onlyPoolManager à la fonction unlockCallback() :

Image

Mais — il existe ici un piège de conception facilement négligé — les versions antérieures de BaseHook n'ajoutaient onlyPoolManager qu'au seul unlockCallback, sans aucune protection pour les autres fonctions de rappel hook (beforeSwap, afterSwap, beforeAddLiquidity, etc.). Le contrôle d'accès à ces fonctions doit être explicitement ajouté par le développeur du hook.

3. Lecture du code de cycle de vie du hook

À titre d'exemple d'un swap à entrée exacte, voici l'analyse de la pile d'appels complète, de l'initiation de la transaction par l'utilisateur jusqu'à son règlement.

3.1 Initialisation du pool et liaison du Hook

N'importe qui peut appeler PoolManager.initialize() pour créer un nouveau pool :

Image

isValidHookAddress vérifie uniquement la compatibilité entre les bits de permissions de l'adresse et le champ fee, sans vérifier si le Hook est déjà utilisé dans un autre pool, ni si ce Hook « accepte » ce PoolKey. Si le Hook n'a pas été conçu avec une logique de liste blanche ou de liaison à un seul pool dans beforeInitialize, n'importe qui peut créer un nouveau pool utilisant le même Hook mais avec n'importe quelle paire de jetons, et déclencher tous les rappels ultérieurs du Hook.

3.2 beforeSwap et BeforeSwapDelta

L'entrée du processus de swap est PoolManager.swap(), qui appelle Hooks.beforeSwap() avant d'exécuter la logique principale de swap :

Image

La valeur de retour de beforeSwap est un triplet (bytes4, BeforeSwapDelta, uint24) :

- bytes4 : doit être égal à IHooks.beforeSwap.selector, sinon PoolManager revert directement

- BeforeSwapDelta : Le hook effectue un ajustement delta pour le token spécifié et le token non spécifié lors de ce swap

- uint24 : valeur de couverture du taux de LP dynamique (actif uniquement lorsque le pool a activé les taux dynamiques)

BeforeSwapDelta est un alias de int256, les 128 bits supérieurs représentent le delta du token spécifié (c'est-à-dire le token dont l'utilisateur a spécifié la quantité), et les 128 bits inférieurs représentent le delta du token non spécifié :

Image

Il faut noter que la sémantique de BeforeSwapDelta est la suivante : le Hook doit retourner une valeur positive lorsqu'il perçoit des frais et une valeur négative lorsqu'il rembourse des jetons. Les développeurs ont facilement tendance à inverser le signe ; en outre, la correspondance entre specified et unspecified dépend du signe de params.zeroForOne et de amountSpecified ; une écriture légèrement erronée peut entraîner un décalage des jetons.

PoolManager ajoute directement specifiedDelta retourné par beforeSwap à amountToSwap :

Image

Cette ligne implique une signification clé : le Hook peut intercepter le montant du swap. Lorsque hookDeltaSpecified est égal à -params.amountSpecified, amountToSwap est directement mis à zéro, ce qui équivaut à ce que le Hook prenne entièrement en charge ce swap — on parle alors de Hook Async ou Custom Curve Hook.

Async Hook est l'un des modèles de conception les plus risqués dans la version v4 : il remplace essentiellement la logique d'échange de Uniswap par sa propre logique via un Hook. Si le Hook contient une vulnérabilité ou est malveillant par nature, les fonds des utilisateurs ne sont plus protégés par la logique de tarification native de Uniswap, mais dépendent principalement de la correction de l'implémentation du Hook.

3.3 Règlement delta et NonzeroDeltaCount

Le delta retourné par beforeSwap et afterSwap ne déclenche pas de transfert immédiat, mais est enregistré dans le registre interne de PoolManager :

Image

Lorsqu'un delta cumulé d'un token passe de zéro à non nul, NonzeroDeltaCount est incrémenté ; lorsqu'il revient à zéro, il est décrémenté. Comme indiqué en 2.1, à la fin de unlock(), si NonzeroDeltaCount != 0, l'ensemble de la transaction est revert.

Hook équilibre son delta grâce à deux actions : settle() (transfert vers PoolManager) et take() (retrait depuis PoolManager) :

Image

La sémantique de sécurité apportée par ce mécanisme est claire : tous doivent finalement équilibrer leurs comptes. Toutefois, il garantit uniquement la « conservation des comptes », et non leur « exactitude ». Si Hook retourne un delta malveillamment construit dans beforeSwap, PoolManager enregistrera fidèlement ce delta ; tant que le compte est finalement réglé, la transaction est considérée comme réussie — même si cela signifie que Hook peut, par la falsification d'états métier, induire le système en erreur en lui faisant croire que l'attaquant possède certains droits sur des actifs, alors que PoolManager ne peut pas détecter cette erreur au niveau métier.

L'événement de sécurité précédent de Cork Protocol était dû à une vulnérabilité dans son Hook, alors qu'il avait déjà été audité par quatre sociétés d'audit avant l'attaque. Après coup, nous avons constaté que :

Trois des quatre audits ne comprennent pas le contrat CorkHook.

Un seul audit de CorkHook a identifié certaines problématiques de code et soumis des recommandations d'amélioration, mais n'a pas couvert entièrement les problèmes de contrôle d'accès.

Un autre auditeur a clairement recommandé dans son rapport : « un suivi intéressant consisterait à prouver les invariants des fonctions CorkHook appelées par différents composants vérifiés dans le cadre de cette mission ». Cette recommandation présente une grande pertinence du point de vue d'une analyse postérieure.

Cela révèle une nouvelle zone aveugle dans l'ère des v4 Hooks : l'explosion de la complexité des protocoles fait de la délimitation du périmètre elle-même une décision de sécurité. Les chaînes d'interactions entre le Hook et les autres contrats du protocole sont très longues ; auditer uniquement le contrat Hook ne permet pas de détecter les problèmes de composition inter-contrats ; à l'inverse, auditer les contrats environnants tout en excluant le Hook du périmètre fait passer sous silence la plus grande surface d'attaque de l'ère v4.

4. Réflexion

En comparant le mécanisme du protocole et la reconstitution de l'attaque Cork, on peut dégager plusieurs points clés du modèle de sécurité v4 Hook :

(1) Si la fonction de rappel Hook dépend du contexte d'appel fourni par PoolManager, il faut explicitement restreindre son appel uniquement à PoolManager. BaseHook ne le fera pas à la place du développeur ; il s'agit de la piège de conception le plus en conflit avec les expériences d'audit de contrats standards en v4.

(2) La liaison entre Hook et le pool n'est pas restreinte par PoolManager. Les développeurs doivent implémenter eux-mêmes une liste blanche de pools ou une liaison à un seul pool dans beforeInitialize.

(3) Les bits d'autorisation de l'adresse Hook doivent être strictement cohérents avec l'implémentation de la fonction. L'adresse calculée doit inclure préalablement tous les bits d'autorisation susceptibles d'être étendus à l'avenir.

(4) Le hook Async / Custom Curve est une implémentation entièrement personnalisée de swap. Il ne comporte aucune protection au niveau du protocole Uniswap et doit être audité selon les normes d’un contrat financier entièrement autonome.

(5) La « conservation » de la delta comptable n'équivaut pas à la « correction ». NonzeroDeltaCount == 0 garantit uniquement que le livre est équilibré à la fin, mais pas que son contenu n'a pas été manipulé de manière malveillante.

(6) La confusion des types de jetons entre marchés constitue une nouvelle surface d'attaque à l'ère v4. Lorsque les protocoles permettent aux utilisateurs de créer des marchés, une vérification sémantique des jetons est obligatoire et ne peut pas se limiter à une vérification d'interface.

Chaque Hook constitue un domaine de confiance indépendant, et la sécurité de chaque pool est déterminée par le Hook qui lui est lié. La complexité de l'audit de sécurité des Hooks n'est donc plus « auditer un seul code », mais « auditer un sous-protocole complet » — ce changement implique une évolution méthodologique pour les équipes de projet ainsi que pour les auditeurs.

Voir l'article original

Clause de non-responsabilité : les informations sur cette page peuvent avoir été obtenues auprès de tiers et ne reflètent pas nécessairement les points de vue ou opinions de KuCoin. Ce contenu est fourni à titre informatif uniquement, sans aucune représentation ou garantie d’aucune sorte, et ne doit pas être interprété comme un conseil en investissement. KuCoin ne sera pas responsable des erreurs ou omissions, ni des résultats résultant de l’utilisation de ces informations. Les investissements dans les actifs numériques peuvent être risqués. Veuillez évaluer soigneusement les risques d’un produit et votre tolérance au risque en fonction de votre propre situation financière. Pour plus d’informations, veuillez consulter nos conditions d’utilisation et divulgation des risques.