Our in-depth exploration of the UniswapV2 ecosystem was very fruitful. We learned how to interact with the router contract, how to get reserve values from a pool contract, how to execute single and multi-hop swaps directly without using the router, and how to execute a flash loan.
We laughed, we cried, we built bots!
Now we are going to begin our exploration of the UniswapV3 ecosystem.
I’m pleased that my time studying UniswapV2 has paid off, because it has made understanding the UniswapV3 contract significantly easier.
Each of the initial lessons in the UniswapV3 series will concentrate on a single contract type. There are many, so there is much to explore.
V3 Pool Construction
Before exploring the pool at the contract level, let’s review briefly how UniswapV2 works. UniswapV2 is known as a constant function market maker (CFMM) that operates on the fundamental equation x * y = k
.
I did a very thorough exploration of this equation in the three-parts Pool Reserves series (Part 1: Fun With Pool Reserves, Part 2: Partial Swaps, Max Profits, Part 3: Swap and Quote Function).
At a high level, a CFMM allows you to trade one asset (x
) for another asset (y
) at amounts that satisfy a constant (invariant) set at the pool level.
UniswapV3 introduced the concept of concentrated liquidity, which allows liquidity providers to set ranges where they will allow their deposits to be traded. This has a positive effect for users, since deep ranges of high liquidity ensure that the price impact of a swap is low. A price impact is the measured increase in the “price” of one asset for another as the proposed trade size increases. Many incorrectly refer to this as slippage, a concept introduced from the front-end user interface.
Concentrated liquidity allows users to experience better trade prices, and allows liquidity providers to closely control the liquidity ranges and fees that their deposits will be subject to.
I encourage you to read through the UniswapV3 White Paper to deeply understand how and why the system was designed, and what problems it proposes to solve.
Real vs. Virtual Liquidity
At the smart contract level, there is no difference in a quantity of tokens being held in a UniswapV2 pool vs a UniswapV3 pool. This is an important distinction, so please do not get confused when I start going into the deeper understanding of the pool pricing. All of the complication involved with UniswapV3 is at the “bookkeeping” level. A token is a token and a swap is a swap, but how the pool manages deposits, fees and prices is where you’ll unlock real understanding.
Consider a fictional UniswapV3 pool with a single deposit of two familiar assets: WETH and DAI. As I write this post, WETH is valued at roughly 1500 DAI/WETH according to the Etherscan tracker.
Say that a liquidity provider chooses to supply tokens to the new UniswapV3 pool at some arbitrary amount. This provider chooses to set the price range between 1600 DAI/WETH and 1700 DAI/WETH. After those tokens are deposited, any user can swap their DAI for WETH, or WETH for DAI, and the pool will offer those swaps at prices inside the 1600-1700 range.
Remember that the pool itself has no idea of the fair price for an asset, so 1600-1700 might be a ridiculous price range to choose. But if users are comfortable with that price, they are free to swap back and forth using our liquidity provider’s deposit and pay the appropriate fee.
Now let’s say another liquidity provider arrives and decides that the 1600-1700 price range is too wide. They decide that 1650 DAI/WETH is the “true” price and they want to supply additional deposits centered on that price point. They deposit their quantities of tokens (in equal amounts) and assign their price range.
Now the pool has two sets of deposits with different price ranges, but the balances of each token are not separate. This is where the complex bookkeeping starts to come in.
Separate and distinct chunks of “virtual liquidity” exist as an artificial construct within the large token pool of “real liquidity”.
Ticks
The CFMM system was easy to bookkeep. Liquidity providers earned fees in proportion to their deposits (relative to the total pool deposits) and captured those fees when they withdrew their liquidity. Simple!
In UniswapV3 it is not so simple, because of the different ranges that everyone might choose. At the pool level, the price range “slices” that liquidity providers choose are called ticks. A tick is an artificial bookkeeping measure that allows the pool to keep track of how much liquidity has been assigned between two price points. The width of these ticks is set by the pool fee, but is otherwise very predictable. The tick width determines the price range of that particular “chunk” of liquidity.
From the UniswapV3 white paper, you can see first example I gave (a single position between prices Pa & Pb) and the second example (a collection of positions with differing depths and prices).
The marks along the X axis represent the ticks where virtual liquidity is earmarked.
Fees
UniswapV2 set a standard fee across pools at 0.3% of input. UniswapV3 introduces the concept of variable-fee pools, and they provide three options (0.05%, 0.3% and 1%).
The tick width is a function of fee:
0.05% fee : tick spacing = 10 (0.1% price difference between ticks)
0.3% fee : tick spacing = 60 (0.3% price difference between ticks)
1% fee : tick spacing = 200 (2.0% price difference between ticks)
The idea is that low fee pools are commonly used for stablecoins with a very tight price correlation, high fee pools are used for highly volatile tokens, and the standard fee pool is used for everything else.
A particular token pair (WETH/DAI for example) can have more than one pool with different fees:
Contract State
A pool contract keeps track of its current state by means of the following variables:
uint128 liquidity 𝐿
uint160 sqrtPriceX96 √𝑃
int24 tick 𝑖𝑐
uint256 feeGrowthGlobal0X128 𝑓𝑔,0
uint256 feeGrowthGlobal1X128 𝑓𝑔,1
uint128 protocolFees.token0 𝑓𝑝,0
uint128 protocolFees.token1 𝑓𝑝,1
We don’t particular care about fees, so we can ignore the last four variables.
The three we care about are liquidity 𝐿, the square root of the price √𝑃, and the current tick 𝑖𝑐. Knowing these will allow us to predict how the pool will behave when swapping assets.
Swaps
An interesting behavior of the UniswapV3 pools is that they have a “current” tick where the next swap will begin. Depending on the trade size it may stay inside that tick, or it may cross tick boundaries. Accurately simulating this behavior is the key to successful arbitrage and manipulation of UniswapV3 pools, so we will spend a lot of time studying this in future lessons.
A UniswapV3 pool swap works like this:
A swap input is proposed via the router / quoter contract
The router / quoter determines the available liquidity, square root of the current price, and the current tick of the pool.
The router begins exchanging the input token for the output token inside the current tick, which changes the square root price, but does not change liquidity.
If the price change exceeds the upper or lower boundary of the current tick, all liquidity in that tick is converted to the input token and all available output token in that tick is reserved for the swapping user. This differs from UniswapV2 in that all of token0 can be exchanged for token1 inside of a given tick, whereas a CFMM will never allow you to swap a pool completely out of one token (the last digit of that token becomes infinitely priced)
If a tick boundary is crossed, the router determines the available liquidity inside the next active tick and repeats the process until all of the swap input has been used.
It’s important to consider here that fees work very differently in UniswapV3. In UniswapV2, the 0.3% fee was added to the pool and resulted in a slightly increased k
value after every swap. A UniswapV3 pool collects and tracks the fees, but they are not re-invested into the pool as additional liquidity. They are held until the LP removes or changes their position, and added to the returned asset balance.
From our POV, the trading fee now “disappears” into the contract. Liquidity values do not change until a tick is crossed or a liquidity provider creates a new deposit, and the square root price value only changes after a swap.