Uniswap — Transaction Prediction (Part III)
🔮 I Predict I'm Almost Done With Predictions
I’ve been adding more V2 and V3 methods to the
UniswapTransaction helper class, with minor detours along the way to support necessary liquidity pool methods.
This is a short post because most of my effort this week involved writing and pushing new code up to github.
I won’t spend a lot of time describing changes under-the-hood, but will describe some highlights.
Tomorrow I will write up a more focused proof-of-concept to demonstrate how to hook one of these TX simulations to a backrun calc.
A key feature necessary to do front- and back-runs is the ability to simulate swaps at an arbitrary state. I implemented state overrides in a clumsy way in the V2 helpers. Luckily the simplistic nature of the V2 pool masked a lot of that. Modifying the calculation methods to override the starting pool reserves was the only real requirement. After that it’s mostly bookkeeping.
I’m fortunate to have significantly more experience now, so adding the state override feature to the V3 helper was much more straightforward. My code is cleaner and I was able to get this implemented without much tail-chasing.
In the interest of interface-first design, I started by considering where to implement the override. The obvious choice was the
V3LiquidityPool class, which already has external methods to calculate swaps of either type
I have adjusted the
__UniswapV3Pool_swap method to support an arbitrary starting state (
liquidity). For simplicity, I have chosen not to implement
tick_bitmap overrides yet. As such, back-running liquidity events (
Burn) is not possible. However, front-running and back-running normal swaps is.
Modifying the swap method was fairly easy. I’ve added three optional arguments:
They work exactly how you’d expect. If any overrides are provided, the swap function will use that as the starting point for the swap instead of the current state value tracked by the helper.
V3LiquidityPool helper, I’ve implemented another external interface called
simulate_swap. This method performs a simulation of the swap (based on various arguments that help it determine the token swap direction and size) and returns information about the swap itself and the final pool state. Importantly, it accepts an argument called
override_state, which is composed of any number of override values, which it passes into the swap function. This method will be the primary method that transaction simulation helpers will use to interact with V3 pools.
I’ve included a method of the same name in the old V2 helper class (
LiquidityPool) that works the same way. It’s less complex since the only thing you can override there are the reserves, but it has the same goal.
UniswapTransaction method should be able to take relevant Uniswap function calls, identify the necessary pool helpers, then perform a full simulation of that function call. Simply put, if I observe a transaction in the mempool, I should expect to simulate it and get some data structure with predicted pool states, then feed those pool states into my arbitrage helper to calculate a backrun.
LiquidityPool and the
V3LiquidityPool classes have a new interface method called
simulate_swap which will return two dictionaries:
A swap value dictionary (+/- amount0, +/- amount1)
A final state dictionary:
(reserves0, reserves1) for V2 pools
(liquidity, sqrt_price_x96, tick) for V3 pools
This allows me to predict the swap result for the user as well as the final state of the pool. This will be useful later for checking actual swap results against the provided minimum outputs (factored for slippage).
I’ve implemented all of the V2 router functions, which was made rather simple since they’re all variations of two types (exactInput and exactOutput, to borrow descriptions from V3). There are differences in the way Ether is handled at the input or output step, but that does not affect the calculations along the pool path.
I’ve built two functions inside the
simulate method called
v2_swap_exact_out. These will handle swaps of any arbitrary path length, returning a list of pool objects and their associated final states.
These are implemented similarly via the
The V3 router only supports these two kinds of swaps, the major difference rest is just path and calldata processing within the router.
For example, the V3 router can perform an
exactInputSingle swap or an
exactInput swap. The only difference being that the single version uses one pool, and the other uses an arbitrary number of them. The swaps themselves are performed the same at the pool level, so the simulation can use the same
v3_swap_exact_in function at the core after “unwrapping” the pool path sent to
I recommend reviewing the decoding and processing chain in the transaction handler source code on github. You will see how input data to the router is separated into values, shuttled into the ported swap function to predict swap and state values, then passed back up the chain to the ultimate return value of
simulate, which is a list of pool objects and their final state values.
I have fully defined the V2 and V3 swap functions within
UniswapTransaction, so if you’re interested in watching an absolute ton of Uniswap info from the mempool, you can run this simple script for a few minute and watch your console fill up.