# Un desconocido apuntó el módulo

> Una Gnosis Safe perdió 7,8 millones de dólares en ETH restakeado — no porque la Safe fallara, sino porque un módulo habilitado dejó que un keeper cualquiera lo apuntara a la pool del propio atacante. La clase es vieja. La lección está en de dónde dejas que venga un destino.

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

---
El 15 de septiembre de 2026, un usuario de Ethereum perdió cerca de **7,8 millones de
dólares** en ETH restakeado de una cartera multifirma. Hay un giro memorable —un bot de
front-running se llevó el dinero antes que el propio atacante— pero el giro no es la lección.
La lección son los cuatro segundos previos, cuando la cartera hizo exactamente lo que se le
ordenó, ordenado por alguien que no debería haber podido ordenarle nada.

Esta es la forma del ataque, porque es una de las maneras más comunes de vaciar una cuenta
inteligente y la parte defectuosa siempre parece correcta.

## Lo que pasó en realidad

La víctima tenía unos 2.900 rsETH —un token de restaking líquido— dentro de una Gnosis Safe,
en su forma envuelta por Aave. El atacante nunca tuvo una clave.

En cambio, usó una **llamada pública de keeper** para apuntar un módulo de liquidez de
Uniswap V4 —un módulo que la cuenta había habilitado— a una **pool que el atacante acababa
de crear**. Esa pool llevaba un hook malicioso. El hook desenvolvió la posición en rsETH
libremente transferible, y el módulo, actuando con la autoridad de la Safe, lo movió.

<Callout type="danger" title="La Safe no era el fallo">
  Esto importa, y es lo primero que la gente confunde. Gnosis Safe hizo lo que una cartera
  multifirma está diseñada para hacer: dejó actuar a un módulo habilitado. El fallo estaba en
  el **módulo** —un contrato de terceros instalado sobre la Safe que aceptaba un destino de
  quien lo llamara. Culpar a la cartera pierde de vista dónde vivía el error, y ese error es
  uno que cualquiera puede repetir en cualquier marco de cuentas.
</Callout>

## El fallo está en de dónde vino el destino

Quita el restaking y los hooks y queda una sola cláusula. A un módulo de confianza le pasaron
un destino —qué pool, qué contrato— quien lo llamaba, y actuó sobre ese destino desde dentro
de su propio contexto privilegiado. La validación que hizo no bastaba, porque el atacante
podía elegir lo único que decidía a dónde iba el dinero.

<Compare left="Lo que el módulo dejaba aportar a quien llamaba" right="Lo que quien llama nunca debería aportar" verdict>
  <Fragment slot="left">
    La pool por la que enrutar. Una dirección de contrato, elegida en el momento de la
    llamada, por un keeper público que cualquiera puede invocar.
  </Fragment>
  <Fragment slot="right">
    Nada que decida un destino. El sitio se nombra desde una lista que instaló el propietario;
    el receptor es la cuenta, siempre; el módulo compone la llamada.
  </Fragment>
</Compare>

Un módulo que toma su destino de quien lo llama es un módulo que confía en que quien lo llama
lo apunte bien. En una ruta de keeper público, quien llama es el público. El atacante no
entró por la fuerza. Le pusieron el volante en las manos y le pidieron que fuera amable.

## El giro, ya que es bueno

La transacción del exploit entró en el mempool público —la sala de espera donde las
transacciones no confirmadas están a la vista. Un bot generalizado de front-running llamado
Yoink vio el drenaje pendiente, lo copió con una comisión más alta y ejecutó primero en el
mismo bloque. El atacante original no se llevó nada.

<Callout type="warn" title="Dos fallos al precio de uno">
  La cartera era drenable porque un desconocido podía apuntar un módulo. Al *atacante* lo
  robaron porque una transacción valiosa es visible para todos en cuanto se difunde. La
  víctima perdió el dinero de cualquier forma. En una cadena pública, ser el primero importa
  más que ser quien empezó la transacción — lo que es otra razón para no dejar un drenaje
  esperando en el mempool a que cualquiera lo reclame.
</Callout>

## ¿Puede pasar esto en una cuenta de DeFiLoops?

Es la pregunta que de verdad hace un lector en nuestro lugar, así que esta es la respuesta
honesta: **el movimiento concreto —apuntar un módulo de confianza al contrato de un
atacante— no se puede expresar contra una cuenta de DeFiLoops, y no por suerte sino por
diseño.** No porque nuestras comprobaciones sean más cuidadosas. Porque al agente nunca se le
entrega un destino que apuntar.

<Spec title="Dónde no tienen adónde ir las entradas del atacante" rows={[
  ['Elegir la pool / contrato por el que enrutar', 'No existe ese campo. El agente aporta parámetros tipados; el adaptador compone la llamada y elige el destino.'],
  ['Nombrar un sitio por dirección', 'Rechazado. Los sitios se nombran desde el conjunto que instaló el propietario, nunca como una dirección en la instrucción.'],
  ['Redirigir el pago a ti mismo', 'El receptor se fuerza a la cuenta, leído desde la cuenta — nunca tomado de la instrucción.'],
  ['Demostrar que te referías a esta cuenta', 'El propietario se lee desde la cuenta. Una firma válida del agente no prueba que se refería a la cuenta, ni al destino, correctos.'],
]} />

La razón de fondo está un nivel por encima. **Una cuenta de DeFiLoops en producción no da al
agente ninguna ruta de llamada directa.** La Safe drenada tenía un módulo que podía ejecutar
una llamada elegida por quien llamaba. Nuestro agente no puede componer una llamada. Aporta
un importe, un sitio nombrado desde una lista, un rango — y un adaptador en la lista blanca
construye la transacción. No hay contrato aportado, porque el agente nunca aporta uno.

<StatRow>
  <Stat value="$0" label="que puede mover un destino elegido por el atacante" accent />
  <Stat value="0" label="rutas de llamada directa para el agente en producción" />
  <Stat value="1" label="transacción para revocar al agente por completo" />
</StatRow>

## El plan del atacante, paso a paso

Supón el atacante realista más fuerte sin robar tu clave de propietario: tiene la clave del
*agente*, la que firma las instrucciones cotidianas de la cuenta. Esto es lo que intentaría,
y dónde termina cada paso.

<Steps>
  <Step title="Apuntar un módulo a una pool que controlas">
    No hay ningún módulo que tome una pool de quien lo llama. El trading pasa por adaptadores
    que leen el sitio desde el conjunto instalado por el propietario. Una pool creada por el
    atacante no está en él, y una dirección en la instrucción se rechaza como llamada
    arbitraria.
  </Step>
  <Step title="Recurrir a una llamada directa y elegir tu propio destino">
    En producción no hay ruta de llamada directa a la que recurrir. No está instalada. El
    movimiento no tiene dónde escribirse.
  </Step>
  <Step title="Enrutar por un adaptador legítimo y redirigir los fondos">
    El receptor se fuerza a la cuenta, el propietario se lee desde la cuenta, el sitio viene
    del conjunto del propietario y el gasto está limitado por periodo. Cada destino lo fija el
    adaptador, no la instrucción.
  </Step>
  <Step title="Instalar un módulo propio, o cambiar la raíz de la cuenta">
    Esos selectores se rechazan. El agente nunca puede instalar el tipo de módulo redirigible
    que drenó la Safe, convertirse en el propietario ni entregar la cuenta.
  </Step>
  <Step title="Mientras tanto, el propietario revoca al agente">
    Una transacción, sin necesitar la cooperación de nadie, y toda instrucción futura de esa
    clave se rechaza antes incluso de inspeccionar su destino. La clave robada muere en cuanto
    el propietario actúa.
  </Step>
</Steps>

Una clave de agente robada, contra una cuenta en producción, no puede apuntar nada a un
atacante ni tomar el control. Esa es toda la afirmación de seguridad, y se sostiene contra
este mismo ataque.

## Lo que no afirmamos

Una entrada de seguridad que termine ahí está vendiendo algo. El límite honesto tiene tres
bordes, y ninguno es *este* ataque.

<Callout type="warn" title="Los riesgos residuales, nombrados">
  **Tu clave de propietario.** Quien la tenga puede hacer cualquier cosa —instalar módulos,
  cambiar la raíz. Cierto en toda cuenta inteligente, y ningún diseño lo cierra. Protege esa
  clave.

  **Modo de desarrollo.** Existe una ruta de llamada directa en los forks, tras un conjunto
  de reglas más débil, para poder probar contra una cadena real. Se rechaza al arrancar en
  producción. Si alguna vez llegara a producción, la superficie de valor se abriría.

  **Disciplina de adaptadores.** Todo el modelo descansa en que *cada* adaptador fije el
  destino y el receptor por sí mismo. Un adaptador que tomara un destino de la instrucción
  sería exactamente el módulo redirigible que drenó la Safe. Sometemos cada adaptador a la
  misma lista de comprobación, y probamos cada uno con un test que apunta el receptor a un
  atacante y observa fallar la aserción.
</Callout>

## La parte que cambió nuestra propia confianza

Nos propusimos demostrar que el guard bloquea esto. Escribir esa demostración reveló que las
pruebas que ya teníamos no lo demostraban — revertían una capa demasiado pronto, en la
fontanería de la cuenta, y pasaban porque el framework de pruebas emparejaba mal el revert.
La protección era real. La evidencia de ella era teatro.

<Quote>
  Una prueba de seguridad que no puede fallar cuando rompes aquello que protege no es una
  prueba débil. Es un adorno, y es más peligrosa que ninguna prueba, porque se lee como
  cobertura.
</Quote>

Así que reconstruimos la demostración contra el guard directamente, afirmando el motivo
exacto por el que se rechaza cada llamada, y luego rompimos cada control a propósito para ver
fallar la prueba correspondiente. Quitar la línea de auto-referencia hace fallar su prueba.
Anular la ruta de revocación hace fallar otra. Esa es la diferencia entre creer que un límite
se sostiene y saberlo.

## Cómo evaluar cualquier afirmación así

No hace falta que confíes en esta página. La pregunta que separa un límite real de un filtro
es la de siempre: **¿qué puede escribir el agente, o un keeper de paso?**

Si la respuesta incluye un destino —una pool, un contrato, una dirección que quien llama
elige— entonces toda salvaguarda a su alrededor es una comprobación entre el destino elegido
por un desconocido y tu dinero, y las comprobaciones se juzgan contra los ataques que alguien
ya imaginó. El drenaje de septiembre es lo que cuesta un destino elegido por quien llama.

Si la respuesta es *parámetros tipados y nada más*, el ataque de esta entrada no tiene dónde
aterrizar, porque aquello que explota —un destino que puedes nombrar— nunca se entregó.

---

*Fuentes: informes de monitorización de PeckShieldAlert y Blockaid (15 de septiembre de
2026), recogidos por PANews, Odaily, Foresight News y BlockBeats; transacciones on-chain vía
Etherscan. Las cifras y el mecanismo son los reportados en su momento y pueden revisarse a
medida que avanza el análisis.*