JavaScript 不可用。

我们检测到浏览器禁用了 JavaScript。请启用 JavaScript 或改用支持的浏览器来继续访问

Uniswap V2 智能合约源码解读(第一部分 v2-core 合约)

作者:Anban Chu

发表日期:2020年07月25日

所属目录:合约分析

前置介绍

发现一个叫 Uniswap 的项目非常火,这是一个类似交易所一样的交易平台,所有全部是合约来执行的。因为没有上币费用,而且任何人都可以上架任何币。身边越来越多的人已经开始使用它了,被身边朋友安利,我也在 Uniswap 上购买了最新比较火的 AMPL 币,全程下来的体验还是很赞的。

这个项目的合约由两部分代码组成,核心是v2-core,配合的是v2-periphery;对应的如下:

v2-core : 只处理最核心的 LP 创建和手续费设置,以及资产的 mint / burn / swap 等。除此之外的数据转换等均不关心。

v2-periphery : 针对用户使用场景提供多种封装方法,比如用户想买 TokenA 这种币。池子里只有 WETH/TokenA的交易对。此时用户手里用户 TokenB,想用 TokenB 去买 TokenA,智能先把 TokenB 转为 WETH,中间的资产转换处理逻辑就是 v2-periphery 做的事情,这些是属于数据整合的事情,一旦牵扯资产的互换,则内部依赖 v2-core 核心 。

依赖关系大概如下:


v2-periphery    | UniswapV2Router02
------------------------------------
                | UniswapV2ERC20
                |     ⬇
v2-core         | UniswapV2Pair         mint / burn / swap
                |     ⬆
                | UniswapV2Factory      createPair

篇幅有限,这里只分析 v2-core 仓库下的合约。

功能的简单预览

这里核心逻辑在 UniswapV2Pair.sol合约, UniswapV2Factory 核心是创建池子(createPair)以及相关的辅助 feeTo、setFeeTofeeToSetter、setFeeToSettercreatePair 内部是使用 UniswapV2Pair 的 bytecode,使用 token0token1 排序 encodePacked 后作为 salt,然后使用type(UniswapV2Pair).creationCode 的 bytecode 做 create2.这样做的好处首先是可以预先知道要创建的合约地址,然后是避免重复创建。通过 createPair 创建出来的是一个两种代币组成的交易对,叫pair,俗称"池子"。

通过 UniswapV2Factory 可以创建一个个 Pair(交易池),Pair 的核心逻辑在 UniswapV2Pair 内,LP 的形态是由 UniswapV2ERC20 来决定的。

  • 每个交易对有一些基本属性:reserve0/reserve1以及totalSupply
    • reserve0/reserve1 是交易对中两种代币的储存量。
    • totalSupply是当前流动性代币的总量。每个交易对都对应一个流动性代币(LP Token / liquidity provider token)。LP Token 记录了所有流动性提供者的贡献。所有 LP Token 的总和就是totalSupply
    • Pair 中管理了 reserve0/reserve1以及totalSupply。用户向合约质押两种 Token,会获得流动性 Token 作为凭证,totalSupply 是所有 Token。
  • Uniswap 协议的思想是 reserve0 * reserve1 的乘积不变。

下面按照合约来逐个分析

  1. UniswapV2Factory.sol
  2. UniswapV2ERC20.sol
  3. UniswapV2Pair.sol

第一、 UniswapV2Factory.sol

  1. 只读方法
  2. 构造函数 (部署时候需要的配置)
  3. 核心方法
  4. 辅助方法
  5. 管理方法
  6. 合约事件

1)只读方法

  • feeTo(): 获取手续费的接收地址。
  • feeToSetter(): 获取feeTo的设置者
  • getPair(tokanA,tokenB): 通过 token 地址获取 pair 地址。
  • allPairs(pair_id): 通过索引获取 pair 地址
  • allPairsLength(): 获取已经生成的 piar 数量

2)构造函数 (部署时候需要的配置)

constructor(address _feeToSetter) public {
    feeToSetter = _feeToSetter;
}

这里部署的时候使用的是 0xc0a4272bb5df52134178Df25d77561CfB17ce407,这里仅设置了 feeToSetter 没有设置 feeTo

3)核心方法

只有一个 createPair 的核心方法。

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

函数的前四行代码是第一部分,核心是将 token0 token1 将地址进行排序,并确保参数符合预期。

  • 将地址进行排序:确保 token0 字面地址小于 token1,这可以 A,B 两个地址无论什么顺序传入,得到的 token0 和 token1 结果都是一样的。
  • 确保参数符合预期:
    • AB 两个 Token 不能相同,否则抛错 'UniswapV2: IDENTICAL_ADDRESSES'
    • Token 都不能等于 address(0),否则抛错 'UniswapV2: ZERO_ADDRESS'
    • 没有创建过 pair,抛错 'UniswapV2: PAIR_EXISTS'

函数的 5 至 10 行是第二部分,核心是创建 pair,并且初始化。

  • 第 5 行,bytecode 使用 UniswapV2Pair 的创建代码。
  • 为了确保 salt 的唯一性,将 token0 和 token1 作为 salt 生成的唯一方式。
  • 然后使用 assembly 的方式创建 create2.
    • 注: 实际上在最新版的 EMV 中,已经直接支持给new方法传递 salt 参数,如下所示:pair = new UniswapV2Pair{salt: salt}();,因为 Uniswap v2 合约在开发时还没有这个功能,所以使用 assembly create2。
  • 然后借助 IUniswapV2Pair 做新创建 pair 的初始化

函数的最后 4 行是做 pair 数据在本合约的记录信息

  • 为了兼容前端的搜索,getPair 上兼容不同顺序 token 的搜索处理。
  • 抛出事件

4)辅助方法

5)管理方法

6)合约事件

第二、 UniswapV2ERC20.sol

这个合约主要定义了 UniswapV2 的 ERC20 标准实现,代码比较简单。这里介绍下 permit 方法:

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 元交易”功能。EIP-712 定义了离线签名的规范,即 digest 的格式定义,用户签名的内容是其(owner)授权(approve)某个合约(spender)可以在截止时间(deadline)之前花掉一定数量(value)的代币(Pair 流动性代币),应用(periphery 合约)拿着签名的原始信息和签名后生成的 v, r, s,可以调用 Pair 合约的 permit 方法获得授权,permit 方法使用 ecrecover 还原出签名地址为代币所有人,验证通过则批准授权。

每个具体实现逻辑在 UniswapV2Pair 中。Pair 合约主要实现了三个方法:mint(添加流动性)、burn(移除流动性)、swap(兑换)。

第三、 UniswapV2Pair.sol

1.mint

每个交易对创建流动性。

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);
    // gas savings, must be defined here since totalSupply can update in _mintFee
    uint _totalSupply = totalSupply;
    if (_totalSupply == 0) {
        liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
        // permanently lock the first MINIMUM_LIQUIDITY tokens
        _mint(address(0), MINIMUM_LIQUIDITY);
    } 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);
}

首先,getReserves()获取两种代币的缓存余额。在白皮书中提到,保存缓存余额是为了防止攻击者操控价格预言机。此处还用于计算协议手续费,并通过当前余额与缓存余额相减获得转账的代币数量。

因为在调用 mint 函数之前,在 addLiquidity 函数已经完成了转账,所以,从这个函数的角度,两种代币数量的计算方式如下:

uint balance0 = IERC20(token0).balanceOf(address(this));
uint balance1 = IERC20(token1).balanceOf(address(this));
uint amount0 = balance0.sub(_reserve0);
uint amount1 = balance1.sub(_reserve1);

_mintFee 用于计算协议手续费:关于协议手续费的计算公式可以参考白皮书。

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

当前的 balance 是当前的 reserve 加上注入的流动性的代币数量。

// gas savings, must be defined here since totalSupply can update in _mintFee
uint _totalSupply = totalSupply;
if (_totalSupply == 0) {
    liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
    // permanently lock the first MINIMUM_LIQUIDITY tokens
    _mint(address(0), MINIMUM_LIQUIDITY);
} else {
    liquidity = Math.min(
        amount0.mul(_totalSupply) / _reserve0,
        amount1.mul(_totalSupply) / _reserve1
    );
}
require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
_mint(to, liquidity);

流动性 liquidity 的计算方式在第一次提供流动性时和其他时候稍稍不同。mint 方法中判断,如果是首次提供该交易对的流动性,则根据根号 xy 生成流动性代币,并销毁其中的 MINIMUM_LIQUIDITY(即 1000wei);否则根据转入的代币价值与当前流动性价值比例铸造流动性代币。

第一次提供流动性的计算公式如下:

liquidity = sqrt(x0*y0) - min

其中 min 是 10^3。也就是说,第一次提供流动性是有最小流动性要求的。

其他提供流动性的计算公式如下:按照注入的流动性和当前的 reserve 的占比一致。

liquidity = min((x0/reserve0*totalsupply), (y0/reserve1*totalsupply))

2.burn

burn 函数用在抽取流动性。burn 逻辑和 mint 逻辑类似。

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

    // gas savings, must be defined here since totalSupply can update in _mintFee
    uint _totalSupply = totalSupply;

    // using balances ensures pro-rata distribution
    amount0 = liquidity.mul(balance0) / _totalSupply;

    // using balances ensures pro-rata distribution
    amount1 = liquidity.mul(balance1) / _totalSupply;
    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);
}

与 mint 类似,burn 方法也会先计算协议手续费。参考白皮书,为了节省交易手续费,Uniswap v2 只在 mint/burn 流动性时收取累计的协议手续费。移除流动性后,根据销毁的流动性代币占总量的比例获得对应的两种代币。

3.swap

swap 函数实现两种代币的兑换。

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;
    { // scope for _token{0,1}, avoids stack too deep errors
    address _token0 = token0;
    address _token1 = token1;
    require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');

    // optimistically transfer tokens
    if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out);
    // optimistically transfer tokens
    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');
    { // 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(balance0, balance1, _reserve0, _reserve1);
    emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
}

一个交易池的 swap 操作支持两个方向的兑换,可以从 TokenA 换到 TokenB,或者 TokenB 换到 TokenA。

if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out);
if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out);

因为在swapExactTokensForTokensgetAmountOut函数已经确定兑换处的金额。所以,先直接转账。

在不做 swap 之前,balance 应该和 reserve 相等的。通过balancereserve的差值,可以反推出输入的代币数量:

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

为了兼容闪电贷功能,以及不依赖特定代币的 transfer 方法,整个 swap 方法并没有类似 amountIn 的参数,而是通过比较当前余额与缓存余额的差值来得出转入的代币数量。

由于在 swap 方法最后会检查余额(扣掉手续费后)符合 k 常值函数约束(参考白皮书公式),因此合约可以先将用户希望获得的代币转出,如果用户之前并没有向合约转入用于交易的代币,则相当于借币(即闪电贷);如果使用闪电贷,则需要在自定义的 uniswapV2Call 方法中将借出的代币归还。

在 swap 方法最后会使用缓存余额更新价格预言机所需的累计价格,最后更新缓存余额为当前余额。

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

注意,其中区块时间戳和累计价格都是溢出安全的。(具体推导过程请参考白皮书)





以上就是本篇文章的全部内容了,希望对你有帮助。

>> 本站不提供评论服务,技术交流请在 Twitter 上找我 @anbang_account