Uniswap v3

New Features

Concentrated Liquidity

  • LPs can concentrate their liquidity by “bounding" it within an arbitrary price range that is smaller than (0, ∞). This improves the pool’s capital efficiency and allows LPs to approximate their preferred reserves curve.

  • Previously liquidity was distributed uniformly along the 𝑥𝑦 = 𝑘 curve.

  • Liquidity concentrated to a finite range is called a position. A position only needs to maintain enough reserves to support trading within its range, and therefore can act like a constant product pool with larger reserves within that range.

  • A position only needs to hold enough of asset X to cover price movement to its upper bound, because upwards price movement corresponds to depletion of the X reserves. Similarly, it only needs to hold enough of asset Y to cover price movement to its lower bound.

  • When the price exits a position’s range, the position’s liquidity is no longer active, and no longer earns fees. At that point, its liquidity is composed entirely of a single asset, because the reserves of the other asset must have been entirely depleted. If the price ever reenters the range, the liquidity becomes active again.

Multiple Pools Per Pair

  • In Uniswap v1 and v2, every pair of tokens corresponds to a single liquidity pool, which applies a uniform fee of 0.30% to all swaps. While this default fee tier historically worked well enough for many tokens, it is likely too high for some pools (such as pools between two stablecoins), and too low for others (such as pools that include highly volatile or rarely traded tokens).

  • Uniswap v3 introduces multiple pools for each pair of tokens, each with a different swap fee. All pools are created by the same factory contract. The factory contract initially allows pools to be created at three fee tiers: 0.05%, 0.30%, and 1%. Additional fee tiers can be enabled by UNI governance.

Non-Fungible Utility

  • Non-Compounding Fees - Fees earned in earlier versions were continuously deposited in the pool as liquidity. This meant that liquidity in the pool would grow over time, even without explicit deposits, and that fee earnings compounded. In Uniswap v3, due to the non-fungible nature of positions, this is no longer possible. Instead, fee earnings are stored separately and held as the tokens in which the fees are paid.

  • Removal of Native Liquidity Tokens - In Uniswap v1 and v2, the pool contract is also an ERC-20 token contract, whose tokens represent liquidity held in the pool. While this is convenient, it actually sits uneasily with the Uniswap v2 philosophy that anything that does not need to be in the core contracts should be in the periphery, and blessing one “canonical" ERC-20 implementation discourages the creation of improved ERC-20 token wrappers. Arguably, the ERC-20 token implementation should have been in the periphery, as a wrapper on a single liquidity position in the core contract.

  • The changes made in Uniswap v3 force this issue by making completely fungible liquidity tokens impossible. Due to the custom liquidity provision feature, fees are now collected and held by the pool as individual tokens, rather than automatically reinvested as liquidity in the pool. As a result, in v3, the pool contract does not implement the ERC-20 standard.

  • Anyone can create an ERC-20 token contract in the periphery that makes a liquidity position more fungible, but it will have to have additional logic to handle distribution of, or reinvestment of, collected fees. Alternatively, anyone could create a periphery contract that wraps an individual liquidity position (including collected fees) in an ERC-721 non-fungible token.

Oracle Upgrades

  • Uniswap v3 includes three significant changes to the time-weighted average price (TWAP) oracle that was introduced by Uniswap v2.

  • Uniswap v3 removes the need for users of the oracle to track previous values of the accumulator externally. Uniswap v3 brings the accumulator checkpoints into core, allowing external contracts to compute onchain TWAPs over recent periods without storing checkpoints of the accumulator value.

  • Another change is that instead of accumulating the sum of prices, allowing users to compute the arithmetic mean TWAP, Uniswap v3 tracks the sum of log prices, allowing users to compute the geometric mean TWAP.

  • Finally, Uniswap v3 adds a liquidity accumulator that is tracked alongside the price accumulator, which accumulates 1/𝐿 for each second. This liquidity accumulator is useful for external contracts that want to implement liquidity mining on top of Uniswap v3. It can also be used by other contracts to inform a decision on which of the pools corresponding to a pair will have the most reliable TWAP.

Core Contracts

UniswapV3Factory

  • Deploys Uniswap V3 pools and manages ownership and control over pool protocol fees

createPool()

  • Creates a pool for the given two tokens and fee

  • The call will revert if the pool already exists, the fee is invalid, or the token arguments are invalid.

function createPool(
    address tokenA,
    address tokenB,
    uint24 fee
  ) external returns (address pool)
  • get tick spacing according to the fee amount desired

  • Calls deploy on PoolDeployer contract:

    function deploy(
            address factory,
            address token0,
            address token1,
            uint24 fee,
            int24 tickSpacing
        ) internal returns (address pool) {
            parameters = Parameters({factory: factory, token0: token0, token1: token1, fee: fee, tickSpacing: tickSpacing});
            pool = address(new UniswapV3Pool{salt: keccak256(abi.encode(token0, token1, fee))}());
            delete parameters;
        }

UniswapV3Pool

swap()

  • Swaps token0 for token1, or token1 for token0

  • The caller of this method receives a callback - ISwapRouter.uniswapV3SwapCallback()

NameTypeDescription

recipient

address

The address to receive the output of the swap

zeroForOne

bool

The direction of the swap, true for token0 to token1, false for token1 to token0

amountSpecified

int256

The amount of the swap, which implicitly configures the swap as exact input (positive), or exact output (negative)

sqrtPriceLimitX96

uint160

The Q64.96 sqrt price limit. If zero for one, the price cannot be less than this value after the swap. If one for zero, the price cannot be greater than this value after the swap

data

bytes

Any data to be passed through to the callback

  • checks that amountSpecified is non-zero (why not positive?)

  • all the ticks bs

  • transfer output tokens to recipient

  • call the SwapCallback in the router contract

function swap(
    address recipient,
    bool zeroForOne,
    int256 amountSpecified,
    uint160 sqrtPriceLimitX96,
    bytes data
  ) external override noDelegateCall returns (int256 amount0, int256 amount1)

Swap Router

  • Router for stateless execution of swaps against Uniswap V3

  • constructor sets singleton factory and WETH addresses as immutable state variables

exactInputSingle()

  • Swaps amountIn of one token for as much as possible of another token

  • Calls exactInputInternal() using input params

  • Checks that amountOut is at least the minimum amount requested in input params, else function reverts

function exactInputSingle(ExactInputSingleParams calldata params)
        external
        payable
        override
        checkDeadline(params.deadline)
        returns (uint256 amountOut)

struct ExactInputSingleParams {
        address tokenIn;
        address tokenOut;
        uint24 fee;
        address recipient;
        uint256 deadline;
        uint256 amountIn;
        uint256 amountOutMinimum;
        uint160 sqrtPriceLimitX96;
    }

exactInput()

  • Swaps amountIn of one token for as much as possible of another along the specified path

  • Calls exactInputInternal() using input params

struct ExactInputParams {
        bytes path;
        address recipient;
        uint256 deadline;
        uint256 amountIn;
        uint256 amountOutMinimum;
    }

exactInputInternal()

  • internal function called by above exactInput external functions

  • if the recipient is the zero address, set recipient as router address

  • calls UniswapV3Pool.swap()

exactOutputSingle()

struct ExactOutputSingleParams {
        address tokenIn;
        address tokenOut;
        uint24 fee;
        address recipient;
        uint256 deadline;
        uint256 amountOut;
        uint256 amountInMaximum;
        uint160 sqrtPriceLimitX96;
    }

exactOutput()

struct ExactOutputParams {
        bytes path;
        address recipient;
        uint256 deadline;
        uint256 amountOut;
        uint256 amountInMaximum;
    }

uniswapV3SwapCallback()

  • Called to msg.sender after executing a swap via IUniswapV3Pool.swap().

  • In the implementation you must pay the pool tokens owed for the swap.

  • The caller of this method must be checked to be a UniswapV3Pool deployed by the canonical UniswapV3Factory.

function uniswapV3SwapCallback(
    int256 amount0Delta,
    int256 amount1Delta,
    bytes data
  ) external override 
  • checks that both amounts are positive

  • call pay() in helper contracts to transfer input tokens from user to

Last updated