Back to Blog

~$18M Perdidos: jaredFromSubway, Aztec y Más | BlockSec Semanal

Code Auditing
June 25, 2026
12 min read
Key Insights

Durante la semana pasada (2026/06/15 - 2026/06/21), observamos 3 incidentes de seguridad notables con pérdidas totales de aproximadamente $18.3M.

Fecha Incidente Tipo Pérdida Estimada
2026/06/18 Aztec Vinculación Incorrecta de Entradas Públicas ~$2.2M
2026/06/20 LABUBU Token Configuración Incorrecta ~$1.1M
2026/06/20 jaredFromSubway Gestión Incorrecta de Aprobaciones ~$15M
  • Aztec: Seleccionado porque el protocolo fue explotado por segunda vez en tres días, esta vez a través de su circuito de salida de emergencia, lo que pone de relieve los problemas recurrentes de vinculación de pruebas ZK.
  • jaredFromSubway: Seleccionado porque el contrato del bot MEV otorgó aprobaciones a contratos de tokens no confiables sin verificar el consumo ni revocar los allowances residuales, lo que permitió al atacante acumular y vaciar aproximadamente $15M.

El Mejor Auditor de Seguridad para Web3

Valida el diseño, el código y la lógica de negocio antes del lanzamiento

Destacado de la Semana: jaredFromSubway

A diferencia de los exploits de aprobación tradicionales —donde los atacantes abusan de vulnerabilidades en contratos DeFi de confianza para vaciar activos que los usuarios aprobaron a dichos contratos—, este ataque actúa en la dirección inversa: el bot MEV otorgó proactivamente aprobaciones sobre sus propios activos a contratos de terceros no confiables como parte de sus operaciones de arbitraje. El atacante construyó un entorno de trading falso (esencialmente un honeypot) donde pools de intercambio falsas emitían eventos reales de Swap y Sync, mientras que los tokens falsos nunca consumían los allowances otorgados, acumulando estas aprobaciones orientadas hacia el exterior antes de cosecharlas, con pérdidas totales reportadas de aproximadamente $15M.

El 20 de junio de 2026, jaredFromSubway, un operador de bot MEV en Ethereum, perdió aproximadamente $15M [1]. Según el análisis on-chain, la causa raíz fue una gestión incorrecta de aprobaciones en el contrato del bot: se otorgaron aprobaciones a contratos wrapper no confiables que nunca las consumieron, y el atacante acumuló estos allowances no consumidos hasta vaciar los saldos reales del bot en una sola transacción.

Contexto

jaredFromSubway es un operador de bot MEV muy conocido en Ethereum, especializado en ataques sandwich y arbitraje on-chain. El contrato víctima (0x1f2f...f387) es una de sus billeteras operativas, que mantiene grandes saldos de capital de trabajo en WETH, USDC y USDT.

Los bots MEV de este tipo deben interactuar dinámicamente con nuevos tokens y pools arbitrarios que aparecen on-chain. Monitorean el mempool, simulan transacciones y aprueban automáticamente interacciones con tokens para capturar oportunidades de arbitraje. Este modelo operativo se basa en la suposición de que los tokens se comportan como se espera: cuando se ejecuta un intercambio, el contrato del token consume el allowance otorgado llamando a transferFrom.

Análisis de la Vulnerabilidad

La causa raíz es la gestión incorrecta de aprobaciones del bot MEV al interactuar con contratos no confiables.

El bot ejecuta diversas rutas de arbitraje a través de pools y routers de Uniswap. En la mayoría de las interacciones, el bot envía tokens directamente a los pools mediante transfer, donde el propio bot es msg.sender y no se necesita ninguna aprobación. Sin embargo, las interacciones con contratos de tokens de tipo wrapper siguen un modelo pull: el bot llama a wrapper.wrapTo(), y dentro de esa llamada el contrato wrapper llama a realToken.transferFrom(bot, wrapper, amount) para extraer los tokens reales del bot. Dado que msg.sender durante transferFrom es el contrato wrapper —no el bot—, se requiere una approve previa:

  1. <real_token>.approve(tokenContract, amount) — otorgar allowance sobre el token real al contrato wrapper
  2. tokenContract.wrapTo()swap() de múltiples saltos a través de pools → tokenContract.unwrap() — envolver el token real, enrutar a través de los pools y desenvolver de vuelta al token real

El bot asumió que wrapTo() consumiría el allowance mediante transferFrom, como lo haría un contrato wrapper bien comportado. Sin embargo, el bot nunca verificó si el allowance fue realmente consumido después de la operación, ni revocó ningún allowance residual. Si wrapTo() no llama a transferFrom, el allowance completo sobrevive a la operación y se convierte en una superficie de ataque persistente: cualquier contrato que tenga dicho allowance puede posteriormente llamar a transferFrom para mover los activos reales del bot.

Análisis del Ataque

Según la reconstrucción on-chain, el atacante construyó un entorno de trading falso con tres componentes para explotar la vulnerabilidad descrita anteriormente:

  1. Tokens wrapper falsos: Cada token falso usaba el nombre del token real, pero prefijaba el símbolo con f (por ejemplo, nombre USD Coin con símbolo fUSDC para USDC). Implementaba wrapTo() y unwrap() para imitar un wrapper legítimo, además de una función withdraw() restringida al atacante que vaciaba los allowances no consumidos mediante transferFrom.

  2. Pools de intercambio falsas: El atacante desplegó aproximadamente 44 pools de estilo Uniswap V2 a través de una factory desplegada por él mismo. Estas pools emparejaban tokens falsos entre sí para formar rutas de intercambio convincentes. Cuando se llamaba a swap(), los pools emitían eventos reales de Sync y Swap indistinguibles de operaciones legítimas.

  3. Beneficios fabricados por el atacante: Durante unwrap(), el token falso enviaba una pequeña cantidad de tokens reales de vuelta al bot mediante transfer. El bot recibía beneficios reales, pero estos eran deliberadamente fabricados por el atacante en lugar de obtenidos de un arbitraje de mercado real.

El atacante controlaba estos componentes mediante un interruptor getStatus() por bloque en un contrato externo. getStatus() devolvía 1 cuando se llamaba en el mismo bloque que una transacción de activación (que establecía _getStatus = block.number), y 0 en caso contrario. Cuando getStatus() == 0, wrapTo() llamaba a transferFrom con normalidad y el allowance era consumido. Cuando getStatus() == 1, wrapTo() omitía transferFrom —el allowance no era consumido—, mientras que unwrap() seguía devolviendo tokens fabricados por el atacante al bot. El atacante probablemente usó sobornos a builders para colocar la transacción de activación en el mismo bloque que la transacción del bot cuando quería acumular allowances.

El ataque se desarrolló en tres fases:

Fase 1: Despliegue de la infraestructura de ataque

  • Paso 1: El atacante configuró la infraestructura entre los bloques 25354424 y 25354519. Esto incluyó el despliegue de un contrato factory de tokens falsos (0x81f2...0091), la creación de ~44 pools falsas de Uniswap V2 a través de una factory desplegada por él mismo, la financiación de los pools con saldos iniciales de tokens para que las llamadas a swap() tuvieran éxito, y el envío de 0.01 ETH al contrato de cosecha (0xb84d...df52) para gas y soborno al builder.

  • Paso 2: El atacante produjo en masa tokens wrapper falsos mediante CREATE2, cada uno imitando a un token real (usando el nombre real pero prefijando el símbolo con f) e incorporando una función withdraw() restringida al atacante. CREATE2 proporcionó direcciones deterministas sobre las que el contrato de cosecha podía iterar.

Fase 2: Generar confianza y acumular aprobaciones

  • Paso 3 (confianza inicial): En las primeras transacciones (por ejemplo, 0x542d...362b en el bloque 25354425), los tokens falsos no tenían el interruptor getStatus()wrapTo() llamaba a transferFrom directamente, consumiendo el allowance. El bot aprobó, envolvió, intercambió, desenvolvió y obtuvo beneficios con normalidad. Esto estableció los tokens falsos como oportunidades de trading rentables.

  • Paso 4 (confianza continua): En transacciones posteriores (por ejemplo, 0x085e...37e51), el interruptor getStatus() estaba desplegado pero devolvía 0 (bloque diferente al de la activación). wrapTo() seguía llamando a transferFrom y consumiendo el allowance. El bot continuó obteniendo beneficios y siguió interactuando.

  • Paso 5 (acumulación): A partir de 0x8560...1915 en el bloque 25360519, el atacante colocó una transacción de activación en el mismo bloque que la transacción del bot mediante un soborno al builder, haciendo que getStatus() devolviera 1. En este modo, wrapTo() omitía transferFrom —el allowance no era consumido—, pero unwrap() seguía enviando una pequeña cantidad de tokens reales de vuelta al bot. El bot detectó una operación rentable y dejó la aprobación en vigor. A lo largo de aproximadamente 600 bloques (~13 transacciones), el bot repitió este patrón con WETH, USDC y USDT, acumulando allowances no consumidos en los tres activos reales.

Fase 3: Cosecha

  • Paso 6: El atacante llamó a withdraw() en todos los tokens falsos en la transacción de cosecha 0x2be870...cf3e65, usando los allowances no consumidos para llamar a transferFrom y mover los saldos reales del bot al atacante. Se incluyó un soborno al builder de 0.01 ETH para garantizar la inclusión en el bloque. La cosecha extrajo 1,474.58 WETH + 2,870,573 USDC + 2,035,760 USDT (~$7.5M) únicamente del contrato víctima.

La transacción de ataque identificada conlleva ~$7.5M en pérdidas, y la pérdida total es de aproximadamente $15M según la afirmación de jaredFromSubway [1].

Conclusión

La causa raíz de este incidente fue la gestión incorrecta de aprobaciones del bot MEV al interactuar con contratos no confiables. A diferencia de los exploits de aprobación tradicionales, donde los atacantes abusan de vulnerabilidades en contratos DeFi de confianza para vaciar activos aprobados por usuarios, este ataque funciona en la dirección inversa: el bot aprobó proactivamente sus propios activos a contratos de terceros no confiables como parte de sus operaciones de arbitraje. El atacante acumuló estas aprobaciones no consumidas orientadas hacia el exterior y las cosechó en una sola transacción.

Para reducir riesgos similares en el futuro, los contratos de bots que interactúan con contratos de tokens no confiables deben verificar que las aprobaciones son consumidas después de cada operación y revocar cualquier allowance residual. Los allowances no consumidos tras una operación aparentemente exitosa son una señal clara de comportamiento malicioso del token.

Comienza con Phalcon Explorer

Profundiza en las Transacciones para Actuar con Sabiduría

Pruébalo gratis ahora

Más Incidentes de Esta Semana

Aztec

El 18 de junio de 2026, una restricción de igualdad faltante en el circuito ZK de escape de emergencia de Aztec permitió a un atacante retirar aproximadamente $2.2M (1,158 ETH, 150K DAI y ~0.47 renBTC) del contrato legacy RollupProcessor en Ethereum [2], [3]. Este fue el segundo exploit de Aztec en tres días (el primero, cubierto en nuestro informe anterior, tuvo como objetivo el RollupProcessorV3 actualizado) a través de un bug relacionado de vinculación de entradas públicas en el circuito ZK.

Contexto

El RollupProcessor legacy de Aztec incluye una función escapeHatch: un mecanismo de seguridad que permite a cualquiera enviar una prueba de transacción única cuando el operador del rollup deja de procesar. A diferencia de processRollup (que requiere un proveedor autorizado), la salida de emergencia se abre en ventanas periódicas basadas en el número de bloque y es invocable por cualquiera:

function escapeHatch(
    bytes calldata proofData,
    bytes calldata signatures,
    bytes calldata viewingKeys
) external override whenNotPaused {
    (bool isOpen, ) = getEscapeHatchStatus();
    require(isOpen, 'Rollup Processor: ESCAPE_BLOCK_RANGE_INCORRECT');
    processRollupProof(proofData, signatures, viewingKeys);
}

La salida de emergencia utiliza un circuito ZK dedicado (escape_hatch_circuit) que procesa una transacción join-split: consume notas de entrada del árbol de Merkle y crea notas de salida. El circuito debe verificar que las notas de entrada existen en el árbol de datos actual (usando old_data_root para la membresía en Merkle), y luego exponer la misma raíz como entrada pública para que el contrato L1 la valide contra el estado on-chain.

Análisis de la Vulnerabilidad

La vulnerabilidad se encuentra en el circuito de salida de emergencia (escape_hatch_circuit.cpp). El valor old_data_root se convierte en dos testigos independientes sin ninguna restricción de igualdad que los conecte.

El primer testigo (línea 33) se pasa al componente del circuito join-split, donde se utiliza para las pruebas de membresía en Merkle para verificar que las notas de entrada existen en el árbol de datos:

join_split_inputs inputs = {
    // ...
    witness_ct(&composer, tx.js_tx.old_data_root),        // línea 33: primer testigo
    // ...
};
auto outputs = join_split_circuit_component(composer, inputs);

El segundo testigo (línea 50) se crea de forma independiente y se expone como entrada pública (línea 88), que el contrato de Solidity extrae y comprueba contra la raíz de datos on-chain:

auto old_data_root = field_ct(witness_ct(&composer, tx.js_tx.old_data_root));  // línea 50: segundo testigo
// ...
composer.set_public_input(old_data_root.witness_index);    // línea 88: expuesto como entrada pública

En un circuito ZK, cada llamada a witness_ct crea una variable independiente. Sin un assert_equal explícito entre las líneas 33 y 50, el probador puede asignar valores diferentes a estos dos testigos. En el lado de Solidity, validateMerkleRoots verifica require(oldDataRoot == dataRoot) usando únicamente la entrada pública de la línea 50, sin visibilidad sobre el valor utilizado en la línea 33.

El mismo patrón de desvinculación también existe para input_owner y output_owner: estos valores se atestiguan en las líneas 38–39 (pasados a join_split_circuit_component para la verificación de propiedad) y nuevamente en las líneas 111–112 (expuestos como testigos públicos independientes). Sin embargo, no hemos identificado una vía de explotación práctica para esta brecha.

Análisis del Ataque

El circuito de salida de emergencia fue eliminado del código fuente de aztec-connect [4], pero el contrato verificador desplegado todavía contiene la clave de verificación EscapeHatchVk, por lo que las pruebas generadas con el circuito vulnerable aún pueden pasar la verificación on-chain. En el momento del ataque, el contrato había estado inactivo durante 142 días y la ventana de salida de emergencia estaba abierta. La dirección del atacante había sido creada apenas 14 horas antes del exploit a través de Union Chain [2]. El ataque consta de tres pasos principales:

  • Paso 1: El atacante construyó un árbol de Merkle falso que contenía notas de propiedad propia con valores arbitrarios. Estas notas no existían en el árbol de datos on-chain real (el árbol de Merkle que almacena todas las notas válidas).

  • Paso 2: El atacante generó una prueba de salida de emergencia explotando los testigos no vinculados descritos anteriormente. El testigo de la línea 33 (usado para la membresía en Merkle en el componente join-split) se estableció en la raíz de Merkle falsa (la verificación de membresía pasó porque las notas fabricadas existían en el árbol falso, y las verificaciones de propiedad pasaron porque el atacante poseía las claves de firma). El testigo de la línea 50 (expuesto como entrada pública comprobada por Solidity) se estableció en la raíz de datos real on-chain (la verificación require(oldDataRoot == dataRoot) de Solidity pasó porque este valor coincidía con la raíz almacenada en el contrato).

  • Paso 3: Con las verificaciones del circuito y de Solidity satisfechas, la prueba se verificó correctamente. El contrato procesó la transacción de salida de emergencia como legítima y liberó los fondos.

El atacante repitió este proceso en tres transacciones (0x9e1d6a...6b03ca, 0xab306c...59c2b5, 0x5c196c...4705c3), apuntando a diferentes activos y extrayendo 1,158 ETH, 150K DAI y ~0.47 renBTC respectivamente, con un total de aproximadamente $2.2M.

Conclusión

La causa raíz de este incidente fue una restricción de igualdad faltante entre dos testigos para old_data_root en el circuito de salida de emergencia. Un testigo se usó para la verificación de membresía de notas privadas dentro del componente join-split, el otro fue expuesto como la entrada pública comprobada por Solidity. Sin una restricción que los vinculara, el atacante demostró la propiedad de notas fabricadas contra un árbol de Merkle falso mientras el contrato L1 veía una raíz on-chain válida. Cabe destacar que eliminar el circuito vulnerable del código fuente no neutralizó el contrato verificador ya desplegado — la función escapeHatch en el RollupProcessor legacy sigue siendo invocable siempre que su ventana de número de bloque esté abierta.

Para reducir riesgos similares en el futuro, cuando el mismo valor lógico aparece en múltiples puntos de un circuito ZK, todas las instancias deben estar explícitamente restringidas a ser iguales — las llamadas independientes a witness_ct para el mismo valor constituyen una brecha de vinculación. Las auditorías de circuitos deben verificar sistemáticamente que cada entrada pública esté vinculada al valor interno del circuito que representa.

Comienza con Phalcon Security

Detecta cada amenaza, alerta sobre lo que importa y bloquea ataques.

Pruébalo gratis ahora

Referencias

Acerca de BlockSec

BlockSec es un proveedor integral de seguridad blockchain y cumplimiento de criptomonedas. Desarrollamos productos y servicios que ayudan a los clientes a realizar auditorías de código (incluyendo contratos inteligentes, blockchain y billeteras), interceptar ataques en tiempo real, analizar incidentes, rastrear fondos ilícitos y cumplir con las obligaciones AML/CFT, a lo largo de todo el ciclo de vida de los protocolos y plataformas.

BlockSec ha publicado múltiples artículos sobre seguridad blockchain en conferencias de prestigio, ha reportado varios ataques de día cero en aplicaciones DeFi, ha bloqueado múltiples hackeos para rescatar más de 20 millones de dólares y ha asegurado miles de millones en criptomonedas.

Best Security Auditor for Web3

Validate design, code, and business logic before launch. Aligned with the highest industry security standards.

BlockSec Audit