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.
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).
Mais typée ou non, une signature ne couvre que les champs qu’elle hache. Et un message EIP-712 hache un sous-ensemble.
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.
- 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é.
- 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.
- 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.
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.
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é.
- 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.
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.
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.