Uniswap-v2 Contract Walk-Through
Introduction
Uniswap v2(opens in a new tab) can create an exchange market between any two ERC-20 tokens. In this article we will go over the source code for the contracts that implement this protocol and see why they are written this way.
What Does Uniswap Do?
Basically, there are two types of users: liquidity providers and traders.
The liquidity providers provide the pool with the two tokens that can be exchanged (we'll call them Token0 and Token1). In return, they receive a third token that represents partial ownership of the pool called a liquidity token.
Traders send one type of token to the pool and receive the other (for example, send Token0 and receive Token1) out of the pool provided by the liquidity providers. The exchange rate is determined by the relative number of Token0s and Token1s that the pool has. In addition, the pool takes a small percent as a reward for the liquidity pool.
When liquidity providers want their assets back they can burn the pool tokens and receive back their tokens, including their share of the rewards.
Click here for a fuller description(opens in a new tab).
Why v2? Why not v3?
Uniswap v3(opens in a new tab) is an upgrade that is much more complicated than the v2. It is easier to first learn v2 and then go to v3.
Core Contracts vs Periphery Contracts
Uniswap v2 is divided into two components, a core and a periphery. This division allows the core contracts, which hold the assets and therefore have to be secure, to be simpler and easier to audit. All the extra functionality required by traders can then be provided by periphery contracts.
Data and Control Flows
This is the flow of data and control that happens when you perform the three main actions of Uniswap:
- Swap between different tokens
- Add liquidity to the market and get rewarded with pair exchange ERC-20 liquidity tokens
- Burn ERC-20 liquidity tokens and get back the ERC-20 tokens that the pair exchange allows traders to exchange
Swap
This is most common flow, used by traders:
Caller
- Provide the periphery account with an allowance in the amount to be swapped.
- Call one of the periphery contract's many swap functions (which one depends on whether ETH is involved or not, whether the trader specifies the amount of tokens to deposit or the amount of tokens to get back, etc).
Every swap function accepts a
path
, an array of exchanges to go through.
In the periphery contract (UniswapV2Router02.sol)
- Identify the amounts that need to be traded on each exchange along the path.
- Iterates over the path. For every exchange along the way it sends the input token and then calls the exchange's
swap
function. In most cases the destination address for the tokens is the next pair exchange in the path. In the final exchange it is the address provided by the trader.
In the core contract (UniswapV2Pair.sol)
- Verify that the core contract is not being cheated and can maintain sufficient liquidity after the swap.
- See how many extra tokens we have in addition to the known reserves. That amount is the number of input tokens we received to exchange.
- Send the output tokens to the destination.
- Call
_update
to update the reserve amounts
Back in the periphery contract (UniswapV2Router02.sol)
- Perform any necessary cleanup (for example, burn WETH tokens to get back ETH to send the trader)
Add Liquidity
Caller
- Provide the periphery account with an allowance in the amounts to be added to the liquidity pool.
- Call one of the periphery contract's
addLiquidity
functions.
In the periphery contract (UniswapV2Router02.sol)
- Create a new pair exchange if necessary
- If there is an existing pair exchange, calculate the amount of tokens to add. This is supposed to be identical value for both tokens, so the same ratio of new tokens to existing tokens.
- Check if the amounts are acceptable (callers can specify a minimum amount below which they'd rather not add liquidity)
- Call the core contract.
In the core contract (UniswapV2Pair.sol)
- Mint liquidity tokens and send them to the caller
- Call
_update
to update the reserve amounts
Remove Liquidity
Caller
- Provide the periphery account with an allowance of liquidity tokens to be burned in exchange for the underlying tokens.
- Call one of the periphery contract's
removeLiquidity
functions.
In the periphery contract (UniswapV2Router02.sol)
- Send the liquidity tokens to the pair exchange
In the core contract (UniswapV2Pair.sol)
- Send the destination address the underlying tokens in proportion to the burned tokens. For example if there are 1000 A tokens in the pool, 500 B tokens, and 90 liquidity tokens, and we receive 9 tokens to burn, we're burning 10% of the liquidity tokens so we send back the user 100 A tokens and 50 B tokens.
- Burn the liquidity tokens
- Call
_update
to update the reserve amounts
The Core Contracts
These are the secure contracts which hold the liquidity.
UniswapV2Pair.sol
This contract(opens in a new tab) implements the actual pool that exchanges tokens. It is the core Uniswap functionality.
1pragma solidity =0.5.16;23import './interfaces/IUniswapV2Pair.sol';4import './UniswapV2ERC20.sol';5import './libraries/Math.sol';6import './libraries/UQ112x112.sol';7import './interfaces/IERC20.sol';8import './interfaces/IUniswapV2Factory.sol';9import './interfaces/IUniswapV2Callee.sol';10બધું બતાવોનકલ કરો
These are all the interfaces that the contract needs to know about, either because the contract implements them (IUniswapV2Pair
and UniswapV2ERC20
) or because it calls contracts that implement them.
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 {2નકલ કરો
This contract inherits from UniswapV2ERC20
, which provides the ERC-20 functions for the liquidity tokens.
1 using SafeMath for uint;2નકલ કરો
The SafeMath library(opens in a new tab) is used to avoid overflows and underflows. This is important because otherwise we might end up with a situation where a value should be -1
, but is instead 2^256-1
.
1 using UQ112x112 for uint224;2નકલ કરો
A lot of calculations in the pool contract require fractions. However, fractions are not supported by the EVM.
The solution that Uniswap found is to use 224 bit values, with 112 bits for the integer part, and 112 bits for the fraction. So 1.0
is represented as 2^112
, 1.5
is represented as 2^112 + 2^111
, etc.
More details about this library are available later in the document.
Variables
1 uint public constant MINIMUM_LIQUIDITY = 10**3;2નકલ કરો
To avoid cases of division by zero, there is a minimum number of liquidity tokens that always exist (but are owned by account zero). That number is MINIMUM_LIQUIDITY, a thousand.
1 bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));2નકલ કરો
This is the ABI selector for the ERC-20 transfer function. It is used to transfer ERC-20 tokens in the two token accounts.
1 address public factory;2નકલ કરો
This is the factory contract that created this pool. Every pool is an exchange between two ERC-20 tokens, the factory is a central point that connects all of these pools.
1 address public token0;2 address public token1;3નકલ કરો
There are the addresses of the contracts for the two types of ERC-20 tokens that can be exchanged by this pool.
1 uint112 private reserve0; // uses single storage slot, accessible via getReserves2 uint112 private reserve1; // uses single storage slot, accessible via getReserves3નકલ કરો
The reserves the pool has for each token type. We assume that the two represent the same amount of value, and therefore each token0 is worth reserve1/reserve0 token1's.
1 uint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves2નકલ કરો
The timestamp for the last block in which an exchange occurred, used to track exchange rates across time.
One of the biggest gas expenses of Ethereum contracts is storage, which persists from one call of the contract to the next. Each storage cell is 256 bits long. So three variables, reserve0
, reserve1
, and blockTimestampLast
, are allocated in such a way a single storage value can include all three of them (112+112+32=256).
1 uint public price0CumulativeLast;2 uint public price1CumulativeLast;3નકલ કરો
These variables hold the cumulative costs for each token (each in term of the other). They can be used to calculate the average exchange rate over a period of time.
1 uint public kLast; // reserve0 * reserve1, as of immediately after the most recent liquidity event2નકલ કરો
The way the pair exchange decides on the exchange rate between token0 and token1 is to keep the multiple of the two reserves constant during trades. kLast
is this value. It changes when a liquidity provider deposits or withdraws tokens, and it increases slightly because of the 0.3% market fee.
Here is a simple example. Note that for the sake of simplicity the table only has three digits after the decimal point, and we ignore the 0.3% trading fee so the numbers are not accurate.
Event | reserve0 | reserve1 | reserve0 * reserve1 | Average exchange rate (token1 / token0) |
---|---|---|---|---|
Initial setup | 1,000.000 | 1,000.000 | 1,000,000 | |
Trader A swaps 50 token0 for 47.619 token1 | 1,050.000 | 952.381 | 1,000,000 | 0.952 |
Trader B swaps 10 token0 for 8.984 token1 | 1,060.000 | 943.396 | 1,000,000 | 0.898 |
Trader C swaps 40 token0 for 34.305 token1 | 1,100.000 | 909.090 | 1,000,000 | 0.858 |
Trader D swaps 100 token1 for 109.01 token0 | 990.990 | 1,009.090 | 1,000,000 | 0.917 |
Trader E swaps 10 token0 for 10.079 token1 | 1,000.990 | 999.010 | 1,000,000 | 1.008 |
As traders provide more of token0, the relative value of token1 increases, and vice versa, based on supply and demand.
Lock
1 uint private unlocked = 1;2નકલ કરો
There is a class of security vulnerabilities that are based on reentrancy abuse(opens in a new tab). Uniswap needs to transfer arbitrary ERC-20 tokens, which means calling ERC-20 contracts that may attempt to abuse the Uniswap market that calls them.
By having an unlocked
variable as part of the contract, we can prevent functions from being called while they are running (within the same transaction).
1 modifier lock() {2નકલ કરો
This function is a modifier(opens in a new tab), a function that wraps around a normal function to change its behavior is some way.
1 require(unlocked == 1, 'UniswapV2: LOCKED');2 unlocked = 0;3નકલ કરો
If unlocked
is equal to one, set it to zero. If it is already zero revert the call, make it fail.
1 _;2નકલ કરો
In a modifier _;
is the original function call (with all the parameters). Here it means that the function call only happens if unlocked
was one when it was called, and while it is running the value of unlocked
is zero.
1 unlocked = 1;2 }3નકલ કરો
After the main function returns, release the lock.
Misc. functions
1 function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {2 _reserve0 = reserve0;3 _reserve1 = reserve1;4 _blockTimestampLast = blockTimestampLast;5 }6નકલ કરો
This function provides callers with the current state of the exchange. Notice that Solidity functions can return multiple values(opens in a new tab).
1 function _safeTransfer(address token, address to, uint value) private {2 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value));3નકલ કરો
This internal function transfers an amount of ERC20 tokens from the exchange to somebody else. SELECTOR
specifies that the function we are calling is transfer(address,uint)
(see definition above).
To avoid having to import an interface for the token function, we "manually" create the call using one of the ABI functions(opens in a new tab).
1 require(success && (data.length == 0 || abi.decode(data, (bool))), 'UniswapV2: TRANSFER_FAILED');2 }3નકલ કરો
There are two ways in which an ERC-20 transfer call can report failure:
- Revert. If a call to an external contract reverts, then the boolean return value is
false
- End normally but report a failure. In that case the return value buffer has a non-zero length, and when decoded as a boolean value it is
false
If either of these conditions happen, revert.
Events
1 event Mint(address indexed sender, uint amount0, uint amount1);2 event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);3નકલ કરો
These two events are emitted when a liquidity provider either deposits liquidity (Mint
) or withdraws it (Burn
). In either case, the amounts of token0 and token1 that are deposited or withdrawn are part of the event, as well as the identity of the account that called us (sender
). In the case of a withdrawal, the event also includes the target that received the tokens (to
), which may not be the same as the sender.
1 event Swap(2 address indexed sender,3 uint amount0In,4 uint amount1In,5 uint amount0Out,6 uint amount1Out,7 address indexed to8 );9નકલ કરો
This event is emitted when a trader swaps one token for the other. Again, the sender and the destination may not be the same. Each token may be either sent to the exchange, or received from it.
1 event Sync(uint112 reserve0, uint112 reserve1);2નકલ કરો
Finally, Sync
is emitted every time tokens are added or withdrawn, regardless of the reason, to provide the latest reserve information (and therefore the exchange rate).
Setup Functions
These functions are supposed to be called once when the new pair exchange is set up.
1 constructor() public {2 factory = msg.sender;3 }4નકલ કરો
The constructor makes sure we'll keep track of the address of the factory that created the pair. This information is required for initialize
and for the factory fee (if one exists)
1 // called once by the factory at time of deployment2 function initialize(address _token0, address _token1) external {3 require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check4 token0 = _token0;5 token1 = _token1;6 }7નકલ કરો
This function allows the factory (and only the factory) to specify the two ERC-20 tokens that this pair will exchange.
Internal Update Functions
_update
1 // update reserves and, on the first call per block, price accumulators2 function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {3નકલ કરો
This function is called every time tokens are deposited or withdrawn.
1 require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');2નકલ કરો
If either balance0 or balance1 (uint256) is higher than uint112(-1) (=2^112-1) (so it overflows & wraps back to 0 when converted to uint112) refuse to continue the _update to prevent overflows. With a normal token that can be subdivided into 10^18 units, this means each exchange is limited to about 5.1*10^15 of each tokens. So far that has not been a problem.
1 uint32 blockTimestamp = uint32(block.timestamp % 2**32);2 uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired3 if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {4નકલ કરો
If the time elapsed is not zero, it means we are the first exchange transaction on this block. In that case, we need to update the cost accumulators.
1 // * never overflows, and + overflow is desired2 price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;3 price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;4 }5નકલ કરો
Each cost accumulator is updated with the latest cost (reserve of the other token/reserve of this token) times the elapsed time in seconds. To get an average price you read the cumulative price is two points in time, and divide by the time difference between them. For example, assume this sequence of events:
Event | reserve0 | reserve1 | timestamp | Marginal exchange rate (reserve1 / reserve0) | price0CumulativeLast |
---|---|---|---|---|---|
Initial setup | 1,000.000 | 1,000.000 | 5,000 | 1.000 | 0 |
Trader A deposits 50 token0 and gets 47.619 token1 back | 1,050.000 | 952.381 | 5,020 | 0.907 | 20 |
Trader B deposits 10 token0 and gets 8.984 token1 back | 1,060.000 | 943.396 | 5,030 | 0.890 | 20+10*0.907 = 29.07 |
Trader C deposits 40 token0 and gets 34.305 token1 back | 1,100.000 | 909.090 | 5,100 | 0.826 | 29.07+70*0.890 = 91.37 |
Trader D deposits 100 token1 and gets 109.01 token0 back | 990.990 | 1,009.090 | 5,110 | 1.018 | 91.37+10*0.826 = 99.63 |
Trader E deposits 10 token0 and gets 10.079 token1 back | 1,000.990 | 999.010 | 5,150 | 0.998 | 99.63+40*1.1018 = 143.702 |
Let's say we want to calculate the average price of Token0 between the timestamps 5,030 and 5,150. The difference in the value of price0Cumulative
is 143.702-29.07=114.632. This is the average across two minutes (120 seconds). So the average price is 114.632/120 = 0.955.
This price calculation is the reason we need to know the old reserve sizes.
1 reserve0 = uint112(balance0);2 reserve1 = uint112(balance1);3 blockTimestampLast = blockTimestamp;4 emit Sync(reserve0, reserve1);5 }6નકલ કરો
Finally, update the global variables and emit a Sync
event.
_mintFee
1 // if fee is on, mint liquidity equivalent to 1/6th of the growth in sqrt(k)2 function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {3નકલ કરો
In Uniswap 2.0 traders pay a 0.30% fee to use the market. Most of that fee (0.25% of the trade) always goes to the liquidity providers. The remaining 0.05% can go either to the liquidity providers or to an address specified by the factory as a protocol fee, which pays Uniswap for their development effort.
To reduce calculations (and therefore gas costs), this fee is only calculated when liquidity is added or removed from the pool, rather than at each transaction.
1 address feeTo = IUniswapV2Factory(factory).feeTo();2 feeOn = feeTo != address(0);3નકલ કરો
Read the fee destination of the factory. If it is zero then there is no protocol fee and no need to calculate it that fee.
1 uint _kLast = kLast; // gas savings2નકલ કરો
The kLast
state variable is located in storage, so it will have a value between different calls to the contract.
Access to storage is a lot more expensive than access to the volatile memory that is released when the function call to the contract ends, so we use an internal variable to save on gas.
1 if (feeOn) {2 if (_kLast != 0) {3નકલ કરો
The liquidity providers get their cut simply by the appreciation of their liquidity tokens. But the protocol fee requires new liquidity tokens to be minted and provided to the feeTo
address.
1 uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));2 uint rootKLast = Math.sqrt(_kLast);3 if (rootK > rootKLast) {4નકલ કરો
If there is new liquidity on which to collect a protocol fee. You can see the square root function later in this article
1 uint numerator = totalSupply.mul(rootK.sub(rootKLast));2 uint denominator = rootK.mul(5).add(rootKLast);3 uint liquidity = numerator / denominator;4નકલ કરો
This complicated calculation of fees is explained in the whitepaper(opens in a new tab) on page 5. We know that between the time kLast
was calculated and the present no liquidity was added or removed (because we run this calculation every time liquidity is added or removed, before it actually changes), so any change in reserve0 * reserve1
has to come from transaction fees (without them we'd keep reserve0 * reserve1
constant).
1 if (liquidity > 0) _mint(feeTo, liquidity);2 }3 }4નકલ કરો
Use the UniswapV2ERC20._mint
function to actually create the additional liquidity tokens and assign them to feeTo
.
1 } else if (_kLast != 0) {2 kLast = 0;3 }4 }5નકલ કરો
If there is no fee set kLast
to zero (if it isn't that already). When this contract was written there was a gas refund feature(opens in a new tab) that encouraged contracts to reduce the overall size of the Ethereum state by zeroing out storage they did not need.
This code gets that refund when possible.
Externally Accessible Functions
Note that while any transaction or contract can call these functions, they are designed to be called from the periphery contract. If you call them directly you won't be able to cheat the pair exchange, but you might lose value through a mistake.
mint
1 // this low-level function should be called from a contract which performs important safety checks2 function mint(address to) external lock returns (uint liquidity) {3નકલ કરો
This function is called when a liquidity provider adds liquidity to the pool. It mints additional liquidity tokens as a reward. It should be called from a periphery contract that calls it after adding the liquidity in the same transaction (so nobody else would be able to submit a transaction that claims the new liquidity before the legitimate owner).
1 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings2નકલ કરો
This is the way to read the results of a Solidity function that returns multiple values. We discard the last returned values, the block timestamp, because we don't need it.
1 uint balance0 = IERC20(token0).balanceOf(address(this));2 uint balance1 = IERC20(token1).balanceOf(address(this));3 uint amount0 = balance0.sub(_reserve0);4 uint amount1 = balance1.sub(_reserve1);5નકલ કરો
Get the current balances and see how much was added of each token type.
1 bool feeOn = _mintFee(_reserve0, _reserve1);2નકલ કરો
Calculate the protocol fees to collect, if any, and mint liquidity tokens accordingly. Because the parameters to _mintFee
are the old reserve values, the fee is calculated accurately based only on pool changes due to fees.
1 uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee2 if (_totalSupply == 0) {3 liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);4 _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens5નકલ કરો
If this is the first deposit, create MINIMUM_LIQUIDITY
tokens and send them to address zero to lock them. They can never be redeemed, which means the pool will never be emptied completely (this saves us from division by zero in some places). The value of MINIMUM_LIQUIDITY
is a thousand, which considering most ERC-20 are subdivided into units of 10^-18'th of a token, as ETH is divided into wei, is 10^-15 to the value of a single token. Not a high cost.
In the time of the first deposit we don't know the relative value of the two tokens, so we just multiply the amounts and take a square root, assuming that the deposit provides us with equal value in both tokens.
We can trust this because it is in the depositor's interest to provide equal value, to avoid losing value to arbitrage. Let's say that the value of the two tokens is identical, but our depositor deposited four times as many of Token1 as of Token0. A trader can use the fact the pair exchange thinks that Token0 is more valuable to extract value out of it.
Event | reserve0 | reserve1 | reserve0 * reserve1 | Value of the pool (reserve0 + reserve1) |
---|---|---|---|---|
Initial setup | 8 | 32 | 256 | 40 |
Trader deposits 8 Token0 tokens, gets back 16 Token1 | 16 | 16 | 256 | 32 |
As you can see, the trader earned an extra 8 tokens, which come from a reduction in the value of the pool, hurting the depositor that owns it.
1 } else {2 liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);3નકલ કરો
With every subsequent deposit we already know the exchange rate between the two assets, and we expect liquidity providers to provide equal value in both. If they don't, we give them liquidity tokens based on the lesser value they provided as a punishment.
Whether it is the initial deposit or a subsequent one, the number of liquidity tokens we provide is equal to the square root of the change in reserve0*reserve1
and the value of the liquidity token doesn't change (unless we get a deposit that doesn't have equal values of both types, in which case the "fine" gets distributed). Here is another example with two tokens that have the same value, with three good deposits and one bad one (deposit of only one token type, so it doesn't produce any liquidity tokens).
Event | reserve0 | reserve1 | reserve0 * reserve1 | Pool value (reserve0 + reserve1) | Liquidity tokens minted for this deposit | Total liquidity tokens | value of each liquidity token |
---|---|---|---|---|---|---|---|
Initial setup | 8.000 | 8.000 | 64 | 16.000 | 8 | 8 | 2.000 |
Deposit four of each type | 12.000 | 12.000 | 144 | 24.000 | 4 | 12 | 2.000 |
Deposit two of each type | 14.000 | 14.000 | 196 | 28.000 | 2 | 14 | 2.000 |
Unequal value deposit | 18.000 | 14.000 | 252 | 32.000 | 0 | 14 | ~2.286 |
After arbitrage | ~15.874 | ~15.874 | 252 | ~31.748 | 0 | 14 | ~2.267 |
1 }2 require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');3 _mint(to, liquidity);4