Degen Code

Degen Code

Rewrite It in Rust: Simulation

Part VI: Sim Pool Really

Jul 22, 2026
∙ Paid

The most painful part of high-volume searching is simulation.

Before every value capture can occur, you must run it against your best view of the chain state. The boundary between your efficient searcher and the generalized (read: slow) RPC is the point where scalability goes to die.

Let’s fix that, shall we?

eth_call

The easiest approach is to use eth_call, which executes everything but stops short of applying the state transition.

This can be used to call read-only functions, such as balanceOf on an ERC-20 token, or to execute a state-changing function without broadcasting it, like checking for the successful execution of a swap.

eth_call has advanced overrides features that allow you to tweak the environment for each simulation:

  • State values: account balance, nonce, code at some address, values in storage, etc.

  • Block values: block number, timestamp, gas limit, blob and base fee, etc.

A key limitation of eth_call is that is can only execute one transaction at a time. The state that would results from TX_1 is not visible on the eth_call of TX_2, which is critically important for multi-transaction bundles.

I covered eth_call in detail:

Avoiding Trap Tokens With eth_call

Avoiding Trap Tokens With eth_call

BowTiedDevil
·
September 1, 2022
Read full story

You could wire up a bunch of state inspection functions to chain multiple eth_calls together, but there’s a better way.

eth_simulateV1

eth_simulateV1 has a similar structure to eth_call, supports the same overrides, but supports multiple sequenced transactions instead of just one. And the state transitions of one transaction persist to the next simulation. In this way, a [TX_1, TX_2, … TX_n] bundle simulation can be executed where the final state of TX_1 is exposed to TX_2, TX_2 feed TX_3, and so on.

I’ve covered eth_simulateV1 as well, and used it as a key element of my Base flashblocks backrunner project:

Project: Base Flashblocks Backrunner

BowTiedDevil
·
July 28, 2025
Read full story

The Hidden Bottleneck

While eth_simulateV1 is very flexible, it still locks you to an external RPC and commits you to unavoidable network I/O. This cost can be substantially reduced by running a local node on the same computer or at least the same network.

But even after minimizing that cost, there is still significant simulation overhead from data marshaling, e.g. searcher process data → JSON-RPC → node process data → JSON-RPC → searcher process data.

And it gets worse! Each RPC request is executed in a vacuum and from a “fresh” global state. Perhaps you see a mempool transaction that you want to backrun (TX_1), so you prepare several eth_simulateV1 bundles across the different paths available from the affected pool. Each time the node receives that simulation bundle, it will execute TX_1 again. Node software may cache the state values to avoid disk I/O and state derivation, but it’s largely out of your hands unless you want to take the burden of maintaining a forked version of your preferred node.

Just Rewrite The EVM Bro

If you zoom out a bit and survey the Ethereum software ecosystem, it quickly becomes clear that nobody wants to reinvent an EVM implementation. Most software builds on top of a reference library for their given language. Python has EELS, Rust has revm, Go has the core-geth vm, and Javascript has EthereumJS.

We’re already on the Rust train, so we’ll focus on revm.

The revm documentation is quite sparse, but the github repo provides several nice examples that illustrate how to use it.

Architecture

We are primarily concerned with state access. Revm provides state access through a Database trait that expects four methods:

  • basic, which gets account information

  • code_by_hash, which gets the code stored on-chain

  • storage, which gets the value stored at an address and slot

  • block_hash, which gets the block hash by number

There is an equivalent trait DatabaseRef which takes a &self instead of &mut self.

Both traits are implemented by several Revm built-in structs:

  • impl Database for BenchmarkDB

  • impl<DB> Database for BalDatabase<DB>
    where DB: Database

  • impl<DB> Database for State<DB>
    where DB: Database

  • impl<E> Database for EmptyDBTyped<E>
    where E: DBErrorMarker + Error + Send + Sync + 'static>

  • impl<ExtDB> CacheDB<ExtDB>
    where ExtDB: DatabaseRef

  • impl<L, R> Database for Either<L, R>
    where L: Database,
    R: Database<Error = <L as Database>::Error>

  • impl<T> Database for WrapDatabaseAsync<T>
    where T: DatabaseAsync

  • impl<T> Database for WrapDatabaseRef<T>
    where T: DatabaseRef

The most interesting of these for our purpose is CacheDb, which wraps a reference to a Database.

If we construct a Database and wrap it in CacheDb, we can execute queries against that database, using the caching feature to retrieve values more quickly.

And since Database and DatabaseRef are public traits, we can implement them on our own structs. Revm even provides a built-in wrapper wraps an Alloy provider. I already showed how fast Alloy’s providers can be, so this is a big win.

Rewrite It in Rust: RPC Provider

BowTiedDevil
·
Jul 9
Read full story

Cached Single Calls

The unlock here is that CacheDb can wrap an Alloy provider connection to your RPC. You can then pass the cached DB to your new EVM instance and then run queries against it.

This post is for paid subscribers

Already a paid subscriber? Sign in
© 2026 BowTiedDevil · Privacy ∙ Terms ∙ Collection notice
Start your SubstackGet the app
Substack is the home for great culture