Ir al contenido principal
BlockchainMar 28, 2026

Deep EVM #15: Simulación MEV — Búsqueda Binaria, State Forks y el Deadline de 12 Segundos

OS
Open Soft Team

Engineering Team

La necesidad de velocidad

En MEV, tienes exactamente 12 segundos entre bloques para detectar oportunidades, simular transacciones, optimizar montos y enviar bundles. Un motor de simulación lento significa oportunidades perdidas.

La cadena de ejecución típica:

  1. Nuevo bloque llega (t=0)
  2. Actualizar estado local (t=0.1s)
  3. Detectar oportunidades (t=0.5s)
  4. Simular y optimizar (t=1-8s)
  5. Construir y enviar bundles (t=8-10s)
  6. Buffer para propagación (t=10-12s)

La simulación consume la mayor parte del tiempo. Optimizarla es la diferencia entre capturar $10K o $0.

State forks con revm

revm es una implementación de la EVM en Rust, optimizada para simulación. Permite crear forks del estado de Ethereum y ejecutar transacciones hipotéticas sin afectar el estado real.

use revm::{EVM, db::CacheDB, primitives::*};

fn simulate_swap(
    db: &CacheDB<EmptyDB>,
    router: Address,
    calldata: Bytes,
    value: U256,
) -> Result<ExecutionResult, EvmError> {
    let mut evm = EVM::new();
    evm.database(db.clone());
    
    evm.env.tx.caller = searcher_address();
    evm.env.tx.transact_to = TransactTo::Call(router);
    evm.env.tx.data = calldata;
    evm.env.tx.value = value;
    evm.env.tx.gas_limit = 500_000;
    
    evm.transact()
}

Búsqueda binaria para monto óptimo

Dado un ciclo de arbitraje, necesitamos encontrar el monto de entrada que maximiza el beneficio. La función beneficio es típicamente cóncava (sube, alcanza un pico, y baja), lo que permite búsqueda ternaria:

fn find_optimal_amount(
    db: &CacheDB<EmptyDB>,
    cycle: &ArbCycle,
    gas_cost: U256,
) -> (U256, U256) {
    let mut lo = eth(0.01);  // Mínimo: 0.01 ETH
    let mut hi = eth(100.0); // Máximo: 100 ETH
    
    for _ in 0..40 {
        let mid1 = lo + (hi - lo) / 3;
        let mid2 = hi - (hi - lo) / 3;
        
        let profit1 = simulate_cycle(db, cycle, mid1)
            .saturating_sub(gas_cost);
        let profit2 = simulate_cycle(db, cycle, mid2)
            .saturating_sub(gas_cost);
        
        if profit1 < profit2 {
            lo = mid1;
        } else {
            hi = mid2;
        }
    }
    
    let optimal = (lo + hi) / 2;
    let profit = simulate_cycle(db, cycle, optimal)
        .saturating_sub(gas_cost);
    (optimal, profit)
}

Pipeline de ejecución paralela

Con miles de oportunidades potenciales por bloque, la simulación secuencial no es suficiente. Usamos rayon para paralelizar:

use rayon::prelude::*;

fn evaluate_all_cycles(
    db: &CacheDB<EmptyDB>,
    cycles: &[ArbCycle],
    gas_price: U256,
) -> Vec<(ArbCycle, U256, U256)> {
    cycles.par_iter()
        .filter_map(|cycle| {
            let gas_cost = estimate_gas(cycle) * gas_price;
            let (amount, profit) = find_optimal_amount(
                db, cycle, gas_cost
            );
            
            if profit > min_profit_threshold() {
                Some((cycle.clone(), amount, profit))
            } else {
                None
            }
        })
        .collect()
}

CacheDB: evitar lecturas redundantes

Cada SLOAD que va al disco es lento. CacheDB cachea los valores leídos:

struct CacheDB<DB> {
    accounts: HashMap<Address, AccountInfo>,
    storage: HashMap<(Address, U256), U256>,
    underlying: DB,
}

Cuando simulamos múltiples ciclos que comparten pools, la caché se llena con la primera simulación y las siguientes son significativamente más rápidas.

Manejo del deadline

Con 12 segundos por bloque, necesitamos un sistema de deadlines:

async fn mev_loop(deadline: Instant) {
    let cycles = find_cycles().await;
    
    let mut best = None;
    
    for cycle in cycles {
        // Verificar si queda tiempo
        if Instant::now() > deadline - Duration::from_secs(2) {
            break; // Reservar 2s para envío
        }
        
        let result = simulate(cycle).await;
        if result.profit > best.map_or(U256::zero(), |b| b.profit) {
            best = Some(result);
        }
    }
    
    if let Some(best) = best {
        submit_bundle(best).await;
    }
}

Métricas de rendimiento

OperaciónTiempo típico
Fork de estado1-5ms
Simulación de swap0.5-2ms
Búsqueda binaria (40 iter)20-80ms
Evaluación de 1000 ciclos (paralela)200-500ms
Construcción de bundle1-5ms
Envío a builder10-50ms

Total: ~300-600ms para evaluar y enviar, dejando suficiente margen dentro del deadline de 12 segundos.

Conclusión

La simulación MEV es un problema de ingeniería de rendimiento. revm proporciona una EVM rápida para forks de estado, la búsqueda ternaria encuentra montos óptimos eficientemente, y la paralelización con rayon escala la evaluación de miles de ciclos. El manejo correcto del deadline de 12 segundos es la diferencia entre un bot rentable y uno que pierde oportunidades sistemáticamente.