This lesson will test our understanding of the Pool, Router and Quoter contracts.
It’s all fine and good to understand what the contracts do, but the goal of MEV enjoyooooors is to identify and profit from misaligned markets. Identifying misaligned markets is all about prices, prices, prices!
So we will take our very familiar and comfy DAI/WETH pool, run a local fork, and then beat on this pool until we understand how swaps affect the price, how liquidity and ticks are affected for various trade sizes, and how to properly predict the end state for some fictional trades.
The end goal is to build another set of degenbot helper classes. Again, nice to know how all this works, but even better to figure it out and then abstract away the details.
Lesson Structure
This will be a two-part lesson. The first part covers how to predict and verify swaps within a single tick boundary. The second part covers the same topic, but for swaps that cross tick boundaries.
Console Exploration
This will be a console-heavy lesson, so let’s get ready. Spin up your local fork at block height 15686250.
We will be working with these addresses:
Liquidity Pool (DAI/WETH): 0xC2e9F25Be6257c210d7Adf0D4Cd6E3E881ba25f8
Quoter: 0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6
QuoterV2: 0x61fFE014bA17989E743c5F6cB21bF9697530B21e
Router: 0xE592427A0AEce92De3Edee1F18E0157C05861564
WETH: 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
DAI: 0x6B175474E89094C44Da98b954EedeAC495271d0F
>>> lp = Contract.from_explorer(
'0xC2e9F25Be6257c210d7Adf0D4Cd6E3E881ba25f8'
)
>>> quoter = Contract.from_explorer(
'0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6'
)
>>> quoterv2 = Contract.from_explorer(
'0x61fFE014bA17989E743c5F6cB21bF9697530B21e'
)
>>> router = Contract.from_explorer(
'0xE592427A0AEce92De3Edee1F18E0157C05861564'
)
>>> weth = Contract.from_explorer(
'0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'
)
>>> dai = Contract.from_explorer(
'0x6B175474E89094C44Da98b954EedeAC495271d0F'
)
Now let’s get ourselves some fake WETH, grant approval for swapping it via the router, and set a chain snapshot to easily return to “zero”.
>>> weth.deposit({'from':accounts[0],'value':500*10**18})
Transaction sent: 0x06e240acfbd41fed3a1d6fc68e2ce93f369419fa60c5d62e8d6c5850b24463b7
Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 6
Transaction confirmed Block: 15686252 Gas used: 43738 (0.36%)
<Transaction '0x06e240acfbd41fed3a1d6fc68e2ce93f369419fa60c5d62e8d6c5850b24463b7'>
>>> weth.approve(router.address,500*10**18,{'from':accounts[0]})
Transaction sent: 0xb98a364af99c1e896bcca4ec40f5add4b59f3744a1274d913db47233dbb7e348
Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 7
Transaction confirmed Block: 15686253 Gas used: 43964 (0.37%)
<Transaction '0xb98a364af99c1e896bcca4ec40f5add4b59f3744a1274d913db47233dbb7e348'>
>>> chain.snapshot()
>>> chain.height
15686253
Now we have 500 WETH, a liquidity pool, and freedom to poke at it until we achieve enlightenment.
A Zen koan for this exercise: what is the sound of one pool swapping?
Same-Tick Swap
The first thing we want to test is swap behavior of the pool inside a single tick. We’ll do it by calculating a WETH swap that will keep us inside the current tick.
First let’s find the current tick, sqrtPriceX96, and liquidity, then get a quote to see the effect:
>>> current_sqrtpricex96, current_tick, *others = lp.slot0()
>>> current_tick
-72269
>>> lp.liquidity()
1548654344361108748118783
Recall again that liquidity is provided inside of a tick range. The lower boundary of the current tick range will be at tickSpacing * (current_tick // tickSpacing)
and the upper boundary will be at tickSpacing * (current_tick // tickSpacing + 1)
.
Here are those values:
>>> lp.tickSpacing() * (current_tick//lp.tickSpacing())
-72300
>>> lp.tickSpacing() * (current_tick//lp.tickSpacing() + 1)
-72240
So the largest possible single-tick swap in the “positive” direction will drive the tick to -72240.
From the white paper, we find this equation:
Δ𝑥 = Δ(1 / √𝑃) · 𝐿 (Eq. 6.16 - token0 (x) swapped out for a given change in sqrtPrice
)
And we know that sqrtPrice
is a function of ticks:
𝑖𝑐 = log(base=√1.0001)√𝑃 (Eq. 6.8 - tick index as a function of sqrtPrice)
First, verify equation 6.8 against the current pool state:
>>> current_sqrtpricex96
2136349984626364120081779203
>>> import math
>>> math.log(
current_sqrtpricex96*2**(-96),
math.sqrt(1.0001)
)
-72268.27446817268
NOTE: the pool contract stores sqrtPriceX96
as a Q64.96 value, so we transform it to sqrtPrice
by multiplying by 2^-96.
EVM does floor division by default, so we can modify this a little by surrounding the expression with floor()
:
>>> math.floor(
math.log(
current_sqrtpricex96*2**(-96),
math.sqrt(1.0001)
)
)
-72269
So now we’ve verified that the current tick matches the current sqrtPrice
. And we know that the a maximum single-tick swap will drive the tick up to -72240. We know the current tick and the final tick, which allows us to solve for the change in sqrtPrice
.
The sqrtPrice
at tick -72240 is, using equation 6.2 from the white paper:
>>> 1.0001**(-72240/2)
0.02700267315767631
And the current sqrtPrice
is:
>>> current_sqrtpricex96*2**(-96)
0.02696452772385997
From the white paper, we know some things:
Liquidity
𝐿 does not change inside a tick intervalThe virtual reserves of token0 (𝑥) and token1 (𝑦) are related to
Liquidity
, 𝐿 = √𝑥𝑦 (Eq. 6.3)Token swaps inside a single tick are related only to
Liquidity
andsqrtPrice
(Eq. 6.14 and 6.16)
From our results above, we calculate the change in √𝑃:
>>> 1.0001**(-72240/2) - current_sqrtpricex96*2**(-96)
3.8145433816340335e-05
And from this value, calculate the amount of token1 (WETH) needed to drive the sqrtPrice
to our target:
>>> (1.0001**(-72240/2)-current_sqrtpricex96*2**(-96)) * lp.liquidity()
5.907409179721461e+19
Which is 59.07 WETH, after correcting for decimals.
So let’s do a test swap and check the new results:
>>> tx = router.exactInputSingle(
(
weth.address,
dai.address,
3000,
accounts[0],
99999999999999999,
59.074*10**18,
0,
0
),
{'from':accounts[0]}
)
Transaction sent: 0xde1f9912477b0a0b28db27113f6e83233430a8ad26b184f7952ba3755cfd27cb
Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 8
Transaction confirmed Block: 15686254 Gas used: 135755 (1.13%)
>>> lp.slot0()
(2139363105995855777310422166, -72241, 622, 1500, 1500, 0, True)
We ended up shy of the target tick by 1. We can confirm we’re still inside the same liquidity range at this new tick:
>>> lp.liquidity()
1548654344361108748118783
Let’s see how close we can get by reverting the chain and trying it again with more WETH: