Uniswap V2 一探
Contents
TL;DR
V2 相较于 V1 的主要特点:
- 支持创建由 ERC20 token 组成的交易对合约。
- 价格预言机使用时间加权平均价格,价格不易被操纵。
- 支持闪电兑。
- 可以开启或关闭 0.05% 的协议手续费,占总手续费 0.3% 的 $$\frac{1}{6}$$,收取的形式是铸造相应数量的 UNI token 转到受益方的地址。
- 用户可以提交合法签名来授权流动性池份额(UNI token)的转账或是获得转账的授权。
- 合约架构拆分成 core 和 periphery 两个合约:core 合约负责管理资产;periphery 合约支撑用户的使用场景,无状态,方便升级。
V2 白皮书
支持 ERC20 token 之间组成交易对
V1 不允许 ERC20 token 之间直接兑换,必须以 ETH 为中介。
- 优点:实现上兑换操作的路由关系简单清晰;减少了碎片化的流动性。
- 缺点:LPs 必须持有 ETH、承担 ETH 的敞口(更多的无常损失);兑换者需要支付两次手续费,承受两次滑点。
- When two assets ABC and XYZ are correlated —— for example, if they are both USD stablecoins —— liquidity providers on a Uniswap pair ABC/XYZ would generally be subject to less impermanent loss than the ABC/ETH or XYZ/ETH pairs.
V2 core 只支持创建由 ERC20 token 组成交易对合约。
ETH 作为 Ethereum 的原生资产,它的交易接口和 ERC20 不同。为了精简代码库,V2 不再支持 ETH,ETH 要包装成 WETH(wrapped ETH,实现了 ERC20)才能用来交易。
- V2 periphery 合约提供了 ETH 和 WETH 互相转换的功能。
价格预言机
在时刻 t,对于资产 a 和资产 b 组成的交易对,Uniswap 计算边际价格(marginal price)$$p_t$$ 的公式(不考虑手续费)是资产 a 的储量除以资产 b 的储量($$p_t = \frac {r^a_t} {r^b_t}$$)。
- $$p_t$$ 也就是 $$\frac{∆a}{∆b}$$;推导见 Uniswap V1 Core 一探 。
由于套利者的存在,V1 的一个流动性池中两种资产的数量可以相对准确地反映这两种资产的市场价格,但在较短时间跨度内交易池中的资产价格可以很容易被攻击者操控,因此不适合作为链上价格预言机。
- Suppose some other contract uses the current ETH-DAI price to settle a derivative. An attacker who wishes to manipulate the measured price can buy ETH from the ETH-DAI pair, trigger settlement on the derivative contract (causing it to settle based on the inflated price), and then sell ETH back to the pair to trade it back to the true price. This might even be done as an atomic transaction, or by a miner who controls the ordering of transactions within a block.
- A real-world example: samczsun: Taking undercollateralized loans for fun and for profit ↗
为描述方便,将 Uniswap 交易定义为调用了合约核心逻辑的交易。
- 涉及的方法有
mint
,burn
,swap
,sync
.
V2 对每个区块中第一笔 Uniswap 交易出现时的资产价格进行累加处理,计算公式是合约生命周期内每一秒的现货价格的加和 $$a_t = \sum\limits^t_{t=1} p_i$$。
- 这一算法把两次累加时机之间的价格都按最近一次执行累加时的价格来进行累加。
- 选取“第一个”价格的好处是这一价格不容易操纵。
- If the attacker submits a transaction that attempts to manipulate the price at the end of a block, some other arbitrageur may be able to submit another transaction to trade back immediately afterward in the same block.
- A miner (or an attacker who uses enough gas to fill an entire block) could manipulate the price at the end of a block, but unless they mine the next block as well, they may not have a particular advantage in arbitraging the trade back.
由此可以推导出 $$t_1$$ 到 $$t_2$$ 时间范围内的时间加权平均价格(time-weighted average price, TWAP),用作价格预言机的价格:
$$p_{t_1,t_2} = \frac{\sum_{i=t_1}^{t_2} p_i}{t_2 - t_1} = \frac{\sum_{i=1}^{t_2} p_i - \sum_{i=1}^{t_1} p_i}{t_2 - t_1} = \frac {a_{t_2} - a_{t_1}}{t_2 - t_1}$$。
使用 TWAP 作为价格预言机的价格引入两个问题:
- 使用 TWAP 后,资产 A 相对于资产 B 的价格与资产 B 相对于 资产 A 的价格之间不再存在倒数关系,用户可能选择资产 A 或资产 B 作为账户资产的单位,因此合约需要同时记录两个价格。
- If the USD/ETH price is 100 in block 1 and 300 in block 2, the average USD/ETH price will be 200 USD/ETH, but the average ETH/USD price will be 1/150 ETH/USD.
- 在新区块的第一笔 Uniswap 交易之前,用户直接向 a 和 b 组成的交易对合约转 ERC20 token(假设转了 n 个 a token),对预言机的价格进行操纵。
- 直接转 ERC20 token 的交易不会调用到合约逻辑,不属于 Uniswap 交易。
- 假设在这样一笔转账交易的 X 秒后,区块中出现了第一笔 Uniswap 交易,此时的边际价格 $$p_{cooked}$$ 实际上被操纵成 $$\frac {r^a + n} {r^b}$$,在价格 $$p_{cooked}$$ 上实际上并没有发生 Uniswap 交易,但在计算累计价格时却会被计算在内;多算的部分的大小是 $$n * X$$。
- 解决方法是在每次 Uniswap 交易后都缓存两种资产的储量,每次用最新的缓存的储量值来计算最新的边际价格,这样就能剔除非 Uniswap 交易的干扰。
V2 采用 UQ112.112 格式编码价格(主要是为了处理除法),小数点左右两边最多支持 112 位二进制数的精度,没有符号位。
// range: [0, 2**112 - 1]
// resolution: 1 / 2**112
library UQ112x112 {
uint224 constant Q112 = 2**112;
// encode a uint112 as a UQ112x112
function encode(uint112 y) internal pure returns (uint224 z) {
// y 最大值是 2^112-1,乘以 Q112 后最大值时 2^224- x^112,z 最大值是 2^224-1,不会溢出
z = uint224(y) * Q112; // never overflows
}
// divide a UQ112x112 by a uint112, returning a UQ112x112
function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) {
z = x / uint224(y);
}
}
uint public price0CumulativeLast;
uint public price1CumulativeLast;
// 区块中的第一笔 Uniswap 交易触发以下更新
// [ ] additional three `SSTORE` operations, why three?
price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
- 1.0 用 UQ112.112 编码的结果是 $$1 * 2^{112}$$,1.5 则是 $$1 * 2^{112} + 0.5 * 2^{112} = 1 * 2^{112} + 1 * 2^{111}$$。
- 从十进制的角度去理解,假设编码成 UQ3.3,则 1.0 就是 $$1 * 1000$$,1.5 就是 $$1 * 1000 + 0.5 * 1000$$,也就是乘以 $$10^3 $$ 将整个浮点数放大,移除小数点。
- Numbers of UQ112.112 format can be stored in a
uint224
, this leaves 32 bits of a 256 bit storage slot free. - Although the price at any given moment (stored as a UQ112.112 number) is guaranteed to fit in 224 bits, the accumulation of this price over an interval is not. The extra 32 bits on the end of the storage slots for the accumulated price of A/B and B/A are used to store overflow bits resulting from repeated summations of prices.
- 某一时刻的价格的范围是 $$[0, 2**112 - 1]$$,以 UQ112.112 格式编码后可以存放在
uint224
中,但累计价格可以会超出uint224
能容纳的最大值,因此累计价格用uint
存,多出 32 位的余量。 - The contract itself does not store historical values for this accumulator——the caller has to call the contract at the beginning of the period to read and store this value. Users of the oracle can choose when to start and end this period. Choosing a longer period makes it more expensive for an attacker to manipulate the TWAP, although it results in a less up-to-date price.
- This design means that the price oracle only adds an additional three
SSTORE
operations (a current cost of about 15,000 gas) to the first (Uniswap) trade in each block.
- 某一时刻的价格的范围是 $$[0, 2**112 - 1]$$,以 UQ112.112 格式编码后可以存放在
- The reserves, each stored in a
uint112
, also leave 32 bits free in a (packed) 256 bit storage slot.- The reserves are stored alongside the timestamp of the most recent block with at least one trade, modded with $$2^{32}$$ so that it fits into 32 bits.
闪电兑
V1 中,用户用 XYZ 兑 ABC 时,必须先把 XYZ 转到合约地址后才能收到 ABC。假设其它平台上 ABC/XYZ 交易对的 XYZ 被低估,用户自然会用 ABC 去购买 XYZ 实施套利,可以分为几种情况讨论:
- 用户持有 ABC,直接发起套利交易,购买把其它平台的 XYZ 并在 Uniswap 中进行兑换;
- 用户持有其它币,但它们都锁定在其它合约中,需要用 ABC 才能解锁,此时形成循环依赖,在这样的情况下 V1 的用户无法实施套利;
- 用户不持有任何币(愿意支付 gas)但想空手套白狼,V1 不支持这种“好事”。
V2 新增了闪电兑(Flash Swap)的特性,在一个原子交易内,用户可以在支付之前就收到并使用它要购买的资产,只要保证在交易结束前将其归还。
- The
swap
function makes a call to an optional user-specified callback contract in between transferring out the tokens requested by the user and enforcing the invariant. Once the callback is complete, the contract checks the new balances and confirms that the invariant is satisfied (after adjusting for fees on the amounts paid in). If the contract does not have sufficient funds, it reverts the entire transaction.- callback 如何构造?
- A user can also repay the Uniswap pool using the same token, rather than completing the swap. This is effectively the same as letting anyone flash-borrow any of assets stored in a Uniswap pool (for the same 0.30% fee as Uniswap charges for trading).
- A user wants to pay the pair back using the same asset, rather than swapping.
V2 支持闪电兑可以鼓励套利者搬平价差,让平台更健康。
协议手续费
V2 可以开启或关闭 0.05% 的协议手续费,占总手续费 0.3% 的 $$\frac{1}{6}$$,收取的形式是铸造相应数量的 UNI token 转到受益方的地址。
- 如果每次一发生交易就收取手续费,每笔交易就需要额外的 gas 费,所以只在执行到提供流动性或移除流动性的逻辑之前结算累计的手续费。
- The contract computes the accumulated fees, and mints new liquidity tokens to the fee beneficiary, immediately before any tokens are minted or burned.
假设协议手续费收取开关在 $$t_1$$ 之前已开启,两次收取手续费的时点是 $$t_1$$ 和 $$t_2$$:
- 根据
mint 计算公式
,$$t_1$$ 时刻 UNI 的总发行量是 $$\sqrt{x_1 \cdot y_1} = \sqrt{k_1}$$,$$t_2$$ 时刻则是 $$\sqrt{x_2 \cdot y_2} = \sqrt{k_2}$$,两者的差值就是这一时间范围内的总手续费 $$\sqrt{k_2} - \sqrt{k_1}$$,它占 $$t_2$$ 时刻的总发行量的占比是 $$\frac{\sqrt{k_2} - \sqrt{k_1}}{\sqrt{k_2}} = 1 - \frac{\sqrt{k_1}}{\sqrt{k_2}} = f_{1,2}$$,这其中属于受益方的部分还要乘以 $$\frac{1}{6}$$。
- 这一差值的 $$\frac{1}{6}$$ 就是 $$t_2$$ 时刻要铸造出来转给受益方的 UNI 的数量,记为 $$s_m$$。
- x 和 y 是储量,k 是恒定乘积。
- 假设 $$t_1$$ 时刻 UNI 总发行量是 $$s_1$$,令 $$ϕ = \frac{1}{6}$$,$$s_m$$ 要满足 $$\frac{s_m}{s_m + s_1} = ϕ \cdot f_{1,2}$$,推导得到 $$s_m = \frac{\sqrt{k_2} - \sqrt{k_1}}{(\frac{1}{ϕ} - 1) \cdot \sqrt{k_2} + \sqrt{k_1}} \cdot s_1 = \frac{\sqrt{k_2} - \sqrt{k_1}}{5 \cdot \sqrt{k_2} + \sqrt{k_1}} \cdot s_1$$。
- ⇒ 代码实现
流动性凭证 UNI 的初始化供应量
V1 UNI 铸造量的公式:
- 流动性池已经存在时:$$s_{minted} = \frac{x_{deposited}}{x_{starting}} \cdot s_{starting}$$
- $$x_{starting}$$ 是本次存入之前 UNI 的总供应量。
- $$x_{starting}$$ 为 0 时,UNI 的铸造量(此时也是 UNI 的初始供应量)是交易对合约中 ETH 数量(wei)。
- 假设交易对合约中一开始的 ETH 是 0,且存入的两种资产比率能反映它们的市场价,此时 1 个流动性池的(初始)份额大约值 2 ETH。
对于 V1 中 $$x_{starting}$$ 为 0 的情况,是以“存入的两种资产比率能反映它们的市场价”这一假设为前提的,但这一假设实际上无法得到保证,而且 V2 不再支持 ETH,无法再沿用此公式。
V2 用存入代币的储量的几何平均数计算 UNI 铸造量:$$\sqrt{x_{deposited} \cdot y_{deposited}}$$
- The above formula ensures that a liquidity pool share will never be worth less than the geometric mean of the reserves in that pool.
针对流动性池份额的元交易
以太坊的原生货币是 ETH,发送交易需要支付的 gas 费也是 ETH,用户若只有 ERC20 token 但没有 ETH,则不能向以太坊发起任何交易。原交易就是一种解决办法。
使用元交易的一种场景是用户 a 不持有 ETH 但持有 ERC20 token ABC,用户 b 持有 ETH,用户 a 想转一定数量的 ABC 给用户 b:
- 用户 a 链下构造授权用户 b 转走一定数量 ABC 的签名,在链下将签名发给用户 b;
- 用户 b 通过调用元交易方法,支付 gas 费,就可以得到用户 a 授权给他的 ABC。
V2 中用户可以提交合法签名来授权流动性池份额(UNI token)的转账或者获得授权,用户调用 permit
方法提交 UNI token 所有者的合法签名和被授权者的地址,该地址就可以获得该笔 UNI token 的转账权限。
合约架构
V2 由 core 和 periphery 两个合约组成:
- core 合约仅包含和用户资产相关的核心逻辑,保持极简,降低引入 bug 的可能性;
- periphery 负责支撑用户的使用场景,底层是对 core 合约的调用。
- 用户在前端操作时的交互合约就是 periphery。
- The periphery contracts are stateless and don’t hold any assets. They can be updated as needed.
V2 core 代码分析
核心内部方法
_update
:
// 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 the time elapsed is not zero, it means we are the first exchange transaction on this block.
// In that case, we need to update the cost accumulators.
// 分母非零判断;首次调用 _update 时两个缓存的储量是 0
if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {
// * never overflows, and + overflow is desired
// uint 最大值时 UQ112x112.uqdiv 返回 uint224,最大值是 2^224 - 1, timeElapsed 最大值是 2^32-1,两者相乘不会超出 uint 的最大值
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);
}
// 两种资产的缓存储量
uint112 private reserve0; // uses single storage slot, accessible via getReserves
uint112 private reserve1; // uses single storage slot, accessible via getReserves
uint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves
// 不是反比关系,两个累计价格都得存,计算 TWAP 时再除以时间间隔
uint public price0CumulativeLast;
uint public price1CumulativeLast;
// 返回缓存值
function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {
_reserve0 = reserve0;
_reserve1 = reserve1;
_blockTimestampLast = blockTimestampLast;
}
- 以 ERC20 token 常见的 18 位小数位来计算,每种 token 池中最多能持有的 token 数量是 $$\frac{2^{112}-1}{10^{18}} \approx 5.19*10^{15}$$ 个。
- Prior to Solidity 0.8.0, arithmetic operations would always wrap in case of under- or overflow, leading to widespread use of libraries that introduce additional checks. Since Solidity 0.8.0, all arithmetic operations revert on over- and underflow by default, thus making the use of these libraries unnecessary.
_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 // 避免反复去 storage 中读取
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)); // totalSupply 对应公式中的 s1
uint denominator = rootK.mul(5).add(rootKLast);
uint liquidity = numerator / denominator;
if (liquidity > 0) _mint(feeTo, liquidity);
}
}
} else if (_kLast != 0) {
// If there is no fee set kLast to zero (if it isn't that already).
// When this contract was written there was a gas refund feature that encouraged contracts to reduce the overall size of the Ethereum state by zeroing out storage they did not need. This code gets that refund when possible.
kLast = 0;
}
}
uint public kLast; // reserve0 * reserve1, as of immediately after the most recent liquidity event
function _mint(address to, uint value) internal {
totalSupply = totalSupply.add(value);
balanceOf[to] = balanceOf[to].add(value);
emit Transfer(address(0), to, value);
}
kLast
在每一次mint
或burn
的逻辑中被更新。- We know that between the time
kLast
was calculated and the present no liquidity was added or removed (because we run this calculation every time liquidity is added or removed, before it actually changes), so any change inreserve0 * reserve1
has to come from transaction fees (without them we’d keepreserve0 * reserve1
constant).- Transaction fee 是用户承担的总手续费,即 0.3%。
_safeTransfer
:
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');
}
bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));
- There are two ways in which an ERC-20 transfer call can report failure:
- Revert. If a call to an external contract reverts, then the boolean return value is
false
. - End normally but report a failure. In that case the return value buffer has a non-zero length, and when decoded as a boolean value it is
false
.
- Revert. If a call to an external contract reverts, then the boolean return value is
lock
modifier:
modifier lock() {
require(unlocked == 1, 'UniswapV2: LOCKED');
unlocked = 0;
_;
unlocked = 1;
}
提供流动性
// this low-level function should be called from a contract which performs important safety checks
function mint(address to) external lock returns (uint liquidity) {
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings // 最近一次 Uniswap 交易后缓存的值
uint balance0 = IERC20(token0).balanceOf(address(this));
uint balance1 = IERC20(token1).balanceOf(address(this));
// 此次 mint 带来的增量
uint amount0 = balance0.sub(_reserve0);
uint amount1 = balance1.sub(_reserve1);
// mint 之前先结算协议手续费
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);
// 将 10^3 个 token 转入零地址,无法取出,这样一来 totalSupply 永远不会为 0,可以避免分母为 0 的问题
_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); // 铸币,转到 to 作为提供流动性的凭证
// 价格预言机:更新缓存储量,累计价格
_update(balance0, balance1, _reserve0, _reserve1);
// 更新 kLast,用于未来计算协议手续费
if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
emit Mint(msg.sender, amount0, amount1);
}
using SafeMath for uint;
uint public constant MINIMUM_LIQUIDITY = 10**3;
library SafeMath {
function sub(uint x, uint y) internal pure returns (uint z) {
require((z = x - y) <= x, 'ds-math-sub-underflow');
}
}
-
In the time of the first deposit we don’t know the relative value of the two tokens, so we just multiply the amounts and take a square root, assuming that the deposit provides us with equal value in both tokens. We can trust this because it is in the depositor’s interest to provide equal value, to avoid losing value to arbitrage.
-
With every subsequent deposit we already know the exchange rate between the two assets, and we expect liquidity providers to provide equal value in both. If they don’t, we give them liquidity tokens based on the lesser value they provided as a punishment.
-
对首次铸币攻击的防范:
if (_totalSupply == 0) { liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY); _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens }
- 若没有
.sub(MINIMUM_LIQUIDITY)
部分,则可以这样发动首次铸币攻击:- 第一次提供流动性时,攻击者存入少量 token,比如 1 wei ABC 和 1 wei XYZ,铸造得到 1 wei UNI,此时
totalSupply
、reserve0
和reserve1
均为 1 wei; - 攻击者在另一笔交易中直接向交易对合约转入大额 ABC 和 XYZ,比如各 2000 ether;
- 攻击者调用
sync
方法将缓存池的 token 数量更新成和当下余额相同,攻击结束;- 此时
totalSupply
为 1 wei,reserve0
和reserve1
均为 1 wei 加上 2000 ether。 - 这步之后攻击者的流动性份额仍是 1 wei,损人不利己。
- 此时
- 后续提供流动性的计算公式是
liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
,假设其它用户向提供 1 wei 的流动性,则需要提供的 ABC 和 XYZ 的数量各约为 2000 ether,小资金用户根本无法参与提供流动性。
- 第一次提供流动性时,攻击者存入少量 token,比如 1 wei ABC 和 1 wei XYZ,铸造得到 1 wei UNI,此时
.sub(MINIMUM_LIQUIDITY)
让首次铸币新增的流动性必须大于 $$10^3$$,否则SafeMath.sub
会回滚交易;同时为了避免攻击者通过burn
将流动性销毁,又将 $$10^3$$ 份流动性发到address(0)
永久锁定。- 结果是,攻击者再次执行第 1 步攻击时,需要初始存入的 ABC 和 XYZ 数量的乘积需要大于 $$10^6$$,假设是 $$1001^2$$,铸造得到 1001 个 UNI,攻击者只能得到其中 1 个;其它用户提供 1 wei 流动性需要的 ABC 和 XYZ 的数量各约为 2 ether。
- 若没有
移除流动性
// this low-level function should be called from a contract which performs important safety checks
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)];
// burn 之前先结算协议手续费
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);
// 更新 kLast,用于未来计算协议手续费
if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
emit Burn(msg.sender, amount0, amount1, to);
}
function _burn(address from, uint value) internal {
balanceOf[from] = balanceOf[from].sub(value);
totalSupply = totalSupply.sub(value);
emit Transfer(from, address(0), value);
}
- The periphery contract transferred the liquidity to be burned to this contract before the call. That way we know how much liquidity to burn, and we can make sure that it gets burned.
ERC20 token 互换
// 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');
// This transfer is optimistic, because we transfer before we are sure all the conditions are met.
// This is OK in Ethereum because if the conditions aren't met later in the call we revert out of it and any changes it created.
if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
// 如果是闪电贷,则需要在自定义的 uniswapV2Call 方法中将借出的代币归还
// [ ] uniswapV2Call
if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
balance0 = IERC20(_token0).balanceOf(address(this));
balance1 = IERC20(_token1).balanceOf(address(this));
}
// [ ] 闪电贷,用一种 token 还款
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));
// 检查 k 值约束,同时也确保了如果是闪电贷交易,借出的 token 得到归还
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);
}
interface IUniswapV2Callee {
function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external;
}
- The periphery contract sends the core contract the tokens before calling the core contract for the swap. This makes it easy for the contract to check that it is not being cheated, a check that has to happen in the core contract (because we can be called by other entities than our periphery contract).
sync
, skim
To protect against bespoke token implementations that can update the pair contract’s balance, and to more gracefully handle tokens whose total supply can be greater than $$2^{112}$$, Uniswap v2 has two bail-out functions: sync()
and skim()
.
- An account can transfer tokens to the exchange without calling either
mint
orswap
.
sync
:
// force reserves to match balances
function sync() external lock {
_update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);
}
- This functions as a recovery mechanism in the case that a token asynchronously deflates the balance of a pair. In this case, trades will receive sub-optimal rates, and if no liquidity provider is willing to rectify the situation, the pair is stuck.
sync
exists to set the reserves of the contract to the current balances.
skim
:
// force balances to match reserves
function skim(address to) external lock {
address _token0 = token0; // gas savings
address _token1 = token1; // gas savings
_safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0));
_safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));
}
- This functions as a recovery mechanism in case enough tokens are sent to an pair to overflow the two
uint112
storage slots for reserves, which could otherwise cause trades to fail. - Any account is allowed to call
skim
because we don’t know who deposited the tokens.
创建交易对合约
V2 同样采用用工厂合约为每个 ERC20 token 部署一个独立的交易所合约(exchange contract),不同的是它用了 CREATE2
opcode,可以生成确定性的地址(不依赖 nonce),V1 用的是 CREATE
opcode。
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'); // token1 >= token0
require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient(插入 map 时的时候的双向同时插入且具备原子性)
bytes memory bytecode = type(UniswapV2Pair).creationCode;
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
/*
create2(v, p, n, s)
create new contract with code at memory p to p + n
and send v wei
and return the new address
where new address = first 20 bytes of keccak256(0xff + address(this) + s + keccak256(mem[p…(p+n)))
s = big-endian 256-bit value
*/
assembly {
// pair = new UniswapV2Pair{salt: salt}(); // 已支持 new 方法
pair := create2(
0,
// actual code starts after skipping the first 32 bytes
// 前 32 位存储的是实际代码以外的信息,如合约代码的大小
add(bytecode, 32),
// load the size of code contained in the first 32 bytes
// mload 读 mem[bytecode, 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);
}
contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 {
}
- We want the address of the new exchange to be deterministic, so it can be calculated in advance off chain (this can be useful for layer 2 transactions).
流动性池份额的元交易
// 在 ERC20 接口基础上扩充了 permit 接口(EIP-712)
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');
// If everything is ok, treat this as an ERC-20 approve.
_approve(owner, spender, value);
}
constructor() public {
uint chainId;
assembly {
// 当前版本的 Yul 要用 chainid()
chainId := chainid
}
DOMAIN_SEPARATOR = keccak256(
abi.encode(
keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),
keccak256(bytes(name)),
keccak256(bytes('1')),
chainId,
address(this)
)
);
}
function _approve(address owner, address spender, uint value) private {
allowance[owner][spender] = value;
emit Approval(owner, spender, value);
}
// keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;
mapping(address => uint) public nonces; // 防止用旧签名进行重放攻击
V2 periphery 代码分析
初始化
import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol';
import './interfaces/IWETH.sol';
address public immutable override factory;
address public immutable override WETH;
constructor(address _factory, address _WETH) public {
factory = _factory;
WETH = _WETH;
}
receive() external payable {
assert(msg.sender == WETH); // only accept ETH via fallback from the WETH contract
}
interface IWETH {
function deposit() external payable;
function transfer(address to, uint value) external returns (bool);
function withdraw(uint) external;
}
提供流动性
// **** ADD LIQUIDITY ****
function _addLiquidity(
address tokenA,
address tokenB,
uint amountADesired,
uint amountBDesired,
uint amountAMin,
uint amountBMin
) internal virtual returns (uint amountA, uint amountB) {
// create the pair if it doesn't exist yet
if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
IUniswapV2Factory(factory).createPair(tokenA, tokenB);
}
// 调用 core 合约查询储量
(uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);
if (reserveA == 0 && reserveB == 0) {
(amountA, amountB) = (amountADesired, amountBDesired);
} else {
// 根据入参充分利用每一个资产,尽可能提供最多的流动性
uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);
if (amountBOptimal <= amountBDesired) {
require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
(amountA, amountB) = (amountADesired, amountBOptimal);
} else {
uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);
assert(amountAOptimal <= amountADesired);
require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
(amountA, amountB) = (amountAOptimal, amountBDesired);
}
}
}
function addLiquidity(
address tokenA,
address tokenB,
uint amountADesired,
uint amountBDesired,
uint amountAMin,
uint amountBMin,
address to,
uint deadline
) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) {
(amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);
liquidity = IUniswapV2Pair(pair).mint(to);
}
function addLiquidityETH(
address token,
uint amountTokenDesired,
uint amountTokenMin,
uint amountETHMin,
address to,
uint deadline
) external virtual override payable ensure(deadline) returns (uint amountToken, uint amountETH, uint liquidity) {
(amountToken, amountETH) = _addLiquidity(
token,
WETH,
amountTokenDesired,
msg.value,
amountTokenMin,
amountETHMin
);
address pair = UniswapV2Library.pairFor(factory, token, WETH); // CREATE2
TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken);
// 存入 ETH 到 WETH 合约,WETH 转到 core 合约
IWETH(WETH).deposit{value: amountETH}();
assert(IWETH(WETH).transfer(pair, amountETH));
liquidity = IUniswapV2Pair(pair).mint(to);
// refund dust eth, if any
if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);
}
// 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
))));
}
// https://github.com/Uniswap/solidity-lib/blob/master/contracts/libraries/TransferHelper.sol
function safeTransferFrom(
address token,
address from,
address to,
uint256 value
) internal {
// bytes4(keccak256(bytes('transferFrom(address,address,uint256)')));
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value));
require(
success && (data.length == 0 || abi.decode(data, (bool))),
'TransferHelper::transferFrom: transferFrom failed'
);
}
function safeTransferETH(address to, uint256 value) internal {
(bool success, ) = to.call{value: value}(new bytes(0));
require(success, 'TransferHelper::safeTransferETH: ETH transfer failed');
}
modifier ensure(uint deadline) {
require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED');
_;
}
移除流动性
// **** REMOVE LIQUIDITY ****
function removeLiquidity(
address tokenA,
address tokenB,
uint liquidity,
uint amountAMin,
uint amountBMin,
address to,
uint deadline
) public virtual override ensure(deadline) returns (uint amountA, uint amountB) {
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair
(uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);
(address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);
(amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);
require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
}
// token 和 WETH 组成的交易对,WETH 地址不用传
function removeLiquidityETH(
address token,
uint liquidity,
uint amountTokenMin,
uint amountETHMin,
address to,
uint deadline
) public virtual override ensure(deadline) returns (uint amountToken, uint amountETH) {
(amountToken, amountETH) = removeLiquidity(
token,
WETH,
liquidity,
amountTokenMin,
amountETHMin,
address(this),
deadline
);
TransferHelper.safeTransfer(token, to, amountToken);
IWETH(WETH).withdraw(amountETH);
TransferHelper.safeTransferETH(to, amountETH);
}
function removeLiquidityWithPermit(
address tokenA,
address tokenB,
uint liquidity,
uint amountAMin,
uint amountBMin,
address to,
uint deadline,
bool approveMax, uint8 v, bytes32 r, bytes32 s
) external virtual override returns (uint amountA, uint amountB) {
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
uint value = approveMax ? uint(-1) : liquidity;
IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
(amountA, amountB) = removeLiquidity(tokenA, tokenB, liquidity, amountAMin, amountBMin, to, deadline);
}
function removeLiquidityETHWithPermit(
address token,
uint liquidity,
uint amountTokenMin,
uint amountETHMin,
address to,
uint deadline,
bool approveMax, uint8 v, bytes32 r, bytes32 s
) external virtual override returns (uint amountToken, uint amountETH) {
address pair = UniswapV2Library.pairFor(factory, token, WETH);
uint value = approveMax ? uint(-1) : liquidity;
IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
(amountToken, amountETH) = removeLiquidityETH(token, liquidity, amountTokenMin, amountETHMin, to, deadline);
}
// **** REMOVE LIQUIDITY (supporting fee-on-transfer tokens) ****
function removeLiquidityETHSupportingFeeOnTransferTokens(
address token,
uint liquidity,
uint amountTokenMin,
uint amountETHMin,
address to,
uint deadline
) public virtual override ensure(deadline) returns (uint amountETH) {
(, amountETH) = removeLiquidity(
token,
WETH,
liquidity,
amountTokenMin,
amountETHMin,
address(this),
deadline
);
TransferHelper.safeTransfer(token, to, IERC20(token).balanceOf(address(this)));
IWETH(WETH).withdraw(amountETH);
TransferHelper.safeTransferETH(to, amountETH);
}
function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens(
address token,
uint liquidity,
uint amountTokenMin,
uint amountETHMin,
address to,
uint deadline,
bool approveMax, uint8 v, bytes32 r, bytes32 s
) external virtual override returns (uint amountETH) {
address pair = UniswapV2Library.pairFor(factory, token, WETH);
uint value = approveMax ? uint(-1) : liquidity;
IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
amountETH = removeLiquidityETHSupportingFeeOnTransferTokens(
token, liquidity, amountTokenMin, amountETHMin, to, deadline
);
}
- 对于 Fee-on-transfer 的 token,转账过程要扣除一定数量的 token 作为手续费,转账时设置的 token 数量不准,实际收到的数量才准。
ERC20 token 互换
// **** SWAP ****
// requires the initial amount to have already been sent to the first pair
function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual {
for (uint i; i < path.length - 1; i++) {
(address input, address output) = (path[i], path[i + 1]);
(address token0,) = UniswapV2Library.sortTokens(input, output);
uint amountOut = amounts[i + 1];
(uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));
// Is this the last exchange? If so, send the tokens received for the trade to the destination.
// If not, send it to the next pair exchange.
address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(
amount0Out, amount1Out, to, new bytes(0)
);
}
}
function swapExactTokensForTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external virtual override ensure(deadline) returns (uint[] memory amounts) {
amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
);
_swap(amounts, path, to);
}
// performs chained getAmountOut calculations on any number of pairs
function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) {
require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');
amounts = new uint[](path.length);
amounts[0] = amountIn;
for (uint i; i < path.length - 1; i++) {
(uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]);
amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);
}
}
function swapTokensForExactTokens(
uint amountOut,
uint amountInMax,
address[] calldata path,
address to,
uint deadline
) external virtual override ensure(deadline) returns (uint[] memory amounts) {
amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
);
_swap(amounts, path, to);
}
// performs chained getAmountIn calculations on any number of pairs
function getAmountsIn(address factory, uint amountOut, address[] memory path) internal view returns (uint[] memory amounts) {
require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');
amounts = new uint[](path.length);
amounts[amounts.length - 1] = amountOut;
for (uint i = path.length - 1; i > 0; i--) {
(uint reserveIn, uint reserveOut) = getReserves(factory, path[i - 1], path[i]);
amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut);
}
}
- 如果为每一对 token 都创建交易对合约,合约数目会过于巨大。实现上采用了兑换路径的概念,例如 A 兑换 D 可以通过 A 兑换 B、B 兑换 C、C 兑换 D 来完成。
swapTokensForTokens
, allows a trader to specify an exact number of input tokens he is willing to give and the minimum number of output tokens he is willing to receive in return.swapTokensForExactTokens
lets a trader specify the number of output tokens he wants, and the maximum number of input tokens he is willing to pay for them.
examples 合约
说明:
WETH
是 Wrapped ETH ERC20 token;WETHPartner
是另一种 ERC20 token;WETHExchangeV1
是 V1 版 WETHPartner/ETH 交易对合约;WETHPair
是 V2 版的 WETHPartner/WETH 交易对合约;
ExampleFlashSwap
contract ExampleFlashSwap is IUniswapV2Callee {
IUniswapV1Factory immutable factoryV1;
address immutable factory;
IWETH immutable WETH;
constructor(address _factory, address _factoryV1, address router) public {
factoryV1 = IUniswapV1Factory(_factoryV1);
factory = _factory;
WETH = IWETH(IUniswapV2Router01(router).WETH());
}
// gets tokens/WETH via a V2 flash swap, swaps for the ETH/tokens on V1, repays V2, and keeps the rest!
function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external override {
address[] memory path = new address[](2);
uint amountToken;
uint amountETH;
{ // scope for token{0,1}, avoids stack too deep errors
address token0 = IUniswapV2Pair(msg.sender).token0();
address token1 = IUniswapV2Pair(msg.sender).token1();
assert(msg.sender == UniswapV2Library.pairFor(factory, token0, token1)); // ensure that msg.sender is actually a V2 pair
assert(amount0 == 0 || amount1 == 0); // this strategy is unidirectional
path[0] = amount0 == 0 ? token0 : token1;
path[1] = amount0 == 0 ? token1 : token0;
amountToken = token0 == address(WETH) ? amount1 : amount0;
amountETH = token0 == address(WETH) ? amount0 : amount1;
}
assert(path[0] == address(WETH) || path[1] == address(WETH)); // this strategy only works with a V2 WETH pair
IERC20 token = IERC20(path[0] == address(WETH) ? path[1] : path[0]);
IUniswapV1Exchange exchangeV1 = IUniswapV1Exchange(factoryV1.getExchange(address(token))); // get V1 exchange
if (amountToken > 0) { // 套利目标是 ETH
(uint minETH) = abi.decode(data, (uint)); // slippage parameter for V1, passed in by caller
token.approve(address(exchangeV1), amountToken);
uint amountReceived = exchangeV1.tokenToEthSwapInput(amountToken, minETH, uint(-1));
uint amountRequired = UniswapV2Library.getAmountsIn(factory, amountToken, path)[0];
assert(amountReceived > amountRequired); // fail if we didn't get enough ETH back to repay our flash loan
WETH.deposit{value: amountRequired}();
assert(WETH.transfer(msg.sender, amountRequired)); // return WETH to V2 pair
(bool success,) = sender.call{value: amountReceived - amountRequired}(new bytes(0)); // keep the rest! (ETH)
assert(success);
} else { // 套利目标是 token
(uint minTokens) = abi.decode(data, (uint)); // slippage parameter for V1, passed in by caller
WETH.withdraw(amountETH);
uint amountReceived = exchangeV1.ethToTokenSwapInput{value: amountETH}(minTokens, uint(-1));
uint amountRequired = UniswapV2Library.getAmountsIn(factory, amountETH, path)[0];
assert(amountReceived > amountRequired); // fail if we didn't get enough tokens back to repay our flash loan
assert(token.transfer(msg.sender, amountRequired)); // return tokens to V2 pair
assert(token.transfer(sender, amountReceived - amountRequired)); // keep the rest! (tokens)
}
}
}
flashSwapExample = await deployContract(
wallet,
ExampleFlashSwap,
[fixture.factoryV2.address, fixture.factoryV1.address, fixture.router.address],
overrides
)
Flash swap:
-
Add liquidity to V1 at a rate of 1 ETH / 200 X;
- X 是 WETHPartner;ETH 被包装成 WETH;
const WETHPartnerAmountV1 = expandTo18Decimals(2000) const ETHAmountV1 = expandTo18Decimals(10) await WETHPartner.approve(WETHExchangeV1.address, WETHPartnerAmountV1) await WETHExchangeV1.addLiquidity(bigNumberify(1), WETHPartnerAmountV1, MaxUint256, { ...overrides, value: ETHAmountV1 })
-
Add liquidity to V2 at a rate of 1 ETH / 100 X;
const WETHPartnerAmountV2 = expandTo18Decimals(1000) const ETHAmountV2 = expandTo18Decimals(10) // 给 msg.sender 预存了 10000 token // const WETHPartner = await deployContract(wallet, ERC20, [expandTo18Decimals(10000)]) await WETHPartner.transfer(WETHPair.address, WETHPartnerAmountV2) // waffle wallet 中默认有 eth // const ethBalanceBefore = await provider.getBalance(wallet.address) await WETH.deposit({ value: ETHAmountV2 }) await WETH.transfer(WETHPair.address, ETHAmountV2) await WETHPair.mint(wallet.address, overrides)
- 相较于 V2,V1 中的 ETH 溢价,闪电兑的方式是从 V2 中借出 ETH 到 V1 中兑换 X,再用兑换得到的 X 归还 V2,归还后还剩下的 X 就是套利收益。
-
Execute arbitrage via
uniswapV2Call
:const balanceBefore = await WETHPartner.balanceOf(wallet.address) // receive 1 ETH from V2, get as much X from V1 as we can, repay V2 with minimum X, keep the rest! const arbitrageAmount = expandTo18Decimals(1) const amount0 = WETHPairToken0 === WETHPartner.address ? bigNumberify(0) : arbitrageAmount const amount1 = WETHPairToken0 === WETHPartner.address ? arbitrageAmount : bigNumberify(0) await WETHPair.swap( amount0, amount1, flashSwapExample.address, defaultAbiCoder.encode(['uint'], [bigNumberify(1)]), overrides ) // if (4thArgument.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0, amount1, 4thArgument);
References