Overview
Uniswap V2 is a decentralized exchange (DEX) protocol initially built on the Ethereum blockchain, which allows users to trade Ether and ERC-20 tokens directly from their wallets. It operates on an automated market maker (AMM) model, where liquidity is provided by users who deposit pairs of tokens into liquidity pools.
Key Features of Uniswap V2
- Decentralization and Non-Custodial Trading:
- Uniswap V2 is fully decentralized, meaning it does not rely on a central authority. Users retain control of their funds at all times, trading directly from their wallets.
- Automated Market Maker (AMM) Model:
- Instead of traditional order books, Uniswap uses AMMs to facilitate trades. Liquidity providers (LPs) add equal values of two tokens to a liquidity pool, and prices are determined by the ratio of these tokens in the pool.
- Liquidity Pools and Providers:
- Anyone can become a liquidity provider by depositing an equal value of two tokens into a pool. In return, LPs earn fees from trades that occur within their pools, proportional to their share of the total liquidity.
- Price Determination and Arbitrage:
- Prices on Uniswap are determined algorithmically based on the ratio of tokens in the pool. Arbitrageurs help keep prices in line with the broader market by exploiting price differences across different exchanges.
- Token Swaps:
- Users can swap any Ether/ERC-20 token for another directly on Uniswap. The protocol supports direct token-to-token swaps, eliminating the need for intermediary steps.
- Flash Swaps:
- Uniswap V2 introduced flash swaps, allowing users to withdraw the full reserve of any ERC-20 token without upfront costs, provided they either return the tokens or pay for them within the same transaction.
Conclusion
Uniswap V2 represents a significant advancement in decentralized exchange technology, providing a robust and efficient platform for token swaps. Its innovative features, such as direct ERC-20 swaps and flash swaps, combined with its decentralized nature, make it a cornerstone of the DeFi landscape.
AMM Algorithm
AMM (Automated Market Maker) uses a mathematical formula to price assets instead of using traditional buy and sell orders. Users add pairs of tokens to these pools. Trades are made against the pool, and prices are determined by the ratio of tokens in the pool.
Uniswap V2’s pool contract is an AMM targeting two tokens: and 。Liquidity providers (LPs) provide and to the pool contract and get liquidity token in return.
Traders trade and against the pool contract and pay some fees to the pool contract in the format of or .
Price formula
and 's reserved amounts satisfy the following formula:
is constant if we ignore trade fees and donation. Then we can get price of one token in the unit of the other token:
In fact, users don't trade based on the price calculated based on the price calculated above, but based on the rule of constant . For example, if user provides of to the pool to exchange for . The below function is satisified (ignore trade fee), which means the is unchanged after the trade:
We can get the exchanged amount of as:
The actual price applie on the trade is:
Trade changes price
For example, if trader sells some exchanges for some , then the reserved amount(liqudity) of in the pool contract decreases, and the liquidity of in the pool contract increases, so the price of increases, and the price of decreases.
Contracts
Name | Network | Address | Description |
Factory | Ethereum | The central contract in Uniswap V2. It is responsible for creating and managing all the liquidity pools. | |
Pool (DAI/USDT) | Ethereum | Hold reserves of two tokens and facilitates trading between them. | |
UniswapV2Router02 | Ethereum | A user-facing interface that facilitates interactions with the pools. It simplifies the process of trading and providing liquidity. | |
UniversalRouter | Ethereum | An ETH, ERC20, and NFT swap router, that can aggregate trades across protocols to give users access highly-flexible and personalised transactions. |
Code analysis
Create Pool
The
UniswapV2Factory.createPair
function is responsible for creating a new pair contract for two tokens. It ensures that a new pair contract is created in a standardized way, with checks to prevent duplication and invalid addresses. It uses the create2
opcode for deterministic deployment and updates the necessary mappings and arrays to keep track of the pairs.Function Definition
- The function is public and can be called externally.
- It takes two parameters:
tokenA
andtokenB
, which are the addresses of the two tokens for which a pair is to be created.
- It returns the address of the created pair contract.
Checking Identical Addresses
- Ensures that
tokenA
andtokenB
are not the same. A pair must consist of two different tokens.
Determining Token Order
- Orders the token addresses so that
token0
is the smaller address andtoken1
is the larger address. This ensures there will be only one pair contract fortokenA
andtokenB
, which helps liquidity concentration .
Checking for Zero Address
- Ensures that neither of the token addresses is the zero address. The zero address is invalid for tokens.
Checking if the Pair Already Exists
- Checks if the pair already exists by looking up the getPair mapping. If a pair already exists, it reverts the transaction.
Creating the Pair Contract
- Retrieves the bytecode for the
UniswapV2Pair
contract.
- Generates a salt using the keccak256 hash of
token0
andtoken1
.
- Uses the
create2
opcode to deploy the pair contract. The create2 opcode allows for deterministic deployment addresses.
Note by using
create2
and the derteministic calculation of salt
, the pair contract address of two tokens are derterministic, which makes integration with UniswapV2 more convenient.Initializing the Pair Contract
- Calls the initialize function of the newly created pair contract to set token0 and token1.
Updating Mappings and Arrays
- Updates the getPair mapping for both
token0
-token1
andtoken1
-token0
directions.
- Adds the new pair contract address to the
allPairs
array.
Emitting the PairCreated Event
- Emits the
PairCreated
event with the addresses oftoken0
,token1
, the new pair contract address, and the total number of pairs created so far.
/// ---v2-core/contracts/UniswapV2Factory.sol--- mapping(address => mapping(address => address)) public getPair; address[] public allPairs; function createPair(address tokenA, address tokenB) external returns (address pair) { // Checking Identical Addresses require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES'); // Determining Token Order (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); // Checking for Zero Address require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS'); // Checking if the Pair Already Exists require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient // Creating the Pair Contract bytes memory bytecode = type(UniswapV2Pair).creationCode; bytes32 salt = keccak256(abi.encodePacked(token0, token1)); assembly { pair := create2(0, add(bytecode, 32), mload(bytecode), salt) } // Initializing the Pair Contract IUniswapV2Pair(pair).initialize(token0, token1); // Updating Mappings and Arrays getPair[token0][token1] = pair; getPair[token1][token0] = pair; // populate mapping in the reverse direction allPairs.push(pair); // Emitting the PairCreated Event emit PairCreated(token0, token1, pair, allPairs.length); }
/// ---v2-core/contracts/UniswapV2Pair.sol--- address public factory; address public token0; address public token1; // called once by the factory at time of deployment function initialize(address _token0, address _token1) external { require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check token0 = _token0; token1 = _token1; }
Add Liquidity
UniswapV2Pair.mint
is responsible for minting new liquidity tokens when liquidity is added to the pool.Function Signature
- address
to
: The address to receive the minted liquidity tokens.
- returns
(uint liquidity)
: The amount of liquidity tokens minted.
Get Reserves and Balances
- Calls
getReserves()
to fetch the current reserves oftoken0
andtoken1
.
- Fetches the current balance of
token0
andtoken1
in the contract.
- Calculates the amount of
token0
andtoken1
added to the pool since the last reserve update.
Mint Fee
- Calls
_mintFee
to handle the protocol fee if it is enabled.
- If fees are enabled, this function may mint additional tokens to the fee recipient.
Calculate Liquidity to Mint
- If it is the first time liquidity is being added (i.e.,
_totalSupply == 0
): - The liquidity is calculated as the square root of the product of
amount0
andamount1
, minus a minimum liquidity value (MINIMUM_LIQUIDITY
). MINIMUM_LIQUIDITY
tokens are minted to the zero address to prevent the total supply from being zero.
- For subsequent liquidity additions:
- The liquidity to mint is calculated proportionally based on the amounts added relative to the existing reserves.
/// ---v2-core/contracts/UniswapV2Pair.sol--- uint public constant MINIMUM_LIQUIDITY = 10**3; // this low-level function should be called from a contract which performs important safety checks function mint(address to) external lock returns (uint liquidity) { (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings uint balance0 = IERC20(token0).balanceOf(address(this)); uint balance1 = IERC20(token1).balanceOf(address(this)); uint amount0 = balance0.sub(_reserve0); uint amount1 = balance1.sub(_reserve1); bool feeOn = _mintFee(_reserve0, _reserve1); uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee if (_totalSupply == 0) { liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY); _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens } else { liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1); } require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED'); _mint(to, liquidity); _update(balance0, balance1, _reserve0, _reserve1); if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date emit Mint(msg.sender, amount0, amount1); } function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) { _reserve0 = reserve0; _reserve1 = reserve1; _blockTimestampLast = blockTimestampLast; } // update reserves and, on the first call per block, price accumulators function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private { require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW'); uint32 blockTimestamp = uint32(block.timestamp % 2**32); uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) { // * never overflows, and + overflow is desired price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed; price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed; } reserve0 = uint112(balance0); reserve1 = uint112(balance1); blockTimestampLast = blockTimestamp; emit Sync(reserve0, reserve1); }
Calculate Liquidity
- If it’s the first time liquidity is being added (i.e.,
_totalSupply == 0
):
- For subsequent liquidity additions:
First Liquidity Addition
During the first liquidity addition, it subtracts 1000 liquidity and mints to zero address.This is to prevent the value of one liquidity token from being too high, causing small liquidity holders to be restricted from participating to provide liquidity.
Because if the deposited value is smaller than the value of one liquidity, the contract won’t mint liquidity due to precision limitation. For example, if
reserve0
is 5, reserve1
is 10, and liquidity totalSupply
is 1. LP needs to provide at least 5 reserve0
and 10 reserve1
to mint 1 liquidity. An attacker can register a pool, add a very small liquidity to the pool, then donate many tokens to the pool to make 1 liquidity’s underlying value very large, which prevents other LPs to add liquidity. But if the pair contract requires to mint minimal liquidity 1000 during the first liquidity addition, the cost of attacker grows 1000 times. 1000 liquidity should be a negligible cost for almost any token pair. But it dramatically increases the cost of the above attack.
Subsequent liquidity addition
The amount of liquidity tokens to be minted is the minimum amount calculated based on
token0
and token1
, which ensures that latter LPs can't get tokens in the pool other than he or she provides.For example, If the amount of liquidity tokens to be minted is calculated based on
token1
. LP A provides 3 token0
and 10 token1
. One day token1
becomes worthless, LP B can provide 1 token0
and 1e18 token1
to exhaust valuable token0
. However, if the amount of liquidity token to be minted is the minimum amount calculated based on token0
and token1
, even LP B provides 1 token0
and 1e18 token1
, he can only mint liquidity calculated by token0
, and thus can't get token0
provided by LP A.If the calculated liquidity differs based on
token0
and token1
, the LP in fact loses some asset. So the best strategy to provide liquidity is to make sure the calculated liquidity based on provided amount of token0
and token1
equals. Fortunately, Router
contract helps us do this, which will be analyzed later in this blog.Handle Protocol Fee
Symbol:
Symbol | Meaning |
reserve of token0 in pair contract at time t | |
total supply of liquidity |
Uniswap v2 includes a 0.05% protocol fee that can be turned on and off. If turned on, this fee would be sent to a
feeTo
address specified in the factory contract.The 0.05% protocol fee is collected from swap fee. If the
feeTo
address is set, the protocol will begin charging a 0.05% fee, which is taken as a cut of the 0.3% fees earned by liquidity providers. Traders will continue to pay a 0.3% fee on all trades.
- 83.3% of that fee(0.25% [0.30% * ] of the amount traded) will go to liquidity providers.
- 16.6% of that fee (0.05% [0.30% * ] of the amount traded) will go to the
feeTo
address.
However, collecting 0.05% fee at the time of the trade would impose an additional gas cost on every trade. To avoid this, rather than transfer fees directly to
feeTo
, Uniswap protocol computes the accumulated fees, and mints new liquidity tokens to the fee beneficiary, immediately before any tokens are minted or burned.
The total collected fees can be computed by measuring the growth in (that is, ) since the last time fees were collected.
If the fee was activated before , the feeTo address should capture of fees that were accumulated between and .
If we use to represent liquidity providers's asset in the pool contract, and let to represent the protocol fee percent , then the below formula should be satisfied, which means that the minted liquidity tokens represents the corresponding accumulated protocol fee during ~ at time :
: liquidity token on .
: liquidity token minted to
feeTo
.Then we can get as:
If , then , which is the implementation in the
UniswapV2Pair._mintFee
./// ---v2-core/contracts/UniswapV2Pair.sol--- // if fee is on, mint liquidity equivalent to 1/6th of the growth in sqrt(k) function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) { address feeTo = IUniswapV2Factory(factory).feeTo(); feeOn = feeTo != address(0); uint _kLast = kLast; // gas savings if (feeOn) { if (_kLast != 0) { uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1)); uint rootKLast = Math.sqrt(_kLast); if (rootK > rootKLast) { uint numerator = totalSupply.mul(rootK.sub(rootKLast)); uint denominator = rootK.mul(5).add(rootKLast); uint liquidity = numerator / denominator; if (liquidity > 0) _mint(feeTo, liquidity); } } } else if (_kLast != 0) { kLast = 0; } }
Burn Liquidity
UniswapV2Pair.burn
is responsible for burning liquidity tokens and transferring the underlying tokens back to the user.Function Signature
- Parameters:
address to
: The address to which the underlying tokens will be sent after burning the liquidity tokens.
- Returns:
uint amount0
: The amount oftoken0
transferred to theto
address.uint amount1
: The amount oftoken1
transferred to theto
address.
Lock Modifier
The
lock
modifier ensures that the function is not reentrant by setting the unlocked
state variable to 0 during execution and back to 1 afterwards.Get Reserves
Retrieves the current reserves of
token0
and token1
.Get Balances
Fetches the current balances of
token0
and token1
in the pair contract.Get Liquidity
Gets the amount of liquidity tokens held by the pair contract.
Mint Fee
Calls
_mintFee
to possibly mint liquidity fees to the feeTo
address if applicable.Calculate Refund Amounts
Calculates the amounts of
token0
and token1
to be transferred to the to
address based on the liquidity burned. This ensures a pro-rata distribution of the tokens. And ensures that the amounts to be transferred are greater than zero.Burn Liquidity Tokens
Burns the liquidity tokens held by the pair contract.
Transfer Tokens
Transfers the calculated amounts of
token0
and token1
to the to
address using the _safeTransfer
function, which ensures the transfer is successful.Update Reserves
Calls the
_update
function to update the reserves and the cumulative price if necessary. If fees are on, it updates kLast
.Emit Burn Event
Emits a
Burn
event with the details of the burned liquidity and the transferred amounts./// ---v2-core/contracts/UniswapV2Pair.sol--- // this low-level function should be called from a contract which performs important safety checks function burn(address to) external lock returns (uint amount0, uint amount1) { // Get Reserves (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings address _token0 = token0; // gas savings address _token1 = token1; // gas savings // Get Balances uint balance0 = IERC20(_token0).balanceOf(address(this)); uint balance1 = IERC20(_token1).balanceOf(address(this)); // Get Liquidity uint liquidity = balanceOf[address(this)]; // Mint Fee bool feeOn = _mintFee(_reserve0, _reserve1); // Calculate Refund Amounts uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED'); // Burn Liquidity Tokens _burn(address(this), liquidity); // Transfer Tokens _safeTransfer(_token0, to, amount0); _safeTransfer(_token1, to, amount1); // Update Reserves and kLast balance0 = IERC20(_token0).balanceOf(address(this)); balance1 = IERC20(_token1).balanceOf(address(this)); _update(balance0, balance1, _reserve0, _reserve1); if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date emit Burn(msg.sender, amount0, amount1, to); } function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) { _reserve0 = reserve0; _reserve1 = reserve1; _blockTimestampLast = blockTimestampLast; } // update reserves and, on the first call per block, price accumulators function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private { require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW'); uint32 blockTimestamp = uint32(block.timestamp % 2**32); uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) { // * never overflows, and + overflow is desired price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed; price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed; } reserve0 = uint112(balance0); reserve1 = uint112(balance1); blockTimestampLast = blockTimestamp; emit Sync(reserve0, reserve1); }
Sync
Update
reserve
to token balance./// ---v2-core/contracts/UniswapV2Pair.sol--- // force reserves to match balances function sync() external lock { _update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1); }
Skim
Update token balance to
reserve
./// ---v2-core/contracts/UniswapV2Pair.sol--- // force balances to match reserves function skim(address to) external lock { address _token0 = token0; // gas savings address _token1 = token1; // gas savings _safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0)); _safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1)); }
Swap
UniswapV2Pair.swap
is responsible for exchanging tokens in the liquidity pool. To use swap
, account should first transfer some token0
/token1
to the pool, then call the swap
to transfer out token1
/token0
. The swap
ensures that the after swap is larger than the before swap considering the fee.Function Signature
- Parameters
uint amount0Out
: The amount oftoken0
to be sent out.uint amount1Out
: The amount oftoken1
to be sent out.address to
: The address to receive the output tokens.bytes calldata data
: Optional data that can be passed to theto
address for a callback.
Function Logic
- Lock Modifier:
- The
lock
modifier ensures that the function is not reentrant by setting theunlocked
state variable to 0 during execution and back to 1 afterward.
- Output Amount Checks:
- Ensures that at least one of the output amounts is greater than zero.
- Get Reserves:
- Retrieves the current reserves of
token0
andtoken1
and checks that the output amounts are less than the respective reserves.
- Transfer Output Tokens:
- Retrieves the addresses of
token0
andtoken1
, ensures theto
address is valid, and transfers the specified amounts oftoken0
andtoken1
to theto
address.
- Callback Execution:
- If the
data
parameter is not empty, it calls theuniswapV2Call
function on theto
address. This allows for flash swaps, where theto
address can execute arbitrary code before the function completes.
- Get Updated Balances:
- Retrieves the updated balances of
token0
andtoken1
in the pair contract after the transfer.
- Calculate Input Amounts:
- Calculates the input amounts of
token0
andtoken1
based on the difference between the updated balances and the previous reserves adjusted for the output amounts. Ensures that at least one of the input amounts is greater than zero.
- Check Invariant:
- Adjusts the balances for the 0.3% fee and checks that the product of the adjusted balances is at least as large as the product of the previous reserves. This ensures that the constant product invariant is maintained.
- Update Reserves:
- Calls the
_update
function to update the reserves and cumulative prices with the new balances.
- Emit Swap Event:
- Emits a
Swap
event with the details of the swap, including the input and output amounts and the recipient address.
Fee
The equation in Check Invariant process:
amount0In
and amount1In
is the amount of token0
and token1
user transferred into the pool before calling swap
.We can get:
which means that swapper needs to pay fee .
Uniswap V2 uses such a converted equation to avoid precision limitation in solidity.
The fee is accumulated in the
reserve
. When LP burns liquidity, fee will be automatically collected. Considering the fee, If we provide
amountIn
, we can calculate the amountOut
:Actual Fee Percentage
The actual fee percentage is not but . Because in the equation we are using the transferred in amount as the base to calculate fee which has already including the fee. From a more intuitive perspective, we should calculate the fee based on the actual trade volume. If swapper uses
token0
to exchange for token1
, then the actual trade volume is: , the fee percentage is , which is ./// ---v2-core/contracts/UniswapV2Pair.sol--- // this low-level function should be called from a contract which performs important safety checks function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock { /// Output Amount Checks require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT'); /// Get Reserves (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY'); uint balance0; uint balance1; { // scope for _token{0,1}, avoids stack too deep errors address _token0 = token0; address _token1 = token1; require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO'); // Transfer Output Tokens if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens // Callback Execution if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data); // Get Updated Balances balance0 = IERC20(_token0).balanceOf(address(this)); balance1 = IERC20(_token1).balanceOf(address(this)); } // Calculate Input Amounts uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0; uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0; require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT'); // Check Invariant { // scope for reserve{0,1}Adjusted, avoids stack too deep errors uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3)); uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3)); require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K'); } // Update Reserves _update(balance0, balance1, _reserve0, _reserve1); emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to); }
Flash Swap
Uniswap V2 supports "flash swap," which allows users to borrow tokens from a Uniswap liquidity pool without any collateral, as long as they return the tokens within the same transaction.
In the
swap
function, pool first transfers token to to
, then calls uniswapV2Call
of to
to execute customized logic. To flash swap, we can implement custom logic in to.uniswapV2Call
, such as arbitrage, and return the token and fee to pool, so that the invariant check in the pool can be satisfied./// ---v2-core/contracts/UniswapV2Pair.sol--- // this low-level function should be called from a contract which performs important safety checks function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock { /// Output Amount Checks require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT'); /// Get Reserves (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY'); uint balance0; uint balance1; { // scope for _token{0,1}, avoids stack too deep errors address _token0 = token0; address _token1 = token1; require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO'); // Transfer Output Tokens if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens // Callback Execution if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data); // Get Updated Balances balance0 = IERC20(_token0).balanceOf(address(this)); balance1 = IERC20(_token1).balanceOf(address(this)); } // Calculate Input Amounts uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0; uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0; require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT'); // Check Invariant { // scope for reserve{0,1}Adjusted, avoids stack too deep errors uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3)); uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3)); require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K'); } // Update Reserves _update(balance0, balance1, _reserve0, _reserve1); emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to); }
Price oracle
Uniswap V2 incorporates a built-in time-weighted average price (TWAP) oracle mechanism, which allows for the reliable calculation of on-chain price feeds.
price0CumulativeLast
and price1CumulativeLast
are the cumulative sum of prices considering the time elapse for each token pair. These variables are updated every time a swap occurs or liquidity is added or removed, reflecting the running total of the product of price and time.To obtain the time-weighted average price for a token pair, an off chain entity can:
- Fetch Cumulative Prices:
- Fetch
price0CumulativeLast
andprice1CumulativeLast
at two different points in time.
- Calculate TWAP:
- Calculate the TWAP using the difference in cumulative prices divided by the time elapsed between the two points.
/// ---v2-core/contracts/UniswapV2Pair.sol--- // update reserves and, on the first call per block, price accumulators function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private { require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW'); uint32 blockTimestamp = uint32(block.timestamp % 2**32); uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) { // * never overflows, and + overflow is desired price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed; price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed; } reserve0 = uint112(balance0); reserve1 = uint112(balance1); blockTimestampLast = blockTimestamp; emit Sync(reserve0, reserve1); } // ---UQ112x112.sol--- library UQ112x112 { uint224 constant Q112 = 2**112; // encode a uint112 as a UQ112x112 function encode(uint112 y) internal pure returns (uint224 z) { z = uint224(y) * Q112; // never overflows } // divide a UQ112x112 by a uint112, returning a UQ112x112 function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) { z = x / uint224(y); } }
Permit
UniswapV2Pair
inherits UniswapV2ERC20
which implements permit
. permit
allows user to sign message to approve liquidity token to another address. The permit
is used in UniswapV2Router02
to help user only send one tx to burn liquidity. Note that the
permit
function can be dangerous. Fishing website can pop up metamask to ask user to sign a the permit
message which approves the liquidity transfer to attacker. For users who don’t understand the logic behind permit
, they may believe this is a safe action because this is not to issue a tx but just to sign a signature. After the attacker has obtained the signature, they can call the permit
to get the liquidity token transfer authority./// ---v2-core/contracts/UniswapV2ERC20.sol--- function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external { require(deadline >= block.timestamp, 'UniswapV2: EXPIRED'); bytes32 digest = keccak256( abi.encodePacked( '\x19\x01', DOMAIN_SEPARATOR, keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline)) ) ); address recoveredAddress = ecrecover(digest, v, r, s); require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE'); _approve(owner, spender, value); }
Router
The
UniswapV2Router02
contract is an implementation of the Uniswap V2 Router, a smart contract that facilitates adding liquidity, removing liquidity, and performing token swaps on the Uniswap V2 decentralized exchange.addLiquidity
UniswapV2Router02.addLiquidity
helps user add liquidity in a single transaction, also prevents user from suffering loss caused by transferring disproportionate amount of two pair tokens into the pool.Remember in the
UniswapV2Pair.mint
, the liquidity to be minted is the minimal of two kinds of calculation:/// ---v2-core/contracts/UniswapV2Pair.sol#mint--- liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
If users transfer disproportionate amount of two pair tokens into the pool, they will suffer loss. Also, to mint liquidity, user should first transfers token to the pool before calling
mint
.In the
UniswapV2Router02.addLiquidity
, it calculates optimal amounts of two tokens, prevents such loss, and complete transfer and mint in a single transaction.Parameters
tokenA
andtokenB
: Addresses of the tokens in the pair.
amountADesired
andamountBDesired
: Desired amounts oftokenA
andtokenB
to add to the pool.
amountAMin
andamountBMin
: Minimum amounts oftokenA
andtokenB
to add to the pool (to protect against significant price changes).
to
: Address that will receive the liquidity tokens.
deadline
: Unix timestamp after which the transaction will revert if it hasn't been mined.
Process
- Call
_addLiquidity
: _addLiquidity
is called to determine the optimal amounts oftokenA
andtokenB
to be added to the liquidity pool, based on the reserves and the desired amounts.
- Transfer Tokens to the Pair Contract:
- The tokens are transferred from the user's address to the pair contract using
TransferHelper.safeTransferFrom
. - This ensures that the necessary tokens are moved into the pool.
- Mint Liquidity Tokens:
- The
mint
function on theIUniswapV2Pair
contract is called to mint the liquidity tokens and send them to the specifiedto
address. - The number of liquidity tokens minted represents the user's share of the liquidity pool.
/// ---v2-periphery/contracts/UniswapV2Router02.sol--- function addLiquidity( address tokenA, address tokenB, uint amountADesired, uint amountBDesired, uint amountAMin, uint amountBMin, address to, uint deadline ) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) { // Determine the optimal amounts of tokenA and tokenB to be added to the liquidity pool (amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin); // Transferring Tokens to the Pair Contract: address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB); TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA); TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB); // Mint Liquidity Tokens liquidity = IUniswapV2Pair(pair).mint(to); } modifier ensure(uint deadline) { require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED'); _; } // **** ADD LIQUIDITY **** function _addLiquidity( address tokenA, address tokenB, uint amountADesired, uint amountBDesired, uint amountAMin, uint amountBMin ) internal virtual returns (uint amountA, uint amountB) { // create the pair if it doesn't exist yet if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) { IUniswapV2Factory(factory).createPair(tokenA, tokenB); } (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB); if (reserveA == 0 && reserveB == 0) { (amountA, amountB) = (amountADesired, amountBDesired); } else { uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB); if (amountBOptimal <= amountBDesired) { require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT'); (amountA, amountB) = (amountADesired, amountBOptimal); } else { uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA); assert(amountAOptimal <= amountADesired); require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT'); (amountA, amountB) = (amountAOptimal, amountBDesired); } } } /// ---v2-periphery/contracts/libraries/UniswapV2Library.sol--- // given some amount of an asset and pair reserves, returns an equivalent amount of the other asset function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) { require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT'); require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY'); amountB = amountA.mul(reserveB) / reserveA; }
In Uniswap V2, ETH is not directly used as a pair token in liquidity pools. Instead, WETH (Wrapped ETH) is used. Because ETH itself is not an ERC-20 token, by using WETH instead of ETH, the Uniswap V2 contracts can handle all tokens uniformly. This avoids the need for special case logic to handle ETH differently from ERC-20 tokens, reducing complexity and potential bugs in the contract code.
To add ETH as liquidity, we should convert the ETH to WETH first.
UniswapV2Router02.addLiquidityETH
helps complete this in a single liquidity addition transaction./// ---v2-periphery/contracts/UniswapV2Router02.sol--- function addLiquidityETH( address token, uint amountTokenDesired, uint amountTokenMin, uint amountETHMin, address to, uint deadline ) external virtual override payable ensure(deadline) returns (uint amountToken, uint amountETH, uint liquidity) { (amountToken, amountETH) = _addLiquidity( token, WETH, amountTokenDesired, msg.value, amountTokenMin, amountETHMin ); address pair = UniswapV2Library.pairFor(factory, token, WETH); TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken); // Deposit ETH to get WETH and transfer to pool. IWETH(WETH).deposit{value: amountETH}(); assert(IWETH(WETH).transfer(pair, amountETH)); liquidity = IUniswapV2Pair(pair).mint(to); // refund dust eth, if any if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH); }
removeLiquidity
In
UniswapV2Pair.burn
, it burns liquidity token belongs to itself. So trader should first transfer liquidity to the pool. UniswapV2Router02.removeLiquidity
helps complete this in a single tx. Also, we can specify minimal amount to be refunded to avoid price manipulation./// ---v2-periphery/contracts/UniswapV2Router02.sol--- // **** REMOVE LIQUIDITY **** function removeLiquidity( address tokenA, address tokenB, uint liquidity, uint amountAMin, uint amountBMin, address to, uint deadline ) public virtual override ensure(deadline) returns (uint amountA, uint amountB) { address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB); // send liquidity to pair IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); (uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to); (address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB); (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0); require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT'); require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT'); } /// ---v2-periphery/contracts/UniswapV2Library.sol--- // returns sorted token addresses, used to handle return values from pairs sorted in this order function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) { require(tokenA != tokenB, 'UniswapV2Library: IDENTICAL_ADDRESSES'); (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); require(token0 != address(0), 'UniswapV2Library: ZERO_ADDRESS'); }
UniswapV2Router02.removeLiquidityETH
supports to withdraw ETH to user after the pool has refunded WETH./// ---v2-periphery/contracts/UniswapV2Router02.sol--- function removeLiquidityETH( address token, uint liquidity, uint amountTokenMin, uint amountETHMin, address to, uint deadline ) public virtual override ensure(deadline) returns (uint amountToken, uint amountETH) { (amountToken, amountETH) = removeLiquidity( token, WETH, liquidity, amountTokenMin, amountETHMin, address(this), deadline ); TransferHelper.safeTransfer(token, to, amountToken); IWETH(WETH).withdraw(amountETH); TransferHelper.safeTransferETH(to, amountETH); }
removeLiquidityWithPermit
enable user to only issue only one tx to remove liquidity with the support of UniswapV2Pair.permit
.Usually, user needs first approve liquidity token to
UniswapV2Router02
, then call UniswapV2Router02.removeLiquidity
to remove liquidity where the liquidity is transferred from user to pool) .With
permit
, user can just sign the approval message, and pass the message signature to the UniswapV2Router02.removeLiquidityWithPermit
, UniswapV2Pair.permit
decodes the signer, and approves the liquidity of the signer to UniswapV2Router02
. So that user can remove liquidity in a single tx which saves gas./// ---v2-periphery/contracts/UniswapV2Router02.sol--- function removeLiquidityWithPermit( address tokenA, address tokenB, uint liquidity, uint amountAMin, uint amountBMin, address to, uint deadline, bool approveMax, uint8 v, bytes32 r, bytes32 s ) external virtual override returns (uint amountA, uint amountB) { address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB); uint value = approveMax ? uint(-1) : liquidity; IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s); (amountA, amountB) = removeLiquidity(tokenA, tokenB, liquidity, amountAMin, amountBMin, to, deadline); }
Swap
UniswapV2Router02
implements methods to support swap, allow traders to swap between multiple pools. This is an essential functionality of Uniswap V2, because each pool only supports the trade between two type of tokens. For example, if there are only USDC/USDT and USDT/WETH pool, user with USDC can’t directly buy WETH, they need first exchange USDT, and then exchange WETH. With UniswapV2Router02
, trader can batch those two swaps into one transaction.swapExactTokensForTokens
swapExactTokensForTokens
allows users to swap an exact amount of input token for a minimum amount of output tokens Function Signature
- Parameters
amountIn
: The exact amount of input tokens the user wants to swap.amountOutMin
: The minimum amount of output tokens the user is willing to receive (protects against significant price slippage).path
: An array of token addresses representing the swap path. For example, if swapping from token A to token B, the path might be[tokenA, tokenB]
.to
: The address that will receive the output tokens.deadline
: The timestamp by which the transaction must be included in a block to be valid (protects against long-pending transactions).
Calculate Output Amounts:
Uses
UniswapV2Library.getAmountsOut
to determine the amounts of tokens that will be received at each step in the path. This function performs a series of calculations based on the reserves in the liquidity pools.Check Minimum Output Amount:
Ensures that the final amount of output tokens is at least the minimum specified (
amountOutMin
). If not, the transaction reverts.Transfer Input Tokens:
Transfers the input tokens from the user to the first pair in the path using
TransferHelper.safeTransferFrom
.Perform Swap:
Calls the internal
_swap
function to execute the token swaps along the specified path. UniswapV2Library.getAmountOut
calculates amountOut
of each swap which will be passed to the UniswapV2Pair.swap
function. The calculation is: refer/// ---v2-periphery/contracts/UniswapV2Router02.sol--- function swapExactTokensForTokens( uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline ) external virtual override ensure(deadline) returns (uint[] memory amounts) { // Calculate Output Amounts amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path); require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); TransferHelper.safeTransferFrom( path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0] ); _swap(amounts, path, to); } // **** SWAP **** // requires the initial amount to have already been sent to the first pair function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual { for (uint i; i < path.length - 1; i++) { (address input, address output) = (path[i], path[i + 1]); (address token0,) = UniswapV2Library.sortTokens(input, output); uint amountOut = amounts[i + 1]; (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0)); address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to; IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap( amount0Out, amount1Out, to, new bytes(0) ); } } // ---v2-periphery/contracts/UniswapV2Library.sol--- // performs chained getAmountOut calculations on any number of pairs function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) { require(path.length >= 2, 'UniswapV2Library: INVALID_PATH'); amounts = new uint[](path.length); amounts[0] = amountIn; for (uint i; i < path.length - 1; i++) { (uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]); amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut); } } // fetches and sorts the reserves for a pair function getReserves(address factory, address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB) { (address token0,) = sortTokens(tokenA, tokenB); (uint reserve0, uint reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves(); (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0); } // given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) { require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT'); require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY'); uint amountInWithFee = amountIn.mul(997); uint numerator = amountInWithFee.mul(reserveOut); uint denominator = reserveIn.mul(1000).add(amountInWithFee); amountOut = numerator / denominator; }
swapExactTokensForTokens
swapExactTokensForTokens
allows users to swap out an exact amount of output token using a maximal amount of input token.UniswapV2Library.getAmountsIn
calculates the exact amount of input token needed to swap the specified amount of output token. The calculation is:/// ---v2-periphery/contracts/UniswapV2Router02.sol--- function swapExactTokensForTokens( uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline ) external virtual override ensure(deadline) returns (uint[] memory amounts) { amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path); require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); TransferHelper.safeTransferFrom( path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0] ); _swap(amounts, path, to); } /// ---v2-periphery/contracts/libraries/UniswapV2Library.sol--- // performs chained getAmountIn calculations on any number of pairs function getAmountsIn(address factory, uint amountOut, address[] memory path) internal view returns (uint[] memory amounts) { require(path.length >= 2, 'UniswapV2Library: INVALID_PATH'); amounts = new uint[](path.length); amounts[amounts.length - 1] = amountOut; for (uint i = path.length - 1; i > 0; i--) { (uint reserveIn, uint reserveOut) = getReserves(factory, path[i - 1], path[i]); amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut); } } // given an output amount of an asset and pair reserves, returns a required input amount of the other asset function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) internal pure returns (uint amountIn) { require(amountOut > 0, 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT'); require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY'); uint numerator = reserveIn.mul(amountOut).mul(1000); uint denominator = reserveOut.sub(amountOut).mul(997); amountIn = (numerator / denominator).add(1); }
Conclusion
Uniswap V2 has established itself as a cornerstone of the DeFi ecosystem, providing a decentralized, permissionless, and highly liquid trading platform. Its innovations in decentralized trading, particularly the introduction of ERC-20 pairs and flash swaps, have significantly influenced the broader DeFi landscape, inspiring further advancements and integrations.
By offering a more versatile and efficient trading platform, Uniswap V2 continues to empower users and developers, driving the growth and adoption of decentralized finance.