Uniswap V2 core源码解析

3,438 阅读12分钟

Uniswap V2合约分为核心合约(core)周边合约(periphery),均使用solidity语言编写。其核心合约实现了Uniswap V2的完整功能:创建交易对,流动性供给,交易代币,价格预言机等,但对用户操作不友好;而周边合约是用来让用户更方便的和核心合约交互。

UniswapV2核心合约主要由factory合约(UniswapV2Factory.sol)、交易对模板合约(UniswapV2Pair.sol)及辅助工具库与接口定义等三部分组成。

1.Uniswap V2Factory.sol

factory合约主要作用是创建流动性池

  • 属性

feeTo是开发者团队的地址。用于切换开发团队手续费开关,在uniswapV2中,会收取0.3%的手续费给LP,如果这里的feeTo地址是0,则表明不给开发者团队手续费,如果不为0,则开发者会收取0.05%手续费。

feeToSetter是用于改变开发者团队地址

allPairs 是用于存放所有交易对(代币对)合约地址信息

    address public feeTo;
    address public feeToSetter;
    address[] public allPairs;
  • 映射和事件

getPair 前两个地址分别对应交易对中的两种代币地址,最后一个地址是交易对合约本身地址

PairCreated 事件在createPair方法中触发,保存交易对的信息(两种代币地址,交易对本身地址,创建交易对的数量)

    mapping(address => mapping(address => address)) public getPair;
    event PairCreated(address indexed token0, address indexed token1, address pair, uint);
  • 构造函数

在部署合约时对feeToSetter地址进行初始化

    constructor(address _feeToSetter) public {
        feeToSetter = _feeToSetter;
    }
  • 只读方法

getPair方法和allPairs方法不是出现在合约代码里,而是solidity的映射和属性如果是public修饰,则自带getX方法

allPairsLengh()返回到目前为止通过工厂创建的交易对的总数。

setFeeToSetter()更改开发者团队地址

    function getPair(address tokenA, address tokenB) external view returns (address pair);
    function allPairs(uint) external view returns (address pair);

    function allPairsLength() external view returns (uint) {
        return allPairs.length;
    }
    
    function setFeeTo(address _feeTo) external {
        require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
        feeTo = _feeTo;
    }

    function setFeeToSetter(address _feeToSetter) external {
        require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
        feeToSetter = _feeToSetter;
    }
  • 状态改变方法

    function createPair(address tokenA, address tokenB) external returns (address pair) {
        require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
        (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
        require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
        require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient
        bytes memory bytecode = type(UniswapV2Pair).creationCode;
        bytes32 salt = keccak256(abi.encodePacked(token0, token1));
        assembly {
            pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
        }
        IUniswapV2Pair(pair).initialize(token0, token1);
        getPair[token0][token1] = pair;
        getPair[token1][token0] = pair; // populate mapping in the reverse direction
        allPairs.push(pair);
        emit PairCreated(token0, token1, pair, allPairs.length);
    }

2.Uniswap V2ERC20.sol

该合约主要是实现了ERC-20代币的功能

  • 常量

定义Uniswap V2代币的名称、代币的标识符、代币的精度全局变量信息

    string public constant name = 'Uniswap V2';
    string public constant symbol = 'UNI-V2';
    uint8 public constant decimals = 18;
  • 定义变量

DOMAIN_SEPARATOR 是用于不同Dapp之间区分相同结构和内容的签名消息,该值有助于用户辨识哪些为信任的dapp

PERMIT_TYPEHASH用于keccak256方法的参数

keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");

nonces用于记录合约中每个地址使用链下签名消息的交易数量,防止重放攻击

    bytes32 public DOMAIN_SEPARATOR;
    bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;
    mapping(address => uint) public nonces;

在以太坊中,用keccak哈希算法来计算公钥的256位哈希,再截取这256位哈希的后160位哈希作为地址值。

  • 重要方法

abi.encodePacked 将输入的参数根据其所需最低空间编码,类似abi.encode,但是会把其中填充的很多0给省略。当我们想要省略空间,且不与合约进行交互,可以使用abi.encodePacked、。例如:算一些数据的hash可以使用。

keccak算法是在以太坊中计算公钥的256位哈希,再截取这256位哈希的后160位哈希作为地址值。是哈希函数其中一种。

    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);
    }

permit方法实现的就是白皮书2.5节中介绍的“Meta transactions for pool shares 元交易”功能。上述代码中的digest的格式定义是来自EIP_721中的离线签名规范。

用户签名的内容是(owner)授权(approve)某个合约(spender)在截止时间(deadline)之前花掉一定数量(value)的代币(Pair流动性代币)。

periphery合约拿着签名的原始信息和签名生成的v,r,s,可以调用permit方法获得授权,permit方法可以使用ecrecover方法还原出签名地址为代币所有人,验证通过则批准授权。

为什么有permit函数?

permit函数主要实现了用户验证与授权,Uniswap V2的core函数虽然功能完善,但是对于用户来说却极不友好,用户需要借助它的周边合约才能和核心合约进行交互,但是在设计到流动性供给是,比如减少用户流动性,此时用户需要将自己的流动性代币燃烧掉,而由于用户调用的是周边合约,所以在未经授权的情况下是无法进行燃烧操作的,此时如果安装常规操作,那么用户需要先调用交易对合约对周边合约进行授权,之后再调用周边合约进行燃烧操作,而这个过程形成了两个不同合约的两个交易(无法合并到一个交易中)

如果我们通过线下消息签名,则可以减少其中一个交易,将所有操作放在一个交易里执行,确保了交易的原子性,在周边合约里,减小流动性来提取资产时,周边合约在一个函数内先调用交易对的permit函数进行授权,接着再进行转移流动性代币到交易对合约,提取代币等操作,所有操作都在周边合约的同一个函数中进行,达成了交易的原子性和对用户的友好性。

3.Unsiwap V2Pair.sol

Uniswap V2Pair 继承自IUniswap V2Pair, Uniswap V2ERC20,其中IUniswap V2Pair中定义了必须要实现的接口

  • 定义全局变量

MINIMUM_LIQUIDITY 定义了最小流动性,在提供初始流动性时会被燃烧掉

SELECTOR 是用于计算ERC-20合约中转移资产的transfer对应的函数选择器

factory 是要用于存储factory合约地址,token0,token1分别表示两种代币的地址

blockTimestampLast 记录交易时的区块创建时间,reserve0,reserve1 分别表示最新恒定乘积中两种代币的数量

price0CumulativeLastprice1CumulativeLast变量用于记录交易对中两种价格的累计值

KLast 用于表示某一时刻恒定乘积中的积的值,主要用于开发团队手续费的计算

unlocked = 1 表示未被锁上的状态,用于下面的修饰器

    uint public constant MINIMUM_LIQUIDITY = 10**3;
    bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));

    address public factory;
    address public token0;
    address public token1;

    uint112 private reserve0;           
    uint112 private reserve1;          
    uint32  private blockTimestampLast;

    uint public price0CumulativeLast;
    uint public price1CumulativeLast;
    uint public kLast; 

    uint private unlocked = 1;
  • modifier修饰器

该modifier的流程为:在调用该lock修饰器的函数首先检查unlocked 是否为1,如果不是则报错被锁上,如果是为1,则将unlocked赋值为0(锁上),之后执行被修饰的函数体,此时unlocked已成为0,之后等函数执行完之后再恢复unlocked为1:

    modifier lock() {
        require(unlocked == 1, 'UniswapV2: LOCKED');
        unlocked = 0;
        _;//表示被修饰的函数内容
        unlocked = 1;
    }
  • 构造器

初始化factory

    constructor() public {
        factory = msg.sender;
    }
  • 函数

getReserves()用于获取交易对的资产数量和最近一次交易的区块时间

    function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {
        _reserve0 = reserve0;
        _reserve1 = reserve1;
        _blockTimestampLast = blockTimestampLast;
    }

_safeTransfer() 函数用于发送代币

此时会使用代币的call函数去调用代币合约transfer来发送代币,在这里会检查call调用是否成功以及返回值是否为true:

    function _safeTransfer(address token, address to, uint value) private {
        (bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value));
        require(success && (data.length == 0 || abi.decode(data, (bool))), 'UniswapV2: TRANSFER_FAILED');
    }

_unpdate()用于更新reserves并进行价格累计的计算

函数内部解释:

  1. require 用于验证 balance0 和 blanace1 是否 uint112 的上限

  2. blockTimestamp只取后32位

  3. timeElapsed 计算当前区块和上一个区块之间的时间差

  4. if语句是要时间差(两个区块的时间差,不是同一个区块)大于0并且两种资产的数量不为0,才可以进行价格累计计算,如果是同一个区块的第二笔交易及以后的交易,timeElapsed则为0,此时不会计算价格累计值。

  5. 更新 reserve0reserve1;同时更新block时间为当前 blockTimestampLast 时间,之后通过emit触发同步事件:

    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);
    }

_mintFee()用于在添加流动性和移除流动性时,计算开发团队手续费

函数内部解释:

  1. feeTo获取开发者团队地址
  2. feeOn如果不是全0地址,那么 feeOn = true
  3. _KLast获取恒定乘积值
  4. if判断手续费开关是否开启(即手续费接受地址是否为空),如果手续费开关打开则根据以下公式来计算手续费的值,公式中的k1为旧的乘积值,对应下述代码中的_klast,k2为新的乘积值

  1. rootK 相当于K2,rootKLast相当于K1(都平方式为了好计算手续费值

  2. if (rootK > rootKLast)表明现在池中是在添加流动性,可以计算手续费(由上节uniswap V2原理详解文章提过每次用户交易资产,K会变大)

  3. numeratordenominator分别计算的是上述公式中的分子和分母

  4. 调用uniswap V2ERC20中的_mint(),传入开发者团队地址和收益

    function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {
        address feeTo = IUniswapV2Factory(factory).feeTo();
        feeOn = feeTo != address(0);
        uint _kLast = kLast; 
        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;
        }
    }

mint()用于用户提供流动性时(提供一定比例的两种ERC-20代币)增加流动性代币给流动性提供者

函数内部解释:

  1. 参数to是接收流动性代币的地址,liquidity是增加流动性的数值
  2. getReserves()获取库存交易对的资产数量
  3. balance0balance1是流动性池中当前交易对的资产数量
  4. amount0amount1是计算用户新注入的两种ERC20代币的数量
  5. feeOn发送开发团队的手续费
  6. _totalSupply是存储当前已发行的流动性代币的总量(之所以写在feeOn后面,是因为在_mintFee()中会更新一次totalSupply值)
  7. if语句中,如果_totalSupply为0,则说明是初次提供流动性,会根据恒定乘积公式的平方根来计算,同时要减去已经燃烧掉的初始流动性值,具体为MINIMUM_LIQUIDITY;如果_totalSupply不为0,则会根据已有流动性按比例增发,由于注入了两种代币,所以会有两个计算公式,每种代币按注入比例计算流动性值,取两个中的最小值。

ZSKI[EKBF@]5IWL2(QK~T5T.png

8.调用_mint(to,liquidity)来增发新的流动性给接收者,_update()更新流动性池中两种资产的值。

  1. 检查手续费是否开启,如果开启了,则更新恒定乘积的值。
    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);
    }

burn()用于燃烧流动性代币来提取相应的两种资产,并减少交易对的流动性

函数内部解释:

  1. getReserves()获取库存交易对的资产数量
  2. _token0token1获取代币地址
  3. balance0balance1获取交易对两种代币实际数量
  4. liquidity获取当前合约中流动性代币
  5. feeOn计算手续费给开发团队
  6. _totalSupply是存储当前已发行的流动性代币的总量(之所以写在feeOn后面,是因为在_mintFee()中会更新一次totalSupply值)
  7. amopunt0amount1按比例计算提取资产
  8. 将用户转入的流动性代币燃烧(通过燃烧代币得到方式来提取两种资产)
  9. 将两种资产token转到对应的地址
  10. 获取现在两种资产的余额

根据白皮书可知,uniswap为了节省交易手续费,uniswap V2只在 mint/burn 流动性时收取累计的手续费。

    function burn(address to) external lock returns (uint amount0, uint amount1) {
        (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
        address _token0 = token0;                                // gas savings
        address _token1 = token1;                                // gas savings
        uint balance0 = IERC20(_token0).balanceOf(address(this));
        uint balance1 = IERC20(_token1).balanceOf(address(this));
        uint liquidity = balanceOf[address(this)];

        bool feeOn = _mintFee(_reserve0, _reserve1);
        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(address(this), liquidity);
        _safeTransfer(_token0, to, amount0);
        _safeTransfer(_token1, to, amount1);
        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);
    }

swap()用于交易对中资产交易

函数内部解释:

1.传入的参数amount0Outamount1Out,to以及data分别是要购买的token0的数量,token1的数量,接收者的地址,接收后执行回调传递数据。

2.先判断要购买的token数量是否大于0,接着使用getReserves()获取当前库存的交易对资产数量,并判断购买的token是否小于reserve的值。

3.如果amount0Out大于0,说明要购买token0,则将token0转给 to;如果amount1Out大于0,则说明要购买token1,则将token1转给to

4.调用合约的uniswapV2Call回调函数将data传递过去,普通交易调用这个data为空

5.获取此时交易对资产的余额

6.amount0In通过当前余额和库存余额比较可得出汇入流动性池的资产数量(使用的是三目运算符)

7.更新恒定乘积公式,并且新的值要大于等于原来的值。

    function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
        require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
        (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
        require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');

        uint balance0;
        uint balance1;
        {
        address _token0 = token0;
        address _token1 = token1;
        require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
        if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); 
        if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out);
        if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
        balance0 = IERC20(_token0).balanceOf(address(this));
        balance1 = IERC20(_token1).balanceOf(address(this));
        }
        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');
        {
        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(balance0, balance1, _reserve0, _reserve1);
        emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
    }

参考文章:UniswapV2协议解析

参考文章深入理解Uniswap v2合约代码

参考文章:Uniswap V2白皮书