Explaining its functionality by grouping lines of code
You probably know the constant product formula (x*y=k) that powers Uniswap. But how does the Uniswap smart contract really work under the hood?
In this article, we are going to understand how Uniswap is implemented by breaking down its smart contract. We are going to examine a couple of hundred lines of Solidity code that generate $1.28 billion in revenue daily.
Spoiler alert: you will see a very efficient, elegant, and secure Solidity code ahead.
Here is the outline of this article:
- How Uniswap works at a high level
- How Uniswap code is organized
- Uniswap functionalities
- Core contracts: Pair (hard)
- Core contracts: Factory (easy)
- Periphery contract: Router (easy)
- Fully annotated code
The whole purpose of Uniswap is to allow you to swap one ERC20 token for another. For example, you need Dogecoin but you only have Shiba coin. Uniswap allows you to sell your Dogecoin and get Shiba in return. This is all done in an automatic and decentralized fashion. Uniswap is just a decentralized exchange.
Exchanges can be implemented in two ways.
- Order book model: Buyers and sellers file orders. And the centralized system matches the buy orders to the sell orders. This is how the traditional stock exchange works.
- Automated market makers (AMM): There is no centralized matchmaker. There are people who provide both tokens (Dogecoin and Shiba). They are called liquidity providers. These liquidity providers create a pool of Dogecoin and Shiba tokens. Now traders can come and deposit Dogecoin and get Shiba in return. This is done automatically, without a centralized entity. Traders pay a small percentage fee for the trade which goes to liquidity providers for their services.
Uniswap uses the AMM technique. How does it determine the exchange rate in a pool? ie how many Shiba tokens is 1 Dogecoin worth? This is determined by the constant product formula
(Dogecoin amount)*(Shiba amount)=k. During trades, this product must remain constant.
Don’t worry if this is not completely clear. We will learn more about this formula and market dynamics as we go along in the rest of the article.
Uniswap has 3 versions.
- We’ll be working with v2.
- v1 is too simple and does not have all the modern features.
- v3 is essentially v2 but improved and optimized — its code is way more complicated than v2.
If you want to know more about the differences between Uniswap versions, check out my screenshot essay.
Uniswap has 4 smart contracts in total. They are divided into core and periphery.
- Core is for storing the funds (the tokens) and exposing functions for swapping tokens, adding funds, getting rewards, etc.
- Periphery is for interacting with the core.
Core It consists of the following smart contracts:
- Pair — a smart contract that implements the functionality for swapping, minting, burning of tokens. This contract is created for every exchange pair like Dogecoin ↔ Shiba.
- Factory — creates and keeps track of all Pair contracts
- ERC20 — for keeping track of ownership of pool. Think of the pool as a property. When liquidity providers provide funds to the pool, they get “pool ownership tokens” in return. These ownership tokens earn rewards (by traders paying a small percentage for each trade). When liquidity providers want their funds back, they just submit the ownership tokens back and get their funds + the rewards that were accumulated. The ERC20 contract keeps track of the ownership tokens.
Periphery It consists of just one smart contract:
- Router is for interacting with the core. Provides functions such as
We talked about the 4 smart contracts that Uniswap has and how they are organized. But what’s the main functionality that these contracts implement? The main functionality is the following:
- Managing the funds (how tokens such as Dogecoin and Shiba are managed in the pool)
- Functions for liquid providers — deposit more funds and withdraw the funds along with the rewards
- Functions for traders — swapping
- Managing pool ownership tokens
- Protocol fee — Uniswap v2 introduced a switchable protocol fee. This protocol fee goes to the Uniswap team for their efforts in maintaining Uniswap. At the moment, this protocol fee is turned off but it can be turned on in the future. When it’s on, the traders will still pay the same fee for trading but 1/6 of this fee will now go to the Uniswap team and the rest 5/6 will go to the liquidity providers as the reward for providing their funds.
In addition to the main functionality described above, Uniswap has another one that is not core to Uniswap but it’s a useful helper for other contracts in the Ethereum ecosystem:
- Price oracle — Uniswap tracks prices of tokens relative to each other and can be used as a price oracle for other smart contracts in the Ethereum ecosystem. Due to arbitrage (which we will learn about later in the article), Uniswap prices tend to closely follow the real market prices of tokens. So the Uniswap price oracle is a pretty good approximation of the real market prices.
Let’s now dig into the actual Solidity code of the Uniswap smart contracts. We will start with the Pair contract. This is the most complex of the 4 smart contracts. The rest will get easier.
The Pair contract implements the exchange between a pair of tokens such as Dogecoin and Shiba. The full code of the Pair smart contract can be found on Github under v2-core/contracts/UniswapV2Pair.sol
Let’s break it down line-by-line.
First, the import statements:
Next, the contract declaration:
- The contract name is
- It implements the
IUniswapV2Pairinterface, which is just an interface for this contract (can be found here). It also extends the
UniswapV2ERC20contract. Why? For managing the pool ownership tokens. We will learn more about it later.
SafeMathis a library for dealing with overflow/underflow.
UQ112x112is a library for supporting floating numbers. Solidity does not support floats by default. This library represents floats using 224 bits. The First 112 bits are for the whole number, and the last 112 bits are for the fractional part.
Next, we will group the code by the functionality that it implements.
Managing the funds
A Uniswap Pair is an exchange between a pair of tokens such as Dogecoin and Shiba. These tokens are represented as
token1 in the contract. They are the addresses of the ERC20 smart contracts that implement them.
reserve variables store how much of the token we have in this Pair.
You might wonder, where is the actual token stored? This is done in the ERC20 contract of the token itself. It’s not done in the Pair contract. The Pair contract just keeps track of the reserves. From the ERC20’s perspective, the Pair contract is just a regular user that can transfer and receive tokens, it has its own balance, etc.
The Pair contract calls ERC20’s functions such as
owner=Pair contract’s address) and
transfer to manage the tokens (see my ERC20 Smart Contract Breakdown if you’re confused). Here is an example of how ERC20’s
transfer function is used in the Pair contract.
_update The function below is called whenever there are new funds deposited or withdrawn by the liquidity providers or tokens are swapped by the traders.
A few things happening in this function:
balance1are the balances of tokens in the ERC20. They are the return value of ERC20’s
_reserve1are Uniswap’s previously known balances (last time
- All we do in this function is check for overflow (line 74), update price oracle (this will be explained in a later section), update reserves, and update a
What’s the difference between the arguments
_reserve0, _reserve1 and the stored variables
reserve0, reserve1 (shown below)? They are essentially the same. The callers of the
_update function already have read the
reserve variables from storage and just pass them as arguments to the
_update function. This is just a way to save on gas. Reading from storage is more expensive than reading from memory
You will notice this again and again: Uniswap absolutely loves efficiency and gas savings. They squeeze out every single performance point they can from Solidity.
_reserve1are one example of it.
Minting and Burning
Now onto the next functionality — minting and burning. Minting is when a liquidity provider adds funds to the pool and as a result, new pool ownership tokens are minted (created out of thin air) for the liquidity provider. Burning is the opposite — liquidity provider withdraws funds (and the accumulated rewards) and his pool ownership tokens are destroyed (destroyed).
Let’s take a look at the
- Immediately you might notice the gas savings again:
totalSupplyare transferred from storage to memory (lines 111 and 118) so that it’s cheaper to read these values.
- We read the balances of our contract (the Pair contract) on lines 112 and 113 and then calculate the amount of each token that was deposited.
- The pink part of the code is for the optional protocol fee. We will examine it later.
totalSupplyindicates the total supply of the pool ownership tokens and is a stored variable in the
UniswapV2ERC20contract (see my breakdown of it here). The Pair contract extends
UniswapV2ERC20which is why it has access to the
totalSupplyis 0, it means that this pool is brand new and we need to lock in
MINIMIUM_LIQUIDITYamount of pool ownership tokens to avoid division by zero in the liquidity calculations. The way it’s locked in is by sending it to the address zero. (No one knows the private key that will lead to the address zero so by sending funds to the address zero, you essentially lock the funds forever).
liquidityvariable is the amount of new pool ownership tokens that need to be minted to the liquidity provider. The liquidity provider gets a proportional amount of pool ownership tokens depending on how much new funds he provides (line 123)
- We finally mint new pool ownership tokens to the
toaddress (line 126).
tois the address of the liquidity provider (this will be provided by the Periphery contract called the Router which calls the
The way adding funds works is: they are just deposited to the ERC20 contracts (by calling
transfer(from: liquidity provider’s address, to: Pair contract’s address, amount) for each token). Then the Pair contract will read the balances (lines 112 and 113) and compare them to the last known balances (lines 114 and 115). This is how the Pair contract can deduce the amounts deposited.
burn function is just the mirror image of the
- We again see gas savings on lines 135, 136, 137 and 143
balance1are total balances of the tokens in this pool.
liquidityis the amount of pool ownership tokens that the liquidity provider (who wishes to cash out) has. Why do access the liquidity as the balance of
address(this)? Because the liquidity was transferred to the Pair contract by the Periphery contract before calling the
- We calculate the amounts of tokens to withdraw to the liquidity provider proportionally to how much liquidity (pool ownership tokens) he has (lines 144 and 145)
- We then burn his liquidity and transfer the tokens to him.
- Rewards to the liquidity provider are automatically withdrawn along with his funds. The math makes sure that rewards are accumulated properly and that you get more than you deposited.