Presiona ESC para cerrar

Ganancias de Bots MEV y Costos de Gas: Guía de Arbitraje

La mera economía del MEV se resume en una competencia brutal por el espacio en los bloques, donde una latencia de red de un par de milisegundos te quema todo el potencial de ganancia. Lanzar tu propio bot en Ethereum o en redes EVM de capa 2 dejó de ser dinero fácil hace un buen rato. El mercado está dominado por searchers especializados que alquilan servidores en centros de datos en Fráncfort y Tokio, colocalizados directo con los nodos de los validadores.

Números reales y rentabilidad

Un bot solitario corriendo desde un server casero o un VPS cloud estándar sale perdiendo el 95% de las veces debido a las subastas de gas. La rentabilidad depende de la estrategia:

  • Arbitraje atómico (Atomic Arbitrage): Los márgenes de ganancia cayeron a una fracción de por ciento. El profit neto diario de un bot exitoso ronda entre los $100 y $3,000 USD en épocas de volatilidad. El riesgo de perder capital es cero, ya que la transacción se envuelve en un único smart contract atómico: si las condiciones no cuadran, toda la cadena hace rollback.
  • Ataques de sándwich (Sandwich Attacks): El rendimiento teórico es más alto, entre $500 y $10,000 USD diarios en pares de baja liquidez en Uniswap v2 o AMMs similares. La práctica es mucho más pesada. Los bots competidores arman filas a través de private transaction pools, superando las coimas (bribes) de los rivales hacia los validadores.
  • Liquidaciones (Liquidations): El mercado spot de protocolos de préstamo como Aave deja una torta grande, pero exige millones de dólares en liquidez para cubrir deudas y liquidar posiciones al instante. Los lobos solitarios acá no pintan nada.

Estructura de costos de gas y operación con builders

Tu mayor gasto al correr un bot de MEV no son las transacciones exitosas, sino los intentos fallidos y las subastas de prioridad de gas (Gas Priority Auction).

  • Mempool pública: Mandar una transacción esquivando los relays privados te garantiza un rojo seguro. Un bot común detecta la tx de la víctima, manda la suya con un gas más alto, pero los bots rivales la interceptan primero vía RPCs privados. Al final, tu transacción se ejecuta al peor precio o se revierte por completo, quemando gas real en la blockchain.
  • Block builders y Flashbots: El arbitraje moderno se arma mediante eth_sendBundle. Empaquetas varias transacciones en un bundle y lo mandas derecho a los block builders (Beaverbuild, Titan, Flashbots).
  • Costo por fallo: La ventaja de los bundles privados es que si el arbitraje no sale, la transacción directamente no entra al bloque. No pagas comisión de red por un tiro al aire. La contra: el validador se chupa la mayor parte de la ganancia en forma de propina directa (coinbase.transfer), dejándole al dev entre un 5% y 15% del profit bruto.

Tabla de métricas de estrategias MEV

EstrategiaCapital inicialProfit neto diario promedioTasa de éxitoPrincipal causa de pérdida
Arbitraje atómicodesde $5,000 USD$50 - $500 USD15-25%Perder en latencia de red frente a la competencia
Ataques de sándwichdesde $20,000 USD$200 - $3,000 USD5-10%Sobrepago de gas en las subastas de validadores
Liquidacionesdesde $100,000+ USD$1,000 - $10,000+ USD< 2%Falta de capital para saldar deudas pesadas

Ejemplo de envío de un bundle privado en Python

Abajo tenés un esqueleto de script funcional en Python usando la librería web3.py para tirar un bundle a través de un proveedor compatible con Flashbots. El código viene limpio de abstracciones innecesarias.

import os
from eth_account import Account
from eth_account.signers.local import LocalAccount
from web3 import Web3
from flashbots import flashbot
# Clave para firmar requests al relay (reputación en Flashbots).
# NO uses la key donde tengas tus fondos reales.
signature_account: LocalAccount = Account.from_key(os.environ["FLASHBOTS_SIGNER_KEY"])
# Key del que manda la transacción
sender: LocalAccount = Account.from_key(os.environ["SENDER_PRIVATE_KEY"])
w3 = Web3(Web3.HTTPProvider(os.environ["ETH_NODE_URL"]))
flashbot(w3, signature_account, "https://relay-sepolia.flashbots.net")
def build_signed_tx(nonce: int) -> bytes:
    base_fee = w3.eth.get_block("latest")["baseFeePerGas"]
    tx = {
        "to": sender.address,          # ejemplo: autotransferencia
        "value": 0,
        "gas": 21_000,
        "maxFeePerGas": base_fee * 2 + Web3.to_wei(2, "gwei"),
        "maxPriorityFeePerGas": Web3.to_wei(2, "gwei"),
        "nonce": nonce,
        "chainId": w3.eth.chain_id,
        "type": 2,
    }
    return sender.sign_transaction(tx).rawTransaction  # en versiones nuevas: .raw_transaction
def send_private_bundle(target_block: int):
    nonce = w3.eth.get_transaction_count(sender.address)
    bundle = [{"signed_transaction": build_signed_tx(nonce)}]
    # Simulación antes de mandar
    w3.flashbots.simulate(bundle, target_block)
    result = w3.flashbots.send_bundle(bundle, target_block_number=target_block)
    result.wait()  # espera a que se procese el target_block
    try:
        return result.receipts()
    except Exception:
        return None  # bundle fuera del bloque: se puede reintentar en el siguiente
if __name__ == "__main__":
    target = w3.eth.block_number + 1
    receipts = send_private_bundle(target)
    print("Incluido en el bloque" if receipts else "Bundle fuera del bloque, reintentar en el próximo")

Un bundle solo sirve para un bloque objetivo, por lo que en la práctica hay que armar un loop para mandarlo durante varios bloques al hilo. Quedarse afuera no significa que haya un error: la pelea se define por el tamaño de la propina que le tiras al builder.

La rentabilidad de tener tu propio bot choca contra los costos de infraestructura. Alquilar un servidor dedicado en rack junto a un validador en un centro de datos arranca en los $500 USD al mes. Sin ese boost de hardware, tu bot físicamente no llega a calcular el estado del pool más rápido que los rivales. Gana el que tiene el cable más corto al validador y el algoritmo de búsqueda de spread más fino.

Los flash loans te dan la chance de armar operaciones de arbitraje zarpadas sin necesidad de poner una gran capital propia, usando préstamos sin garantía dentro de una misma transacción con rollback asegurado si la operación se va a pérdida.

Flash loans y la arquitectura del smart contract

Tener una banda de plata propia para arrancar con el arbitraje ya no es un requisito excluyente. Los contratos de integración con los pools de Aave v3 o Balancer te dejan sacar flash loans sin garantía en el marco de una sola transacción. El bot pide el activo prestado, lo pasa por los pares de DEX, devuelve la guita con la comisión del 0.09% y se guarda el resto en la billetera del creador. Si la cuenta final da negativa, toda la transacción se cancela con un revert y el emisor solo pierde el gas de haber llamado a la función.

Escribir un contrato de arbitraje en Solidity exige una optimización extrema para el bytecode de la EVM. Usar las interfaces estándar de ERC-20 mediante SafeERC20 mete validaciones de más que queman unidades valiosas de gas al chequear los límites de la transacción. Los devs con cancha tiran llamadas de bajo nivel directas con call, achicando al mínimo las operaciones de lectura y escritura en la memoria persistente (SLOAD/SSTORE).

Ejemplo de template en Solidity

Acá abajo tenés un template basado en flash loan de Aave V3 para pools de Uniswap V2, limpio de librerías de terceros:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
interface IERC20 {
    function balanceOf(address account) external view returns (uint256);
    function transfer(address to, uint256 amount) external returns (bool);
    function approve(address spender, uint256 amount) external returns (bool);
}
/// Aave V3: flash loan de un solo activo
interface IPool {
    function flashLoanSimple(
        address receiverAddress,
        address asset,
        uint256 amount,
        bytes calldata params,
        uint16 referralCode
    ) external;
}
interface IUniswapV2Pair {
    function token0() external view returns (address);
    function getReserves() external view returns (uint112, uint112, uint32);
    function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) external;
}
/// Arbitraje en flash loan de Aave V3: asset -> tokenB (pair1) -> asset (pair2).
/// Ejemplo para pools tipo Uniswap V2. No auditado.
contract FlashArbitrage {
    error Unauthorized();
    error InvalidCaller();
    error InvalidInitiator();
    error TransferFailed();
    error ApproveFailed();
    error InsufficientProfit(uint256 balanceAfter, uint256 required);
    address private immutable OWNER;
    IPool private immutable POOL;
    constructor(address aavePool) {
        OWNER = msg.sender;
        POOL = IPool(aavePool); // la dirección de Pool se saca del PoolAddressesProvider de la red correspondiente
    }
    modifier onlyOwner() {
        if (msg.sender != OWNER) revert Unauthorized();
        _;
    }
    /// Punto de entrada. Pide prestado `amount` del token `asset` y arranca el loop en executeOperation.
    function executeArbitrage(
        address asset,
        uint256 amount,
        address tokenB,
        address pair1,
        address pair2,
        uint256 minProfit
    ) external onlyOwner {
        bytes memory params = abi.encode(tokenB, pair1, pair2, minProfit);
        POOL.flashLoanSimple(address(this), asset, amount, params, 0);
    }
    /// Callback de Aave V3. Lo llama el pool una vez que emite el préstamo.
    function executeOperation(
        address asset,
        uint256 amount,
        uint256 premium,
        address initiator,
        bytes calldata params
    ) external returns (bool) {
        // 1. El callback solo lo puede disparar el pool de Aave...
        if (msg.sender != address(POOL)) revert InvalidCaller();
        // 2. ...y únicamente a pedido de este mismo contrato
        if (initiator != address(this)) revert InvalidInitiator();
        (address tokenB, address pair1, address pair2, uint256 minProfit) =
            abi.decode(params, (address, address, address, uint256));
        // El balance ya incluye la guita prestada
        uint256 balanceBefore = IERC20(asset).balanceOf(address(this));
        uint256 amountB = _swap(pair1, asset, amount);
        _swap(pair2, tokenB, amountB);
        // Ganancia = balanceAfter - balanceBefore - premium (comisión del préstamo)
        uint256 balanceAfter = IERC20(asset).balanceOf(address(this));
        uint256 required = balanceBefore + premium + minProfit;
        if (balanceAfter < required) revert InsufficientProfit(balanceAfter, required);
        // Aave cobra la deuda solito mediante transferFrom, así que hace falta un approve
        _safeApprove(asset, address(POOL), amount + premium);
        return true;
    }
    function withdraw(address token, uint256 amount) external onlyOwner {
        _safeTransfer(token, OWNER, amount);
    }
    function _swap(address pair, address tokenIn, uint256 amountIn) private returns (uint256 amountOut) {
        (uint256 r0, uint256 r1, ) = IUniswapV2Pair(pair).getReserves();
        bool zeroForOne = tokenIn == IUniswapV2Pair(pair).token0();
        (uint256 reserveIn, uint256 reserveOut) = zeroForOne ? (r0, r1) : (r1, r0);
        // Fórmula de Uniswap V2 (comisión del 0.3%)
        uint256 amountInWithFee = amountIn * 997;
        amountOut = (amountInWithFee * reserveOut) / (reserveIn * 1000 + amountInWithFee);
        _safeTransfer(tokenIn, pair, amountIn);
        IUniswapV2Pair(pair).swap(
            zeroForOne ? 0 : amountOut,
            zeroForOne ? amountOut : 0,
            address(this),
            "" // data vacío: es un swap común, no un flash swap
        );
    }
    function _safeTransfer(address token, address to, uint256 amount) private {
        if (token.code.length == 0) revert TransferFailed();
        (bool ok, bytes memory data) = token.call(abi.encodeCall(IERC20.transfer, (to, amount)));
        if (!ok || (data.length != 0 && !abi.decode(data, (bool)))) revert TransferFailed();
    }
    function _safeApprove(address token, address spender, uint256 amount) private {
        if (token.code.length == 0) revert ApproveFailed();
        (bool ok, bytes memory data) = token.call(abi.encodeCall(IERC20.approve, (spender, amount)));
        if (!ok || (data.length != 0 && !abi.decode(data, (bool)))) revert ApproveFailed();
    }
}

El callejón sin salida de la infraestructura en AWS

Meter un bot buscador en servidores virtuales de Amazon Web Services o Google Cloud es la forma más rápida de quemar presupuesto a lo pavote. La virtualización le mete latencia (jitter) a la pila de red, y los canales compartidos sufren pérdida de paquetes cuando el mempool está explotado de tráfico.

Un setup competitivo pide a gritos un servidor dedicado físico (bare-metal) con un procesador de alta frecuencia por núcleo, discos NVMe bien rápidos para correr un nodo local de Geth o Reth, y una conexión de red gigabit directa a los datacenters de Frankfurt. Levantar tu propio nodo lightweight es imperativo, porque los proveedores de RPC públicos como Infura o Alchemy te ratean la velocidad de entrega de bloques nuevos y te cajonean las notificaciones de transacciones pending unos 100 a 300 milisegundos que duelen un montón.

En el MEV no gana el que tiene el modelo matemático más rebuscado, sino el que está físicamente más pegado al validador y la tiene re clara calculando los costos de gas hasta la última instrucción de assembly en el bytecode.

Resumir esta publicación de blog con:

FAQ

La ganancia neta diaria de un bot de MEV independiente ronda entre $50 y $500 dólares en arbitraje atómico entre DEXs, alcanzando de $200 a $3,000 dólares en ataques de tipo sandwich durante periodos de alta volatilidad en el mercado. La falta de colocation física con los nodos validadores genera latencia y obliga al operador a pagar comisiones excesivas en las subastas de bloques, haciendo que las configuraciones comunes operen frecuentemente con pérdidas.

Los gastos de gas de un bot de MEV se componen de las tarifas base de ejecución en la red y las sobornos de prioridad pagados directamente a los constructores de bloques a través de relays privados como Flashbots mediante eth_sendBundle. Aunque el uso de relays privados evita la quema de gas en intentos fallidos, el éxito de la operación exige que el bot transfiera entre el 85% y el 95% del lucro bruto directamente al validador utilizando la función coinbase.transfer.

Los bots independientes de ataques sandwich en las redes EVM principales son mayoritariamente inviables debido a la desventaja de latencia, los altos requisitos de capital de trabajo y el dominio de los mempools privados. Sin acceso a servidores bare-metal dedicados con respuestas inferiores a un milisegundo, los desarrolladores solitarios terminan pagando de más en las tarifas de prioridad de los constructores, destruyendo por completo el margen de ganancia en los intercambios de tokens.
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 *