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.
State Overrides
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.
Hoorah!
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 exactInput
or exactOutput
.
I have adjusted the __UniswapV3Pool_swap
method to support an arbitrary starting state (sqrtPrice
, tick
, liquidity
). For simplicity, I have chosen not to implement tick_data
and tick_bitmap
overrides yet. As such, back-running liquidity events (Mint
, 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:
override_start_liquidity
override_start_sqrt_price_x96
override_start_tick
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.
Within the 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.
Transaction Simulation
The new 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.
Pool Methods
Both the 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).
V2 Swaps
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_in
and v2_swap_exact_out
. These will handle swaps of any arbitrary path length, returning a list of pool objects and their associated final states.
V3 Swaps
These are implemented similarly via the v3_swap_exact_in
and v3_swap_exact_out
methods.
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 exactInput
.
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.
Transaction Watcher
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.