Uniswap V2-兑换(swap)


一旦一个 token pair 具有了流动性,任何用户都可以进行兑换(swap)。
兑换过程,是按照 恒定乘积公式 进行的,即 x * y = k。其中,x 表示其中一种 token 的存量;y 表示另外一种 token 的存量。k 值是一个常量,任何一次交易之后,x 与 y 的乘积,需要始终保持常量 k。

UniswapV2-创建 UniswapV2Pair
UniswapV2-添加流动性
UniswapV2-移除流动性
UniswapV2-兑换(swap)
UniswapV2-公式分析

一、概述

实际上,每次用户在兑换时,都需要提供手续费给 LP,这种机制,对 LP 是一种激励。
我们假定用户输入了 dx 数量的 token0,并希望兑取 token1。如果 Uniswap 中存在 token0 和 token1 的 pair,则其兑换过程会满足:

其中,dy 就是兑换后可以收到的 token1 的数量。
如果,Uniswap 中并没有直接的 token0 和 token1 对,但是可以通过路由接力的方式进行兑换,则这个过程中,每经过一次路由,都需要收取 0.3% 的手续费,这 0.3% 实际上从输入量(本金)上扣除的,类似于“砍头息”。

那么,0.3% 的手续费​去哪里了呢?直接进入了 pair 中 token0 的池子里,-造市商移除流动性 的时候,会根据 “占股” 瓜分 token0,平台也可能会参与到瓜分过程中,如果​协议费开关打开的话。

如下图所示,如果从 From 无法达到 to,是无法完成兑换的。

假设,我们希望兑换指定数量的 ETH

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// https://docs.uniswap.org/protocol/V2/reference/smart-contracts/router-02#swapexacttokensfortokens
// https://github.com/Uniswap/v2-periphery/blob/master/contracts/UniswapV2Router02.sol#L224
/**
* @amountIn 输入的 token 总量
* @amountOutMin 要接收到的最小 token 总量
* @path 从输入的 token 地址到输出的 token 地址之间的路径。因此,数组的第一个元素,就输入,最后一个元素是输出,中间,如果有的话,就是路由路径。path 的长度,必须 ≥ 2。
* @to 接收地址
* @deadline
* */
function swapExactTokensForTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external virtual override ensure(deadline) returns (uint[] memory amounts) {
// 获取 token 交易路由路径下的所有额度。
// 注意,这里,每次路由,都会消耗手续费。先扣掉手续费再往下算。
amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
// 先将全额 token (amounts[0] = amountIn)转入第一个 pair
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
);
_swap(amounts, path, to);
}

二、计算每次路由的输入和输出(扣除手续费)

这个过程,实际上会就计算好最初的用户输入额度途径每个 pair 后的输入和输出。
在下述代码块中的循环中,我们可以看到,每次路过一个 pair,都会扣除 0.3% 的手续费。所以,如果能够直接兑换,所付手续费是最小的。

1
2
3
4
5
6
7
8
9
10
11
12
// https://github.com/Uniswap/v2-periphery/blob/master/contracts/libraries/UniswapV2Library.sol#L62
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);
// 第一个值,是 token 的输入量。中间,是路由时需要的转换量。最后一个值,是最终的输出量。
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);
}
}

将输入量 amountIn 先扣除 0.3% 的手续费,再通过恒定乘积公式得到 amountOut.

1
2
3
4
5
6
7
8
9
10
11
12
13
// given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset
// https://github.com/Uniswap/v2-periphery/blob/master/contracts/libraries/UniswapV2Library.sol#L43
// 给定了输入量,得到输出量。
// 注意,这里在计算 amountOut 的时候,amountIn 将按照 99.7% 计算,
// dx * 99.7%
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;
}

三、转账

在第二步骤中,已经计算好了途经每个 pair 的输入和输出数量,这里,将在每个途经的 pair 中,完成转账动作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// **** SWAP ****
// requires the initial amount to have already been sent to the first pair
// https://github.com/Uniswap/v2-periphery/blob/master/contracts/UniswapV2Router02.sol#L212
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]);
// sortTokens 将地址从小到大排序,并要求地址中,不得有 零地址,否则将抛出异常,终止执行。
// 为什么这里要排序呢?因为在一个 pair 中,token0 总是对应这小地址,token1 总是对应着大地址。
// 需要做参数对其
(address token0,) = UniswapV2Library.sortTokens(input, output);
// 第 i+1(i是从零开始的) 次路由时输出的金额。当 amounts[0] = amountIn.
uint amountOut = amounts[i + 1];
// 确定一个 pair 中,哪一个 token 是需要被输出的,如果 input 是小地址,则其对应着 pair 中的 token0,此时需要输出的就是 token1;反之亦然。
// amount0Out 最终由 pair 中的 token0 输出;amount1Out 最终由 pair 中的 token1 输出。
(uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));
// 输出到 to 地址。其实 UniswapV2Library.pairFor(factory, output, path[path.length - 1]) = _to
address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
// 在 pair 中,开始输出金额到 to。从第一个 pair 开始。
IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(
amount0Out, amount1Out, to, new bytes(0)
);
}
}

真正的 swap,是在 UniswapV2Pair 中完成的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* @amount0Out 将收到的 token0 的额度
* @amount1Out 将收到的 token1 的额度
* @to 将收到的 token 发送到 to
* @data
* */
// this low-level function should be called from a contract which performs important safety checks
// https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2Pair.sol#L159
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
// 获取 pair 中,当前两种币种的金额。
(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');
// 从 _token0 向 to 转入 amount0Out
if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
// 从 _token1 向 to 转入 amount0Out
if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
// _token0 在 pair 中所剩下的余额
balance0 = IERC20(_token0).balanceOf(address(this));
// _token1 在 pair 中所剩下的余额
balance1 = IERC20(_token1).balanceOf(address(this));
}
// token0 的输入量
uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
// token1 的输入量
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 值需要满足的条件
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);
}

Transfer 方法如下:

1
2
3
4
5
6
bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));

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

四、更新余额

​转账完成后,更新 pair 中 token 的存量。更新完成后,​则兑换过程顺利结束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// update reserves and, on the first call per block, price accumulators
// https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2Pair.sol#L73
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;
}
// 更新当前 token0 所存储的金额
reserve0 = uint112(balance0);
// 更新当前 token1 所存储的金额
reserve1 = uint112(balance1);
blockTimestampLast = blockTimestamp;
emit Sync(reserve0, reserve1);
}

​五、小结

无论是添加流动性,还是兑换过程,都是在智能合约内完成的,谓之“去中心化”,用数学构建的 “去中心化”。

兑换导致价值改变,即只要兑换持续,价值波动也将一直存在,起起伏伏,谓之“无常”,市场价值的 “无常”。

参考链接
Router02

© 2024 YueGS