# Una firma dice quién, no qué

> Una transacción firmada parece definitiva. Pero una firma prueba quién aprobó algo válido, no que lo que se ejecuta sea lo que aprobó. Esto es lo que DeFiLoops pone en ese hueco: un recibo de contenido y una simulación que juzga lo que una transacción hace de verdad.

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

---
Una transacción firmada parece el final de la historia. Alguien con autoridad la aprobó, la
firma verifica, la red la honrará. Listo.

Pero entre *aprobado* y *ejecutado* hay un hueco, y es más ancho de lo que parece. Entre el
momento en que un agente firma y el momento en que un bloque incluye la transacción se elige
una ruta, se construye el calldata y pasan minutos. Una firma es una afirmación sobre **quién**
aprobó; no dice casi nada sobre **qué** acaba ejecutándose. Cerrar ese hueco es casi todo el
trabajo de seguridad, y nada de ello es la firma.

## Qué firma realmente el agente

Nuestro agente nunca firma una transacción en bruto. Firma una **intención tipada** —un
importe, un mercado nombrado de una lista, un rango— y el adaptador construye la llamada a
partir de esos parámetros. Esa es toda la razón por la que un agente comprometido no puede
apuntar una llamada a un atacante: nunca se le dio un destino al que apuntar
([lo escribimos aquí](/blog/the-executor-that-trusted-itself)).

Pero tipada o no, una firma solo cubre los campos que hashea. Y un mensaje EIP-712 hashea un
*subconjunto*.

<Callout type="danger" title="Una firma afirma algo sobre una clase, no sobre una instancia">
  En una sola intención de colateralizar, **veintiuno de veintiséis campos opcionales nunca
  llegan a la cadena.** La firma se resuelve al agente a la perfección: sencillamente nunca los
  cubrió. Así que una firma válida prueba que el agente aprobó *algo dentro de una familia
  amplia de intenciones válidas*. No prueba cuál. La validez es una propiedad de la familia; la
  identidad no.
</Callout>

Esa es la grieta. Cualquier cosa capaz de cambiar uno de esos campos no cubiertos entre la
firma y la ejecución cambia *lo que ocurre* dejando *todas las firmas verificando*. Volver a
comprobar que la intención sigue siendo válida no ayuda: la intención cambiada también es
válida. Ambas comprobaciones coinciden, sobre intenciones distintas.

## Dos aprobaciones, con minutos de diferencia

Un plan de adaptadores se autoriza dos veces, a propósito, porque en la primera aprobación el
calldata todavía no existe.

<Steps>
  <Step title="Firmar el grupo y acuñar un recibo">
    El firmante valida cada paso, firma el mensaje EIP-712 de cada uno y acuña un **recibo de
    validación**: una afirmación, firmada con la misma clave, que dice *validé intenciones con
    exactamente estos hashes de contenido, bajo exactamente estos límites, para esta cuenta en
    esta red.* El hash de contenido cubre **todos** los campos, no el subconjunto firmado.
  </Step>
  <Step title="Construir la ruta que el plan aún no podía conocer">
    Solo ahora el enrutado elige los pools del intercambio y ensambla el calldata real. Esta es
    toda la razón de que haya una segunda aprobación: los bytes no existían hace un momento.
  </Step>
  <Step title="Autorizar los bytes exactos y comprobar el recibo">
    El segundo salto recalcula el hash de contenido de lo que tiene entre manos y lo contrasta
    con el recibo. La misma intención, bit a bit, incluyendo todos los campos que la cadena
    nunca verá. Si un solo campo no cubierto cambió, los hashes discrepan y rechaza.
  </Step>
</Steps>

El recibo hace una cosa precisa: sustituye *«¿esto es válido?»* —que el primer salto ya
respondió y el segundo no puede volver a responder de forma útil— por *«¿esto es lo mismo?»*,
que es la pregunta que importa de verdad. Todo lo que el segundo salto solo puede saber en el
segundo salto, como si la ruta se llenó bien, lo sigue sabiendo allí. La frescura nunca se
confía al recibo: uno acuñado a las 12:00 no dice nada sobre las 12:20, así que el momento se
vuelve a comprobar igualmente.

<Callout type="warn" title="Por qué el recibo va firmado y no sellado">
  Va firmado con la clave del propio agente, con separación de dominio mediante una etiqueta
  fija, de modo que el resumen de un recibo nunca pueda colisionar con un mensaje EIP-712 que
  mueva dinero. No es un HMAC: eso significaría un secreto compartido que distribuir, rotar y
  terminar filtrando, y dentro de un enclave seguro, un secreto que tiene que *entrar*. La clave
  ya está allí. El coste es una firma extra por grupo, no por paso.
</Callout>

## La comprobación que mira lo que una transacción *hace*

Todo lo anterior prueba que la transacción es la que se aprobó. No prueba que sea *segura*: una
intención correctamente firmada y correctamente emparejada aún puede enrutarse por un pool que
se llena pésimamente, o dejar atrás una autorización. Así que antes de emitir nada, la
ejecutamos.

El simulador ejecuta la transacción contra un bloque real y reciente **sin emitirla**
—`debug_traceCall`— e informa de cada llamada, log y cambio de estado. Luego una sola regla
juzga el resultado. Es la única regla del sistema que mira lo que una transacción *hace* en vez
de lo que se pidió.

<Spec title="Lo que la simulación rechaza antes de emitir" rows={[
  ['Una pérdida neta que el plan no declaró', 'La cuenta solo puede perder lo que el plan dice que pierde. Cualquier cosa más allá, rechazada.'],
  ['Cualquier autorización sobrante', 'Toda autorización (propietario, gastador) debe acabar en cero. Una autorización en pie es lo más valioso que un atacante puede dejar atrás: sobrevive a la transacción y nada más la busca.'],
  ['Una salida por debajo del suelo', 'Lo que el plan compra debe llegar al suelo que el plan ya firmó, o por encima. Una ruta mal llenada se rechaza aquí —barato, antes del gas— en vez de revertir en la cadena.'],
  ['Un NFT que ningún paso movió', 'Ninguna posición ni token se mueve salvo que un paso del plan lo mueva. Una posición de Uniswap es un NFT, y su id es un número que ya se ha confundido antes con un importe.'],
]} />

El truco que lo hace posible es que la simulación devuelve los números *reales*, así que no
necesita oráculo de precios: compara contra el suelo que el plan ya lleva. Y una llamada que
revierte no es un error que esquivar; es el hallazgo. Si cualquier llamada del paquete revierte,
ningún efecto de la transacción persiste, y rechazar antes de emitir convierte una reversión en
cadena con gas desperdiciado en un *no* barato y legible.

<Callout type="warn" title="Lo que la simulación no es">
  No sustituye a las comprobaciones de la propia cadena. Una simulación es una predicción en un
  bloque; la transacción aterriza en otro posterior, así que `minOut` se sigue firmando y el
  adaptador lo sigue aplicando. Esto es un rechazo más barato, más temprano y más legible, no
  una nueva fuente de verdad. Y no es un juicio sobre el precio: una ruta que se llena mal pero
  por encima del suelo pasa. Acotar *eso* es una pregunta sobre cuán ancho puede ser un suelo, no
  sobre simulación.
</Callout>

## Todo lo que hay en el hueco falla cerrado

Ponlos en fila y la forma es la misma en cada paso: la respuesta segura es *no*, y es la de por
defecto.

<StatRow>
  <Stat value="quién" label="lo que prueba una firma" />
  <Stat value="qué" label="lo que prueban el recibo y la simulación" accent />
  <Stat value="no" label="la respuesta cuando algo discrepa" />
</StatRow>

Una guarda sin límites rechaza. Un recibo ausente rechaza. Un hash de contenido que no coincide
rechaza. Una simulación que no coincide con el plan rechaza, antes de emitir un solo byte.
Ninguna de esas cosas es la firma. La firma nunca fue lo difícil; solo probaba que *alguien*
aprobó *algo*. La cadena de comprobaciones existe para probar que es *esto*, y que *esto* hace
lo que el plan decía.

## Cómo evaluar cualquiera de esto

No hace falta que confíes en esta página. La pregunta que separa una cadena de ejecución real de
una firma y una plegaria es corta: **entre el momento en que se aprueba y el momento en que
aterriza, ¿qué se comprueba?**

Si la respuesta es «la firma verifica», entonces todo campo que una firma no cubre queda
abierto, y se confía por completo en lo que sea que construya el calldata. Si la respuesta
nombra un *recibo de contenido* y una *simulación de efectos*, entonces las dos cosas que una
firma no puede hacer —probar identidad y juzgar lo que ocurre de verdad— las está haciendo algo
que sí puede.