跳转至主要内容

Uniswap-v2 合约概览

solidity
中级
Ori Pomerantz
2021年5月1日
81 分钟阅读 minute read

介绍

Uniswap v2(opens in a new tab) 可以在任何两个 ERC-20 代币之间创建一个兑换市场。 在本文中,我们将深入探讨实现此协议的合约的源代码,了解为何要如此编写协议。

Uniswap 是做什么的?

一般来说有两类用户:流动资金提供者和交易者。

流动性提供者为资金池提供两种可以兑换的代币(称为 Token0Token1)。 作为回报,他们会收到第三种叫做流动性代币的代币,代表他们对资金池的部分所有权。

交易者将一种代币发送到资金池,并从资金池中接收流动性提供者提供的另一种代币(例如,发送 Token0 并获得 Token1)。 兑换汇率由资金池中 Token0Token1 的相对数量决定。 此外,资金池将收取汇率的一小部分作为流动性资金池的奖励。

当流动性提供者想要收回他们的代币资产时,他们可以销毁资金池代币并收回他们的代币,其中包括属于他们的奖励。

点击此处查看更完整的描述(opens in a new tab)

为什么选择 v2? 而不是 v3?

Uniswap v3(opens in a new tab) 是 v2 的升级,远比 v2 复杂得多。 比较容易的方法是先学习 v2,然后再学习 v3。

核心合约与外围合约

Uniswap v2 可以分为两个部分,一个为核心部分,另一个为外围部分。 核心合约存放着资产,因而必须确保安全,这种分法就使核心合约更加简洁且更便于审核。 而所有交易者需要的其它功能可以通过外围合约提供。

数据和控制流程

执行 Uniswap 的三个主要操作时,会出现以下数据和控制流程:

  1. 兑换不同代币
  2. 将资金添加到市场中提供流动性,并获得兑换中奖励的流动池 ERC-20 代币
  3. 消耗流动池 ERC-20 代币并收回交易所允许交易者兑换的 ERC-20 代币

兑换

这是交易者最常用的流程:

调用者

  1. 向外围帐户提供兑换额度。
  2. 调用外围合约中的一个兑换函数。外围合约有多种兑换函数,调用哪一个取决于是否涉及以太币、交易者是指定了存入的代币金额还是提取的代币金额等。 每个兑换函数都接受一个 path,即要执行的一系列兑换。

在外围合约 (UniswapV2Router02.sol) 中

  1. 确定兑换路径中,每次兑换所需交易的代币数额。
  2. 沿路径迭代。 对于路径上的每次兑换,首先发送输入代币,然后调用交易所的 swap 函数。 在大多数情况下,代币输出的目的地址是路径中下一个配对交易。 在最后一个交易所中,该地址是交易者提供的地址。

在核心合约 (UniswapV2Pair.sol) 中

  1. 验证核心合约没有被欺骗,可在兑换后保持足够的流动资金。
  2. 检查除了现有的储备金额外,还有多少额外的代币。 此数额是我们收到的要用于兑换的输入代币数量。
  3. 将输出代币发送到目的地址。
  4. 调用 _update 来更新储备金额

回到外围合约 (UniswapV2Router02.sol)

  1. 执行所需的必要清理工作(例如,消耗包装以太币代币以返回以太币给交易者)

增加流动资金

调用者

  1. 向外围帐户提交准备加入流动资金池的资金额度。
  2. 调用外围合约的其中一个 addLiquidity 函数。

在外围合约 (UniswapV2Router02.sol) 中

  1. 必要时创建一个新的配对交易
  2. 如果有现有的币对交易所,请计算要增加的代币金额。 该金额对于两种代币应该是相同的,因此新代币对现有代币的比率是相同的。
  3. 检查金额是否可接受(调用者可以指定一个最低金额,低于此金额他们就不增加流动性)
  4. 调用核心合约。

在核心合约 (UniswapV2Pair.sol) 中

  1. 生成流动池代币并将其发送给调用者
  2. 调用 _update 来更新储备金额

撤回流动资金

调用者

  1. 向外围帐户提供一个流动池代币的额度,作为兑换底层代币所需的消耗。
  2. 调用外围合约的其中一个 removeLiquidity 函数。

在外围合约 (UniswapV2Router02.sol) 中

  1. 将流动池代币发送到该配对交易

在核心合约 (UniswapV2Pair.sol) 中

  1. 向目的地址发送底层代币,金额与销毁的代币成比例。 例如,如果资金池里有 1000 个 A 代币,500 个 B 代币和 90 个流动性代币,而我们收到请求销毁 9 个流动性代币,那么,我们将销毁 10% 的流动性代币,然后将返还用户 100 个 A 代币和 50 个 B 代币。
  2. 销毁流动性代币
  3. 调用_update来更新储备金额

核心合约

这些是持有流动资金的安全合约。

UniswapV2Pair.sol

本合约(opens in a new tab)实现用于交易代币的实际资金池。 这是 Uniswap 的核心功能。

1pragma solidity =0.5.16;
2
3import './interfaces/IUniswapV2Pair.sol';
4import './UniswapV2ERC20.sol';
5import './libraries/Math.sol';
6import './libraries/UQ112x112.sol';
7import './interfaces/IERC20.sol';
8import './interfaces/IUniswapV2Factory.sol';
9import './interfaces/IUniswapV2Callee.sol';
显示全部
复制

这些都是该合约需要知道的接口,因为该合约实现了它们(IUniswapV2PairUniswapV2ERC20),或因为该合约调用了实现它们的合约。

1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 {
复制

此合约继承自 UniswapV2ERC20,为流动池代币提供 ERC-20 代币功能。

1 using SafeMath for uint;
复制

SafeMath 库(opens in a new tab)用于避免整数上溢和下溢。 这很重要,否则最终可能会出现这样的情况:本该是 -1 的值,结果却成了 2^256-1

1 using UQ112x112 for uint224;
复制

流动池合约中的许多计算都需要分数。 但是,以太坊虚拟机本身不支持分数。 Uniswap 找到的解决方案是使用 224 位数值,整数部分为 112 位,小数部分为 112 位。 因此,1.02^112 表示,1.52^112 + 2^111 表示,以此类推。

关于这个函数库的更详细内容在文档的稍后部分

变量

1 uint public constant MINIMUM_LIQUIDITY = 10**3;
复制

为了避免分母为零的情况,始终存在最低数量的流动性代币(但为帐户零所拥有)。 该数字,即 MINIMUM_LIQUIDITY,为 1000。

1 bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));
复制

这是 ERC-20 传输函数的应用程序二进制接口选择程序。 它用于在两个代币帐户中转移 ERC-20 代币。

1 address public factory;
复制

这就是由工厂合约创造的资金池地址。 每个资金池都是两种 ERC-20 代币之间的交易所,工厂是连接所有这些资金池的中心点。

1 address public token0;
2 address public token1;
复制

这两个地址是该资金池可以交易的两种 ERC-20 代币的合约地址。

1 uint112 private reserve0; // uses single storage slot, accessible via getReserves
2 uint112 private reserve1; // uses single storage slot, accessible via getReserves
复制

每个代币类型都有储备的资源库。 我们假定两者代表相同数量的值,因此每个 token0 的价值都等同于 reserve1/reserve0 token1。

1 uint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves
复制

发生兑换的最后一个区块的时间戳,用来追踪一段时间内的汇率。

以太坊合约中燃料消耗量最大的一项是存储,这种燃料消耗从一次合约调用持续到下一次调用。 每个存储单元长度为 256 位。 因此,reserve0reserve1blockTimestampLast 三个变量的分配方式让单个存储值可以包含全部这三个变量 (112+112+32=256)。

1 uint public price0CumulativeLast;
2 uint public price1CumulativeLast;
复制

这些变量存放每种代币的累计成本(每种代币在另一种代币的基础上计算)。 可以用来计算一段时间内的平均汇率。

1 uint public kLast; // reserve0 * reserve1, as of immediately after the most recent liquidity event
复制

币对交易所决定 token0 和 token1 之间汇率的方式是在交易中保持两种储备金的乘数恒定不变。 即 kLast 这个值。 当流动性提供者存入或提取代币时,该乘数就会变化,由于兑换市场的费用为 0.3%,它会略有增加。

下面是一个示例。 请注意,为了简单起见,表格中的数字仅保留了小数点后三位,我们忽略了 0.3% 交易费,因此数字并不准确。

事件reserve0reserve1reserve0 * reserve1平均汇率 (token1 / token0)
初始设置1,000.0001,000.0001,000,000
交易者 A 用 50 个 token0 兑换 47.619 个 token11,050.000952.3811,000,0000.952
交易者 B 用 10 个 token0 兑换 8.984 个 token11,060.000943.3961,000,0000.898
交易者 C 用 40 个 token0 兑换 34.305 个 token11,100.000909.0901,000,0000.858
交易者 D 用 100 个 token1 兑换 109.01 个 token0990.9901,009.0901,000,0000.917
交易者 E 用 10 个 token0 兑换 10.079 个 token11,000.990999.0101,000,0001.008

由于交易者提供了更多 token0,token1 的相对价值增加了,反之亦然,这取决于供求。

锁定

1 uint private unlocked = 1;
复制

有一类基于重入攻击(opens in a new tab)的安全漏洞。 Uniswap 需要转让不同数值的 ERC-20 代币,这意味着调用的 ERC-20 合约可能会导致调用合约的 Uniswap 市场遭受攻击。 通过在合约中使用 unlocked 变量,我们可以防止函数在运行时被调用(同一笔交易中)。

1 modifier lock() {
复制

此函数是一个修改器(opens in a new tab),它对正常函数进行包装数,以便以某种方式改变其行为。

1 require(unlocked == 1, 'UniswapV2: LOCKED');
2 unlocked = 0;
复制

如果 unlocked 变量值为 1,将其设置为 0。 如果已经是 0,则撤销调用,返回失败。

1 _;
复制

在修改器中,_; 是原始函数调用(含所有参数)。 此处,这意味着仅在 unlocked 变量值为 1 时调用函数,该函数调用才有效;而当函数运行时,unlocked 值为 0。

1 unlocked = 1;
2 }
复制

当主函数返回后,释放锁定。

其他 函数

1 function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {
2 _reserve0 = reserve0;
3 _reserve1 = reserve1;
4 _blockTimestampLast = blockTimestampLast;
5 }
复制

此函数返回给调用者当前的兑换状态。 请注意,Solidity 函数可以返回多个值(opens in a new tab)

1 function _safeTransfer(address token, address to, uint value) private {
2 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value));
复制

此内部函数可以从交易所转账一定数额的 ERC20 代币给其他帐户。 SELECTOR 指定我们调用的函数是 transfer(address,uint)(参见上面的定义)。

为了避免必须为代币函数导入接口,我们需要使用其中一个应用程序二进制接口函数(opens in a new tab)来“手动”创建调用。

1 require(success && (data.length == 0 || abi.decode(data, (bool))), 'UniswapV2: TRANSFER_FAILED');
2 }
复制

ERC-20 的转移调用有两种方式可能失败:

  1. 回滚 如果对外部合约的调用回滚,则布尔返回值为 false
  2. 正常结束但报告失败。 在这种情况下,返回值的缓冲为非零长度,将其解码为布尔值时,其值为 false

一旦出现这两种情况,转移调用就会回滚。

事件

1 event Mint(address indexed sender, uint amount0, uint amount1);
2 event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);
复制

当流动资金提供者存入流动资金 (Mint) 或提取流动资金 (Burn) 时,会发出这两个事件。 在这两种情况下,存入或提取的 token0 和 token1 金额是事件的一部分,以及调用合约的帐户身份 (Sender) 也是事件的一部分。 在提取资金时,事件中还包括获得代币的目的地址 (to),这个地址可能与发送人不同。

1 event Swap(
2 address indexed sender,
3 uint amount0In,
4 uint amount1In,
5 uint amount0Out,
6 uint amount1Out,
7 address indexed to
8 );
复制

当交易者用一种代币交换另一种代币时,会激发此事件。 同样,代币发送者和兑换后代币的存入目的帐户可能不一样。 每种代币都可以发送到交易所,或者从交易所接收。

1 event Sync(uint112 reserve0, uint112 reserve1);
复制

最后,无论出于何种原因,每次存入或提取代币时都会触发 Sync 事件,以提供最新的储备金信息(从而提供汇率)。

设置函数

这些函数应在建立新的配对交易时调用。

1 constructor() public {
2 factory = msg.sender;
3 }
复制

构造函数确保我们能够跟踪产生配对的工厂合约的地址。 initialize 函数和工厂交易费(如果有)需要此信息

1 // called once by the factory at time of deployment
2 function initialize(address _token0, address _token1) external {
3 require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check
4 token0 = _token0;
5 token1 = _token1;
6 }
复制

这个函数允许工厂(而且只允许工厂)指定配对中进行兑换的两种 ERC-20 代币。

内部更新函数

_update
1 // update reserves and, on the first call per block, price accumulators
2 function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {
复制

每次存入或提取代币时,会调用此函数。

1 require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');
复制

如果 balance0 或 balance1 (uint256) 大于 uint112(-1) (=2^112-1)(因此当转换为 uint112 时会溢出并返回 0),拒绝继续执行 _update 以防止溢出。 一般的代币可以细分成 10^18 个单元,这意味在每个交易所,每种代币的限额为 5.1*10^15 个。 迄今为止,这并不是一个问题。

1 uint32 blockTimestamp = uint32(block.timestamp % 2**32);
2 uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired
3 if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {
复制

如果流逝的时间值不是零,这意味着本交易是此区块上的第一笔兑换交易。 在这种情况下,我们需要更新累积成本值。

1 // * never overflows, and + overflow is desired
2 price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
3 price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
4 }
复制

每个累积成本值都用最新成本值(另一个代币的储备金额/本代币的储备金额)与以秒为单位的流逝时间的乘积加以更新。 要获得平均价格,需要读取两个时间点的累计价格,并除以两个时间点之间的时间差。 例如,假设下面这些事件序列:

事件reserve0reserve1时间戳边际汇率 (reserve1 / reserve0)price0CumulativeLast
初始设置1,000.0001,000.0005,0001.0000
交易者 A 存入 50 个代币 0 获得 47.619 个代币 11,050.000952.3815,0200.90720
交易者 B 存入 10 个代币 0 获得 8.984 个代币 11,060.000943.3965,0300.8920+10*0.907 = 29.07
交易者 C 存入 40 个代币 0 获得 34.305 个代币 11,100.000909.0905,1000.82629.07+70*0.890 = 91.37
交易者 D 存入 100 个代币 0 获得 109.01 个代币 1990.9901,009.0905,1101.01891.37+10*0.826 = 99.63
交易者 E 存入 10 个代币 0 获得 10.079 个代币 11,000.990999.0105,1500.99899.63+40*1.1018 = 143.702

比如说我们想要计算时间戳 5,030 到 5,150 之间代币 0 的平均价格。 price0Cumulative 的差值为 143.702-29.07=114.632。 此为两分钟(120 秒)间的平均值。 因此,平均价格为 114.632/120 = 0.955。

此价格计算是我们需要知道原有资金储备规模的原因。

1 reserve0 = uint112(balance0);
2 reserve1 = uint112(balance1);
3 blockTimestampLast = blockTimestamp;
4 emit Sync(reserve0, reserve1);
5 }
复制

最后,更新全局变量并发布一个 Sync 事件。

_mintFee
1 // if fee is on, mint liquidity equivalent to 1/6th of the growth in sqrt(k)
2 function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {
复制

在 Uniswap 2.0 的合约中规定交易者为使用兑换市场支付 0.30% 的费用。 这笔费用的大部分(交易的 0.25%)支付给流动性提供者。 余下的 0.05% 可以支付给流动性提供者或支付给工厂指定的地址作为协议费,用于支付 Uniswap 团队的开发费用。

为了减少计算次数(因此减少燃料费用),仅在向资金池中增加或减少流动性时才计算该费用,而不是在每次兑换交易时都计算。

1 address feeTo = IUniswapV2Factory(factory).feeTo();
2 feeOn = feeTo != address(0);
复制

读取工厂的费用支付地址。 如果返回值为零,则代表没有协议费,也不需要计算这笔费用。

1 uint _kLast = kLast; // gas savings
复制

kLast 状态变量位于内存中,所以在合约的不同调用中都有一个值。 虽然易失性内存每次在函数调用合约结束后都会清空,但由于访问存储的费用比访问内存高得多,所以我们使用内部变量,以降低燃料费用。

1 if (feeOn) {
2 if (_kLast != 0) {
复制

流动资金提供者仅仅因为提供流动性代币而得到所属的费用。 但是协议费用要求铸造新的流动性代币,并提供给 feeTo 地址。

1 uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));
2 uint rootKLast = Math.sqrt(_kLast);
3 if (rootK > rootKLast) {
复制

如果有新的流动性变化需要收取协议费。 你可以在本文后面部分看到平方根函数。

1 uint numerator = totalSupply.mul(rootK.sub(rootKLast));
2 uint denominator = rootK.mul(5).add(rootKLast);
3 uint liquidity = numerator / denominator;
复制

这种复杂的费用计算方法在白皮书(opens in a new tab)第 5 页中作了解释。 从计算 kLast 的时间到当前为止,流动性没有增加或减少(因为每次计算都是在流动性增加或减少并发生实际变化之前进行),所以 reserve0 * reserve1 的任何变化一定是从交易费用中产生(如果没有交易费,reserve0 * reserve1 值为常量)。

1 if (liquidity > 0) _mint(feeTo, liquidity);
2 }
3 }
复制

使用 UniswapV2ERC20._mint 函数产生更多的流动池代币并发送到 feeTo 地址。

1 } else if (_kLast != 0) {
2 kLast = 0;
3 }
4 }
复制

如果不需收费则将 klast 设为 0(如果 klast 不为 0)。 编写该合约时,有一个燃料返还功能(opens in a new tab),用于鼓励合约将其不需要的存储释放,从而减少以太坊上状态的整体存储大小。 此段代码在可行时返还。

外部可访问函数

请注意,虽然任何交易或合约都可以调用这些函数,但这些函数在设计上是从外围合约调用。 如果直接调用,你无法欺骗币对交易所,但可能因为错误而丢失价值。

铸币
1 // this low-level function should be called from a contract which performs important safety checks
2 function mint(address to) external lock returns (uint liquidity) {
复制

当流动资金提供者为资金池增加流动资金时,将会调用此函数。 它铸造额外的流动性代币作为奖励。 应从外围合约中调用该函数,在同一笔交易中增加流动性后外围合约就调用该函数(因此其他人都不能在合法所有者之前提交要求新增加流动性的交易)。

1 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
复制

这是 Solidity 函数中读取多个返回值的方式。 我们丢弃了最后返回的值区块时间戳,因为不需要它。

1 uint balance0 = IERC20(token0).balanceOf(address(this));
2 uint balance1 = IERC20(token1).balanceOf(address(this));
3 uint amount0 = balance0.sub(_reserve0);
4 uint amount1 = balance1.sub(_reserve1);
复制

获取当前余额并查看每个代币类型中添加的数量。

1 bool feeOn = _mintFee(_reserve0, _reserve1);
复制

如果有协议费用的话,计算需要收取的费用,并相应地产生流动池代币。 因为输入 _mintFee 函数的参数是原有的储备金数值,相应费用仅依据费用导致的资金池变化来精确计算。

1 uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
2 if (_totalSupply == 0) {
3 liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
4 _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens
复制

如果这是第一笔存款,会创建数量为 MINIMUM_LIQUIDITY 的代币并将它们发送到地址 0 进行锁定。 这些代币永远无法赎回,也就是说资金池永远不会完全变空(避免某些情况下出现分母为零错误)。 MINIMUM_LIQUIDITY 的值是 1000,因为考虑到大多数 ERC-20 细分成 1 个代币的 10^-18 个单位,而以太币则被分为 wei,为 1 个代币价值的 10^-15。 成本不高。

在首次存入时,我们不知道两种代币的相对价值,所以假定两种代币都具有相同的价值,只需要两者数量的乘积并取一下平方根。

我们可以相信这一点,因为提供同等价值、避免套利符合存款人的利益。 比方说,这两种代币的价值是相同的,但我们的存款人存入的 Token1Token0 的四倍。 交易者可以利用币对交易所认为 Token0 的价值更高这种情况,减少其价值。

事件reserve0reserve1reserve0 * reserve1流动池价值 (reserve0 + reserve1)
初始设置83225640
交易者存入 8 个 Token0 代币,获得 16 个 Token1 代币161625632

正如你可以看到的,交易者额外获得了 8 个代币,这是由于流动池价值下降造成的,损害了拥有流动池的存款人。

1 } else {
2 liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
复制

对于随后每次存入,我们已经知道两种资产之间的汇率。我们期望流动性提供者提供等值的两种代币。 如果他们没有,我们根据他们提供的较低价值代币来支付他们的流动池代币以做惩罚。

无论是最初存入还是后续存入,流动性代币的数量均等于 reserve0*reserve1 变化的平方根,而流动性代币的价值不变(除非存入的资金为不等值的代币类型,那么就会分派“罚金”)。 下面是另一个示例,两种代币具有相同价值,进行了三次良性存入和一次不良存入(即只存入一种类型的代币,所以不会产生任何流动性代币)。

事件reserve0reserve1reserve0 * reserve1流动池价值 (reserve0 + reserve1)存入资金而产生的流动池代币流动池代币总值每个流动池代币的值
初始设置8.0008.0006416.000882.000
每种代币存入 4 个12.00012.00014424.0004122.000
每种代币存入 2 个14.00014.00019628.0002142.000
不等值的存款18.00014.00025232.000014~2.286
套利后~15.874~15.874252~31.748014~2.267
1 }
2 require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
3 _mint(to, liquidity);
复制

使用 UniswapV2ERC20._mint 函数产生更多流动池代币并发送到正确的帐户地址。

1
2 _update(balance0, balance1, _reserve0, _reserve1);
3 if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
4 emit Mint(msg.sender, amount0, amount1);
5 }
复制

更新相应的状态变量(reserve0reserve1,必要时还包含 kLast)并激发相应事件。

销毁
1 // this low-level function should be called from a contract which performs important safety checks
2 function burn(address to) external lock returns (uint amount0, uint amount1) {
复制

当流动资金被提取且相应的流动池代币需要被销毁时,将调用此函数。 还需要从外围帐户调用。

1 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
2 address _token0 = token0; // gas savings
3 address _token1 = token1; // gas savings
4 uint balance0 = IERC20(_token0).balanceOf(address(this));
5 uint balance1 = IERC20(_token1).balanceOf(address(this));
6 uint liquidity = balanceOf[address(this)];
复制

外围合约在调用函数之前,首先将要销毁的流动资金转到本合约中。 这样,我们知道有多少流动资金需要销毁,并可以确保它被销毁。

1 bool feeOn = _mintFee(_reserve0, _reserve1);
2 uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
3 amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution
4 amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution
5 require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');
复制

流动资金提供者获得等值数量的两种代币。 这样不会改变兑换汇率。

1 _burn(address(this), liquidity);
2 _safeTransfer(_token0, to, amount0);
3 _safeTransfer(_token1, to, amount1);
4 balance0 = IERC20(_token0).balanceOf(address(this));
5 balance1 = IERC20(_token1).balanceOf(address(this));
6
7 _update(balance0, balance1, _reserve0, _reserve1);
8 if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
9 emit Burn(msg.sender, amount0, amount1, to);
10 }
11
显示全部
复制

burn 函数的其余部分是上述 mint 函数的镜像。

兑换
1 // this low-level function should be called from a contract which performs important safety checks
2 function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
复制

此函数也应该从外围合约调用。

1 require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
2 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
3 require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');
4
5 uint balance0;
6 uint balance1;
7 { // scope for _token{0,1}, avoids stack too deep errors
复制

本地变量可以存储在内存中,或者如果变量数目不太多,直接存储进堆栈。 如果我们可以限制变量数量,那么建议使用堆栈以减少燃料消耗。 欲了解更多详情,请参阅以太坊黄皮书(以前的以太坊规范)(opens in a new tab)第 26 页上的“方程式 298”。

1 address _token0 = token0;
2 address _token1 = token1;
3 require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
4 if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
5 if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
复制

这种转移应该是会成功的,因为在转移之前我们确信所有条件都得到满足。 在以太坊中这样操作是可以的,原因在于如果在后面的调用中条件没有得到满足,我们可以回滚操作和造成的所有变化。

1 if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
复制

如果收到请求,则通知接收者要进行兑换。

1 balance0 = IERC20(_token0).balanceOf(address(this));
2 balance1 = IERC20(_token1).balanceOf(address(this));
3 }
复制

获取当前余额。 外围合约在调用交换函数之前,需要向合约发送要兑换的代币。 这让合约可以方便检查它有没有受到欺骗,这是在核心合约中必须进行的检查(因为除外围合约之外的其他实体也可以调用该函数)。

1 uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
2 uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
3 require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');
4 { // scope for reserve{0,1}Adjusted, avoids stack too deep errors
5 uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
6 uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
7 require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');
复制

这是一项健全性检查,确保我们不会因兑换而损失代币。 在任何情况下兑换都不应减少 reserve0*reserve1。 这也是我们确保为兑换发送 0.3% 费用的方式;在对 K 值进行完整性检查之前,我们将两个余额乘以 1000 减去 3 倍的金额,这意味着在将其 K 值与当前准备金 K 值进行比较之前,从余额中扣除 0.3% (3/1000 = 0.003 = 0.3%)。

1 }
2
3 _update(balance0, balance1, _reserve0, _reserve1);
4 emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
5 }
复制

更新 reserve0reserve1 的值,并在必要时更新价格累积值和时间戳并激发相应事件。

同步或提取

实际余额有可能与配对交易所认为的储备金余额没有同步。 没有合约的认同,就无法撤回代币,但存款却不同。 帐户可以将代币转移到交易所,而无需调用 mintswap

在这种情况下,有两种解决办法:

  • sync,将储备金更新为当前余额
  • skim,撤回额外的金额。 请注意任何帐户都可以调用 skim 函数,因为无法知道是谁存入的代币。 此信息是在一个事件中发布的,但这些事件无法从区块链中访问。
1 // force balances to match reserves
2 function skim(address to) external lock {
3 address _token0 = token0; // gas savings
4 address _token1 = token1; // gas savings
5 _safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0));
6 _safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));
7 }
8
9
10
11 // force reserves to match balances
12 function sync() external lock {
13 _update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);
14 }
15}
显示全部
复制

UniswapV2Factory.sol

此合约(opens in a new tab)创建币对交易所。

1pragma solidity =0.5.16;
2
3import './interfaces/IUniswapV2Factory.sol';
4import './UniswapV2Pair.sol';
5
6contract UniswapV2Factory is IUniswapV2Factory {
7 address public feeTo;
8 address public feeToSetter;
复制

这些状态变量是执行协议费用所必需的(请见白皮书(opens in a new tab)的第 5 页)。 feeTo 地址用于累积流动性代币以收取协议费,而 feeToSetter 地址可用于将 feeTo 更改为不同地址。

1 mapping(address => mapping(address => address)) public getPair;
2 address[] public allPairs;
复制

这些变量用以跟踪配对,即两种代币之间的兑换。

第一个变量 getPair 是一个映射,它根据兑换的两种 ERC-20 代币来识别币对交易所合约。 ERC-20 代币通过实现它们的合约的地址来识别,因此关键字和值都是地址。 为了获取币对交易所地址,以便能够将 tokenA 兑换成 tokenB,可以使用 getPair [<tokenA address><tokenB address>](或其他方式)。

第二个变量 allPairs 是一个数组,其中包括该工厂创建的所有币对交易所的地址。 在以太坊中,无法迭代映射内容,或获取所有关键字的列表,所以,该变量是了解此工厂管理哪些交易所的唯一方式。

注意:不能迭代所有映射关键字的原因是合约数据存储费用昂贵,所以我们越少用存储越好,且越少改变 越好。 可以创建支持迭代的映射(opens in a new tab),但它们需要额外存储关键字列表。 但在大多数应用程序中并不需要。

1 event PairCreated(address indexed token0, address indexed token1, address pair, uint);
复制

当新的配对交易创建时,将激发此事件。 它包括代币地址、币对交易所地址以及工厂管理的交易所总数。

1 constructor(address _feeToSetter) public {
2 feeToSetter = _feeToSetter;
3 }
复制

构造函数做的唯一事情是指定 feeToSetter。 工厂开始时没有费用,只有 feeSetter 可以改变这种情况。

1 function allPairsLength() external view returns (uint) {
2 return allPairs.length;
3 }
复制

此函数返回交易配对的数量。

1 function createPair(address tokenA, address tokenB) external returns (address pair) {
复制

这是工厂的主要函数,可以在两个 ERC-20 代币之间创建配对交易。 注意,任何人都可以调用此函数。 不需要 Uniswap 许可就能创建新的币对交易所。

1 require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
2 (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
复制

我们希望新交易所的地址是可以确定的,这样就可以在链下提前计算(这对于二层网络交易来说比较有用)。 为此,无论收到代币地址的顺序如何,我们需要代币地址始终按顺序排列,因此我们在此处对它们排序。

1 require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
2 require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient
复制

大流动资金池优于小流动资金池,因为其价格比较稳定。 我们不希望每一对代币有多个流动性池。 如果已经有一个交易所,则无需为相同代币对创建另一个交易所。

1 bytes memory bytecode = type(UniswapV2Pair).creationCode;
复制

要创建新合约,我们需要使用创建它的代码(包括构造函数和写入用于存储实际合约以太坊虚拟机字节码的代码)。 在 Solidity 语言中,通常只需使用 addr = new <name of contract>(<constructor parameters>) 的格式语句,然后编译器就可以完成所有的工作,不过为了获取一个确定的合约地址,需要使用 CREATE2 操作码(opens in a new tab)。 在编写这个代码时,Solidity 还不支持操作码,因此需要手动获取该代码。 目前这已经不再是问题,因为 Solidity 现已支持 CREATE2(opens in a new tab)

1 bytes32 salt = keccak256(abi.encodePacked(token0, token1));
2 assembly {
3 pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
4 }
复制

当 Solidity 不支持操作码时,我们可以通过内联汇编(opens in a new tab)来调用。

1 IUniswapV2Pair(pair).initialize(token0, token1);
复制

调用 initialize 函数来告诉新兑换交易可以兑换哪两种代币。

1 getPair[token0][token1] = pair;
2 getPair[token1][token0] = pair; // populate mapping in the reverse direction
3 allPairs.push(pair);
4 emit PairCreated(token0, token1, pair, allPairs.length);
5 }
复制

在状态变量中保存新的配对信息,并激发一个事件来告知外界新的配对交易合约已生成。

1 function setFeeTo(address _feeTo) external {
2 require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
3 feeTo = _feeTo;
4 }
5
6 function setFeeToSetter(address _feeToSetter) external {
7 require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
8 feeToSetter = _feeToSetter;
9 }
10}
显示全部
复制

这两个函数允许 feeSetter 管理费用接收人(如果有)并将 feeSetter 更改为新地址。

UniswapV2ERC20.sol

本合约(opens in a new tab)实现 ERC-20 流动性代币。 它与 OpenZeppelin ERC-20 合约相似,因此这里仅解释不同的部分,即 permit 的功能。

以太坊上的交易需要消耗以太币 (ETH),相当于实际货币。 如果你有 ERC-20 代币但没有以太币,就无法发送交易,因而不能用代币做任何事情。 避免该问题的一个解决方案是元交易(opens in a new tab)。 代币的所有者签署一个交易,允许其他人从链上提取代币,并通过网络发送给接收人。 接收人拥有以太币,可以代表所有者提交许可。

1 bytes32 public DOMAIN_SEPARATOR;
2 // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
3 bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;
复制

此哈希值是这种交易类型的标识(opens in a new tab)。 在这里,我们仅支持带有这些参数的 Permit

1 mapping(address => uint) public nonces;
复制

接收人无法伪造数字签名。 但是,可以将同一笔交易发送两次(这是一种重放攻击(opens in a new tab))。 为防止发生这种情况,我们使用了随机数(opens in a new tab)。 如果新 Permit 的随机数不是上次使用的随机数加一,我们认为它无效。

1 constructor() public {
2 uint chainId;
3 assembly {
4 chainId := chainid
5 }
复制

这是获取链标识符(opens in a new tab)的代码。 它使用一种名为 Yul(opens in a new tab) 的以太坊虚拟机汇编语言。 请注意,在当前版本 Yul 中,必须使用 chainid(),而非 chainid

1 DOMAIN_SEPARATOR = keccak256(
2 abi.encode(
3 keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),
4 keccak256(bytes(name)),
5 keccak256(bytes('1')),
6 chainId,
7 address(this)
8 )
9 );
10 }
显示全部
复制

计算 EIP-712 的域分隔符(opens in a new tab)

1 function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external {
复制

这是实现批准功能的函数。 它接收相关字段作为参数,并将三个标量值(v、r 和 s)作为签名(opens in a new tab)

1 require(deadline >= block.timestamp, 'UniswapV2: EXPIRED');
复制

截止日期后请勿接受交易。

1 bytes32 digest = keccak256(
2 abi.encodePacked(
3 '\x19\x01',
4 DOMAIN_SEPARATOR,
5 keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))
6 )
7 );
复制

abi.encodePacked(...) 是我们预计将收到的信息。 我们知道随机数应该是什么,所以不需要将它作为一个参数。

以太坊签名算法预计获得 256 位用于签名,所以我们使用 keccak256 哈希函数。

1 address recoveredAddress = ecrecover(digest, v, r, s);
复制

从摘要和签名中,我们可以用 ecrecover(opens in a new tab) 函数计算出签名的地址。

1 require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE');
2 _approve(owner, spender, value);
3 }
4
复制

如果一切正常,则将其视为 ERC-20 批准(opens in a new tab)

外围合约

外围合约是用于 Uniswap 的 API(应用程序接口)。 它们可用于其他合约或去中心化应用程序进行的外部调用。 你可以直接调用核心合约但更为复杂,如果你出错,则可能会损失价值。 核心合约只包含确保它们不会遭受欺骗的测试,不会对其他调用者进行健全性检查。 它们在外围,因此可以根据需要进行更新。

UniswapV2Router01.sol

本合约(opens in a new tab)存在问题,不应该再使用(opens in a new tab)。 幸运的是,外围合约无状态,也不拥有任何资产,弃用外围合约比较容易。建议使用 UniswapV2Router02 来替代。

UniswapV2Router02.sol

在大多数情况下,你会通过该合约(opens in a new tab)使用 Uniswap。 有关使用说明,你可以在这里(opens in a new tab)找到。

1pragma solidity =0.6.6;
2
3import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol';
4import '@uniswap/lib/contracts/libraries/TransferHelper.sol';
5
6import './interfaces/IUniswapV2Router02.sol';
7import './libraries/UniswapV2Library.sol';
8import './libraries/SafeMath.sol';
9import './interfaces/IERC20.sol';
10import './interfaces/IWETH.sol';
显示全部
复制

其中大部分我们都曾遇到过,或相当明显。 一个例外是 IWETH.sol。 Uniswapv2 允许兑换任意一对 ERC-20 代币,但以太币 (ETH) 本身并不是 ERC-20 代币。 它早于该标准出现,并采用独特的机制转换。 为了在适用于 ERC-20 代币的合约中使用以太币,人们制定出包装以太币 (WETH)(opens in a new tab) 合约。 你发送以太币到该合约,它会为你铸造相同金额的包装以太币。 或者你可以销毁包装以太币,然后换回以太币。

1contract UniswapV2Router02 is IUniswapV2Router02 {
2 using SafeMath for uint;
3
4 address public immutable override factory;
5 address public immutable override WETH;
复制

路由需要知道使用哪个工厂,以及对于需要包装以太币的交易,要使用什么包装以太币合约。 这些值是不可修改(opens in a new tab)的,意味着它们只能在构造函数中设置。 这使得用户相信没有人能够改变这些值,让它们指向有风险的合约。

1 modifier ensure(uint deadline) {
2 require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED');
3 _;
4 }
复制

此修改函数确保有时间限制的交易(如果可以,请在 Y 之前执行 X)不会在时限后发生。

1 constructor(address _factory, address _WETH) public {
2 factory = _factory;
3 WETH = _WETH;
4 }
复制

构造函数仅用于设置不可变的状态变量。

1 receive() external payable {
2 assert(msg.sender == WETH); // only accept ETH via fallback from the WETH contract
3 }
复制

当我们将代币从包装以太币合约换回以太币时,需要调用此函数。 只有我们使用的包装以太币合约才有权完成此操作。

增加流动资金

这些函数添加代币进行配对交易,从而增大了流动资金池。

1
2 // **** ADD LIQUIDITY ****
3 function _addLiquidity(
复制

此函数用于计算应存入币对交易所的 A 代币和 B 代币的金额。

1 address tokenA,
2 address tokenB,
复制

这些是 ERC-20 代币合约的地址。

1 uint amountADesired,
2 uint amountBDesired,
复制

这些是流动资金提供者想要存入的代币数额。 它们也是要存入的代币 A 和 B 的最大金额。

1 uint amountAMin,
2 uint amountBMin
复制

这些是可接受的最低存款数额。 如果在达到最小金额或更高金额时交易无法完成,则会回滚交易。 如果不想要此功能,将它们设定为零即可。

流动性提供者指定最低金额,往往是因为他们想要限制交易汇率,使其在与当前汇率接近。 如果汇率波动太大,可能意味着基础价值可能发生改变,流动性提供者需要自己决定采取什么措施。

例如,想象汇率是一比一时,流动性提供者指定了以下值:

参数
amountADesired1000
amountBDesired1000
amountAMin900
amountBMin800

只要汇率保持在 0.9 至 1.25 之间,交易就会进行。 如果汇率超出这个范围,交易将被取消。

这种预防措施的原因是交易不是即时的,你提交交易,最后验证者才会将它们包含在一个区块中(除非你的燃料价格非常低,在这种情况下你需要提交另一个具有相同随机数的交易以及更高的燃料价格来覆盖它)。 在提交交易和交易包含到区块中之间发生的事情是无法控制的。

1 ) internal virtual returns (uint amountA, uint amountB) {
复制

该函数返回流动性提供者应存入的金额,存入该金额是为了让比率等于当前储备金之间的比率。

1 // create the pair if it doesn't exist yet
2 if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
3 IUniswapV2Factory(factory).createPair(tokenA, tokenB);
4 }
复制

如果还没有此代币对的兑换交易,则创建一个。

1 (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);
复制

获取配对中的当前储备金。

1 if (reserveA == 0 && reserveB == 0) {
2 (amountA, amountB) = (amountADesired, amountBDesired);
复制

如果当前储备金为空,那么这是一笔新的配对交易。 存入的金额应与流动性提供者想要提供的金额完全相同。

1 } else {
2 uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);
复制

如果我们需要知道这些金额是多少,可以使用此函数(opens in a new tab)获得最佳金额。 我们想要与当前储备相同的比率。

1 if (amountBOptimal <= amountBDesired) {
2 require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
3 (amountA, amountB) = (amountADesired, amountBOptimal);
复制

如果 amountBOptimal 小于流动性提供者想要存入的金额,意味着代币 B 目前比流动性存款人所认为的价值更高,所以需要更少的金额。

1 } else {
2 uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);
3 assert(amountAOptimal <= amountADesired);
4 require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
5 (amountA, amountB) = (amountAOptimal, amountBDesired);
复制

如果 B 代币的最佳金额大于想要存入的 B 代币金额,意味着代币 B 目前比流动性存款人所认为的价值更低,所以需要更多的金额。 然而,需要存入的金额是最大值,意味着我们无法存入更多金额的 B 代币。 可以选择的另一种方法是,我们计算所需 B 代币数额对应的最佳 A 代币数额。

把数值汇总起来,我们就会得到这张图表。 假定你正在试图存入 1000 个 A 代币(蓝线)和 1000 个 B 代币(红线)。 X 轴是汇率,A/B。 如果 x=1,两种代币价值相等,每种代币各存入 1000 个。 如果 x=2,A 的价值是 B 的两倍(每个 A 代币可换两个 B 代币),因此你存入 1000 个 B 代币,但只能存入 500 个 A 代币。 如果是 x=0.5,情况就会逆转,即可存 1000 个 A 代币或 500 个 B 代币。

图表

可以将流动资金直接存入核心合约(使用 UniswapV2Pair::mint(opens in a new tab)),但核心合约只是检查自己没有遭受欺骗。因此,如果汇率在提交交易至执行交易之间发生变化,你将面临损失资金价值的风险。 如果使用外围合约,它会计算你应该存入的金额并会立即存入,所以汇率不会改变,你不会有任何损失。

1 function addLiquidity(
2 address tokenA,
3 address tokenB,
4 uint amountADesired,
5 uint amountBDesired,
6 uint amountAMin,
7 uint amountBMin,
8 address to,
9 uint deadline
显示全部
复制

此函数可以在交易中调用,用于存入流动资金。 大多数参数与上述 _addLiquidity 中相同,但有两个例外:

. to 是会获取新流动池代币的地址,这些代币铸造用于显示流动资金提供者在池中所占比率 deadline 是交易的时间限制

1 ) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) {
2 (amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
3 address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
复制

我们计算实际存入的金额,然后找到流动资金池的帐户地址。 为了节省燃料,我们不是通过询问工厂执行此操作,而是使用库函数 pairFor(参见如下程序库)

1 TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
2 TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);
复制

将正确数额的代币从用户帐户转到配对交易。

1 liquidity = IUniswapV2Pair(pair).mint(to);
2 }
复制

反过来,将流动资金池的部分所有权赋予 to 地址的流动性代币。 核心合约的 mint 函数查看合约有多少额外代币(与上次流动性发生变化时合约持有的金额比较),并相应地铸造流动性代币。

1 function addLiquidityETH(
2 address token,
3 uint amountTokenDesired,
复制

当流动资金提供者想要向代币/以太币配对交易提供流动资金时,存在一些差别。 合约为流动性提供者处理以太币包装。 用户不需要指定想要存入多少以太币,因为用户直接通过交易发送以太币(金额在 msg.value 中)。

1 uint amountTokenMin,
2 uint amountETHMin,
3 address to,
4 uint deadline
5 ) external virtual override payable ensure(deadline) returns (uint amountToken, uint amountETH, uint liquidity) {
6 (amountToken, amountETH) = _addLiquidity(
7 token,
8 WETH,
9 amountTokenDesired,
10 msg.value,
11 amountTokenMin,
12 amountETHMin
13 );
14 address pair = UniswapV2Library.pairFor(factory, token, WETH);
15 TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken);
16 IWETH(WETH).deposit{value: amountETH}();
17 assert(IWETH(WETH).transfer(pair, amountETH));
显示全部
复制

为了将以太币存入合约,首先将其包装成包装以太币,然后将包装以太币转入配对。 请注意转账在 assert 中包装。 这意味着如果转账失败,此合约调用也会失败,因此包装不会真正发生。

1 liquidity = IUniswapV2Pair(pair).mint(to);
2 // refund dust eth, if any
3 if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);
4 }
复制

用户已经向我们发送了以太币,因此,如果还有任何额外以太币剩余(因为另一种代币比用户所认为的价值更低),我们需要发起退款。

撤回流动资金

下面的函数将撤回流动资金并还给流动资金提供者。

1 // **** REMOVE LIQUIDITY ****
2 function removeLiquidity(
3 address tokenA,
4 address tokenB,
5 uint liquidity,
6 uint amountAMin,
7 uint amountBMin,
8 address to,
9 uint deadline
10 ) public virtual override ensure(deadline) returns (uint amountA, uint amountB) {
显示全部
复制

最简单的流动资金撤回案例。 对于每种代币,都有一个流动性提供者同意接受的最低金额,必须在截止时间之前完成。

1 address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
2 IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair
3 (uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);
复制

核心合约的 burn 函数处理返还给用户的代币。

1 (address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);
复制

某个函数返回多个值时,如果我们只对其中部分值感兴趣,以下便是我们只获取那些值的方式。 从消耗燃料的角度来说,这样比读取那些从来不用的值更加经济。

1 (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);
复制

将按从核心合约返回代币的路径(低位代币地址优先)调整为用户期望的方式(对应于 tokenAtokenB)。

1 require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
2 require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
3 }
复制

可以首先进行转账,然后再核实转账是否合法,因为如果不合法,我们可以回滚所有的状态更改。

1 function removeLiquidityETH(
2 address token,
3 uint liquidity,
4 uint amountTokenMin,
5 uint amountETHMin,
6 address to,
7 uint deadline
8 ) public virtual override ensure(deadline) returns (uint amountToken, uint amountETH) {
9 (amountToken, amountETH) = removeLiquidity(
10 token,
11 WETH,
12 liquidity,
13 amountTokenMin,
14 amountETHMin,
15 address(this),
16 deadline
17 );
18 TransferHelper.safeTransfer(token, to, amountToken);
19 IWETH(WETH).withdraw(amountETH);
20 TransferHelper.safeTransferETH(to, amountETH);
21 }
显示全部
复制

撤回以太币流动性的方式几乎是一样的,区别在于我们首先会收到包装以太币代币,然后将它们兑换成以太币并退还给流动性提供者。

1 function removeLiquidityWithPermit(
2 address tokenA,
3 address tokenB,
4 uint liquidity,
5 uint amountAMin,
6 uint amountBMin,
7 address to,
8 uint deadline,
9 bool approveMax, uint8 v, bytes32 r, bytes32 s
10 ) external virtual override returns (uint amountA, uint amountB) {
11 address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
12 uint value = approveMax ? uint(-1) : liquidity;
13 IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
14 (amountA, amountB) = removeLiquidity(tokenA, tokenB, liquidity, amountAMin, amountBMin, to, deadline);
15 }
16
17
18 function removeLiquidityETHWithPermit(
19 address token,
20 uint liquidity,
21 uint amountTokenMin,
22 uint amountETHMin,
23 address to,
24 uint deadline,
25 bool approveMax, uint8 v, bytes32 r, bytes32 s
26 ) external virtual override returns (uint amountToken, uint amountETH) {
27 address pair = UniswapV2Library.pairFor(factory, token, WETH);
28 uint value = approveMax ? uint(-1) : liquidity;
29 IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
30 (amountToken, amountETH) = removeLiquidityETH(token, liquidity, amountTokenMin, amountETHMin, to, deadline);
31 }
显示全部
复制

这些函数转发元交易,通过许可证机制使没有以太币的用户能够从资金池中提取资金。

1
2 // **** REMOVE LIQUIDITY (supporting fee-on-transfer tokens) ****
3 function removeLiquidityETHSupportingFeeOnTransferTokens(
4 address token,
5 uint liquidity,
6 uint amountTokenMin,
7 uint amountETHMin,
8 address to,
9 uint deadline
10 ) public virtual override ensure(deadline) returns (uint amountETH) {
11 (, amountETH) = removeLiquidity(
12 token,
13 WETH,
14 liquidity,
15 amountTokenMin,
16 amountETHMin,
17 address(this),
18 deadline
19 );
20 TransferHelper.safeTransfer(token, to, IERC20(token).balanceOf(address(this)));
21 IWETH(WETH).withdraw(amountETH);
22 TransferHelper.safeTransferETH(to, amountETH);
23 }
24
显示全部
复制

此函数可以用于在传输或存储时收取费用的代币。 当代币有这类费用时,我们无法依靠 removeLiquidity 函数来告诉我们可以撤回多少代币。因此,我们需要先提取然后查询余额。

1
2
3 function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens(
4 address token,
5 uint liquidity,
6 uint amountTokenMin,
7 uint amountETHMin,
8 address to,
9 uint deadline,
10 bool approveMax, uint8 v, bytes32 r, bytes32 s
11 ) external virtual override returns (uint amountETH) {
12 address pair = UniswapV2Library.pairFor(factory, token, WETH);
13 uint value = approveMax ? uint(-1) : liquidity;
14 IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
15 amountETH = removeLiquidityETHSupportingFeeOnTransferTokens(
16 token, liquidity, amountTokenMin, amountETHMin, to, deadline
17 );
18 }
显示全部
复制

最后这个函数将存储费用计入元交易。

交易

1 // **** SWAP ****
2 // requires the initial amount to have already been sent to the first pair
3 function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual {
复制

公开给交易者的函数可以调用此函数以执行内部处理。

1 for (uint i; i < path.length - 1; i++) {
复制

在撰写此教程时,已有 388,160 个 ERC-20 代币(opens in a new tab)。 如果每个代币对都有币对交易所,币对交易所将超过 1500 亿个。 目前,整个链上的帐户数量仅为该数量的 0.1%(opens in a new tab)。 实际上,兑换函数支持路径这一概念。 交易者可以将 A 代币兑换成 B、B 代币兑换成 C、C 代币兑换成 D,因此不需要直接的 A-D 币对交易所。

这些市场上的价格往往是同步的,因为当价格不同步时,就会为套利创造机会。 设想一下,例如有三种代币 A、B 和 C。有三个币对交易所,每对代币一个。

  1. 初始情况
  2. 交易者出售 24.695 A 代币,获得 25.305 B 代币。
  3. 交易者卖出 24.695 个 B 代币得到 25.305 个 C 代币,大约获得 0.61 个 B 代币的利润。
  4. 随后,该交易者卖出 24.695 个 C 代币得到 25.305 个 A 代币,大约获得 0.61 个 C 代币的利润。 该交易者还多出了 0.61 个 A 代币(交易者最终拥有的 25.305 个A 代币,减去原始投资 24.695 个 A 代币)。
步骤A-B 兑换B-C 兑换A-C 兑换
1A:1000 B:1050 A/B=1.05B:1000 C:1050 B/C=1.05A:1050 C:1000 C/A=1.05
2A:1024.695 B:1024.695 A/B=1B:1000 C:1050 B/C=1.05A:1050 C:1000 C/A=1.05
3A:1024.695 B:1024.695 A/B=1B:1024.695 C:1024.695 B/C=1A:1050 C:1000 C/A=1.05
4A:1024.695 B:1024.695 A/B=1B:1024.695 C:1024.695 B/C=1A:1024.695 C:1024.695 C/A=1
1 (address input, address output) = (path[i], path[i + 1]);
2 (address token0,) = UniswapV2Library.sortTokens(input, output);
3 uint amountOut = amounts[i + 1];
复制

获取我们当前处理的配对,排序后(以便与配对一起使用)获得预期的输出金额。

1 (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));
复制

获得预期的金额后,按配对交易所需方式排序。

1 address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
复制

这是最后一次兑换吗? 如果是,将收到用于交易的代币发送到目的地址。 如果不是,则将代币发送到下一个币对交易所。

1
2 IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(
3 amount0Out, amount1Out, to, new bytes(0)
4 );
5 }
6 }
复制

真正调用配对交易来兑换代币。 我们不需要回调函数来了解交易信息,因此没有在该字段中发送任何字节。

1 function swapExactTokensForTokens(
复制

交易者直接使用此函数来兑换代币。

1 uint amountIn,
2 uint amountOutMin,
3 address[] calldata path,
复制

此参数包含 ERC-20 合约的地址。 如上文所述,此参数是一个数组,因为可能需要通过多个币对交易所将现有资产变为想要的资产。

Solidity 中的函数参数可以存入 memory 或者 calldata。 如果此函数是合约的入口点,在由用户(通过交易)直接调用或从另一个合约调用时,那么参数的值可以直接从调用数据中获取。 如果函数是内部调用,如上述 _swap 函数,则参数必须存储在 memory 中。 从所调用合约的角度来看,calldata 为只读变量。

对于标量类型,如 uintaddress,编译器可以为我们处理存储选择,但对于数组,由于它们需要更多的存储空间也消耗更多的燃料,我们需要指定要使用的存储类型。

1 address to,
2 uint deadline
3 ) external virtual override ensure(deadline) returns (uint[] memory amounts) {
复制

返回值总是返回内存中。

1 amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
2 require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
复制

计算每次兑换时要购买的代币金额。 如果金额低于交易者愿意接受的最低金额,则回滚该交易。

1 TransferHelper.safeTransferFrom(
2 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
3 );
4 _swap(amounts, path, to);
5 }
复制

最后,将初始的 ERC-20 代币转到第一个配对交易的帐户中,然后调用 _swap。 所有这些都发生在同一笔交易中,因此币对交易所知道任何意料之外的代币都是此次转账的一部分。

1 function swapTokensForExactTokens(
2 uint amountOut,
3 uint amountInMax,
4 address[] calldata path,
5 address to,
6 uint deadline
7 ) external virtual override ensure(deadline) returns (uint[] memory amounts) {
8 amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
9 require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
10 TransferHelper.safeTransferFrom(
11 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
12 );
13 _swap(amounts, path, to);
14 }
显示全部
复制

前一个函数 swapTokensForTokens,使交易者可以指定自己愿意提供的输入代币的准确数量和愿意接受的输出代币的最低数量。 此函数可以撤销兑换,使交易者能够指定想要的输出代币数量以及愿意支付的输入代币最大数量。

在这两种情况下,交易者必须首先给予此外围合约一定的额度,用于转账。

1 function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline)
2 external
3 virtual
4 override
5 payable
6 ensure(deadline)
7 returns (uint[] memory amounts)
8 {
9 require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
10 amounts = UniswapV2Library.getAmountsOut(factory, msg.value, path);
11 require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
12 IWETH(WETH).deposit{value: amounts[0]}();
13 assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));
14 _swap(amounts, path, to);
15 }
16
17
18 function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline)
19 external
20 virtual
21 override
22 ensure(deadline)
23 returns (uint[] memory amounts)
24 {
25 require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
26 amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
27 require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
28 TransferHelper.safeTransferFrom(
29 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
30 );
31 _swap(amounts, path, address(this));
32 IWETH(WETH).withdraw(amounts[amounts.length - 1]);
33 TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);
34 }
35
36
37
38 function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)
39 external
40 virtual
41 override
42 ensure(deadline)
43 returns (uint[] memory amounts)
44 {
45 require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
46 amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
47 require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
48 TransferHelper.safeTransferFrom(
49 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
50 );
51 _swap(amounts, path, address(this));
52 IWETH(WETH).withdraw(amounts[amounts.length - 1]);
53 TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);
54 }
55
56
57 function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline)
58 external
59 virtual
60 override
61 payable
62 ensure(deadline)
63 returns (uint[] memory amounts)
64 {
65 require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
66 amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
67 require(amounts[0] <= msg.value, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
68 IWETH(WETH).deposit{value: amounts[0]}();
69 assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));
70 _swap(amounts, path, to);
71 // refund dust eth, if any
72 if (msg.value > amounts[0]) TransferHelper.safeTransferETH(msg.sender, msg.value - amounts[0]);
73 }
显示全部
复制

这四种转换方式都涉及到以太币和代币之间的交易。 唯一不同的是,我们要么从交易者处收到以太币,并使用以太币铸造包装以太币,要么从路径上的最后一个交易所收到包装以太币并销毁,然后将产生的以太币再发送给交易者。

1 // **** SWAP (supporting fee-on-transfer tokens) ****
2 // requires the initial amount to have already been sent to the first pair
3 function _swapSupportingFeeOnTransferTokens(address[] memory path, address _to) internal virtual {
复制

此内部函数用于兑换有转账或存储费用的代币,以解决(此问题(opens in a new tab))。

1 for (uint i; i < path.length - 1; i++) {
2 (address input, address output) = (path[i], path[i + 1]);
3 (address token0,) = UniswapV2Library.sortTokens(input, output);
4 IUniswapV2Pair pair = IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output));
5 uint amountInput;
6 uint amountOutput;
7 { // scope to avoid stack too deep errors
8 (uint reserve0, uint reserve1,) = pair.getReserves();
9 (uint reserveInput, uint reserveOutput) = input == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
10 amountInput = IERC20(input).balanceOf(address(pair)).sub(reserveInput);
11 amountOutput = UniswapV2Library.getAmountOut(amountInput, reserveInput, reserveOutput);
显示全部
复制

由于有转账费用,我们不能依靠 getAmountsOut 函数来告诉我们每次转账完成后的金额(调用原来的 _swap 函数之前可以这样做)。 相反,我们必须先完成转账然后再查看我们获得的代币数量。

注意:理论上我们可以使用此函数而非 _swap,但在某些情况下(例如,如果因为在最后无法满足所需最低金额而导致转账回滚),最终会消耗更多燃料。 有转账费用的代币很少见,所以,尽管我们需要接纳它们,但不需要让所有的兑换都假定至少需要兑换一种需要收取转账费用的代币。

1 }
2 (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOutput) : (amountOutput, uint(0));
3 address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
4 pair.swap(amount0Out, amount1Out, to, new bytes(0));
5 }
6 }
7
8
9 function swapExactTokensForTokensSupportingFeeOnTransferTokens(
10 uint amountIn,
11 uint amountOutMin,
12 address[] calldata path,
13 address to,
14 uint deadline
15 ) external virtual override ensure(deadline) {
16 TransferHelper.safeTransferFrom(
17 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn
18 );
19 uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);
20 _swapSupportingFeeOnTransferTokens(path, to);
21 require(
22 IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,
23 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'
24 );
25 }
26
27
28 function swapExactETHForTokensSupportingFeeOnTransferTokens(
29 uint amountOutMin,
30 address[] calldata path,
31 address to,
32 uint deadline
33 )
34 external
35 virtual
36 override
37 payable
38 ensure(deadline)
39 {
40 require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
41 uint amountIn = msg.value;
42 IWETH(WETH).deposit{value: amountIn}();
43 assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn));
44 uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);
45 _swapSupportingFeeOnTransferTokens(path, to);
46 require(
47 IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,
48 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'
49 );
50 }
51
52
53 function swapExactTokensForETHSupportingFeeOnTransferTokens(
54 uint amountIn,
55 uint amountOutMin,
56 address[] calldata path,
57 address to,
58 uint deadline
59 )
60 external
61 virtual
62 override
63 ensure(deadline)
64 {
65 require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
66 TransferHelper.safeTransferFrom(
67 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn
68 );
69 _swapSupportingFeeOnTransferTokens(path, address(this));
70 uint amountOut = IERC20(WETH).balanceOf(address(this));
71 require(amountOut >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
72 IWETH(WETH).withdraw(amountOut);
73 TransferHelper.safeTransferETH(to, amountOut);
74 }
显示全部
复制

这些方式与用于普通代币的相同,区别在于它们调用的是_swapSupportingFeeOnTransferTokens

1 // **** LIBRARY FUNCTIONS ****
2 function quote(uint amountA, uint reserveA, uint reserveB) public pure virtual override returns (uint amountB) {
3 return UniswapV2Library.quote(amountA, reserveA, reserveB);
4 }
5
6 function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut)
7 public
8 pure
9 virtual
10 override
11 returns (uint amountOut)
12 {
13 return UniswapV2Library.getAmountOut(amountIn, reserveIn, reserveOut);
14 }
15
16 function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut)
17 public
18 pure
19 virtual
20 override
21 returns (uint amountIn)
22 {
23 return UniswapV2Library.getAmountIn(amountOut, reserveIn, reserveOut);
24 }
25
26 function getAmountsOut(uint amountIn, address[] memory path)
27 public
28 view
29 virtual
30 override
31 returns (uint[] memory amounts)
32 {
33 return UniswapV2Library.getAmountsOut(factory, amountIn, path);
34 }
35
36 function getAmountsIn(uint amountOut, address[] memory path)
37 public
38 view
39 virtual
40 override
41 returns (uint[] memory amounts)
42 {
43 return UniswapV2Library.getAmountsIn(factory, amountOut, path);
44 }
45}
显示全部
复制

这些函数仅仅是调用 UniswapV2Library 函数的代理。

UniswapV2Migrator.sol

这个合约用于将交易从旧版 v1 迁移至 v2。 目前版本已经迁移,便不再相关。

程序库

SafeMath 库(opens in a new tab)是一个文档很完备的程序库,这里便无需赘述了。

数学

此库包含一些 Solidity 代码通常不需要的数学函数,因而它们不是 Solidity 语言的一部分。

1pragma solidity =0.5.16;
2
3// a library for performing various math operations
4
5library Math {
6 function min(uint x, uint y) internal pure returns (uint z) {
7 z = x < y ? x : y;
8 }
9
10 // babylonian method (https://wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method)
11 function sqrt(uint y) internal pure returns (uint z) {
12 if (y > 3) {
13 z = y;
14 uint x = y / 2 + 1;
显示全部
复制

首先赋予 x 一个大于平方根的估值(这是我们需要把 1-3 当作特殊情况处理的原因)。

1 while (x < z) {
2 z = x;
3 x = (y / x + x) / 2;
复制

获取一个更接近的估值,即前一个估值与我们试图找到其方根值的数值的平均数除以前一个估值。 重复计算,直到新的估值不再低于现有估值。 欲了解更多详情,请参见此处(opens in a new tab)

1 }
2 } else if (y != 0) {
3 z = 1;
复制

我们永远不需要零的平方根。 1、2 和 3 的平方根大致为 1(我们使用的是整数,所以忽略小数)。

1 }
2 }
3}
复制

定点小数 (UQ112x112)

该库处理小数,这些小数通常不属于以太坊计算的一部分。 为此,它将数值编码xx*2^112。 这使我们能够使用原来的加法和减法操作码,无需更改。

1pragma solidity =0.5.16;
2
3// a library for handling binary fixed point numbers (https://wikipedia.org/wiki/Q_(number_format))
4
5// range: [0, 2**112 - 1]
6// resolution: 1 / 2**112
7
8library UQ112x112 {
9 uint224 constant Q112 = 2**112;
显示全部
复制

Q112 是 1 的编码。

1 // encode a uint112 as a UQ112x112
2 function encode(uint112 y) internal pure returns (uint224 z) {
3 z = uint224(y) * Q112; // never overflows
4 }
复制

因为 y 是uint112,所以最多可以是 2^112-1。 该数值还可以编码为 UQ112x112

1 // divide a UQ112x112 by a uint112, returning a UQ112x112
2 function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) {
3 z = x / uint224(y);
4 }
5}
复制

如果我们需要两个 UQ112x112 值相除,结果不需要再乘以 2^112。 因此,我们为分母取一个整数。 我们需要使用类似的技巧来做乘法,但不需要将 UQ112x112 的值相乘。

UniswapV2Library

此库仅被外围合约使用

1pragma solidity >=0.5.0;
2
3import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol';
4
5import "./SafeMath.sol";
6
7library UniswapV2Library {
8 using SafeMath for uint;
9
10 // returns sorted token addresses, used to handle return values from pairs sorted in this order
11 function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) {
12 require(tokenA != tokenB, 'UniswapV2Library: IDENTICAL_ADDRESSES');
13 (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
14 require(token0 != address(0), 'UniswapV2Library: ZERO_ADDRESS');
15 }
显示全部
复制

按地址对这两个代币排序,所以我们将能够获得相应的配对交易地址。 这很有必要,否则就会出现两种可能性,一种是参数 A、B,而另一种是参数 B、A,这导致两次交易而非一次。

1 // calculates the CREATE2 address for a pair without making any external calls
2 function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) {
3 (address token0, address token1) = sortTokens(tokenA, tokenB);
4 pair = address(uint(keccak256(abi.encodePacked(
5 hex'ff',
6 factory,
7 keccak256(abi.encodePacked(token0, token1)),
8 hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' // init code hash
9 ))));
10 }
显示全部
复制

此函数计算两种代币的配对交易地址。 此合约使用 CREATE2 操作码(opens in a new tab)创建,如果我们知道它使用的参数,我们可以使用相同的算法计算地址。 这比查询工厂便宜得多,而且

1 // fetches and sorts the reserves for a pair
2 function getReserves(address factory, address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB) {
3 (address token0,) = sortTokens(tokenA, tokenB);
4 (uint reserve0, uint reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves();
5 (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
6 }
复制

此函数返回配对交易所拥有的两种代币的储备金。 请注意,它可以任意顺序接收代币并将代币排序,以便内部使用。

1 // given some amount of an asset and pair reserves, returns an equivalent amount of the other asset
2 function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) {
3 require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT');
4 require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
5 amountB = amountA.mul(reserveB) / reserveA;
6 }
复制

如果不涉及交易费用的话,此函数将返回给你代币 A 兑换得到的代币 B。 此计算考虑到转账可能会改变汇率。

1 // given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset
2 function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {
复制

如果使用配对交易没有手续费,上述 quote 函数非常有效。 然而,如果有 0.3% 的手续费,你实际得到的金额就会低于此值。 此函数可以计算缴纳交易费用后的金额。

1
2 require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT');
3 require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
4 uint amountInWithFee = amountIn.mul(997);
5 uint numerator = amountInWithFee.mul(reserveOut);
6 uint denominator = reserveIn.mul(1000).add(amountInWithFee);
7 amountOut = numerator / denominator;
8 }
复制

Solidity 本身不能进行小数计算,所以不能简单地将金额乘以 0.997。 作为替代方法,我们将分子乘以 997,分母乘以 1000,也能取得相同的效果。

1 // given an output amount of an asset and pair reserves, returns a required input amount of the other asset
2 function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) internal pure returns (uint amountIn) {
3 require(amountOut > 0, 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT');
4 require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
5 uint numerator = reserveIn.mul(amountOut).mul(1000);
6 uint denominator = reserveOut.sub(amountOut).mul(997);
7 amountIn = (numerator / denominator).add(1);
8 }
复制

此函数大致完成相同的功能,但它会获取输出数额并提供输入代币的数量。

1
2 // performs chained getAmountOut calculations on any number of pairs
3 function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) {
4 require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');
5 amounts = new uint[](path.length);
6 amounts[0] = amountIn;
7 for (uint i; i < path.length - 1; i++) {
8 (uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]);
9 amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);
10 }
11 }
12
13 // performs chained getAmountIn calculations on any number of pairs
14 function getAmountsIn(address factory, uint amountOut, address[] memory path) internal view returns (uint[] memory amounts) {
15 require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');
16 amounts = new uint[](path.length);
17 amounts[amounts.length - 1] = amountOut;
18 for (uint i = path.length - 1; i > 0; i--) {
19 (uint reserveIn, uint reserveOut) = getReserves(factory, path[i - 1], path[i]);
20 amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut);
21 }
22 }
23}
显示全部
复制

在需要进行数次配对交易时,可以通过这两个函数获得相应数值。

转账帮助

此库(opens in a new tab)添加了围绕 ERC-20 和以太坊转账的成功检查,并以同样的方式处理回滚和返回 false 值。

1// SPDX-License-Identifier: GPL-3.0-or-later
2
3pragma solidity >=0.6.0;
4
5// helper methods for interacting with ERC20 tokens and sending ETH that do not consistently return true/false
6library TransferHelper {
7 function safeApprove(
8 address token,
9 address to,
10 uint256 value
11 ) internal {
12 // bytes4(keccak256(bytes('approve(address,uint256)')));
13 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x095ea7b3, to, value));
14
显示全部
复制

我们可以通过以下两种方式调用不同的合约:

1 require(
2 success && (data.length == 0 || abi.decode(data, (bool))),
3 'TransferHelper::safeApprove: approve failed'
4 );
5 }
复制

为了与之前的 ERC-20 标准创建的代币反向兼容,ERC-20 调用失败可能有两种情况:回退(在这种情况下 success 即是 false),或者调用成功但返回 false 值(在这种情况下有输出数据,将其解码为布尔值,会得到 false)。

1
2
3 function safeTransfer(
4 address token,
5 address to,
6 uint256 value
7 ) internal {
8 // bytes4(keccak256(bytes('transfer(address,uint256)')));
9 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, value));
10 require(
11 success && (data.length == 0 || abi.decode(data, (bool))),
12 'TransferHelper::safeTransfer: transfer failed'
13 );
14 }
显示全部
复制

此函数实现了 ERC-20 的转账功能(opens in a new tab),可使一个帐户花掉由不同帐户所提供的额度。

1
2 function safeTransferFrom(
3 address token,
4 address from,
5 address to,
6 uint256 value
7 ) internal {
8 // bytes4(keccak256(bytes('transferFrom(address,address,uint256)')));
9 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value));
10 require(
11 success && (data.length == 0 || abi.decode(data, (bool))),
12 'TransferHelper::transferFrom: transferFrom failed'
13 );
14 }
显示全部
复制

此函数实现了 ERC-20 的 transferFrom 功能(opens in a new tab),可使一个帐户花掉由不同帐户所提供的额度。

1
2 function safeTransferETH(address to, uint256 value) internal {
3 (bool success, ) = to.call{value: value}(new bytes(0));
4 require(success, 'TransferHelper::safeTransferETH: ETH transfer failed');
5 }
6}
复制

此函数将以太币转至一个帐户。 任何对不同合约的调用都可以尝试发送以太币。 因为我们实际上不需要调用任何函数,就不需要在调用中发送任何数据。

结论

本篇文章较长,约有 50 页。 如果你已读到此处,恭喜你! 希望你现在已经了解编写真实应用程序(相对于短小的示例程序)时的考虑因素,并且能够更好地为自己的用例编写合约。

现在去写点实用的东西吧,希望你能给我们惊喜。

上次修改时间: @wackerow(opens in a new tab), 2024年4月2日

本教程对你有帮助吗?