# Une signature dit qui, pas quoi

> Une transaction signée paraît définitive. Mais une signature prouve qui a approuvé quelque chose de valide, pas que ce qui s'exécute soit ce qu'il a approuvé. Voici ce que DeFiLoops met dans cet écart : un reçu de contenu et une simulation qui juge ce qu'une transaction fait réellement.

- Source: https://defiloops.com/fr/blog/a-signature-says-who-not-what
- Published: 2026-09-15
- Category: Security
- Tags: security, agentic-wallet, signatures, simulation, execution
- Author: DeFiLoops

---
Une transaction signée ressemble à la fin de l'histoire. Quelqu'un d'habilité l'a approuvée, la
signature se vérifie, la chaîne l'honorera. Terminé.

Mais entre *approuvé* et *exécuté* il y a un écart, et il est plus large qu'il n'y paraît. Entre
l'instant où un agent signe et celui où un bloc inclut la transaction, une route est choisie, le
calldata est construit, et des minutes passent. Une signature est une affirmation sur **qui** a
approuvé ; elle ne dit presque rien sur **ce qui** finit par s'exécuter. Combler cet écart
constitue l'essentiel du travail de sécurité, et rien de tout cela n'est la signature.

## Ce que l'agent signe réellement

Notre agent ne signe jamais une transaction brute. Il signe une **intention typée** — un
montant, un lieu nommé depuis une liste, une plage — et l'adaptateur construit l'appel à partir
de ces paramètres. C'est toute la raison pour laquelle un agent compromis ne peut pas pointer un
appel vers un attaquant : on ne lui a jamais donné de cible à viser
([nous l'avons écrit ici](/blog/the-executor-that-trusted-itself)).

Mais typée ou non, une signature ne couvre que les champs qu'elle hache. Et un message EIP-712
hache un *sous-ensemble*.

<Callout type="danger" title="Une signature affirme quelque chose sur une classe, pas sur une instance">
  Sur une seule intention de collatéralisation, **vingt et un des vingt-six champs facultatifs
  n'atteignent jamais la chaîne.** La signature se résout parfaitement vers l'agent : elle ne
  les a simplement jamais couverts. Une signature valide prouve donc que l'agent a approuvé
  *quelque chose au sein d'une large famille d'intentions valides*. Elle ne prouve pas laquelle.
  La validité est une propriété de la famille ; l'identité, non.
</Callout>

C'est la fissure. Tout ce qui peut modifier l'un de ces champs non couverts entre la signature
et l'exécution change *ce qui se passe* tout en laissant *toutes les signatures se vérifier*.
Revérifier que l'intention est toujours valide n'aide pas : l'intention modifiée est valide
elle aussi. Les deux vérifications sont d'accord, au sujet d'intentions différentes.

## Deux approbations, à quelques minutes d'écart

Un plan d'adaptateurs est autorisé deux fois, délibérément, parce que le calldata n'existe pas
au moment de la première approbation.

<Steps>
  <Step title="Signer le groupe et frapper un reçu">
    Le signataire valide chaque étape, signe le message EIP-712 de chacune et frappe un **reçu de
    validation** — une affirmation, signée par la même clé, qui dit : *j'ai validé des intentions
    portant exactement ces empreintes de contenu, sous exactement ces limites, pour ce compte sur
    cette chaîne.* L'empreinte de contenu couvre **tous** les champs, pas le sous-ensemble signé.
  </Step>
  <Step title="Construire la route que le plan ne pouvait pas encore connaître">
    Ce n'est que maintenant que le routage choisit les pools de l'échange et assemble le vrai
    calldata. C'est toute la raison d'être d'une seconde approbation : les octets n'existaient
    pas il y a un instant.
  </Step>
  <Step title="Autoriser les octets exacts et vérifier le reçu">
    Le second saut recalcule l'empreinte de contenu de ce qu'il tient et la compare au reçu. La
    même intention, bit pour bit — y compris chaque champ que la chaîne ne verra jamais. Si un
    seul champ non couvert a changé, les empreintes divergent et il refuse.
  </Step>
</Steps>

Le reçu fait une chose précise : il remplace *« est-ce valide ? »* — question à laquelle le
premier saut a déjà répondu et que le second ne peut pas utilement reposer — par *« est-ce la
même chose ? »*, qui est la question qui compte vraiment. Tout ce que le second saut ne peut
apprendre qu'au second saut, comme la qualité du remplissage de la route, il continue de
l'apprendre là. La fraîcheur n'est jamais tirée du reçu : un reçu frappé à 12h00 ne dit rien de
12h20, et le moment est donc revérifié de toute façon.

<Callout type="warn" title="Pourquoi le reçu est signé et non tamponné">
  Il est signé par la clé de l'agent lui-même, avec une séparation de domaine par une étiquette
  fixe, afin qu'un condensé de reçu ne puisse jamais entrer en collision avec un message EIP-712
  qui déplace de l'argent. Pas un HMAC : cela supposerait un secret partagé à distribuer, à
  faire tourner et à finir par laisser fuiter — et, dans une enclave sécurisée, un secret qui
  doit *entrer*. La clé, elle, est déjà là. Le coût est d'une signature supplémentaire par
  groupe, pas par étape.
</Callout>

## La vérification qui regarde ce qu'une transaction *fait*

Tout ce qui précède prouve que la transaction est bien celle qui a été approuvée. Cela ne prouve
pas qu'elle soit *sûre* : une intention correctement signée et correctement appariée peut encore
passer par un pool qui remplit très mal, ou laisser derrière elle une autorisation. Aussi, avant
de diffuser quoi que ce soit, nous l'exécutons.

Le simulateur exécute la transaction contre un bloc réel et récent **sans la diffuser** —
`debug_traceCall` — et rapporte chaque appel, chaque log et chaque changement d'état. Puis une
seule règle juge le résultat. C'est la seule règle du système qui regarde ce qu'une transaction
*fait* plutôt que ce qui a été demandé.

<Spec title="Ce que la simulation refuse avant diffusion" rows={[
  ['Une perte nette que le plan n\'a pas déclarée', 'Le compte ne peut perdre que ce que le plan dit qu\'il perd. Tout ce qui dépasse est refusé.'],
  ['Toute autorisation résiduelle', 'Chaque autorisation (propriétaire, dépensier) doit finir à zéro. Une autorisation qui subsiste est la chose la plus précieuse qu\'un attaquant puisse laisser : elle survit à la transaction, et rien d\'autre ne la cherche.'],
  ['Une sortie sous le plancher', 'Ce que le plan achète doit arriver au plancher que le plan a déjà signé, ou au-dessus. Une route mal remplie est refusée ici — pour pas cher, avant le gas — au lieu d\'échouer sur la chaîne.'],
  ['Un NFT qu\'aucune étape n\'a déplacé', 'Aucune position ni token ne bouge sans qu\'une étape du plan le déplace. Une position Uniswap est un NFT, et son identifiant est un nombre que l\'on a déjà pris pour un montant.'],
]} />

L'astuce qui rend cela possible, c'est que la simulation renvoie les *vrais* chiffres : elle n'a
donc besoin d'aucun oracle de prix, et compare au plancher que le plan porte déjà. Et un appel
qui échoue n'est pas une erreur à contourner ; c'est la trouvaille. Si un seul appel du lot
échoue, aucun effet de la transaction ne persiste — et refuser avant diffusion transforme un
échec sur la chaîne avec du gas gaspillé en un *non* peu coûteux et lisible.

<Callout type="warn" title="Ce que la simulation n'est pas">
  Elle ne remplace pas les vérifications de la chaîne elle-même. Une simulation est une
  prédiction à un bloc ; la transaction atterrit à un bloc ultérieur, donc `minOut` est toujours
  signé et l'adaptateur l'applique toujours. C'est un refus moins cher, plus précoce et plus
  lisible — pas une nouvelle source de vérité. Et ce n'est pas un jugement sur le prix : une
  route qui remplit mal mais au-dessus du plancher passe. Borner *cela* est une question sur la
  largeur admissible d'un plancher, pas sur la simulation.
</Callout>

## Tout ce qui se trouve dans l'écart échoue en se fermant

Alignez-les et la forme est la même à chaque étape : la réponse sûre est *non*, et c'est celle
par défaut.

<StatRow>
  <Stat value="qui" label="ce qu'une signature prouve" />
  <Stat value="quoi" label="ce que le reçu et la simulation prouvent" accent />
  <Stat value="non" label="la réponse dès que l'un diverge" />
</StatRow>

Une garde sans bornes refuse. Un reçu absent refuse. Une empreinte de contenu qui ne correspond
pas refuse. Une simulation qui ne correspond pas au plan refuse — avant qu'un seul octet ne soit
diffusé. Rien de tout cela n'est la signature. La signature n'a jamais été la partie difficile ;
elle prouvait seulement que *quelqu'un* avait approuvé *quelque chose*. La chaîne de
vérifications existe pour prouver que c'est *cette* chose-là, et que *cette* chose-là fait ce
que le plan annonçait.

## Comment évaluer tout ceci

Vous n'avez pas besoin de croire cette page. La question qui sépare une vraie chaîne d'exécution
d'une signature accompagnée d'une prière est courte : **entre le moment où c'est approuvé et le
moment où cela atterrit, qu'est-ce qui est vérifié ?**

Si la réponse est « la signature se vérifie », alors tout champ qu'une signature ne couvre pas
reste ouvert, et l'on fait entièrement confiance à ce qui construit le calldata. Si la réponse
nomme un *reçu de contenu* et une *simulation des effets*, alors les deux choses qu'une
signature ne peut pas faire — prouver l'identité et juger ce qui se passe réellement — sont
faites par quelque chose qui le peut.