Press ESC to close

Anti-MEV Stealth Swap: Build and Simulate a Bundle

In part one, we set up our "secure communication pipeline." In this part, we’re diving into the actual swap logic. Our goal is to craft a Uniswap V3 transaction that isn't just private, but is also guaranteed to go through.

1. Prepping the Swap Transaction

For this example, we’ll set up an ETH to USDC swap. To make it work in a bundle, we need to build the transaction object ahead of time without broadcasting it to the network.

Here’s what we need on the table:

  • The Uniswap V3 Router address.
  • A minimal ABI for the exactInputSingle function.
  • A gas calculation that accounts for the priority fee.

 

The Golden Rule: In Flashbots bundles, the standard gasPrice is swapped out for maxFeePerGas and maxPriorityFeePerGas. It’s the priorityFee—essentially a tip to the validator—that determines if your bundle actually lands in a block.

 

 

2. Code: Building and Simulating

Let’s add a swap.ts file to the project. The real star of the show here is the .simulate() method. This is the Flashbots "killer feature"—it lets you dry-run your transaction against the current state of the blockchain without burning a single cent on gas.

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

// Bare-bones ABI to talk to the Uniswap V3 Router
const ROUTER_ABI = [
    "function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 deadline, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) external payable returns (uint256 amountOut)"
];

const ROUTER_ADDRESS = "0xE592427A0AEce92De3Edee1F18E0157C05861564";

export async function createAndSimulateBundle(wallet: ethers.Wallet, flashbotsProvider: any, provider: ethers.Provider) {
    const block = await provider.getBlock("latest");
    const nextBlockNumber = block!.number + 1;

    // 1. Set up the interface and prep the transaction data
    const iface = new ethers.Interface(ROUTER_ABI);
    const deadline = Math.floor(Date.now() / 1000) + 60 * 20; // 20-minute window
    
    const amountIn = ethers.parseEther("0.1"); // Swapping 0.1 ETH
    const params = {
        tokenIn: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", // WETH
        tokenOut: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
        fee: 3000, // 0.3% pool
        recipient: wallet.address,
        deadline: deadline,
        amountIn: amountIn,
        amountOutMinimum: 0, // In a real scenario, ALWAYS calculate slippage!
        sqrtPriceLimitX96: 0
    };

    const data = iface.encodeFunctionData("exactInputSingle", [params]);

    // 2. Define the transaction structure
    const transaction = {
        to: ROUTER_ADDRESS,
        value: amountIn,
        data: data,
        chainId: 1,
        type: 2, // EIP-1559
        gasLimit: 250000,
        maxFeePerGas: ethers.parseUnits("50", "gwei"),
        maxPriorityFeePerGas: ethers.parseUnits("2", "gwei"), // The validator "bribe"
        nonce: await wallet.getNonce()
    };

    // 3. Create the signed bundle
    const signedBundle = await flashbotsProvider.signBundle([
        {
            signer: wallet,
            transaction: transaction
        }
    ]);

    // 4. SIMULATION (The most important step)
    console.log("Kicking off bundle simulation...");
    const simulation = await flashbotsProvider.simulate(signedBundle, nextBlockNumber);

    if ("error" in simulation) {
        console.error(`Simulation failed: ${simulation.error.message}`);
        return;
    }

    console.log("Simulation lookin' good!", JSON.stringify(simulation, null, 2));
    return signedBundle;
}

 

3. The Tech Lowdown: Why is simulation a must?

On the "public" mempool, if your transaction fails (say, you hit a gas limit or the price moves against you), it still gets included in a block, and you still get slapped with the gas fee.

In the Flashbots world:

  • If the simulation throws an error, you simply don't send the bundle. No harm, no foul.
  • If the bundle is sent but market conditions shift making the trade unfavorable, the validator just ignores it.

Bottom line: You only pay for gas when your Stealth Swap actually crosses the finish line.

4. How to calculate the "Bribe" (Priority Fee)

Validators pick bundles based on how much profit they bring in. They calculate a bundle's "Gas Price Score" like this:

formula7
 

For a standard swap, a maxPriorityFeePerGas of 1-2 gwei is usually plenty. But when things get volatile, the competition for block space heats up, even on these private tracks.

 

Where are we at?

We’ve now got a signed, verified bundle ready to roll. We’re 100% sure the Uniswap code will behave and the wallet has the funds to cover the move.

Coming up in the final part: we'll build the block-wait loop, handle the actual bundle broadcast, and wrap it all in a clean CLI interface.

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....

Leave a comment

Your email address will not be published. Required fields are marked *