Appuyez sur ESC pour fermer

Anti-MEV Stealth Swap : Le dénouement – Envoi et CLI

Bon, dans les deux articles précédents, on a mis en place notre « canal de communication sécurisé » et codé la logique du swap. Dans cette dernière partie, on passe aux finitions. On va implémenter la boucle d'attente des blocs et packager le tout dans une interface en ligne de commande (CLI) bien pratique.

1. Logique d'envoi : Pourquoi on ne peut pas juste cliquer sur « Send » ?

Le réseau Ethereum génère des blocs environ toutes les 12 secondes. Votre bundle n'est valide que pour un numéro de bloc précis. S'il n'est pas inclus dans le bloc suivant (par exemple, à cause d'un pourboire trop faible), il faut le reconstruire pour le bloc d'après avec un numéro mis à jour.

On va donc utiliser une boucle qui va tenter de « pousser » notre Stealth Swap sur les 10 prochains blocs.

 

2. Code : Implémentation de l'envoi et du CLI

Ajoutons la méthode finale sendBundle et une gestion simple des arguments en ligne de commande.

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(`Démarrage au bloc : ${currentBlock}`);

    // On tente d'envoyer le bundle sur les 10 prochains blocs
    for (let i = 0; i < 10; i++) {
        const targetBlock = currentBlock + i;
        
        // On reconstruit le bundle pour le bloc cible (simulation incluse)
        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(`Erreur d'envoi : ${bundleSubmission.error.message}`);
            continue;
        }

        console.log(`Bundle envoyé. En attente du bloc ${targetBlock}...`);
        const waitResponse = await bundleSubmission.wait();
        
        if (waitResponse === FlashbotsBundleResolution.BundleIncluded) {
            console.log(`VICTOIRE ! Transaction incluse dans le bloc ${targetBlock}`);
            console.log(`Hash : https://etherscan.io/tx/${(await signedBundle)[0].hash}`); // Calcul approximatif du hash
            return;
        } else if (waitResponse === FlashbotsBundleResolution.BlockPassedWithoutInclusion) {
            console.log(`Raté. Le bloc ${targetBlock} est passé sans nous. On tente le suivant...`);
        } else if (waitResponse === FlashbotsBundleResolution.AccountNonceTooHigh) {
            console.error("Erreur : Nonce trop élevé. Vérifiez vos transactions en attente.");
            return;
        }
    }
}

// Interface CLI basique
const amount = process.argv[2] || "0.01";
runStealthSwap(amount);

Évidemment, ce qu'on a fait là, c'est le « Hello World » de Flashbots pour un swap via Uniswap V3. Pour en faire un outil de production sérieux, on va peaufiner un peu la bête.

 

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);

    // --- VÉRIFICATION PRÉALABLE ---
    const balance = await weth.balanceOf(wallet.address);
    if (balance < baseAmount) throw new Error("PAS ASSEZ DE WETH");

    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 {
            // --- SAUT ALÉATOIRE (casse les patterns prévisibles) ---
            if (Math.random() < 0.6) return;

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

            // --- SEUIL (Threshold) ---
            if (quote.amountOut < minThreshold) return;

            // --- DÉTECTION DE CHANGEMENT ---
            if (lastQuote !== 0n) {
                const diff = (quote.amountOut * 1000n) / lastQuote;
                // On ignore si le changement est < 0.3%
                if (diff > 997n && diff < 1003n) return;
            }

            // --- TEMPS DE REPOS (Cool Down) ---
            if (block - lastExecutionBlock < 2) return;

            // --- MONTANT ALÉATOIRE (casse la signature du bot) ---
            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++; // CRUCIAL
            if (res === FlashbotsBundleResolution.BundleIncluded) {
                console.log("EXÉCUTÉ AU BLOC :", target);
                process.exit(0);
            }

            lastQuote = quote.amountOut;
            lastExecutionBlock = block;
        } catch (e) {
            // mode silencieux
        }
    });
}

main();

 

3. Comment utiliser Anti-MEV Stealth Swap

1. Installation

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

2. Créez votre fichier .env

ETH_RPC_URL=https://mainnet.infura.io/v3/VOTRE_CLE
SENDER_PRIVATE_KEY=VOTRE_CLE_PRIVEE
FLASHBOTS_AUTH_KEY=N_IMPORTE_QUELLE_NOUVELLE_CLE_PRIVEE

3. Préparation du wallet

Obligatoire :

  • Avoir de l'Ethereum (ETH) pour payer le gaz.
  • Avoir du Wrapped Ether (WETH) pour le swap lui-même.

Si vous n'avez pas de WETH :

  • Convertissez d'abord vos ETH en WETH via n'importe quelle interface (Uniswap, etc.).

4. Lancement

node app.js 0.1 200

Où :

  • 0.1 → montant de WETH à swapper.
  • 200 → montant minimum de USDC attendu en sortie.

5. Comment ça marche

Une fois lancé :

  • Le script vérifie le solde WETH.
  • Il fait l'approve (une seule fois).
  • Il commence à écouter chaque nouveau bloc.
  • À chaque bloc :
    • Il saute parfois son tour (aléatoire).
    • Il check le prix via le Quoter.
    • Il vérifie le seuil de rentabilité.
    • Il s'assure que le prix bouge (pas de stagnation).
    • Il modifie légèrement le montant (pour la discrétion).
    • Il simule la transaction.
    • Il l'envoie via Flashbots.

6. Conditions de succès

  • Prix ≥ au seuil indiqué.
  • Le prix a changé récemment.
  • Le cooldown est respecté.
  • La simulation est un succès.

7. Résultat

Quand la console affiche :

EXECUTED: 19483921

Cela signifie que :

  • Le swap est fait.
  • Le script s'arrête proprement.

8. Paramètres clés
Seuil (2ème argument)

node app.js 0.1 220

Plus le seuil est haut =

  • Moins de transactions.
  • Meilleur prix d'exécution.

Montant

node app.js 0.05 200

Moins de montant =

  • Moins d'impact sur le marché.
  • Risque plus faible.

9. Ce qu'il faut bien comprendre

  • Ça ne garantit pas le meilleur prix absolu.
  • Ça ne protège pas contre toutes les attaques imaginables.
  • Mais ça réduit drastiquement vos chances d'être la « proie » d'un bot de front-run.

10. Quand NE PAS l'utiliser

  • Si vous ne comprenez pas ce qu'est le slippage.
  • Si votre balance est trop juste.
  • Si le réseau est totalement congestionné.

L'essentiel

  • Lancement en 1 commande.
  • Automatique à 100%.
  • Swap « furtif » via Flashbots.

Ce code :

  • Ne battra pas les bots MEV pros en duel frontal.
  • N'est pas une armure parfaite.
  • Ne crée pas d'« edge » magique.

MAIS :
👉 Vous n'êtes plus une cible facile.

 

Cet outil n'est que la partie émergée de l'iceberg. On peut le faire évoluer pour de l'arbitrage, des liquidations ou simplement pour déplacer de gros volumes de liquidité en toute sécurité.

Souvenez-vous : Dans la « forêt sombre » de la blockchain, ce n'est pas celui qui crie le plus fort qui survit, c'est celui qui sait avancer sans bruit.

Contenu préparé pour l'Académie EXMON. Expérimentez sur le Mainnet avec prudence et surveillez toujours vos paramètres 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....

Partager votre avis

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués *