How to Test an MEV Bot on a Mainnet Fork With Anvil

The most expensive way to test an MEV bot is on mainnet with real money. A bug in the swap math, a wrong token decimal, an approval missing in the contract, and the "test" costs a few hundred dollars in gas or a lot more in bad trades.

The cheap way is a mainnet fork. You run a local copy of Ethereum that starts from the real chain state at a block you choose. Every pool, every token balance and every contract is there, but the ETH is fake and nothing you do touches the real chain. Foundry's Anvil makes this really easy.

This is the setup I'd recommend for testing any bot: arbitrage, liquidation, sniping, anything.

Install Foundry and start a fork

If you don't have Foundry yet, install it from getfoundry.sh. Then you need an archive-capable RPC URL. A free tier from a node provider works to start, or your own node if you have one.

Start the fork:

anvil --fork-url $ETH_RPC_URL --fork-block-number 21000000

That gives you a local node on http://127.0.0.1:8545 with mainnet state as of that block. Anvil fetches the state it needs from your RPC as you go and caches it, so the first calls are slower than the rest.

Always pin the block number. If you don't, you fork from the latest block, and every test run starts from a different state. Pinned blocks make your tests reproducible, and Anvil can reuse its cache. The forking docs cover the options.

Don't use the default accounts with real keys

Anvil starts with ten accounts, each with 10,000 fake ETH, from a well-known test mnemonic. Those keys are public. Everyone has them. If you ever use one of them on a real network by accident, whatever's in it gets taken immediately.

The Anvil docs warn about this. Use your own mnemonic for forks:

anvil --fork-url $ETH_RPC_URL --fork-block-number 21000000 \
  --mnemonic "your own test mnemonic that is never used on mainnet"

And keep your real bot keys out of the test environment completely.

The custom methods that make forks useful

Anvil has a set of custom RPC methods beyond normal Ethereum JSON-RPC. These are the ones I use most for bots.

Impersonate any address

You can send transactions as any address without its private key. That's how you get tokens for testing, by impersonating a whale, or how you replay a transaction from a real user.

cast rpc anvil_impersonateAccount $WHALE
cast send $USDC "transfer(address,uint256)" $MY_BOT 1000000000 \
  --from $WHALE --unlocked
cast rpc anvil_stopImpersonatingAccount $WHALE

Or start Anvil with --auto-impersonate to allow it for every address.

Set balances, code and storage directly

cast rpc anvil_setBalance $MY_BOT 0xDE0B6B3A7640000   # 1 ETH
cast rpc anvil_setCode $ADDRESS $BYTECODE
cast rpc anvil_setStorageAt $ADDRESS $SLOT $VALUE

anvil_setCode is great for swapping a contract for a modified version, for example a mock oracle you can move whenever you want. That's how you test a liquidation bot: change the price, see if your bot finds the position and closes it.

Control mining

This is the important one for MEV. By default, Anvil mines a block for every transaction instantly. That's useless if your bot needs to react to pending transactions. So turn it off:

anvil --fork-url $ETH_RPC_URL --fork-block-number 21000000 --no-mining

Now transactions sit in Anvil's pool until you mine. You can see them with txpool_content, the same way your bot would watch a mempool, and then mine when you're ready:

cast rpc txpool_content
cast rpc anvil_mine

This lets you simulate the real flow: a "victim" transaction arrives in the pool, your bot sees it, builds its response, and you mine one block to see the result. You can also use --block-time 12 for regular 12-second blocks.

Snapshots

cast rpc evm_snapshot     # returns an id
# ... run your test ...
cast rpc evm_revert $SNAPSHOT_ID   # back to the snapshot

Snapshot before each test case and revert afterwards. You can run the same scenario a hundred times without restarting Anvil or fetching state again.

Traces

Start Anvil with --print-traces and you'll see the full call trace for every transaction in the terminal. When your bot's transaction reverts with a useless error, this tells you which call failed and why.

Replay a real opportunity

This is where forks really help. Find a real MEV transaction on Etherscan, a liquidation or an arbitrage that another bot won. Note its block number, then:

  1. Fork at the block before it: --fork-block-number <block - 1>.
  2. Disable mining.
  3. Send the transactions that came before it in that block (impersonating their senders), so the state is the same as when the opportunity appeared.
  4. Run your bot and see if it finds the same opportunity, and how much it would have made.
  5. Compare your gas usage to the winner's.

You can also inspect any historical transaction directly with cast run:

cast run $TX_HASH --rpc-url $ETH_RPC_URL

That replays the transaction locally and prints its trace, which is great for understanding what a competitor's contract actually does.

Do this for twenty historical opportunities and you'll learn more about your strategy than from a month of reading.

Forge tests for the contract

For the on-chain part of the bot, write Foundry tests that run against a fork:

function setUp() public {
    vm.createSelectFork(vm.envString("ETH_RPC_URL"), 21_000_000);
    deal(USDC, address(bot), 1_000e6);
}

function test_arbitrageIsProfitable() public {
    uint256 before = IERC20(WETH).balanceOf(address(bot));
    bot.execute(route);
    assertGt(IERC20(WETH).balanceOf(address(bot)), before);
}

deal from forge-std sets a token balance for you, so you don't need to find a whale. Add tests for the failure cases too: what happens if the price moved, if the pool is empty, if the token takes a fee. Those are the ones that lose real money.

What a fork can't tell you

A fork is great for checking that your logic and contracts work. It doesn't tell you whether you'll win on mainnet:

  • No competition. On your fork, you're the only bot. On mainnet, ten others see the same opportunity.
  • No builder auction. Your bundle doesn't compete for inclusion. To learn how that part works, read Flashbots bundles explained.
  • No latency. Real bots fail because they're 200ms too slow. A fork won't show that.
  • Your RPC is the bottleneck. Forks fetch state on demand, so heavy tests can hit rate limits on free RPC plans.

So use the fork to make sure the bot is correct, then go live with small size to find out if it's competitive.

My testing checklist before going live

  • Contract tests pass on a fork, including failure cases
  • Bot finds and handles at least ten replayed historical opportunities
  • Gas usage is close to (or lower than) the bots that won those opportunities
  • Profit calculation includes gas and the builder payment
  • No private key from the test setup is used anywhere real
  • Kill switch and max position size are tested

If you're building a liquidation bot, I wrote about the strategy side in how liquidation bots work on Aave. For arbitrage, start with how to build a profitable MEV arbitrage bot.

And if you want me to build or review a bot for you, message me on Telegram. The ready-made MEVDAO tools, the Sniper Bot, Sniping Group and Token Rescue Bot, are on the features page. Join the MEV Secrets email list right below for weekly tips.

← All articles