Atomic arbitrage through entirely Uniswap V2-based pools is very simple and easy to optimize. The accounting is done after the swap is performed, and checked against reserve values held in storage prior to the transaction.
Since a V2 swap is pre-paid, you can implement a very simple chaining technique to arbitrage across multiple pools — transfer the input token to the first pool, then call swap()
with the second pool as the recipient. Then repeat swap()
at successive pools until done.
Uniswap V3 takes a different approach to the pool and its swaps.
I explored many differences in Uniswap V3 — Pool Contract.
A key difference in the V3 structure is the swap accounting. It takes place after the swap, just like in V2, but it loads the pre-swap balance before doing the swap.
For more on this topic, please review the Uniswap V3 — Swap Callback exploration.
A big downside to the V3 structure is that you can no longer daisy-chain swaps.
Here is the flow of V3 pool swap accounting:
User calls
swap()
The pool performs liquidity accounting to determine how much of either token can be withdrawn by the swap
The pool checks its balance of the payment token
The pool transfers the withdrawl token to the recipient
The pool calls the special
uniswapV3SwapCallback()
function atmsg.sender
The pool checks its balance of the payment token
If the balance is insufficient, it reverts
Otherwise, it performs further fee accounting and updates state values
Notice that steps 3 and 7 are repeated before and after the callback. We simply can’t pre-pay a V3 pool because the balance check would assume it was supposed to be there pre-callback, and then revert.
Naive Method
Since the payment for a V3 swap needs to occur inside the callback, an easy assumption is that the payment amount must be held by the smart contract prior to calling for the swap. After all, Uniswap’s own V3 Router and newer Universal Router contracts work this way.