Uniswap V2: Overview and Code Walkthrough

1. Introduction

1.1. What is Uniswap?

Uniswap is a decentralized exchange built on Ethereum, Ethereum’s L2s, and Celo.

A decentralized application (dApp) is one that operates on a blockchain network using smart contracts. Though the exact criteria for decentralization is a philosophy-of-crypto question that this article does not aim to debate, dApps are usually autonomously-running, open-source, not owned by any individual entity, and able to be updated according to community consensus.

A decentralized exchange (dex) allows any user to swap a token they hold in some wallet for some other token on the same network. This is the blockchain-analogue of the centralized exchange (cex), like Robinhood, Binance or FTX ( 😔) which allows users to trade financial instruments. While cexes are companies that directly take custody of your assets, trading on a dex is non-custodial, meaning you can trade directly from a wallet you own yourself. This is more secure, since you aren’t entrusting your money to a third party – just ask anyone who got burnt by the collapse of FTX or Celsius – but also just more convenient if you’re interacting with other dApps in the ecosystem.

It’s hard to understate how revolutionary dexes were when first implemented. Instead of a traditional orderbook exchange which constantly relies on market makers to actively price the asset and provide liquidity, dexes completely automate the pricing process, vastly decreasing the amount of active management liquidity providers actually have to perform.

1.2. How does it work?

Generally, there are two ways exchanges operate: With an orderbook, or an automated market maker. In an orderbook exchange, market makers create orders to buy or sell assets at a certain price, and these orders remain open until they are filled by a taker, who agrees to the market maker’s offer.

In contrast, Uniswap is an automated market maker. Specifically, it is a constant-product automated market maker (CPAMM). Uniswap doesn’t need people to fill its book with buy and sell orders to enable trading; instead, it uses the xy = k formula to determine the price of each trade. This formula groups tokens into liquidity pools of 2 tokens each, where one token can be swapped for the other and vice-versa, and dictates that the number of token X and token Y in a given pool, when multiplied together, must always equal some constant k. This means that any trader looking to buy some token X, for instance, must deposit an amount of token Y back into the pool such that xy is still equal to k. As a result, if – for instance – demand for token X exceeds that of token Y, then the price of token X in terms of Y increases as more people buy it.

Generally, there are three ways users can directly interact with the Uniswap protocol:

  • Swaps:
    Trading one token for another in a certain liquidity pool. This can be executed manually or automatically (e.g. by a script interacting with a smart contract to make automatic arbitrage trades).

  • Liquidity Provision:
    Supplying a liquidity pool with tokens. Liquidity providers (LPs) are necessary market makers on Uniswap: without anyone supplying tokens to a liquidity pool, traders won’t be able to swap one token for another. The more tokens in a given liquidity pool, the smaller the impact on price when someone makes large swaps. LPs are incentivised to provide liquidity as they earn a share of protocol fees that are paid each time a swap is made.

    To keep track of the proportion of liquidity in a pool provided by each LP, an LP receives LP tokens whenever they add liquidity. These tokens, which are specific to each liquidity pool, represent the share of liquidity an LP has provided to that pool.

  • Governance:
    Voting on proposals to make changes to the Uniswap protocol itself. Holders of the $UNI token get to vote on proposals, with their votes weighted in proportion to the number of tokens they hold.

Uniswap uses the same swap logic to allow users to buy NFTs with any Ethereum-based token, a feature they’ve rolled out relatively recently – for the sake of simplicity, though, we won’t cover that in this walkthrough.

2. How is Uniswap Implemented?

For ease of explanation, this walkthrough will focus on Uniswap V2. Uniswap V3 – the current version – has much more complicated mechanics to explain (so much so that it’s been the subject of multiple academic papers). We will begin with a general overview of Uniswap’s smart contract architecture, followed by a walkthrough of what happens when a user interacts with Uniswap in any of the 4 ways listed in the previous section.

2.1. Overview

Uniswap’s codebase comprises a core and a periphery repository. By separating the protocol’s smart contracts into two repositories, security audits on the core contracts can be completed more quickly and easily, without the periphery contracts – which serve to provide additional, nonessential extra functionality – getting in the way.

The core repository contains the contracts directly responsible for managing funds. Its key contracts are UniswapV2Pair, which is a template for a liquidity pool containing any two given tokens, and UniswapV2Factory, which creates and keeps track of individual instances of pair contracts.

The periphery repository also contains two key contracts: UniswapV2Router, which is used to find the most efficient ‘routes’ to perform certain actions (e.g. swapping tokens across multiple liquidity pools), and UniswapV2Library, which contains helper functions used to perform certain calculations.

2.2. Liquidity Provision (and Pair Creation)

We’ll first go through the process of creating, and providing liquidity to, a liquidity pool. Recall that these are pools of 2 tokens each, which allow users to swap one token for the other.

The addLiquidity function

This is the key function from the UniswapV2Router smart contract that enables us to create new liquidity pools and add liquidity to existing liquidity pools.

Arguments

This function enables the function caller to add liquidity for two tokens – tokenA and tokenB – with the amount of liquidity the user wants to add being specified by amountADesired and amountBDesired.

The caller also specifies the minimum amount of each token they would like to provide – amountAMin and amountBMin – below which the transaction will revert. This effectively allows the caller to express a price range within which they’re comfortable with providing liquidity. Because transactions are not immediate, given that they take time to be included in a block and validated, we need to leave some room for any changes in exchange rate between submission and execution.

The deadline argument, which is a Unix date, is fed to the ensure modifier to ensure that the function executes by a certain time (and revert it otherwise). Finally, the to argument specifies the address to which the newly-minted LP tokens should be sent.

Outputs

This function returns the result of the user’s attempt to provide liquidity: amountA and amountB, which are the amounts of the two tokens the user actually ended up providing. Recall that transactions are not immediate, which is why the actual and desired amount of tokens may differ. It also returns liquidity, which is the amount of LP tokens minted.

Body

Once the addLiquidity function is called, the internal function _addLiquidity is called next. We’ll examine it more in detail after this function, but essentially, it calculates and returns amountA and amountB, which – again – are the amounts of the two tokens the user will actually provide.

Then, the pairFor function is called from the UniswapV2Library smart contract to determine the address of the smart contract for the tokenA - tokenB liquidity pool, and the safeTransferFrom function is used to transfer amountA and amountB of tokenA and tokenB respectively from the LP to the pool. (We use safeTransferFrom instead of the standard ERC-20 transfer function to more clearly handle unsuccessful token transfers - see here for a more detailed explanation.)

Finally, we call mint from the specific instance of UniswapV2Pair for this liquidity pool, to mint and transfer the appropriate amount of LP tokens to our LP.

The _addLiquidity function

Recall that this function is meant to calculate the actual amount of tokens the LP will need to provide to the liquidity pool, based on the desired amount of tokens specified. It is an internal function, as denoted with the underscore by convention, meaning that it should only be called from within its own contract.

Arguments

_addLiquidity is passed most of the arguments from the original addLiquidity function. tokenA and tokenB are the two tokens the LP wants to provide as liquidity, amountADesired and amountBDesired are the desired amounts of tokens they want to provide (and also the maximum acceptable amounts), and amountAMin and amountBMin are the minimum acceptable amounts of tokens they want to provide.

Outputs

_addLiquidity has two outputs: amountA and amountB, which are the actual amounts of tokens the LP will have to provide.

Body

Before performing any calculation, the function calls getPair from UniswapV2Factory to check if a liquidity pool for the two tokens passed to it already exists. If it’s returned a null address, meaning there isn’t an existing liquidity pool, it calls createPair to create a new liquidity pool for those tokens.

Now, we first account for the simplest case: Using the getReserves from UniswapV2Library, we check if existing liquidity – i.e. ‘reserves’ – already exists for both tokens. If no liquidity exists, then this is a newly-created pool and amountA and amountB will simply equal amountADesired and amountBDesired, since the LP can add however much liquidity they want.

Then we account for cases where the real exchange rate (and thus, the reserve ratio between the two tokens) differs from what the LP initially thought. If we continue with depositing amountADesired and amountBDesired, we’ll lose out on some LP tokens for providing liquidity that’s unbalanced with respect to the reserve ratio.

To visualize why, imagine an ETH-WETH pool. Now, our LP wants to deposit 1 ETH and 1 WETH, with the belief that the ETH-WETH ratio is 1:1. This would result in the LP depositing 2 ETH worth of liquidity. However, suppose that for whatever reason, the real ETH-WETH ratio has fluctuated to 0.9:1, meaning WETH is less valuable than ETH. Now, the LP only deposits 1.9 ETH worth of liquidity – thus, their share in the liquidity pool is smaller, and they receive fewer LP tokens for the same deposit.

We call the quote function from UniswapV2Library to find the optimal amount of token B to deposit, and store this value in amountBOptimal. If this value is between amountBDesired and amountBMin, then we use this as our adjusted amount of token B to provide; else, if it falls outside the range, then we do the same to find amountAOptimal and adjust the amount of token A we provide instead. After this process, we can finally return amountA and amountB.

To remove liquidity, we go through a similar process – burning LP tokens and returning a proportionate share to the LP, including the trading fees they’ve accumulated – but also accounting for any transfer or storage fees the tokens might have.

2.3. Swaps

UniswapV2Router contains a number of swap functions, based on whether the trader wants to input an exact amount of tokens to receive a variable output, input a variable amount of tokens to receive an exact output, or do the same but with ETH as one of the tokens. Recall that exchange rates can fluctuate between a transaction’s submission and confirmation, which is why we can’t specify with certainty an exact amount of input and output tokens we want for our swap.

These swap functions are pretty similar, so we’ll just focus on one for this walkthrough:

The swapExactTokensForTokens function

This function allows us to swap an exact amount of input tokens to receive a variable amount of output tokens.

Arguments

The function takes amountIn and amountOutMin as the exact amount of input tokens we want to swap, and the minimum amount of output tokens we want to receive, respectively. It also takes path, an array of addresses, which represents the different token pair addresses that swaps need to be made between in order to accomplish the function caller’s desired swap. For instance, if we want to swap token A for token C but the only existing liquidity pools are A <-> B and B <-> C, we’ll need to first swap token A for token B, then token B for token C. This array is stored in the calldata (and indeed, all data types are stored somewhere – but this is usually handled automatically by the compiler, except in the case of arrays, which occupy more space and thus must be specified manually). path is not calculated by the smart contract itself, and is instead determined offchain.

Finally, like before, this function takes a to address, which is where the token output will be sent to, and a deadline by which all transactions must be completed, or else reverted.

Outputs

The function returns one array, amounts, which represent the amount of each token swapped in each pool along the path.

Body

First, we use the getAmountsOut function from UniswapV2Library to determine the amounts of each token we’ll have to swap, based on amountIn and our desired path. We use require to check that the final item in amounts, which represents the amount of tokens we’ll actually receive, is at least equal to our minimum preference, amountOutMin, and revert the transaction otherwise.

Then, we once again use safeTransferFrom to transfer our initial input tokens to the first liquidity pool, and call _swap, an internal function that uses a for loop to iterate over, and swap between, the rest of the pools on our path.

2.4. Governance

The governance of Uniswap depends on an entirely different set of smart contracts that doesn’t directly interact with any of the code we went through above. Uniswap’s governance doesn’t have any features that are particularly unique to the protocol, so we won’t examine the code itself, but I will provide an overview of Uniswap’s governance mechanics:

Uniswap’s governance token is UNI. Holders of UNI get to vote on governance proposals, which represent proposed changes to the Uniswap protocol. All voting and discussion happens on Uniswap’s governance forums, where each proposal is represented by a thread. The more UNI held by a single entity, the more votes they get to cast for or against any given proposal, and a proposal passes if and only if it receives a majority vote and at least 4% of all UNI tokens have voted on said proposal (this is known as a quorum).

From a technical standpoint, we know smart contracts – once deployed – are immutable. How, then, do proposals manage to effect changes on chain? In short, we either deploy a new set of smart contracts (such as Uniswap’s upgrade from V2 to V3, though this was a unilateral upgrade by Uniswap Labs that didn’t take governance into account), or we adjust variables on existing smart contracts (such as Uniswap’s ‘fee switch’, which gives each individual liquidity pool the option to divert some protocol fees to UNI holders instead of LPs).

3. Thinking Forward

3.1. Forks

Since Uniswap is one of crypto’s biggest open-source projects, it’s not surprising to hear that it’s been forked many times in the past. PancakeSwap, SushiSwap and SpookySwap are some notable DEXes that were built from Uniswap forks.

But why fork Uniswap if it’s already so deeply entrenched in Ethereum’s ecosystem? Many reasons exist: such as deploying a DEX on a different EVM chain (SpookySwap on Fantom), or in the case of SushiSwap, making an improvement to Uniswap’s mechanics and using a vampire attack to siphon liquidity away from Uniswap.

When SushiSwap was created, Uniswap didn't yet have a governance token. So, SushiSwap created the SUSHI token, which – other than governance – entitled its holders to a percentage of SushiSwap's trading fee revenue. SushiSwap then offered SUSHI tokens in exchange for Uniswap LP tokens, and, after amassing a sizable amount of Uniswap LP tokens, exercised their right to withdraw a massive amount of liquidity from Uniswap to Sushiswap. Uniswap was then forced to implement UNI shortly after.

This exemplifies the adversarial and gamelike nature of the DeFi ecosystem, where anyone can make a move to fork and improve an existing protocol, or – more maliciously – exploit a vulnerability. As devs, we need to keep on our toes and remain on the lookout both for opportunities and for chinks in our own armor.

3.2. Value Accrual

The UNI token hasn’t been performing well. It reached an all-time high of around $45 in mid-2021, but has since declined more than 80%. While part of this can certainly be attributed to the macroeconomic slowdown and market crash that has affected crypto in general as of late, we can also call UNI’s tokenomics into question.

Currently, the only benefit that UNI holders enjoy is governance of the Uniswap protocol itself. Compare this to Sushiswap, where SUSHI holders earn a small portion of protocol fees, or more recent exchanges like GMX which do the same, and we come to realize there’s little reason for the average crypto user to hold UNI. Governance just isn’t good enough of an incentive: Most users, in general, are uninterested to participate actively in discussions and votes, and this is especially so for Uniswap, where a large amount of UNI is concentrated in the hands of a small number of holders, rendering the average user’s votes even more insignificant.

Then again, we could also consider the opposing argument that the massive volume and TVL Uniswap experiences is precisely because Uniswap directs all protocol fees to its LPs, creating a great incentive for users to deposit liquidity. Tokenomic design is another deep, deep rabbit hole that this walkthrough doesn’t aim to go down, so I’ll leave these considerations here as some additional food for thought.

And this concludes my walkthrough of Uniswap – thanks for reading!

4. Resources/Citations

Uniswap code deep-dives:

Explanation of Uniswap’s LP mechanics:

Sushiswap-Uniswap Vampire Attack:

Governance thread on Uniswap’s fee switch:

Subscribe to Hive
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.