# Un inconnu a pointé le module

> Un Gnosis Safe a perdu 7,8 millions de dollars en ETH restaké — non parce que le Safe était défaillant, mais parce qu'un module activé a laissé un keeper quelconque le pointer vers la pool de l'attaquant. La classe est ancienne. La leçon tient à l'endroit d'où vous laissez venir une destination.

- Source: https://defiloops.com/fr/blog/the-executor-that-trusted-itself
- Published: 2026-09-15
- Category: Security
- Tags: security, agentic-wallet, authorization, smart-accounts, restaking
- Author: DeFiLoops

---
Le 15 septembre 2026, un utilisateur d'Ethereum a perdu environ **7,8 millions de dollars**
en ETH restaké depuis un portefeuille multisignature. Il y a un rebondissement mémorable —un
bot de front-running a pris l'argent avant l'attaquant lui-même— mais le rebondissement n'est
pas la leçon. La leçon, ce sont les quatre secondes d'avant, quand le portefeuille a fait
exactement ce qu'on lui a ordonné, ordonné par quelqu'un qui n'aurait dû pouvoir lui ordonner
quoi que ce soit.

Voici la forme de l'attaque, car c'est l'une des façons les plus courantes de vider un compte
intelligent et la partie défaillante paraît toujours correcte.

## Ce qui s'est réellement passé

La victime détenait environ 2 900 rsETH —un jeton de restaking liquide— dans un Gnosis Safe,
sous sa forme enveloppée par Aave. L'attaquant n'a jamais eu de clé.

À la place, il a utilisé un **appel public de keeper** pour pointer un module de liquidité
Uniswap V4 —un module que le compte avait activé— vers une **pool que l'attaquant venait de
créer**. Cette pool portait un hook malveillant. Le hook a déballé la position en rsETH
librement transférable, et le module, agissant avec l'autorité du Safe, l'a déplacé.

<Callout type="danger" title="Le Safe n'était pas le bug">
  C'est important, et c'est la première chose que les gens confondent. Gnosis Safe a fait ce
  qu'un portefeuille multisignature est conçu pour faire : il a laissé agir un module activé.
  Le défaut était dans le **module** —un contrat tiers installé au-dessus du Safe qui
  acceptait une destination de la part de son appelant. Blâmer le portefeuille, c'est manquer
  l'endroit où vivait l'erreur, et cette erreur, n'importe qui peut la répéter sur n'importe
  quel cadre de comptes.
</Callout>

## Le bug est dans l'origine de la destination

Retirez le restaking et les hooks, il reste une seule clause. Un module de confiance a reçu
une destination —quelle pool, quel contrat— de son appelant, et il a agi sur cette
destination depuis l'intérieur de son propre contexte privilégié. La validation qu'il faisait
ne suffisait pas, car l'attaquant pouvait choisir la seule chose qui décidait où allait
l'argent.

<Compare left="Ce que le module laissait fournir à l'appelant" right="Ce qu'un appelant ne devrait jamais fournir" verdict>
  <Fragment slot="left">
    La pool par laquelle router. Une adresse de contrat, choisie au moment de l'appel, par un
    keeper public que n'importe qui peut invoquer.
  </Fragment>
  <Fragment slot="right">
    Rien qui décide d'une destination. Le lieu est nommé depuis une liste que le propriétaire
    a installée ; le destinataire est le compte, toujours ; le module compose l'appel.
  </Fragment>
</Compare>

Un module qui prend sa destination de son appelant est un module qui fait confiance à
l'appelant pour le pointer. Sur une voie de keeper public, l'appelant est le public.
L'attaquant n'a pas forcé l'entrée. On lui a mis le volant entre les mains en lui demandant
d'être poli.

## Le rebondissement, puisqu'il est bon

La transaction de l'exploit est entrée dans le mempool public —la salle d'attente où les
transactions non confirmées sont à découvert. Un bot généralisé de front-running nommé Yoink
a vu le drainage en attente, l'a copié avec des frais plus élevés et a exécuté en premier
dans le même bloc. L'attaquant initial n'a rien eu.

<Callout type="warn" title="Deux échecs pour le prix d'un">
  Le portefeuille était vidable parce qu'un inconnu pouvait pointer un module. L'*attaquant* a
  été volé parce qu'une transaction de valeur est visible de tous dès qu'elle est diffusée. La
  victime a perdu l'argent dans les deux cas. Sur une chaîne publique, être premier compte
  plus que d'être celui qui a lancé la transaction — ce qui est une raison de plus de ne pas
  laisser un drainage attendre dans le mempool que n'importe qui le réclame.
</Callout>

## Cela peut-il arriver à un compte DeFiLoops ?

C'est la question que se pose vraiment un lecteur à notre place, alors voici la réponse
honnête : **le geste précis —pointer un module de confiance vers le contrat d'un attaquant—
ne peut pas s'exprimer contre un compte DeFiLoops, et non par chance mais par conception.**
Non parce que nos vérifications sont plus soigneuses. Parce qu'on ne remet jamais à l'agent de
destination à pointer.

<Spec title="Où les entrées de l'attaquant n'ont nulle part où aller" rows={[
  ['Choisir la pool / le contrat par lequel router', "Aucun champ de ce genre. L'agent fournit des paramètres typés ; l'adaptateur compose l'appel et choisit la destination."],
  ['Nommer un lieu par adresse', "Refusé. Les lieux sont nommés depuis l'ensemble que le propriétaire a installé, jamais comme une adresse dans l'instruction."],
  ['Rediriger le paiement vers soi', 'Le destinataire est forcé au compte, lu depuis le compte — jamais pris de l\'instruction.'],
  ['Prouver que vous visiez ce compte', 'Le propriétaire est lu depuis le compte. Une signature valide de l\'agent ne prouve pas que le bon compte, ni la bonne destination, étaient visés.'],
]} />

La raison plus profonde se situe un niveau au-dessus. **Un compte DeFiLoops en production ne
donne à l'agent aucune voie d'appel directe.** Le Safe vidé avait un module qui pouvait
exécuter un appel choisi par l'appelant. Notre agent ne peut pas composer d'appel. Il fournit
un montant, un lieu nommé depuis une liste, une plage — et un adaptateur inscrit sur la liste
blanche construit la transaction. Il n'y a pas de contrat fourni, car l'agent n'en fournit
jamais.

<StatRow>
  <Stat value="0 $" label="qu'une destination choisie par l'attaquant peut déplacer" accent />
  <Stat value="0" label="voie d'appel directe pour l'agent en production" />
  <Stat value="1" label="transaction pour révoquer entièrement l'agent" />
</StatRow>

## Le plan de l'attaquant, étape par étape

Supposez l'attaquant réaliste le plus fort, hormis le vol de votre clé de propriétaire : il
détient la clé de l'*agent*, celle qui signe les instructions quotidiennes du compte. Voici ce
qu'il tenterait, et où chaque étape s'arrête.

<Steps>
  <Step title="Pointer un module vers une pool que vous contrôlez">
    Aucun module ne prend de pool de son appelant. Le trading passe par des adaptateurs qui
    lisent le lieu depuis l'ensemble installé par le propriétaire. Une pool créée par
    l'attaquant n'y figure pas, et une adresse dans l'instruction est refusée comme appel
    arbitraire.
  </Step>
  <Step title="Se rabattre sur un appel direct et choisir sa propre destination">
    En production, il n'y a aucune voie d'appel directe sur laquelle se rabattre. Elle n'est
    pas installée. Le geste n'a nulle part où s'écrire.
  </Step>
  <Step title="Router par un adaptateur légitime et rediriger les fonds">
    Le destinataire est forcé au compte, le propriétaire est lu depuis le compte, le lieu vient
    de l'ensemble du propriétaire et la dépense est plafonnée par période. Chaque destination
    est fixée par l'adaptateur, pas par l'instruction.
  </Step>
  <Step title="Installer son propre module, ou changer la racine du compte">
    Ces sélecteurs sont refusés. L'agent ne peut jamais installer le genre de module
    redirigeable qui a vidé le Safe, devenir le propriétaire ou céder le compte.
  </Step>
  <Step title="Pendant ce temps, le propriétaire révoque l'agent">
    Une transaction, sans la coopération de personne, et toute instruction future de cette clé
    est refusée avant même que sa destination soit inspectée. La clé volée meurt dès que le
    propriétaire agit.
  </Step>
</Steps>

Une clé d'agent volée, contre un compte en production, ne peut rien pointer vers un attaquant
ni prendre le contrôle. C'est toute l'affirmation de sécurité, et elle tient contre cette
attaque précise.

## Ce que nous n'affirmons pas

Un article de sécurité qui s'arrête là vend quelque chose. La limite honnête a trois bords, et
aucun n'est *cette* attaque.

<Callout type="warn" title="Les risques résiduels, nommés">
  **Votre clé de propriétaire.** Qui la détient peut tout faire —installer des modules,
  changer la racine. Vrai pour tout compte intelligent, et aucune conception ne le referme.
  Protégez cette clé.

  **Le mode développement.** Une voie d'appel direct existe sur les forks, derrière un
  ensemble de règles plus faible, pour pouvoir tester contre une vraie chaîne. Elle est
  refusée au démarrage en production. Si elle atteignait un jour la production, la surface de
  valeur s'ouvrirait.

  **La discipline des adaptateurs.** Tout le modèle repose sur le fait que *chaque* adaptateur
  fixe lui-même la destination et le destinataire. Un adaptateur qui prendrait une destination
  de l'instruction serait exactement le module redirigeable qui a vidé le Safe. Nous soumettons
  chaque adaptateur à la même liste de contrôle, et prouvons chacun par un test qui pointe le
  destinataire vers un attaquant et observe l'assertion échouer.
</Callout>

## La partie qui a changé notre propre confiance

Nous voulions prouver que le guard bloque ceci. Écrire cette preuve a révélé que les tests que
nous avions déjà ne le prouvaient pas — ils révertaient une couche trop tôt, dans la
plomberie du compte, et passaient parce que le framework de test appariait mal le revert. La
protection était réelle. La preuve, elle, était du théâtre.

<Quote>
  Un test de sécurité qui ne peut pas échouer quand on casse ce qu'il protège n'est pas un
  test faible. C'est une décoration, et il est plus dangereux qu'aucun test, car il se lit
  comme une couverture.
</Quote>

Nous avons donc reconstruit la preuve contre le guard directement, en affirmant la raison
exacte pour laquelle chaque appel est refusé, puis en cassant chaque contrôle à dessein pour
voir le test correspondant échouer. Retirer la ligne d'auto-référence fait échouer son test.
Neutraliser la voie de révocation en fait échouer une autre. C'est la différence entre croire
qu'une limite tient et le savoir.

## Comment évaluer toute affirmation de ce genre

Vous n'avez pas à faire confiance à cette page. La question qui sépare une vraie limite d'un
filtre est toujours la même : **que peut écrire l'agent, ou un keeper de passage ?**

Si la réponse inclut une destination —une pool, un contrat, une adresse que l'appelant
choisit— alors chaque garde-fou autour est une vérification placée entre la cible choisie par
un inconnu et votre argent, et les vérifications se jugent contre les attaques que quelqu'un a
déjà imaginées. Le drainage de septembre, c'est le coût d'une destination choisie par
l'appelant.

Si la réponse est *des paramètres typés et rien d'autre*, l'attaque de cet article n'a nulle
part où atterrir, car ce qu'elle exploite —une cible que vous pouvez nommer— n'a jamais été
remis.

---

*Sources : rapports de surveillance de PeckShieldAlert et Blockaid (15 septembre 2026),
repris par PANews, Odaily, Foresight News et BlockBeats ; transactions on-chain via Etherscan.
Les chiffres et le mécanisme sont ceux rapportés à l'époque et peuvent être révisés à mesure
que l'analyse avance.*