Uniswap (Decentralized Exchange)

Table of Contents

1. Uniswap 简介

Uniswap 是 Ethereum 平台的去中心化交易所。提供了 ETH 和 ERC20 代币之间,以及 ERC20 代币和 ERC20 代币之间的兑换服务。

Uniswap V1 于 2018 年 11 月发布,其智能合约采用 Vyper 编写,合约源码在:https://github.com/Uniswap/uniswap-v1 。Uniswap V2 于 2020 年 5 月发布,其智能合约采用 Solidity 编写,合约源码在:https://github.com/Uniswap/uniswap-v2-corehttps://github.com/Uniswap/uniswap-v2-periphery 。V1 和 V2 采用的交易模型都是 x-y-k,后文会介绍。

本文以 Uniswap V2 为例进行介绍。

2. 基本使用步骤

在中心化的交易所中,用户可以创建“限价订单”,用户可以设置 Token 的兑换汇率,满足这个条件后系统才会完成订单。但 Uniswap 中没有这些概念。

在 Uniswap 中每个交易对(比如 Token A 和 Token B),都对应一个池。用户要实现 Token A 和 Token B 之间的兑换,首先这个池中要同时存在 Token A 和 Token B。

第一步:“流动性提供者”往池中增加流动性(Add Liquidity)。
如果某个交易对已经存在,则我们可以往相应的池“等比率地”(比如池中 Token A 和 Token B 目前的量分别为 100 和 10,那么增加流动性时也只能按这个比率来增加)充入更多的 Token A 和 Token B,以增加流动性。如果交易对还不存在,则第一个增加流动性的人可以确定 Token A 和 Token B 之间的兑换比率。提供流动性时,智能合约会返回一个新 Token(记为 Liquidity Provider Token,简称 LP Token)给“流动性提供者”。凭 LP Token,可以从池中“等比率地”取出 Token A 和 Token B,这称为“删除流动性”。

第二步:“普通用户”进行交易(Swap)。
用户可以在池中进行 Token 的兑换。兑换的基本规则是 \(x \times y = k\) ,它的意思是 池中的 Token A 和 Token B 的数量乘积始终维持一个常数。 举例来说,假设池中有 100 个 Token A 和 10 个 Token B,如果用户想从池中获得 2 个 Token B,那么他需要付出多少个 Token A 放入池中呢?假设需要付出 \(n\) 个 Token A,维持兑换前后 Token A 和 Token B 的数量乘积不变,则有 \((100 + n) \times (10 - 2) = 100 \times 10\) ,从而 \(n=25\) ,这个交易结束后,池中的 Token A 为 125 个,而 Token B 为 8 个。如果用户还想从池中获得 2 个 Token B,那么这一次他需要付出多少个 Token A 放入池中呢?假设需要付出 \(m\) 个 Token A,维持兑换前后 Token A 和 Token B 的数量乘积不变,则有 \((125 + m) \times (8 - 2) = 125 \times 8\) ,从而 \(m=41.6667\) ,这个交易结束后,池中的 Token A 为 166.6667 个,而 Token B 为 6 个。注:这里讨论的是没有交易费的情况。

注:当引入手续费后,随着交易的频繁进行,池中会积攒更多的 Token,这样流动性提供者在“删除流动性”时,很可能获得比“提供流动性时所充入的 Token”还要多的 Token,额外多出的部分就是流动性提供者的利润。

2.1. LP Token 的发行

流动性提供者往池中增加流动性时,会发行一定数量的 LP Token 给流动性提供者。每个交易对都对应一个不同的 LP Token。

如果池已经存在,则发行的 LP Token 数量由下面公式决定:
\[S_{minted} = \frac{x_{deposited}}{x_{starting}} \cdot s_{starting}\]
上式中, \(x_{deposited}\) 表示这次增加流动性时存入的 Token A 数量, \(x_{starting}\) 第一次增加流动性时存入的 Token A 数量, \(s_{starting}\) 表示第一次发行的 LP Token 数量。

如果池不存在(也就是说第一次发行的 LP Token 数量),则下面公式决定:
\[S_{minted} = \sqrt{x_{deposited} \cdot y_{deposited}}\]

比如,第一次添加交易对时,存入了 1 个 Token A 和 100 个 Token B,则会给你 \(\sqrt{1 \cdot 100} = 10\) 个 LP Token。如果第一次添加交易对时,存入了 2 个 Token A 和 200 个 Token B,则会给你 \(\sqrt{2 \cdot 200} = 20\) 个 LP Token。

3. x-y-k 模型(常量乘积做市商模型)

Uniswap 使用的是 x-y-k 做市商模型,实现 Token 之间的自动交易。节 2 中已经介绍过了没有手续费时 x-y-k 做市商模型的基本情况。

3.1. 无手续费情况

假设池中 Token A 和 Token B 的数量目前分别为 \(x\) 和 \(y\) 。

情况一:用户想从池中得到 \(\Delta y\) 个 Token B,那么用户需要付出的 Token A 数量记为 \(\Delta x\) ,则 \(\Delta x\) 可以这样计算(即池中的 Token A 和 Token B 的数量乘积始终维持一个常数):
\[(x + \Delta x) (y - \Delta y) = xy\]
从而:

\begin{align*} \Delta x &= x - \frac{xy}{y - \Delta y} \\ &= \frac{\Delta y}{y - \Delta y} x \end{align*}

如果定义 \(\beta = \frac{\Delta y}{y}\) ,则上式可写为:
\[\Delta x = \frac{\beta}{1 - \beta} x\]

情况二:用户往池中放入 \(\Delta x\) 个 Token A,那么用户可以得到的 Token B 的数量记为 \(\Delta y\) ,则 \(\Delta y\) 可以这样计算:
\[(x + \Delta x) (y - \Delta y) = xy\]
从而:

\begin{align*} \Delta y &= y - \frac{xy}{x + \Delta x} \\ &= \frac{\Delta x}{x + \Delta x} y \end{align*}

如果定义 \(\alpha = \frac{\Delta x}{x}\) ,则上式可写为:
\[\Delta y = \frac{\alpha}{1 + \alpha} y\]

3.2. 有手续费情况

假设用户交易时,要收 \(\rho\) 的手续费。比如 \(\rho = 0.003\) 时,表示收 \(0.3 \%\) 的手续费。

情况一:用户想从池中得到 \(\Delta y\) 个 Token B,收手续费情况下,用户需要付出的 Token A 数量记为 \(\Delta x\) 。如果不收手续费的话,上一节算出了 \(\Delta x_{\text{no fee}} = \frac{\beta}{1-\beta}x\) ,现在收手续费,显然用户要付出得更多了,设手续费为 \(\rho\) ,那么有:
\[(1 - \rho) \Delta x = \Delta x_{\text{no fee}}\]
从而:
\[\Delta x = \frac{1}{1 - \rho} \frac{\beta}{1 - \beta} x\]

上式就是由 \(x,y,\Delta y,\rho\) 求 \(\Delta x\) 的公式,它就是节 3.2.2 中函数 getAmountIn 所使用的公式。

情况二:用户往池中放入 \(\Delta x\) 个 Token A,收手续费情况下,用户可以得到的 Token B 的数量记为 \(\Delta y\) 。由前面得到的公式有:
\[\Delta x = \frac{1}{1 - \rho} \frac{\frac{\Delta y}{y}}{1 - \frac{\Delta y}{y}} x\]
从而推出:

\begin{align*} \Delta y &= \frac{\Delta x (1 - \rho) y}{x + \Delta x (1 - \rho)} \\ &= \frac{\alpha(1 - \rho)}{1 + \alpha(1 - \rho)} y \end{align*}

上式就是由 \(x,y,\Delta x,\rho\) 求 \(\Delta y\) 的公式,它就是节 3.2.2 中函数 getAmountOut 所使用的公式。

3.2.1. k 会一直增加

池中两个 Token 的现有数量分别为 \(x,y\) ,一次交易完成后,池中两个 Token 的数量记为 \(x'_{\rho}\) 和 \(y'_{\rho}\) ,以前面的“情况一”的公式为例,计算一下 \(x'_{\rho}\) 和 \(y'_{\rho}\) :

\begin{align*} x'_{\rho} &= x + \Delta x \\ &= (1 + \frac{1}{1 - \rho} \frac{\beta}{1-\beta} )x \\ &= \frac{1 + \beta(\frac{1}{1 - \rho} - 1)}{1-\beta} x \\ y'_{\rho} &= y - \Delta y \\ &= (1 - \beta) y \end{align*}

从而:
\[x'_{\rho} \cdot y'_{\rho} = \left(1 + \beta(\frac{1}{1 - \rho} - 1) \right) x \cdot y\]

上式表示,引入交易手续费后,每次交易完成时, \(k\) (即 \(xy\) )不再是常量,而是以 \(1 + \beta(\frac{1}{1 - \rho} - 1)\) 的倍数增加。

3.2.2. 智能合约代码实现

情况一:池中 Token A 和 Token B 目前分别有 reserveIn(即 \(x\) )和 reserveOut(即 \(y\) )个,手续费 \(\rho = 0.003\) ,用户想从池中得到 amountOut(即 \(\Delta y\) )个 Token B,那么需要付出的 Token A 的数量由智能合约函数 getAmountIn 得到:

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

计算细节如下:

\begin{align*} \Delta x &= \frac{1}{1 - \rho} \frac{\beta}{1 - \beta} x \\ &= \frac{1}{0.997} \frac{\Delta y}{y - \Delta y} x \\ &= \frac{1000 \Delta y x}{997 (y - \Delta y)} \\ &= {\Big \lfloor } \frac{1000 \Delta y x}{997 (y - \Delta y)} {\Big \rfloor } + 1 \end{align*}

说明,Solidity 不支持浮点运算,除法只会得到整数部分,余数部分会丢掉。为了避免出现 0,在结果上人为加了 1。

情况二:池中 Token A 和 Token B 目前分别有 reserveIn(即 \(x\) )和 reserveOut(即 \(y\) )个,手续费 \(\rho = 0.003\) ,用户往池中放入 amountIn(即 \(\Delta x\) )个 Token A,那么可以获得的 Token B 的数量由智能合约函数 getAmountOut 得到:

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

计算细节如下:

\begin{align*} \Delta y &= \frac{\Delta x (1 - \rho) y}{x + \Delta x (1 - \rho)} \\ &= \frac{\Delta x 0.997 y}{x + \Delta x 0.997} \\ &= \frac{997 \Delta x y}{1000 x + 997 \Delta x} \end{align*}

这个地方,并没有向前面那样考虑整数除法时余数部分被丢掉的问题,计算出来的 \(\Delta y\) 可能为 0。

智能合约函数 getAmountIn 和 getAmountOut 的源码摘自:https://etherscan.io/address/0x7a250d5630b4cf539739df2c5dacb4c659f2488d#code

4. Uniswap V2 合约接口

调用接口 addLiquidity 可以添加流动性,调用接口 removeLiquidity 可以删除流动性:

addLiquidity(address tokenA, address tokenB, uint amountADesired, uint amountBDesired, uint amountAMin, uint amountBMin, address to, uint deadline ) external returns (uint amountA, uint amountB, uint liquidity)
removeLiquidity(address tokenA, address tokenB, uint liquidity, uint amountAMin, uint amountBMin, address to, uint deadline) external returns (uint amountA, uint amountB)

1addLiquidity 各个参数的说明。

Table 1: addLiquidity 的各个参数
参数 含义
tokenA Token A 代币合约地址
tokenB Token B 代币合约地址
amountADesired 想存入多少 tokenA
amountBDesired 想存入多少 tokenB
amountAMin 用于限制对价格的影响。不关心,可设置为 0
amountBMin 用于限制对价格的影响。不关心,可设置为 0
to 增加流动性后,会得到 LP 代币,这是接收 LP 代币的地址
deadline 这是一个未来的 Unix timestamp,超过这个时间没有打包就会放弃交易

使用 addLiquidity 第 1 次对某个交易对增加流动性时,会自动部署 LP 代币合约。存入的 Token A 和 Token B 都会记做是 LP 合约地址名下。

下面几个接口可以实现 ETH 和 ERC20 代币之间,以及 ERC20 代币和 ERC20 代币之间的兑换:

swapExactTokensForTokens(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) returns (uint[] memory amounts)
swapTokensForExactTokens(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline) returns (uint[] memory amounts)
swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline) returns (uint[] memory amounts)
swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline) returns (uint[] memory amounts)
swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) returns (uint[] memory amounts)
swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline) returns (uint[] memory amounts)

下面介绍一下接口 swapExactTokensForTokens,它表示使用固定数量(由 amountIn 指定)的 Token A(由 path[0] 指定)去兑换 Token B(由 path[length-1] 指定),把得到的 Token B 存入到由参数 to 指定的地址,其各个参数如表 2 所示。

Table 2: swapExactTokensForTokens 的各个参数
参数 含义
amountIn The amount of input tokens to send.
amountOutMin The minimum amount of output tokens that must be received for the transaction not to revert.
path An array of token addresses. path.length must be >= 2.
to Recipient of the output tokens.
deadline Unix timestamp after which the transaction will revert.

比如,我们要用 100 个 USDC 去兑换 USDT,把收到的 USDT 放入地址 0xXX,那么参数可以分别设置为表 3 所示。

Table 3: swapExactTokensForTokens 参数实例
参数 实例 说明
amountIn 100 * 10^6 USDC 的精度是 6
amountOutMin 95 * 10^6 USDT 的精度是 6,设置如果收到的 USDT 少于 95 个,则放弃交易。这是最小阈值,不是成交值,所以可以设置为 0
path 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48, 0xdAC17F958D2ee523a2206206994597C13D831ec7 分别为 USDC,USDT 的地址
to 0xXX 接收 USDT 的地址,请修改为合法的以太坊地址
deadline now.add(1800) 半小时交易没打包就放弃

参考:https://uniswap.org/docs/v2/smart-contracts/router02

5. 部署 Uniswap V2 合约

下面介绍如何从零开始部署一套 Uniswap 合约。

用户直接使用的是 UniswapV2Router02 合约,该合约会调用 UniswapV2Factory 合约,我们只需要部署这两个合约。可以从 etherscan 上获取这两个合约的源码并使用 Remix IDE 部署(etherscan 中的源码已经把分散的 Uniswap 合约整合到了一起,这样部署更方便)。

需要注意的是: UniswapV2Router02 合约中写死了“init code hash”,需要修改它为 “UniswapV2Pair 的 bytecode 的 keccak256 哈希” (注:在 Remix IDE 中编译 UniswapV2Factory 后,可以复制出合约 UniswapV2Pair 的 bytecode,然后找个工具计算它的 keccak256 哈希后,再替换一下即可)。

    // calculates the CREATE2 address for a pair without making any external calls
    function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) {
        (address token0, address token1) = sortTokens(tokenA, tokenB);
        pair = address(uint(keccak256(abi.encodePacked(
                hex'ff',
                factory,
                keccak256(abi.encodePacked(token0, token1)),
                hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' // init code hash, change this!
            ))));
    }

要进行这个修改的具体原因如下:在增加交易对时,UniswapV2Factory 会使用 CREATE2 创建 UniswapV2Pair 合约的实例;但 UniswapV2Router02 合约并没有从 UniswapV2Factory 合约中去查询新创建的合约地址,而是根据 CREATE2 生成合约地址的方式自己重新算出地址(这种方式比跨合约查询要更省 gas)。CREATE2 指令有一个参数:init code hash(这个参数变化时,CREATE2 创建的合约地址也会发生变化),这个例子中参数 init code hash 是“UniswapV2Pair 的 bytecode 的 keccak256 哈希”(注:CREATE2 的参数 init code hash 是“合约 bytecode 再加上 ABI encode 编码后的构造函数参数”的 keccak256 值,但由于 UniswapV2Pair 的构造函数没有参数,所以这里不用考虑构造函数参数,直接用 bytecode 算 keccak256 即可)。由于“UniswapV2Pair 的 bytecode”这个值会随编译器的不同设置而发生改变,所以要把 init code hash 相应地修改为真实部署时的“UniswapV2Pair 的 bytecode 的 keccak256 哈希”。

详细部署过程如下:
第 1 步,编译 UniswapV2Factory 合约(包含了 UniswapV2Pair 合约),在 Remix 中复制出合约 UniswapV2Pair 的 bytecode,找个工具计算它的 keccak256 哈希,后面会用到。

第 2 步,部署 UniswapV2Factory 合约,它的构造函数为:

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

没特别要求,把 _feeToSetter 设置为部署者自己的地址即可。部署成功后记下刚部署的 UniswapV2Factory 合约的地址,后面部署 UniswapV2Router02 时需要用到。

第 3 步,修改 UniswapV2Router02 合约中的 init code hash 为第 1 步计算出来的值。

第 4 步,编译 UniswapV2Router02 合约,并部署,它的构造函数为:

    constructor(address _factory, address _WETH) public {
        factory = _factory;
        WETH = _WETH;
    }

它需要两个参数,一个是刚部署的 UniswapV2Factory 合约地址,另一个是 WETH 的地址(可以找现有的 WETH 或者自行部署一个 WETH 也行)。

6. 无常损失(Impermanent Loss)

当用户向某交易对的资金池添加流动性后,当价格上涨或者下跌时,用户删除流动性后所得的资产与单纯持有两种资产相比会出现一定的损失,这个损失叫做无常损失(Impermanent Loss)。

下面通过例子来对此进行说明。假设交易对为 SUSHI 和 USDT,用户添加流动性时,1 SUSHI 兑换 50 USDT;用户往池中添加了 10 SUSHI 和 500 USDT,该用户占总流动性池的 10%;这时,用户的总资产价值为 1000 USDT。

假设 SUSHI 价格发生了变化,涨为了 200 USDT,根据恒定常数自动做市商的机制,总流动性池中资产的量变为 50 SUSHI 与 10000 USDT,若此时用户删除流动性,由于该用户提供的流动性占总流动性池的 10%,则该用户可取出 5 SUSHI 与 1000 USDT,此时用户的资产价值为 2000 USDT(这里不考虑用户的手续费收入)。

但如果用户不提供流动性,而是单纯持有 10 SUSHI 和 500 USDT,则用户资产的价值为 10 * 200 + 500 = 2500 USDT,中间的损失为 500 USDT,提供流动性与单纯持有之间相较产生的损失称为无常损失 。

显然,仅当删除流动性时的两币种兑换比率和添加流动性时的兑换比率保持不变时,才没有无常损失。

参考:https://uniswap.org/docs/v2/advanced-topics/understanding-returns/

7. Price Oracle

怎么得到一个不易被操控的 Token 价格呢?Uniswap V2 中给出了 Time-Weighted Average Prices (TWAPs) 方案。

不过,并没有一个现成的 API 可以直接使用,需要开发者根据 UniswapV2Pair 合约中的下面两个状态变量:

price0CumulativeLast
price1CumulativeLast

自己去构造 Price Oracle,构造方法可参考:https://uniswap.org/docs/v2/smart-contract-integration/building-an-oracle/

下面是构造 Price Oracle 时的几种常用策略:

  1. Fixed windows, data freshness is not important and recent prices are weighted equally with historical prices. 这种策略的例子可参考: https://github.com/Uniswap/uniswap-v2-periphery/blob/master/contracts/examples/ExampleOracleSimple.sol
  2. Simple moving averages, give equal weight to each price measurement. 这种策略的例子可参考:https://github.com/Uniswap/uniswap-v2-periphery/blob/master/contracts/examples/ExampleSlidingWindowOracle.sol
  3. Exponential moving averages, give more weight to the most recent price measurements.

上面 3 种策略实现时都需要开发者自己定期地“维护”Price Oracle。以上面第 1 个策略为例,“维护”Price Oracle 就是开发者要周期性地调用例子合约中的 update 方法:

// From https://github.com/Uniswap/uniswap-v2-periphery/blob/master/contracts/examples/ExampleOracleSimple.sol

function update() external {
    (uint price0Cumulative, uint price1Cumulative, uint32 blockTimestamp) =
        UniswapV2OracleLibrary.currentCumulativePrices(address(pair));
    uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired

    // ensure that at least one full period has passed since the last update
    require(timeElapsed >= PERIOD, 'ExampleOracleSimple: PERIOD_NOT_ELAPSED');

    // overflow is desired, casting never truncates
    // cumulative price is in (uq112x112 price * seconds) units so we simply wrap it after division by time elapsed
    price0Average = FixedPoint.uq112x112(uint224((price0Cumulative - price0CumulativeLast) / timeElapsed));
    price1Average = FixedPoint.uq112x112(uint224((price1Cumulative - price1CumulativeLast) / timeElapsed));

    price0CumulativeLast = price0Cumulative;
    price1CumulativeLast = price1Cumulative;
    blockTimestampLast = blockTimestamp;
}

也有一种不需要进行定期维护的 Price Oracle,不过这种方案只能利用最近 256 个区块中的 price0CumulativeLast/price1CumulativeLast 数据,这种方案可参考:https://github.com/Keydonix/uniswap-oracle

8. Flash Swap

使用 Uniswap V2 的 Flash Swap 本质是调用 UniswapV2Pair 合约的 swap 方法,它的源码如下:

    // 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 {
        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');
        if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
        if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
        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 函数,这和 Flash Swap 的区别在于 data.length 是否为 0。从上面代码中可知, 如果 data.length 大于 0,则会回调 to 所指定的合约地址中的 uniswapV2Call 函数,这就是 Flash Swap。 data 参数会原封不动地传给 uniswapV2Call,在使用 Flash Swap 时,尽管有时我们不需要这个额外的参数,但调用 swap 时也必须给 data 参数传一字节的无用数据(从而实现 data.length 大于 0)。在函数 uniswapV2Call 中,用户可以实现自己的业务逻辑,且需要归还所借代币外加手续费到 UniswapV2Pair 合约中,uniswapV2Call 的实现例子可以参考:https://github.com/Uniswap/uniswap-v2-periphery/blob/master/contracts/examples/ExampleFlashSwap.sol

9. 参考

  1. Formal specification of constant product (xy=k) market maker model and implementation, 2018: https://github.com/runtimeverification/verified-smart-contracts/blob/uniswap/uniswap/x-y-k.pdf
  2. Uniswap v2 Core, 2020: https://uniswap.org/whitepaper.pdf
  3. How Uniswap works: https://uniswap.org/docs/v2/protocol-overview/how-uniswap-works/
  4. Uniswap v2 Core Concepts, Oracles: https://uniswap.org/docs/v2/core-concepts/oracles/
  5. Smart Contract Integration, Flash Swaps: https://uniswap.org/docs/v2/smart-contract-integration/using-flash-swaps/

Author: cig01

Created: <2020-09-05 Sat>

Last updated: <2020-11-04 Wed>

Creator: Emacs 27.1 (Org mode 9.4)