Given the all-in-one nature of the PoolManager, I was hopeful that V4-only arbitrage would be simple. I am pleased to discover that it’s true! Sidestepping the complexities of nested V4/V3 callbacks is a welcome change.
If you’re feeling overwhelmed by the previous project, perhaps this will be more your speed.
I mentioned at the end of the previous post that I had deployed a V4/V4 bot for testing. The Uniswap V4 ecosystem on Base is quite small so the profits from the past week are nothing exciting, but the strategy appears legitimate and I’ve sent a few thousand transactions through the contract without any issues.
I initially developed a contract and helper to capture V4/V4 two-pool opportunities, but quickly realized that generalizing to multi-pool paths is quite easy. So rather than roll out separate projects, I’m going to go straight to the multi-pool version — it works just as well for pools of length 2.
Callback Strategy
The smart contract is only concerned with operations at the PoolManager, so we only need to give it the minimum input to accomplish the swaps.
Recall from Parts II and III that the PoolManager needs two inputs to perform a single swap: PoolKey
and a SwapParams
.
A PoolKey is the set of values that identifies a unique V4 liquidity pool, and SwapParams is the set of values for performing a swap through that pool.
From PoolKey.sol:
struct PoolKey {
Currency currency0;
Currency currency1;
uint24 fee;
int24 tickSpacing;
IHooks hooks;
}
And from Pool.sol:
struct SwapParams {
int256 amountSpecified;
int24 tickSpacing;
bool zeroForOne;
uint160 sqrtPriceLimitX96;
uint24 lpFeeOverride;
}
These values can be specified entirely off-chain and injected into the contract at the entry point, but I have taken a dynamic approach which requires only an ordered array of PoolKey
s and an initial swap amount.
Swap Accounting
The return value from a swap()
call at PoolManager is a close-packed set of int128
s representing the currency0 and currency1 amounts that must be zeroed by the end of the callback. A zero-for-one swap (trading currency0 for currency1) will result in a negative delta (an amount due) for currency0 and a positive delta (a credit to be withdrawn) for currency1.
Consider the case of two pools with the same currencies — one is Ether in the currency0 position, the other is an arbitrary pair token called X in the currency1 position.
The arbitrage will take this form:
Pool 1: Ether → X
Pool 2: X → Ether