UniswapV2价格机制及验证

UniswapV2价格机制及验证

本文所有内容均不构成任何投资建议,仅供技术交流参考。

Uniswap是加密货币去中心化交易所(Automated Market Maker),根据 coinmarketcap.com 2022年8月31日的统计,UniswapV3(Ethereum主网)的日交易量超过13亿美元,UniswapV2(Ethereum主网)的日交易量达到7709万美元,是全球最大的去中性化加密市场交易所。深入理解Uniswap的价格机制对理解整个DEFI运行模式都很有帮助,本文将对UniswapV2的价格机制作出解释,同时给出代码测试价格的推导是否和UniswapV2自带的价格预测函数一致。

UniswapV1

在聊UniswapV2之前,先简单了解一下UniswapV1。UniswapV1上线之前,EtherDelta是唯一的去中心化交易所,它很自然地采用了传统交易所的报价式(order book)交易,但是由于流动性不足,买家和卖家的报价往往无法匹配,交易很难完成撮合,从而进一步降低了市场的流动性。

UniswapV1于2018年11月2日在主网上线,采用两两token配对的方式创建资产池,根据资产池中token的数量以及常量乘积函数动态调整每次swap之后资产池中token的数量。这极大提升了资产的流动性,只要用户拥有资产池中的tokenA,无需交易对手,就可以使用资产池兑换tokenB,而且兑换的数量完全由资产池中两种token的数量决定,完美解决了流动性不足问题。

如果资产池中tokenA数量占比偏多(高出正常兑换比例时的占比),一定有聪明的用户会用tokenB兑换出tokenA,以获得比正常兑换时更多的tokenA。反之亦然。从而实现了tokenA和tokenB数量及兑换比例(即价格)上的动态平衡。

常量乘积函数

post image

x:tokenA的数量;

y:tokenB的数量;

K:流动性平衡下的常量值。

然而UniswapV1只支持ETH-ERC20形式的配对,兑换效率不高,每个ERC20只能通过ETH才能实现和其他ERC20的兑换。

UniswapV2

UniswapV2于2020年5月上线,继续沿用了常量乘积函数来实现自动兑换的功能,同时在UniswapV1的基础上实现了ERC20-ERC20的兑换功能,另外还增加了Flashswap

前文简单回顾了UniswapV1的历史,继续回到常量乘积函数,先来解决两个实际会遇到的数学问题:

1)在常量乘积函数下,如果用户存入Δx 个tokenA,可以兑换出多少个tokenB?

2)在常量乘积函数下,如果用户希望兑出Δx 个tokenA,则需要存入多少个tokenB?

如果以tokenA作为基准,对于用户来说,问题1相当于tokenA的卖出价,而问题2相当于tokenA的买入价

先从常识的角度出发下一个结论:和外汇交易一样,在相同的市场环境下,卖出价≤买入价,否则就是虚幻的永动机了。

假设用户存入Δx个tokenA,可以兑换出 Δy个tokenB。兑换前后tokenA和tokenB的数量均符合常量乘积函数

post image

UniswapV2会收取交易费率 ρ(0.3%)。交易费属于固定费率,每一笔交易都需要收取。交易费用属于前端收费,即收取Δx × ρ个tokenA作为费用。

问题1)已知当前pool的x和y,存入Δx,拟兑出 Δy,常量乘积函数如下:

post image

则Δy为:

post image
post image

问题2)已知当前pool的x和y,希望兑出Δx,拟存入Δy,常量乘积函数如下:

post image

则Δy为:

post image
post image

代码测试

本文使用UniswapV2前10个创建的资产池,以每个资产池中token0作为基础资产,计算token0的买入价和卖出价。

本地存有前10个资产池的{token0Addr, token1Addr, pairAddr, token0Decimal, token1Decimal}的csv文件,读取csv文件,通过链上部署的合约,一次性获取10个资产池最新的{reserve0, reserve1}。以10^token0Decimal作为token0的拟存入或兑换资产,通过上面的公式计算token0的买入价和卖出价。

function getPairPrice(params: PairPriceTuple[]) {
  const index = params.length;
  const result: Array<PriceType> = [];

  for (let i = 0; i < index; i++) {
    const xDelta: BigNumber = BigNumber.from(10).pow(params[i][3].valueOf());
    const x = params[i][5];
    const y = params[i][6];

    const sellNumerator = y.mul(xDelta).mul(997);
    const sellDenominator = x.mul(1000).add(xDelta.mul(997));
    const sellPrice = sellNumerator.div(sellDenominator);

    let buyPrice = BigNumber.from(0);
    // reserve must be enough to cover swap out
    if (xDelta.lt(params[i][5])) {
      const buyNumerator = y.mul(xDelta);
      const buyDenominator = x.sub(xDelta).mul(997).div(1000);
      // to follow UV2Library to add 1
      buyPrice = buyNumerator.div(buyDenominator).add(1);
    }
    result[i] = {
      paddr: params[i][2],
      buyprice: buyPrice,
      sellprice: sellPrice,
    };
  }
  return result;
}

UniswapV2在链上部署了UniswapV2Router02合约,可以用来估计token的价格,主要调用的是getAmountsOut()getAmountsIn()两个函数。

function getAmountsOut(uint256 amountIn, address[] calldata path)
        external
        view
        returns (uint256[] memory amounts);
function getAmountsIn(uint256 amountOut, address[] calldata path)
        external
        view
        returns (uint256[] memory amounts);

由于涉及链上合约调用,所以使用hardhat作为测试工具,但hardhat不是必须的,如果已经部署了链上合约,可以直接使用web3.js/ethers.jstypescript进行交互。测试时直接fork主网的真实数据。

由于计算精度,链上获取数据和本地测算数据可能在个位数有一点差别,所以如果两个数相等或者差别在百万分之一以内即认为测算数据正确。

**既然链上合约可以获取价格,为什么还要本地计算价格?**如果你的工作(如MEV)需要频繁获取最新的UniswapV2池token价格,那么本地计算token价格就是有意义的,因为这能节省大量时间和gas费用。

测试代码链接

https://github.com/twpony/AmmPriceVerification

至此,本文总结推导了UniswapV2的价格形成机制,同时给出了测试代码来评判测算逻辑是否正确。如果你对文章有任何建议、异议、转载,欢迎联系交流。

参考链接:

https://uniswap.org/whitepaper.pdf

https://betterprogramming.pub/uniswap-v2-in-depth-98075c826254