In Decentralized Finance, protocols live and die by liquidity. Billions of dollars are thrown around trying to manage and direct it. There are entire protocols that only exist to manage liquidity. For much of DeFi history, there was only one answer when it came to efficient swaps for large amounts: Curve. That changed with the advent of Uniswap V3. In this Uniswap V3 overview post, we’ll build a model of the entire on-chain ecosystem.
Uniswap V3 is a quantum leap in the efficiency of DEX architecture. That improvement is not without cost, however. The Uniswap V2 model is extremely simple and robust – the Toyota Camry of DeFi. At the code level, Uniswap V3 has much higher complexity, and trades simplicity for optimization. The primary innovation, concentrated liquidity, is also much more difficult for unsophisticated users to navigate versus traditional-style LPs.
I previously did a very detailed, line-by-line code walkthrough of the Uniswap V2 liquidity pair smart contract. We will not go into that detail here, but will instead focus on a high-level architectural overview, similar to my overview of the Fuse protocol.
Let’s dive in!
What Is Concentrated Liquidity?
To understand what makes UniV3 different, we need to understand the basics. Most DEXs use a model called a constant-product AMM (automated market maker). This model uses some function, usually (but not always)
y*x=k. Other models exist, such as Curve’s formula (optimized for stablecoins and other highly correlated assets) and the Balancer multi-asset model, but regardless of the model, one common property remains.
When you add liquidity to a constant-product AMM, the liquidity is added over the entire price curve – from 0 to infinite price. This leads to inefficiency, especially for stable assets, as much of the pool’s depth is at prices distant from the current market price.
Uniswap V3 introduces the idea of concentrated liquidity. Users can select the price range they want to provide liquidity for, ex. ETH from $2700-$3300, or USDC from $0.99 to $1.01. Liquidity providers on UniV3 pools only earn a cut of fees for swaps occuring within their liquidity range, which incentivizes them to manage their positions to provide liquidity at the market price.
This development theoretically leads to extremely deep liquidity around the target price and its expected fluctuation range, and low liquidity elsewhere. The deeper the liquidity, the lower the slippage, and the more efficient the market. Real-life example: The main USDC/DAI pair on Uniswap V3. Note the massive liquidity around peg, with basically no liquidity elsewhere. You can unload a clip of 70 million DAI into this pool and barely budge the price.
Now, let’s look at the liquidity curve for a less stable pool: the Apecoin/ETH pool. See here that the liquidity is concentrated around the market price, but much less sharply. There is also more liquidity at higher prices, which indicates that users either A) expect price to go up, or B) were providing liquidity at a higher range and did not adjust their position when price fell.
In theory, LPs can optimize their positions to receive the maximum possible cut of fees. In practice, many users are not sophisticated enough to manage their positions well, and underperform passive LP models like UniV2. Gas fees also introduce friction, as users can’t micromanage their position to be entirely concentrated at the market price down to the infinitesimal at all times.
For more reading on DEXs and liquidity, please consult the excellent work by the DeFi Education team (Uniswap Overview Pt. 1, Liquidity). My fellow author and turbo autist BowTiedDevil has detailed material (with math!) on Uniswap V2 liquidity calculations on his Substack.
The Anatomy of a DEX
In my DeFi Education guest post series, I covered the anatomy of the Uniswap V2 DEX. Much of the architecture is the same, with some important distinctions. I will cover everything here, so you don’t need to reference the DeFi Education post (although you should check it out).
Uniswap V3 smart contracts are divided, very broadly, into two categories. The core contracts handle the actual business logic. They hold the system funds. The core contracts are all that is strictly necessary for UniV3 to function. However, the core contracts are not easy to use directly.
To avoid intense frustration, UniV3 implements a suite of periphery contracts. These contracts provide interfaces for swapping, retrieving information, and managing liquidity positions. Without these contracts, using Uniswap would be prohibitively difficult.
Core Contracts: The Pool
The pool, or pair, is the heart of the DEX. It is the fundamental contract that lets you swap one asset from another.
The pair contract takes two ERC-20 fungible assets, as the name would suggest. Contrary to a UniV2 pool, which uses the
y*x=k constant product formula, the UniV3 pool uses some calculus. The continuous price curve is broken into ticks. Each of these ticks has a certain amount of liquidity in it, based on how much concentrated liquidity has been assigned to it by liquidity providers.
When you start a swap, the contract steps through each tick, beginning at the current price, and consumes liquidity from it.
If your swap size is sufficient to consume all the liquidity from that tick, it moves on to the next tick, and begins consuming liquidity from that tick. It does this until either A) your desired token amount has been fulfilled and the swap succeeds, or B) the next tick is below your minimum specified price and your token output is not reached, so the swap fails.
In the graph above you can see the start price, the two blue totally-consumed liquidity ticks, and the orange partially-consumed liquidity tick. If you’ve had calculus, you may recognize this as a Riemann Sum for approximating the area under a curve.
The pool contract is a self-contained unit which has all the low-level functions to swap, add liquidity, and remove liquidity, as well as helper functions to gather information about the pool.
Core Contracts: The Factory
Let’s go really high level for a minute. To qualify as a decentralized exchange, we need a few important properties:
- Permissionless access
- Permissionless operation
- The ability to exchange tokens
If users can be blacklisted from using it, or if operation is controlled exclusively by whitelisted accounts, the protocol is not really that decentralized. That an exchange needs to be able to exchange tokens should be self-evident. Okay, great.
What does that mean from an architectural standpoint?
- Any user should be able to interact with any pool
- Any user should be able to perform liquidity management operations on pools
- Pools should operate indefinitely without requiring centralized inputs
- Any user should be able to trustlessly create new pools
The first two points can be simply attained by just not requiring permissioned access to those functions. The second two are more complex. To solve those, we introduce the contract factory.
Smart contracts are immutable by design. It’s part of what makes them trustless. Contract A’s code can’t turn into Contract B’s code. Contract A can’t even change to act like Contract B unless some very specific design patterns are used.
This means you can reliably trust a smart contract to do what its code tells it to do. Combine this with the fact that smart contracts can deploy other smart contracts, and you get an important decentralization primitive: the contract factory.
A contract factory is a specialist smart contract that has exactly one job: deploy copies of its code payload as new smart contracts. In the case of Uniswap V3, the factory’s job is to deploy shiny new Uniswap V3 pools.
Using a factory pattern ensures that:
- The source code is the same for all pools
- Each pool behaves exactly the same as any other
- Each pool can be interacted with in exactly the same way
- One and only one pool exists for each allowable combination
And these properties enable decentralization! A well-designed factory can’t be made to deploy bad contracts, so anyone can be trusted to call it, for any input tokens, without compromising any other part of the ecosystem.
The on-chain ecosystem of our DEX now looks something like this. “Uniswap” is not actually a monolith, it’s actually thousands of identical smart contracts, all deployed by a single pool factory.
The pool factory for UniV3 has an important distinction versus the UniV2 model. The UniV2 factory enforces unique combinations of tokens. You cannot have both a USDC/DAI pair and a DAI/USDC pair, for instance. The UniV3 factory also enforces this, but adds a third parameter, the fee.
Uniswap V3 Fees
When a pool is created, the creator must specify the transaction fee enacted on each swap. This allows users to provide liquidity into a pool with a fee that they think appropriately matches the risk of holding the underlying asset. A stablecoin, for instance, does not need a very high fee, while a new, riskier token may require a larger fee.
Currently, there are four fee levels which can be chosen: 0.01%, 0.05%, 0.3%, and 1%. More fee levels may be added by Uniswap governance, as the 0.01% fee was. The factory will allow only one of each unique combination of fee and underlying tokens. For example, I may create a USDC/DAI pair at 0.01% fee and a USDC/DAI pair at 0.05% fee, but I cannot create two different USDC/DAI pairs both at 0.05% fee.
Core Contracts: The Pool Deployer
The contract factory has some under-the-hood detail which most users don’t need to interact with, so Uniswap V3 provides an outward-facing interface contract, the Pool Deployer. This contract gives a very simple and easy way to deploy new pools with a single high-level function call.
In total, the core Uniswap V3 contracts look like this:
Hopefully the calculus didn’t scare you off! Much of Uniswap V3’s high level core functionality remains architecturally similar to Uniswap V2. At the micro level, the code is wildly more complex, but normal users don’t have to deal with that.
The real bugbear comes when you start to deal with providing liquidity. Another innovation of Uniswap V3 is that its concentrated liquidity positions, by definition, are not fungible. They have unique sizes and boundary prices. This means that they can be represented as NFTs. Wrapping the positions into NFTs enables composability with other protocols, so you can take out loans against your position, or whatever else the market comes up with.
I won’t sugarcoat it. The liquidity positions are kind of a pain in the ass. I’ll try to lay this out in as clear a manner as possible.
The Anatomy Of A UniV3 Liquidity Position
Before we talk about the NFTs and everything associated with that, let’s talk about the actual, low-level act of adding liquidity to a pool. This is done through the
This function requires you to define the upper and lower liquidity ticks you want to add liquidity between, and the total value you want to add. This value will be evenly distributed across this range. It also requires you to define the recipient, who will be the owner of the position.
That’s not too bad. However, there are two important caveats. First, this is a low level function. The way this function is made requires you to call it from a smart contract. Second, this function does not mint an NFT, only creates the liquidity position.
Periphery Contracts: Nonfungible Position Manager
You could call this low-level mint function yourself using your own smart contract if you were an absolute turbo. Never fear, Uniswap has provided one for us. That contract is the Nonfungible Position Manager (NPM). This contract serves two purposes.
- Adding, removing, and modifying liquidity positions in any Uniswap pool
- Representing those liquidity positions as NFTs
When you tell the NPM to mint a new liquidity position, you pass it a group of parameters that dictate what pool to add liquidity to, how much of each token you are adding, and some slippage/MEV protection parameters such as the minimum amount of liquidity addition you will accept, and the timestamp it must be executed by. The NPM will then take care of adding the liquidity to the target pool, and mints you an NFT representing that position.
The owner of that NFT is then the owner of the liquidity position. If you transfer the NFT, the new owner owns that liquidity position, and all its underlying token value and accrued fees. He can modify it or withdraw it at his discretion.
Periphery Contracts: Token Descriptor
That’s not all, though. The NFTs have some rather fancy generative art associated with them. If you visit the Inventory tab of the token tracker on Etherscan (remember the NPM is an ERC-721 NFT token contract), you can see what I mean.
Let’s look at one of these up close (chosen at random), and you can note a few things. The title tells you the pool it’s in (USDC/USDT and 0.01% fee), and the price range associated with it. The NFT ID, token addresses of the underlying tokens, and the liquidity ticks it encompasses are printed on the NFT body. There’s also a nice little graphic in the bottom right of the card that shows the position along the LP curve that this position occupies – in this case square in the middle.
This art and detail is all generated on-chain, by the Nonfungible Token Position Descriptor contract. We’ll call it the descriptor for short. To understand what it does, let’s back up for a moment and understand the idea of a token URI. The URI, or Unique Resource Identifier, is all the information attached to an NFT. Without the URI, the NFT is just a number in a contract. With the URI, it can have art, and resources, and descriptions, and properties, and all the other lovely fun stuff you can see on Opensea.
The entire data of the URI can be stored on-chain, or the token URI can just return a link to an off-chain cache containing the data. When you ask an NFT contract what the token URI for token ID #6969 is, it will retrieve or calculate the URI and pass it back to you. Your application, or Opensea, or whatever, will then parse that information and retrieve the art, generate the text, etc.
For UniV3, the complex code to generate the art and descriptive text for these cards is located in the descriptor contract. Whenever someone asks for the token URI of an LP NFT, the NPM asks the descriptor to generate the card.
That brings the total ecosystem so far up to this:
Periphery Contracts: Router
So far, we’ve ignored the fact that there is an enormous constellation of UniV3 pools. Among these thousands of contracts, how do you find the pair you need? What if there’s not a pair for your desired swap? That’s not to mention that the UniV3 design patterns require you to call the swaps from a smart contract.
For those of us who don’t want to create custom smart contracts and infrastructure to be able to ape into tokens, Uniswap has provided a way. The router contract serves as an on-chain interface point for swaps. You tell it a token, or series of tokens, to swap between, and it will handle the low-level swap calls and funds transfer to make it all happen.
Periphery Contracts: Lenses
UniV3 also implements several utility contracts to retrieve information from pools. The term for this design pattern is a lens contract, because it helps you see the underlying data.
The Quoter works like a skeleton router. It is designed to simulate swap calls, for the purposes of retrieving the output token amounts. This contract design is gas-intensive and should not be called on-chain, but rather simulated off-chain.
The TickLens is a tool to retrieve the liquidity at every tick for a given pool. This is used to populate the liquidity depth graphs you see on the Uniswap info website.
With the lenses in place, it’s getting a little messy, so I’ve added colors to the different groups of contracts.
Periphery Contracts: Staker
If you’ve provided liquidity to a pool before, you’re probably aware that it gives you a cut of the fees in the pool. But what about liquidity mining/liquidity farming? This takes the form of a second reward, given to you by the protocol to incentivize you to provide liquidity to that pool.
UniV3 does this via a contract which lets you stake your LP NFTs: the staker. If a pool is being incentivized by Uniswap governance, the staker contract will reward you with UNI tokens when you stake that LP NFT in it.
And finally, we come to the governance layer. The exact mechanisms of the governance system are a subject for another overview, so “governance” is going to go in its own tidy black box. What I will cover is what governance is allowed to do in this system. There are a few main privileges reserved for governance:
- Creating liquidity incentive programs via the staker
- Creating new fee levels for pools (ex. the creation of the 0.01% fee tier)
- Adjusting the proportion of fees reserved for the protocol on individual pools
Importantly, the governance cannot set the fee of a pool (ex. they can’t set the fee to 100% of your swap and rug you). They can only adjust the proportion of the fee that goes to the protocol vs. the liquidity providers. For example, they could set the split to 10% protocol, 90% LPs, on a 0.3% fee tier pool. That means 0.03% of every swap will be allotted to Uniswap, and 0.27% will go to the LPs.
You made it this far anon, so enjoy the final Uniswap V3 system diagram!
The Uniswap V3 ecosystem is a little more complicated than the V2 variant. This increase in complexity, however, comes with a lot of power. You can see for yourself what a difference concentrated liquidity makes. Other protocols like SushiSwap really haven’t come back with an answer to V3 yet. As time goes on, UniV3 forks may become as prevalent as UniV2 forks. Until that time, Uniswap V3 and Curve will remain the only answers to stablecoin swaps at large volume.