Presiona ESC para cerrar

Anti-MEV Stealth Swap: El duelo final - Envío y CLI

Muy bien, en los dos artículos anteriores establecimos nuestro "canal de comunicación seguro" y escribimos la lógica del swap. En esta última parte, terminamos la herramienta. Implementaremos el bucle de espera de bloques y empaquetaremos todo en una interfaz de línea de comandos (CLI) práctica.

1. Lógica de envío: ¿Por qué no podemos simplemente hacer clic en "Enviar"?

La red Ethereum genera bloques aproximadamente cada 12 segundos. Tu bundle (paquete de transacciones) solo es válido para un número de bloque específico. Si no entra en el bloque más cercano (por ejemplo, debido a una propina de gas demasiado baja), debes reconstruirlo para el siguiente bloque con el número actualizado.

Usaremos un bucle que intentará "empujar" nuestro Stealth Swap durante los próximos 10 bloques.

 

2. Código: Implementación del envío y CLI

Añadamos el método final sendBundle y una gestión sencilla de argumentos por terminal.

import { FlashbotsBundleResolution } from "@flashbots/ethers-provider-bundle";

async function runStealthSwap(amountInEth: string) {
    const { wallet, flashbotsProvider, provider } = await initStealthProvider();
    const amountIn = ethers.parseEther(amountInEth);
    let currentBlock = await provider.getBlockNumber();

    console.log(`Iniciando en el bloque: ${currentBlock}`);

    // Intentamos enviar el bundle durante los próximos 10 bloques
    for (let i = 0; i < 10; i++) {
        const targetBlock = currentBlock + i;
        
        // Reconstruimos el bundle para el bloque específico (incluyendo simulación)
        const signedBundle = await createAndSimulateBundle(wallet, flashbotsProvider, provider, amountIn, targetBlock);
        
        if (!signedBundle) continue;

        const bundleSubmission = await flashbotsProvider.sendBundle(signedBundle, targetBlock);
        
        if ("error" in bundleSubmission) {
            console.error(`Error en el envío: ${bundleSubmission.error.message}`);
            continue;
        }

        console.log(`Bundle enviado. Esperando al bloque ${targetBlock}...`);
        const waitResponse = await bundleSubmission.wait();
        
        if (waitResponse === FlashbotsBundleResolution.BundleIncluded) {
            console.log(`¡VICTORIA! Transacción incluida en el bloque ${targetBlock}`);
            console.log(`Hash: https://etherscan.io/tx/${(await signedBundle)[0].hash}`); // Hash aproximado
            return;
        } else if (waitResponse === FlashbotsBundleResolution.BlockPassedWithoutInclusion) {
            console.log(`Fallo. El bloque ${targetBlock} pasó sin nosotros. Intentando el siguiente...`);
        } else if (waitResponse === FlashbotsBundleResolution.AccountNonceTooHigh) {
            console.error("Error: Nonce demasiado alto. Revisa tus transacciones pendientes.");
            return;
        }
    }
}

// Interfaz CLI básica
const amount = process.argv[2] || "0.01";
runStealthSwap(amount);

Obviamente, lo que hemos hecho hasta ahora es solo un "Hola Mundo" de Flashbots para swaps en Uniswap V3. Para que esto funcione de verdad en condiciones reales, necesitamos refinar un poco la lógica.

 

import { ethers } from "ethers";
import { FlashbotsBundleProvider, FlashbotsBundleResolution } from "@flashbots/ethers-provider-bundle";
import * as dotenv from "dotenv";

dotenv.config();

const WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
const ROUTER = "0xE592427A0AEce92De3Edee1F18E0157C05861564";
const QUOTER = "0x61fFe014bA17989E743c5F6cB21bF9697530B21e";

const ABI = [
    "function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 deadline, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) external returns (uint256 amountOut)",
    "function quoteExactInputSingle((address tokenIn, address tokenOut, uint256 amountIn, uint24 fee, uint160 sqrtPriceLimitX96)) external returns (uint256 amountOut, uint160 sqrtPriceX96After, uint32 initializedTicksCrossed, uint256 gasEstimate)",
    "function approve(address spender, uint256 amount) external returns (bool)",
    "function allowance(address owner, address spender) external view returns (uint256)",
    "function balanceOf(address account) external view returns (uint256)"
];

async function main() {
    const provider = new ethers.JsonRpcProvider(process.env.ETH_RPC_URL);
    const wallet = new ethers.Wallet(process.env.SENDER_PRIVATE_KEY, provider);
    const authSigner = new ethers.Wallet(process.env.FLASHBOTS_AUTH_KEY, provider);
    const flashbots = await FlashbotsBundleProvider.create(provider, authSigner);

    const quoter = new ethers.Contract(QUOTER, ABI, provider);
    const weth = new ethers.Contract(WETH, ABI, wallet);

    const baseAmount = ethers.parseEther(process.argv[2] || "0.1");
    const minThreshold = ethers.parseUnits(process.argv[3] || "200", 6);

    // --- VERIFICACIÓN PREVIA ---
    const balance = await weth.balanceOf(wallet.address);
    if (balance < baseAmount) throw new Error("WETH INSUFICIENTE");

    const allowance = await weth.allowance(wallet.address, ROUTER);
    if (allowance < baseAmount) {
        await (await weth.approve(ROUTER, ethers.MaxUint256)).wait();
    }

    let nonce = await wallet.getNonce();
    let lastQuote = 0n;
    let lastExecutionBlock = 0;

    provider.on("block", async (block) => {
        try {
            // --- SALTO ALEATORIO (rompe patrones de comportamiento) ---
            if (Math.random() < 0.6) return;

            const quote = await quoter.quoteExactInputSingle.staticCall({
                tokenIn: WETH,
                tokenOut: USDC,
                amountIn: baseAmount,
                fee: 3000,
                sqrtPriceLimitX96: 0
            });

            // --- UMBRAL (Threshold de rentabilidad) ---
            if (quote.amountOut < minThreshold) return;

            // --- DETECCIÓN DE CAMBIO ---
            if (lastQuote !== 0n) {
                const diff = (quote.amountOut * 1000n) / lastQuote;
                // ignorar si el cambio es < 0.3%
                if (diff > 997n && diff < 1003n) return;
            }

            // --- TIEMPO DE ESPERA (Cool Down) ---
            if (block - lastExecutionBlock < 2) return;

            // --- CANTIDAD ALEATORIA (rompe la firma de la transacción) ---
            const randomFactor = BigInt(95 + Math.floor(Math.random() * 10)); // 95–105%
            const amountIn = (baseAmount * randomFactor) / 100n;

            // --- SLIPPAGE ---
            const minOut = (quote.amountOut * 995n) / 1000n;

            const feeData = await provider.getFeeData();
            const tx = {
                to: ROUTER,
                data: new ethers.Interface(ABI).encodeFunctionData("exactInputSingle", [{
                    tokenIn: WETH,
                    tokenOut: USDC,
                    fee: 3000,
                    recipient: wallet.address,
                    deadline: Math.floor(Date.now() / 1000) + 90,
                    amountIn: amountIn,
                    amountOutMinimum: minOut,
                    sqrtPriceLimitX96: 0
                }]),
                chainId: 1,
                type: 2,
                gasLimit: 250000,
                maxFeePerGas: feeData.maxFeePerGas,
                maxPriorityFeePerGas: feeData.maxPriorityFeePerGas || 2n,
                nonce: nonce
            };

            const signed = await flashbots.signBundle([{ signer: wallet, transaction: tx }]);
            const target = block + 1;

            const sim = await flashbots.simulate(signed, target);
            if ("error" in sim) return;

            const sub = await flashbots.sendBundle(signed, target);
            if ("error" in sub) return;

            const res = await sub.wait();
            nonce++; // IMPORTANTE
            if (res === FlashbotsBundleResolution.BundleIncluded) {
                console.log("EJECUTADO EN EL BLOQUE:", target);
                process.exit(0);
            }

            lastQuote = quote.amountOut;
            lastExecutionBlock = block;
        } catch (e) {
            // modo silencioso ante errores
        }
    });
}

main();

 

3. Cómo usar Anti-MEV Stealth Swap

1. Instalación

npm install ethers @flashbots/ethers-provider-bundle dotenv

2. Crea tu archivo .env

ETH_RPC_URL=https://mainnet.infura.io/v3/TU_KEY
SENDER_PRIVATE_KEY=TU_LLAVE_PRIVADA
FLASHBOTS_AUTH_KEY=CUALQUIER_NUEVA_LLAVE_PRIVADA

3. Preparación del Wallet

Indispensable:

  • Debes tener Ethereum (ETH) en el wallet → para las comisiones de gas.
  • Y Wrapped Ether (WETH) → para el intercambio en sí.

Si no tienes WETH:

  • Convierte primero ETH a WETH a través de cualquier interfaz (ej. Uniswap).

4. Ejecución

node app.js 0.1 200

Donde:

  • 0.1 → cuánto WETH quieres cambiar.
  • 200 → cantidad mínima aceptable de USDC a la salida.

5. ¿Cómo funciona internamente?

Tras el inicio:

  • Verifica el saldo de WETH.
  • Realiza el approve (solo una vez).
  • Comienza a escuchar nuevos bloques.
  • En cada bloque:
    • A veces salta deliberadamente (aleatoriedad para ocultar el bot).
    • Consulta el precio a través del Quoter.
    • Verifica tu umbral de rentabilidad.
    • Verifica si el precio se ha movido (evita estancamientos).
    • Modifica ligeramente la cantidad de la transacción (para disfrazarse).
    • Realiza una simulación.
    • Envía a través de Flashbots.

6. ¿Cuándo ocurre la transacción?

  • Precio ≥ umbral especificado.
  • El precio ha cambiado.
  • Ha pasado el tiempo de cooldown.
  • La simulación fue exitosa.

7. Resultado

Cuando veas en la consola:

EXECUTED: 19483921

Significa que:

  • El swap se completó con éxito.
  • El script terminó su trabajo.

8. Parámetros clave
Umbral (segundo argumento)

node app.js 0.1 220

Umbral más alto = menos transacciones, pero mejor precio de ejecución.

Cantidad

node app.js 0.05 200

Cantidad menor = menor impacto en el mercado (price impact) y menor riesgo.

9. Qué hay que tener en cuenta

  • Esto no garantiza siempre el mejor precio absoluto del mundo.
  • No protege contra absolutamente todos los tipos de ataque.
  • Pero reduce drásticamente las posibilidades de que alguien te haga un "front-run".

10. Cuándo NO usarlo

  • Si no entiendes cómo funciona el slippage.
  • Si tienes fondos insuficientes para cubrir el gas.
  • Si la red está completamente congestionada.

Resumen

  • Inicio con un comando.
  • Funciona automáticamente.
  • Realiza un swap "silencioso" vía Flashbots.

Este código: no ganará a bots MEV profesionales en un duelo directo, no te asegurará en cada escenario y no da una ventaja mágica (edge).

PERO:
👉 Dejas de ser una presa fácil.

 

Esta herramienta es solo la punta del iceberg. Se puede ampliar para arbitraje, liquidaciones o mover grandes volúmenes de liquidez de forma segura.

El principio es simple: En el "bosque oscuro" de la blockchain, no sobrevive quien grita más fuerte, sino quien sabe escabullirse sin hacer ruido.

Material preparado para EXMON Academy. ¡Experimenta en Mainnet con precaución y vigila siempre tus ajustes de slippage!

Sying Yu

I am a blockchain developer specializing in building secure, scalable, and innovative decentralized solutions. My expertise covers smart contracts, payment systems, and integrating crypto with fiat to optimize financial workflows. I thrive on creating modern, efficient tools for the evolving digital economy....

Escribe una opinión

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