Uniswap Smart Contract Breakdown (Part 2)
Explaining its functionality by grouping lines of code
This is Part 2 of the Uniswap Smart Contract Breakdown. In Part 1, we covered:
- How Uniswap works at a high level
- How Uniswap code is organized
- Uniswap functionalities
- Core contracts: Pair (hard): managing the funds, minting, and burning
In this article, we will cover the rest:
- Core contracts: Pair (hard): swapping, pool ownership tokens, protocol fee, and price oracle
- Core contracts: Factory (easy)
- Periphery contract: Router (easy)
- Fully annotated code
Core contracts: Pair (hard)
Swapping
Theswap
function is used by traders to swap tokens:
- First, we have a bunch of assertions
- Then on lines 170 and 171, we transfer tokens out (to the trader) optimistically (without making sure that the trader has already transferred corresponding tokens into our balance. We can optimistically transfer tokens out because we have assertions later in the function to check if we received corresponding tokens (the Periphery contract should send us the tokens before calling us for the swap). If we have not, assertions will fail and Solidity will revert the entire function.
- Line 172 will inform the receiver about the swap if requested.
- Then on lines 176 and 177 we actually check how many tokens we received. We assert that we received >0 amount for at least one token on line 178. If this assertion fails, the entire function will revert and nothing will have taken place.
- Then, on lines 180 and 181, we subtract the trading fee (0.3%) from the balance, and on line 182 check if the k value (x*y=k) has decreased after the trade. The k value can never decrease because otherwise, Uniswap would lose from the swap.
- Finally, we update our known reserves with the new balances and emit a
Swap
event.
A note about fees and rewards
Uniswap works by taking a small percentage (0.3%) fee from the traders on each trade. It then later (optionally) takes some of those fees (1/6th) to itself and distributes the rest to the liquidity providers in proportion to how much the liquidity provider contributed to the pool.
Where are these fees/rewards stored? They are actually stored right in the pool itself.
When traders pay their fee, this fee is added to the pool. Later when liquidity providers either add or withdraw funds, the liquidity provider's rewards are calculated using complicated math formulas.
This is Uniswap’s elegance at play again — instead of creating a separate pool/storage for the fees/rewards, they just add everything to the pool and then use clever math formulas to deduce how much of the pool is from the fees. And Uniswap has an efficient way of keeping track of absolutely the bare minimum to derive these values at any point in the future.
Pool ownership tokens
When liquidity providers add funds to the pool, they are given pool ownership tokens. After some time, these ownership tokens gain in value due to traders’ fees. When the tokens are exchanged back, liquidity providers get more than they deposited.
The pool ownership tokens are implemented as a standard ERC20 token. It’s implemented in the UniswapV2ERC20.sol
contract of Uniswap ( v2-core/contracts/UniswapV2ERC20.sol). It is this part of our diagram from Part 1:
I already did a breakdown of Uniswap’s ERC20 contract in my ERC20 Smart Contract Breakdown article, so I won’t be repeating myself.
The Pair contract gets access to the ERC20 implementation by extending it:
That way the Pair contract gets access to ERC20’s _mint
and _burn
functions:
Protocol fee
Uniswap v2 introduced a switchable protocol fee — a fee that can be turned on/off by Uniswap which goes to Uniswap for maintaining the service. It’s equal to 1/6th of the fees paid by the traders. Let’s examine how the protocol fee is handled in the Pair contract.
The main function of the protocol fee is _mintFee
:
This function looks complicated, so let’s just focus on these lines:
- We first get the
feeTo
address from thefactory
.factory
is the contract that created this Pair contract. - If it’s set to something other than address zero, that means the protocol fee is on.
feeTo
address indicated the address where the protocol fee should be sent to. - If the fee is on, we mint some liquidity to the
feeTo
address. (_mint
function is the ERC20’s_mint
function)
The rest of the code is for calculating liquidity
. Liquidity here indicates the amount of new pool ownership tokens that need to be minted to the feeTo
address. This is how Uniswap implements the protocol fees: it just mints new pool ownership tokens to itself. That, in effect, dilutes everyone else in the pool (the liquidity providers).
IMHO, _mintFee
is not the best name for this function because it has a side effect of minting new liquidity. A better name would be _collectProtocolFee
.
How protocol fee is calculated
A straightforward way to implement the protocol fee would be to take 1/6th of the traders’ fee every time there is a swap of tokens. But as you probably noticed, Uniswap does not like the easy way out. It absolutely loves efficiency and gas savings even if it means the code becomes 10x more complicated 😁
Uniswap does not calculate fees on every trade because that would incur extra gas on every single swap trade. Since there are lots and lots of trades happening every day, that would sum up to large amounts of gas cost. Uniswap instead calculates the protocol fee only when funds are either deposited or withdrawn from the pool by the liquidity providers. This is a much rarer event than the trades.
So the_mintFee
function is called only from the mint
and burn
functions.
Protocol fee is accumulated during trades into the pool so the pool becomes a mix of the exchange tokens, protocol fees, and rewards for liquidity providers. Clever math formulas allow us to calculate how much of each constituent there is.
The protocol fee, in particular, is calculated using a complicated formula which you can find in the Uniswap V2 whitepaper:
The k value here is the product of the reserves (k=x*y). This is why we keep track of the kLast
value throughout the code: kLast
value allows us to calculate the total accumulated protocol fee (from every trade) so far and collect all this fee in one go either in mint or burn functions.
Price oracle
Uniswap implements a price oracle that can be used by other smart contracts in the Ethereum ecosystem to query the price of tokens relative to each other.
To implement the price oracle, Uniswap uses only 3 variables: price0CumulativeLast
, price1CumulativeLast
, and blockTimestampLast
.
The relative price can be calculated by subtracting cumulative prices at 2 points in time and dividing by the elapsed time. Check the Uniswap whitepaper’s “Price oracle” section for more details.
The variables are updated only once per block here:
Lines 75–77 calculate whether this is the first time the code is executed in a particular block.
Why do we update values only once per block? Because it’s harder that way for someone to manipulate prices in order to gain something. See the “Price oracle” section of the Uniswap whitepaper for more details on these price manipulators.
Misc
A liquidity event is when funds are either added or withdrawn by the liquidity providers.
lock
is for guarding against reentrancy abuse. Essentially this function modifier prevents 2 different parts of this contract to be executed simultaneously. It kinda makes the contract execute with a single thread.
skim and sync
are needed when balances on the ERC20 contracts of the exchange tokens, fall out of sync with the reserve
variables in the Pair contract. This can happen for example when someone just transfers some Dogecoin to Pair contract’s account for no reason. There are 2 solutions to keep reserve variables in sync with the actual balances on ERC20 contracts:
skim
allows someone to withdraw the extra funds from the ERC20 contract. Anyone can call this function!sync
updates thereserve
variables to match the balances.
A note on the market dynamics
Uniswap prices tokens according to the proportion of them in the pool. The greater disbalance of them in the pool the greater the price difference (in favor of the rarer token).
But how does Uniswap make sure that the relative price of tokens in the pool matches the market rate? Arbitrage. Uniswap takes advantage of arbitrage to ensure the prices in the pool closely track the market prices.
Arbitrage is when a smart investor sees a discrepancy between the market rate and Uniswap exchange rate, he will use it to make a profit and as a result, bring the Unsiwap rate closer to the market rate.
For example, if Uniswap offered a lower Dogecoin-to-Shiba price compared to the market rate, a smart investor would exchange his Shiba for Dogecoin on Uniswap and sell the Dogecoin at a higher price in the market. He will have made a profit and as a result, brought the Unsiwap rate closer to the market rate because he decreased the supply of Dogecoin and increased the supply of Shiba in the Uniswap pool (Dogecoin-to-Shiba price will increase because of how Uniswap price tokens relative to each other).
This will continue until the Uniswap rate matches the market rate. Thus Uniswap rate tends to closely track the market rate and that’s why it can be used as an on-chain price oracle.
- As Uniswap v2 whitepaper put it: The first liquidity provider to join a pool sets the initial exchange rate by depositing what they believe to be an equivalent value of ETH and ERC20 tokens. If this ratio is off, arbitrage traders will bring the prices to equilibrium at the expense of the initial liquidity provider.
- And Uniswap v1 whitepaper put it: Large trades cause price slippage as well, but arbitrage will ensure that the price will not shift too far from that of other exchanges.
Summary
That’s it for the Pair contract — the most complex contract in Uniswap! The other 2 contracts are much much easier. Let’s summarize what we learned.
The Pair contract is a mix of a bunch of functionality:
- Managing funds
- Adding/removing liquidity
- Swapping tokens
- Managing fees/rewards
- Calculating the protocol fee
- Implementing a price oracle
Here is the fully annotated code for the Pair contract, color-coded according to the functionality:
Core contract: Factory
Here is a breakdown of the Factory contract (v2-core/contracts/UniswapV2Fatory.sol):
This Factory contract is referenced throughout the Pair contract:
Periphery contract: Router
The Periphery contract is the API for Uniswap. You could call the Core contracts directly but that’s more complicated (periphery provides wrapper functions) and more dangerous (you could lose money if you’re not careful).
Core contracts have checks to make sure they aren’t cheated on. But they don’t provide checks for anybody else. Those checks are in the periphery. So if you don’t want to lose money, use the periphery contract to interact with Uniswap.
Let’s break down the Router contract which is the only contract in the periphery. It can be found at v2-periphery/contracts/UniswapV2Router02.sol.
- This contract has a bunch of similar functions for adding liquidity, removing liquidity, and swapping tokens. Different variants of functions are for different trading/liquidity preferences.
- I removed the body of most functions because they are pretty similar.
- From the Ethereum website:
UniswapV2Router01
has problems and should no longer be used.
That’s it for Uniswap! I hope this was helpful. Let me know in the comments if you have any questions.
I am planning to do more breakdowns of popular smart contracts like Axie Infinity and BAYC, so follow me either on Medium or Twitter to get updates.
You can also check out breakdowns of other smart contracts and more stuff for Solidity noobs at solidnoob.com.
Want to Connect?Follow me on Twitter.