Decode Uniswap V2

Decode Uniswap V2

Tags
Web3
Defi
Published
June 3, 2024
Author
Senn

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

  1. 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.
  1. 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.
  1. 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.
  1. 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.
  1. 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.
  1. 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 and tokenB, 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 and tokenB 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 and token1 is the larger address. This ensures there will be only one pair contract for tokenA and tokenB, 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 and token1.
  • 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 and token1-token0 directions.
  • Adds the new pair contract address to the allPairs array.
Emitting the PairCreated Event
  • Emits the PairCreated event with the addresses of token0, 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 of token0 and token1.
  • Fetches the current balance of token0 and token1 in the contract.
  • Calculates the amount of token0 and token1 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 and amount1, 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 of token0 transferred to the to address.
    • uint amount1: The amount of token1 transferred to the to 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 of token0 to be sent out.
    • uint amount1Out: The amount of token1 to be sent out.
    • address to: The address to receive the output tokens.
    • bytes calldata data: Optional data that can be passed to the to address for a callback.
Function Logic
  1. 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 afterward.
  1. Output Amount Checks:
      • Ensures that at least one of the output amounts is greater than zero.
  1. Get Reserves:
      • Retrieves the current reserves of token0 and token1 and checks that the output amounts are less than the respective reserves.
  1. Transfer Output Tokens:
      • Retrieves the addresses of token0 and token1, ensures the to address is valid, and transfers the specified amounts of token0 and token1 to the to address.
  1. Callback Execution:
      • If the data parameter is not empty, it calls the uniswapV2Call function on the to address. This allows for flash swaps, where the to address can execute arbitrary code before the function completes.
  1. Get Updated Balances:
      • Retrieves the updated balances of token0 and token1 in the pair contract after the transfer.
  1. Calculate Input Amounts:
      • Calculates the input amounts of token0 and token1 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.
  1. 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.
  1. Update Reserves:
      • Calls the _update function to update the reserves and cumulative prices with the new balances.
  1. 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:
  1. Fetch Cumulative Prices:
      • Fetch price0CumulativeLast and price1CumulativeLast at two different points in time.
  1. 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 and tokenB: Addresses of the tokens in the pair.
  • amountADesired and amountBDesired: Desired amounts of tokenA and tokenB to add to the pool.
  • amountAMin and amountBMin: Minimum amounts of tokenA and tokenB 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
  1. Call _addLiquidity:
      • _addLiquidity is called to determine the optimal amounts of tokenA and tokenB to be added to the liquidity pool, based on the reserves and the desired amounts.
  1. 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.
  1. Mint Liquidity Tokens:
      • The mint function on the IUniswapV2Pair contract is called to mint the liquidity tokens and send them to the specified to 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.

Reference