I deployed some rough V4/V2 arbitrage bots to Base for observation and testing early this week. They landed a few hundred tiny arbs — couch cushion money to be fair — but watching them helped me squish some bugs, and the system is functioning as expected. So let’s dig in!
I typically finish off an article series with a project like this. But the complexity of V4 is high enough that I’m going to stay on the subject. I will roll out multiple projects over the coming weeks to extend this initial V4/V2 example to cover V4/V4, V4/V3, and arbitrary length V4/V3/V2 multi-hop arbitrage.
Version Info
This project uses degenbot version 0.4.2 and Vyper version 0.4.1. You can install both using pip
or similar.
EDIT: The project was originally published with degenbot version 0.4.0. Since publishing, I have fixed some bugs related to V4/V2 assertions in releases 0.4.1 and 0.4.2.
The minimum required version of Python is 3.12.
I developed and tested this project against Reth version 1.3.4 and op-node version 1.12.2.
New Bot Features
Web3py Subscription Listener
Web3py v7.7 introduced a nice abstraction above subscribing and listening to events over WebSocket or IPC connections.
I have covered listening to events over a websocket here before.
It is great that web3py has this baked into the library, which eliminates the need to import the websockets library and manage that communication manually.
As a bonus, the same API is exposed for listening to events over IPC. IPC is a high performance way for processes running on the same physical machine to communicate at the socket layer — I recommend it wherever you can.
The bot uses the new subscription API to get events and new blocks.
eth_simulateV1
The eth_simulateV1 RPC endpoint is a high-performance method of doing multi-block multiple-call transaction simulations. The original proposal can be found in PR 484 in the ethereum/execution-apis Github repository.
The endpoint has been integrated in Geth, Reth, and Nethermind (and likely others). It is now common enough to design against, and web3py merged support for the endpoint last month.
Fun fact: the endpoint support in web3py is so new that the documentation doesn't match the code! The endpoint is documented as available at the Eth.simulateV1
method, but the actual method is Eth.simulate_v1
.
Because this method was unavailable to me previously, I built the AnvilFork class to launch and manage Anvil forks for bundle simulation testing.
I still find AnvilFork
very helpful for testing and chain manipulation, but for simple transaction testing, eth_simulateV1 is a higher performance method.
Multiple Processing Workers
Concurrent programming is hard, though we have some nice tools to mostly do it right. A performance bottleneck that typically affects Python bots is a long-running CPU-bound calculation.
It simply takes time to identify valid arbitrage paths, optimize the profit for each, and collect the results. For small bots monitoring a handful of pairs, this is barely noticeable. But for bots tracking tens of thousands or pairs, and potentially millions of arbitrage paths, the overhead can affect performance.
The heaviest arbitrage paths to process involve concentrated liquidity positions, which Uniswap V3 & V4 are built on. Calculations can be sped up in two main ways:
Having access to a complete liquidity map
Using multiple processes
Providing a complete liquidity map speeds calculation because there is no need to backfill missing liquidity regions with slow chain queries. Maintaining the liquidity map is challenging, but we’ve gotten pretty good at it since I introduced the liquidity mapping technique during my exploration of the Uniswap V3 pool helper.
See below if you’re unfamiliar:
Processor architecture has been multi-core for nearly a decade. If you’re doing multiple independent calculations that can be initialized cheaply, spreading that work to multiple cores (via separate Python processes) is a no-brainer. I introduced this technique using the ProcessPoolExecutor
from the concurrent.futures module.
By using a process pool, multiple workers can be started and stand ready to perform their own CPU-bound calculations without affecting the primary executor bot. This means that the long-running tasks described above can be handled without blocking latency-sensitive operations (listening for events, performing pool updates, sending transactions to the network).
A subtle requirement of using ProcessPoolExecutor
is that data sent to and from the worker must be pickled and unpickled. This process is fairly fast, but the task time is highly variable depending on the nature of the arbitrage path. Tools like CVXPY allow for very fast convex calculations, and can drastically speed up processing.
Unfortunately the absolute worst performer in terms of calculation time is for a V3/V4 arb with very low liquidity in one of the pools. I am working on good filtering heuristics to discard these from consideration, but the best way to not let these screw up our bot is to fan out the processing and collect the results whenever they are ready, instead of waiting for them.
To that end, I’ve refactored the processing workers to operate on one item at a time, dump their results into an asyncio queue, and begin work on another. This means that the consumer of these work items does not have to wait around for every worker to be done.
Dedicated Submission Worker
The multiple worker approach described above is commonly called “fan-out”. The flip side of this is “fan-in”, where the distributed results are collected and evaluated.
Keep reading with a 7-day free trial
Subscribe to Degen Code to keep reading this post and get 7 days of free access to the full post archives.