介绍
Uniswap v2 可以在任何两个 ERC-20 代币之间创建一个兑换市场。 在本文中,我们将深入探讨实现此协议的合约的源代码,了解为何要如此编写协议。
Uniswap 是做什么的?
一般来说有两类用户:流动资金提供者和交易者。
流动性提供者为资金池提供两种可以兑换的代币(称为 Token0 和 Token1)。 作为回报,他们会收到第三种叫做流动性代币的代币,代表他们对资金池的部分所有权。
交易者将一种代币发送到资金池,并从资金池中接收流动性提供者提供的另一种代币(例如,发送 Token0 并获得 Token1)。 兑换汇率由资金池中 Token0 和 Token1 的相对数量决定。 此外,资金池将收取汇率的一小部分作为流动性资金池的奖励。
当流动性提供者想要收回他们的代币资产时,他们可以销毁资金池代币并收回他们的代币,其中包括属于他们的奖励。
为什么选择 v2? 而不是 v3?
Uniswap v3 是 v2 的升级,远比 v2 复杂得多。 比较容易的方法是先学习 v2,然后再学习 v3。
核心合约与外围合约
Uniswap v2 可以分为两个部分,一个为核心部分,另一个为外围部分。 核心合约存放着资产,因而必须确保安全,这种分法就使核心合约更加简洁且更便于审核。 而所有交易者需要的其它功能可以通过外围合约提供。
数据和控制流程
执行 Uniswap 的三个主要操作时,会出现以下数据和控制流程:
兑换不同代币
将资金添加到市场中提供流动性,并获得兑换中奖励的流动池 ERC-20 代币
消耗流动池 ERC-20 代币并收回交易所允许交易者兑换的 ERC-20 代币
兑换
这是交易者最常用的流程:
调用者
向外围帐户提供兑换额度。
调用外围合约中的一个兑换函数。外围合约有多种兑换函数,调用哪一个取决于是否涉及以太币、交易者是指定了存入的代币金额还是提取的代币金额等。 每个兑换函数都接受一个
path
,即要执行的一系列兑换。
在外围合约 (UniswapV2Router02.sol) 中
确定兑换路径中,每次兑换所需交易的代币数额。
沿路径迭代。 对于路径上的每次兑换,首先发送输入代币,然后调用交易所的
swap
函数。 在大多数情况下,代币输出的目的地址是路径中下一个配对交易。 在最后一个交易所中,该地址是交易者提供的地址。
在核心合约 (UniswapV2Pair.sol) 中
验证核心合约没有被欺骗,可在兑换后保持足够的流动资金。
检查除了现有的储备金额外,还有多少额外的代币。 此数额是我们收到的要用于兑换的输入代币数量。
将输出代币发送到目的地址。
调用
_update
来更新储备金额
回到外围合约 (UniswapV2Router02.sol)
执行所需的必要清理工作(例如,消耗包装以太币代币以返回以太币给交易者)
增加流动资金
调用者
向外围帐户提交准备加入流动资金池的资金额度。
调用外围合约的其中一个
addLiquidity
函数。
在外围合约 (UniswapV2Router02.sol) 中
必要时创建一个新的配对交易
如果有现有的币对交易所,请计算要增加的代币金额。 该金额对于两种代币应该是相同的,因此新代币对现有代币的比率是相同的。
检查金额是否可接受(调用者可以指定一个最低金额,低于此金额他们就不增加流动性)
调用核心合约。
在核心合约 (UniswapV2Pair.sol) 中
生成流动池代币并将其发送给调用者
调用
_update
来更新储备金额
撤回流动资金
调用者
向外围帐户提供一个流动池代币的额度,作为兑换底层代币所需的消耗。
调用外围合约的其中一个
removeLiquidity
函数。
在外围合约 (UniswapV2Router02.sol) 中
将流动池代币发送到该配对交易
在核心合约 (UniswapV2Pair.sol) 中
向目的地址发送底层代币,金额与销毁的代币成比例。 例如,如果资金池里有 1000 个 A 代币,500 个 B 代币和 90 个流动性代币,而我们收到请求销毁 9 个流动性代币,那么,我们将销毁 10% 的流动性代币,然后将返还用户 100 个 A 代币和 50 个 B 代币。
销毁流动性代币
调用
_update
来更新储备金额
核心合约
这些是持有流动资金的安全合约。
UniswapV2Pair.sol
本合约实现用于交易代币的实际资金池。 这是 Uniswap 的核心功能。
1pragma solidity =0.5.16;23import './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';显示全部 复制
这些都是该合约需要知道的接口,因为该合约实现了它们(IUniswapV2Pair
和 UniswapV2ERC20
),或因为该合约调用了实现它们的合约。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制
此合约继承自 UniswapV2ERC20
,为流动池代币提供 ERC-20 代币功能。
1 using SafeMath for uint; 复制
SafeMath 库用于避免整数上溢和下溢。 这很重要,否则最终可能会出现这样的情况:本该是 -1
的值,结果却成了 2^256-1
。
1 using UQ112x112 for uint224; 复制
流动池合约中的许多计算都需要分数。 但是,以太坊虚拟机本身不支持分数。 Uniswap 找到的解决方案是使用 224 位数值,整数部分为 112 位,小数部分为 112 位。 因此,1.0
用 2^112
表示,1.5
用 2^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 getReserves2 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 位。 因此,reserve0
、reserve1
和 blockTimestampLast
三个变量的分配方式让单个存储值可以包含全部这三个变量 (112+112+32=256)。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制0
这些变量存放每种代币的累计成本(每种代币在另一种代币的基础上计算)。 可以用来计算一段时间内的平均汇率。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制1
币对交易所决定 token0 和 token1 之间汇率的方式是在交易中保持两种储备金的乘数恒定不变。 即 kLast
这个值。 当流动性提供者存入或提取代币时,该乘数就会变化,由于兑换市场的费用为 0.3%,它会略有增加。
下面是一个示例。 请注意,为了简单起见,表格中的数字仅保留了小数点后三位,我们忽略了 0.3% 交易费,因此数字并不准确。
事件 | reserve0 | reserve1 | reserve0 * reserve1 | 平均汇率 (token1 / token0) |
---|---|---|---|---|
初始设置 | 1,000.000 | 1,000.000 | 1,000,000 | |
交易者 A 用 50 个 token0 兑换 47.619 个 token1 | 1,050.000 | 952.381 | 1,000,000 | 0.952 |
交易者 B 用 10 个 token0 兑换 8.984 个 token1 | 1,060.000 | 943.396 | 1,000,000 | 0.898 |
交易者 C 用 40 个 token0 兑换 34.305 个 token1 | 1,100.000 | 909.090 | 1,000,000 | 0.858 |
交易者 D 用 100 个 token1 兑换 109.01 个 token0 | 990.990 | 1,009.090 | 1,000,000 | 0.917 |
交易者 E 用 10 个 token0 兑换 10.079 个 token1 | 1,000.990 | 999.010 | 1,000,000 | 1.008 |
由于交易者提供了更多 token0,token1 的相对价值增加了,反之亦然,这取决于供求。
锁定
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制2
有一类基于重入攻击的安全漏洞。 Uniswap 需要转让不同数值的 ERC-20 代币,这意味着调用的 ERC-20 合约可能会导致调用合约的 Uniswap 市场遭受攻击。 通过在合约中使用 unlocked
变量,我们可以防止函数在运行时被调用(同一笔交易中)。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制3
此函数是一个修改器,它对正常函数进行包装数,以便以某种方式改变其行为。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制4
如果 unlocked
变量值为 1,将其设置为 0。 如果已经是 0,则撤销调用,返回失败。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制5
在修改器中,_;
是原始函数调用(含所有参数)。 此处,这意味着仅在 unlocked
变量值为 1 时调用函数,该函数调用才有效;而当函数运行时,unlocked
值为 0。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制6
当主函数返回后,释放锁定。
其他 函数
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制7
此函数返回给调用者当前的兑换状态。 请注意,Solidity 函数可以返回多个值。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制8
此内部函数可以从交易所转账一定数额的 ERC20 代币给其他帐户。 SELECTOR
指定我们调用的函数是 transfer(address,uint)
(参见上面的定义)。
为了避免必须为代币函数导入接口,我们需要使用其中一个应用程序二进制接口函数来“手动”创建调用。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制9
ERC-20 的转移调用有两种方式可能失败:
回滚 如果对外部合约的调用回滚,则布尔返回值为
false
正常结束但报告失败。 在这种情况下,返回值的缓冲为非零长度,将其解码为布尔值时,其值为
false
一旦出现这两种情况,转移调用就会回滚。
事件
1 using SafeMath for uint; 复制0
当流动资金提供者存入流动资金 (Mint
) 或提取流动资金 (Burn
) 时,会发出这两个事件。 在这两种情况下,存入或提取的 token0 和 token1 金额是事件的一部分,以及调用合约的帐户身份 (Sender
) 也是事件的一部分。 在提取资金时,事件中还包括获得代币的目的地址 (to
),这个地址可能与发送人不同。
1 using SafeMath for uint; 复制1
当交易者用一种代币交换另一种代币时,会激发此事件。 同样,代币发送者和兑换后代币的存入目的帐户可能不一样。 每种代币都可以发送到交易所,或者从交易所接收。
1 using SafeMath for uint; 复制2
最后,无论出于何种原因,每次存入或提取代币时都会触发 Sync
事件,以提供最新的储备金信息(从而提供汇率)。
设置函数
这些函数应在建立新的配对交易时调用。
1 using SafeMath for uint; 复制3
构造函数确保我们能够跟踪产生配对的工厂合约的地址。 initialize
函数和工厂交易费(如果有)需要此信息
1 using SafeMath for uint; 复制4
这个函数允许工厂(而且只允许工厂)指定配对中进行兑换的两种 ERC-20 代币。
内部更新函数
_update
1 using SafeMath for uint; 复制5
每次存入或提取代币时,会调用此函数。
1 using SafeMath for uint; 复制6
如果 balance0 或 balance1 (uint256) 大于 uint112(-1) (=2^112-1)(因此当转换为 uint112 时会溢出并返回 0),拒绝继续执行 _update 以防止溢出。 一般的代币可以细分成 10^18 个单元,这意味在每个交易所,每种代币的限额为 5.1*10^15 个。 迄今为止,这并不是一个问题。
1 using SafeMath for uint; 复制7
如果流逝的时间值不是零,这意味着本交易是此区块上的第一笔兑换交易。 在这种情况下,我们需要更新累积成本值。
1 using SafeMath for uint; 复制8
每个累积成本值都用最新成本值(另一个代币的储备金额/本代币的储备金额)与以秒为单位的流逝时间的乘积加以更新。 要获得平均价格,需要读取两个时间点的累计价格,并除以两个时间点之间的时间差。 例如,假设下面这些事件序列:
事件 | reserve0 | reserve1 | 时间戳 | 边际汇率 (reserve1 / reserve0) | price0CumulativeLast |
---|---|---|---|---|---|
初始设置 | 1,000.000 | 1,000.000 | 5,000 | 1.000 | 0 |
交易者 A 存入 50 个代币 0 获得 47.619 个代币 1 | 1,050.000 | 952.381 | 5,020 | 0.907 | 20 |
交易者 B 存入 10 个代币 0 获得 8.984 个代币 1 | 1,060.000 | 943.396 | 5,030 | 0.89 | 20+10*0.907 = 29.07 |
交易者 C 存入 40 个代币 0 获得 34.305 个代币 1 | 1,100.000 | 909.090 | 5,100 | 0.826 | 29.07+70*0.890 = 91.37 |
交易者 D 存入 100 个代币 0 获得 109.01 个代币 1 | 990.990 | 1,009.090 | 5,110 | 1.018 | 91.37+10*0.826 = 99.63 |
交易者 E 存入 10 个代币 0 获得 10.079 个代币 1 | 1,000.990 | 999.010 | 5,150 | 0.998 | 99.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 using SafeMath for uint; 复制9
最后,更新全局变量并发布一个 Sync
事件。
_mintFee
1 using UQ112x112 for uint224; 复制0
在 Uniswap 2.0 的合约中规定交易者为使用兑换市场支付 0.30% 的费用。 这笔费用的大部分(交易的 0.25%)支付给流动性提供者。 余下的 0.05% 可以支付给流动性提供者或支付给工厂指定的地址作为协议费,用于支付 Uniswap 团队的开发费用。
为了减少计算次数(因此减少燃料费用),仅在向资金池中增加或减少流动性时才计算该费用,而不是在每次兑换交易时都计算。
1 using UQ112x112 for uint224; 复制1
读取工厂的费用支付地址。 如果返回值为零,则代表没有协议费,也不需要计算这笔费用。
1 using UQ112x112 for uint224; 复制2
kLast
状态变量位于内存中,所以在合约的不同调用中都有一个值。 虽然易失性内存每次在函数调用合约结束后都会清空,但由于访问存储的费用比访问内存高得多,所以我们使用内部变量,以降低燃料费用。
1 using UQ112x112 for uint224; 复制3
流动资金提供者仅仅因为提供流动性代币而得到所属的费用。 但是协议费用要求铸造新的流动性代币,并提供给 feeTo
地址。
1 using UQ112x112 for uint224; 复制4
如果有新的流动性变化需要收取协议费。 你可以在本文后面部分看到平方根函数。
1 using UQ112x112 for uint224; 复制5
这种复杂的费用计算方法在白皮书第 5 页中作了解释。 从计算 kLast
的时间到当前为止,流动性没有增加或减少(因为每次计算都是在流动性增加或减少并发生实际变化之前进行),所以 reserve0 * reserve1
的任何变化一定是从交易费用中产生(如果没有交易费,reserve0 * reserve1
值为常量)。
1 using UQ112x112 for uint224; 复制6
使用 UniswapV2ERC20._mint
函数产生更多的流动池代币并发送到 feeTo
地址。
1 using UQ112x112 for uint224; 复制7
如果不需收费则将 klast
设为 0(如果 klast 不为 0)。 编写该合约时,有一个燃料返还功能,用于鼓励合约将其不需要的存储释放,从而减少以太坊上状态的整体存储大小。 此段代码在可行时返还。
外部可访问函数
请注意,虽然任何交易或合约都可以调用这些函数,但这些函数在设计上是从外围合约调用。 如果直接调用,你无法欺骗币对交易所,但可能因为错误而丢失价值。
铸币
1 using UQ112x112 for uint224; 复制8
当流动资金提供者为资金池增加流动资金时,将会调用此函数。 它铸造额外的流动性代币作为奖励。 应从外围合约中调用该函数,在同一笔交易中增加流动性后外围合约就调用该函数(因此其他人都不能在合法所有者之前提交要求新增加流动性的交易)。
1 using UQ112x112 for uint224; 复制9
这是 Solidity 函数中读取多个返回值的方式。 我们丢弃了最后返回的值区块时间戳,因为不需要它。
1 uint public constant MINIMUM_LIQUIDITY = 10**3; 复制0
获取当前余额并查看每个代币类型中添加的数量。
1 uint public constant MINIMUM_LIQUIDITY = 10**3; 复制1
如果有协议费用的话,计算需要收取的费用,并相应地产生流动池代币。 因为输入 _mintFee
函数的参数是原有的储备金数值,相应费用仅依据费用导致的资金池变化来精确计算。
1 uint public constant MINIMUM_LIQUIDITY = 10**3; 复制2
如果这是第一笔存款,会创建数量为 MINIMUM_LIQUIDITY
的代币并将它们发送到地址 0 进行锁定。 这些代币永远无法赎回,也就是说资金池永远不会完全变空(避免某些情况下出现分母为零错误)。 MINIMUM_LIQUIDITY
的值是 1000,因为考虑到大多数 ERC-20 细分成 1 个代币的 10^-18 个单位,而以太币则被分为 wei,为 1 个代币价值的 10^-15。 成本不高。
在首次存入时,我们不知道两种代币的相对价值,所以假定两种代币都具有相同的价值,只需要两者数量的乘积并取一下平方根。
我们可以相信这一点,因为提供同等价值、避免套利符合存款人的利益。 比方说,这两种代币的价值是相同的,但我们的存款人存入的 Token1 是 Token0 的四倍。 交易者可以利用币对交易所认为 Token0 的价值更高这种情况,减少其价值。
事件 | reserve0 | reserve1 | reserve0 * reserve1 | 流动池价值 (reserve0 + reserve1) |
---|---|---|---|---|
初始设置 | 8 | 32 | 256 | 40 |
交易者存入 8 个 Token0 代币,获得 16 个 Token1 代币 | 16 | 16 | 256 | 32 |
正如你可以看到的,交易者额外获得了 8 个代币,这是由于流动池价值下降造成的,损害了拥有流动池的存款人。
1 uint public constant MINIMUM_LIQUIDITY = 10**3; 复制3
对于随后每次存入,我们已经知道两种资产之间的汇率。我们期望流动性提供者提供等值的两种代币。 如果他们没有,我们根据他们提供的较低价值代币来支付他们的流动池代币以做惩罚。
无论是最初存入还是后续存入,流动性代币的数量均等于 reserve0*reserve1
变化的平方根,而流动性代币的价值不变(除非存入的资金为不等值的代币类型,那么就会分派“罚金”)。 下面是另一个示例,两种代币具有相同价值,进行了三次良性存入和一次不良存入(即只存入一种类型的代币,所以不会产生任何流动性代币)。
事件 | reserve0 | reserve1 | reserve0 * reserve1 | 流动池价值 (reserve0 + reserve1) | 存入资金而产生的流动池代币 | 流动池代币总值 | 每个流动池代币的值 |
---|---|---|---|---|---|---|---|
初始设置 | 8.000 | 8.000 | 64 | 16.000 | 8 | 8 | 2.000 |
每种代币存入 4 个 | 12.000 | 12.000 | 144 | 24.000 | 4 | 12 | 2.000 |
每种代币存入 2 个 | 14.000 | 14.000 | 196 | 28.000 | 2 | 14 | 2.000 |
不等值的存款 | 18.000 | 14.000 | 252 | 32.000 | 0 | 14 | ~2.286 |
套利后 | ~15.874 | ~15.874 | 252 | ~31.748 | 0 | 14 | ~2.267 |
1 uint public constant MINIMUM_LIQUIDITY = 10**3; 复制4
使用 UniswapV2ERC20._mint
函数产生更多流动池代币并发送到正确的帐户地址。
1 uint public constant MINIMUM_LIQUIDITY = 10**3; 复制5
更新相应的状态变量(reserve0
、reserve1
,必要时还包含 kLast
)并激发相应事件。
销毁
1 uint public constant MINIMUM_LIQUIDITY = 10**3; 复制6
当流动资金被提取且相应的流动池代币需要被销毁时,将调用此函数。 还需要从外围帐户调用。
1 uint public constant MINIMUM_LIQUIDITY = 10**3; 复制7
外围合约在调用函数之前,首先将要销毁的流动资金转到本合约中。 这样,我们知道有多少流动资金需要销毁,并可以确保它被销毁。
1 uint public constant MINIMUM_LIQUIDITY = 10**3; 复制8
流动资金提供者获得等值数量的两种代币。 这样不会改变兑换汇率。
1 uint public constant MINIMUM_LIQUIDITY = 10**3; 复制9
burn
函数的其余部分是上述 mint
函数的镜像。
兑换
1 bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)'))); 复制0
此函数也应该从外围合约调用。
1 bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)'))); 复制1
本地变量可以存储在内存中,或者如果变量数目不太多,直接存储进堆栈。 如果我们可以限制变量数量,那么建议使用堆栈以减少燃料消耗。
1 bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)'))); 复制2
这种转移应该是会成功的,因为在转移之前我们确信所有条件都得到满足。 在以太坊中这样操作是可以的,原因在于如果在后面的调用中条件没有得到满足,我们可以回滚操作和造成的所有变化。
1 bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)'))); 复制3
如果收到请求,则通知接收者要进行兑换。
1 bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)'))); 复制4
获取当前余额。 外围合约在调用交换函数之前,需要向合约发送要兑换的代币。 这让合约可以方便检查它有没有受到欺骗,这是在核心合约中必须进行的检查(因为除外围合约之外的其他实体也可以调用该函数)。
1 bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)'))); 复制5
这是一项健全性检查,确保我们不会因兑换而损失代币。 在任何情况下兑换都不应减少 reserve0*reserve1
。 这也是我们确保为兑换发送 0.3% 费用的方式;在对 K 值进行完整性检查之前,我们将两个余额乘以 1000 减去 3 倍的金额,这意味着在将其 K 值与当前准备金 K 值进行比较之前,从余额中扣除 0.3% (3/1000 = 0.003 = 0.3%)。
1 bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)'))); 复制6
更新 reserve0
和 reserve1
的值,并在必要时更新价格累积值和时间戳并激发相应事件。
同步或提取
实际余额有可能与配对交易所认为的储备金余额没有同步。 没有合约的认同,就无法撤回代币,但存款却不同。 帐户可以将代币转移到交易所,而无需调用 mint
或 swap
。
在这种情况下,有两种解决办法:
sync
,将储备金更新为当前余额skim
,撤回额外的金额。 请注意任何帐户都可以调用skim
函数,因为无法知道是谁存入的代币。 此信息是在一个事件中发布的,但这些事件无法从区块链中访问。
1 bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)'))); 复制7
UniswapV2Factory.sol
1 bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)'))); 复制8
这些状态变量是执行协议费用所必需的
1 bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)'))); 复制9
这些变量用以跟踪配对,即两种代币之间的兑换。
第一个变量 getPair
是一个映射,它根据兑换的两种 ERC-20 代币来识别币对交易所合约。 ERC-20 代币通过实现它们的合约的地址来识别,因此关键字和值都是地址。 为了获取币对交易所地址,以便能够将 tokenA
兑换成 tokenB
,可以使用 getPair [<tokenA address><tokenB address>]
(或其他方式)。
第二个变量 allPairs
是一个数组,其中包括该工厂创建的所有币对交易所的地址。 在以太坊中,无法迭代映射内容,或获取所有关键字的列表,所以,该变量是了解此工厂管理哪些交易所的唯一方式。
注意:不能迭代所有映射关键字的原因是合约数据存储费用昂贵,所以我们越少用存储越好,且越少改变 越好。 可以创建支持迭代的映射,但它们需要额外存储关键字列表。 但在大多数应用程序中并不需要。
1 address public factory; 复制0
当新的配对交易创建时,将激发此事件。 它包括代币地址、币对交易所地址以及工厂管理的交易所总数。
1 address public factory; 复制1
构造函数做的唯一事情是指定 feeToSetter
。 工厂开始时没有费用,只有 feeSetter
可以改变这种情况。
1 address public factory; 复制2
此函数返回交易配对的数量。
1 address public factory; 复制3
这是工厂的主要函数,可以在两个 ERC-20 代币之间创建配对交易。 注意,任何人都可以调用此函数。 不需要 Uniswap 许可就能创建新的币对交易所。
1 address public factory; 复制4
我们希望新交易所的地址是可以确定的,这样就可以在链下提前计算(这对于二层网络交易来说比较有用)。 为此,无论收到代币地址的顺序如何,我们需要代币地址始终按顺序排列,因此我们在此处对它们排序。
1 address public factory; 复制5
大流动资金池优于小流动资金池,因为其价格比较稳定。 我们不希望每一对代币有多个流动性池。 如果已经有一个交易所,则无需为相同代币对创建另一个交易所。
1 address public factory; 复制6
要创建新合约,我们需要使用创建它的代码(包括构造函数和写入用于存储实际合约以太坊虚拟机字节码的代码)。 在 Solidity 语言中,通常只需使用 addr = new <name of contract>(<constructor parameters>)
的格式语句,然后编译器就可以完成所有的工作,不过为了获取一个确定的合约地址,需要使用 CREATE2 操作码。 在编写这个代码时,Solidity 还不支持操作码,因此需要手动获取该代码。 目前这已经不再是问题,因为 Solidity 现已支持 CREATE2。
1 address public factory; 复制7
当 Solidity 不支持操作码时,我们可以通过内联汇编来调用。
1 address public factory; 复制8
调用 initialize
函数来告诉新兑换交易可以兑换哪两种代币。
1 address public factory; 复制9
在状态变量中保存新的配对信息,并激发一个事件来告知外界新的配对交易合约已生成。
1 address public token0;2 address public token1; 复制0
这两个函数允许 feeSetter
管理费用接收人(如果有)并将 feeSetter
更改为新地址。
UniswapV2ERC20.sol
本合约实现 ERC-20 流动性代币。 它与 OpenZeppelin ERC-20 合约相似,因此这里仅解释不同的部分,即 permit
的功能。
以太坊上的交易需要消耗以太币 (ETH),相当于实际货币。 如果你有 ERC-20 代币但没有以太币,就无法发送交易,因而不能用代币做任何事情。 避免该问题的一个解决方案是元交易。 代币的所有者签署一个交易,允许其他人从链上提取代币,并通过网络发送给接收人。 接收人拥有以太币,可以代表所有者提交许可。
1 address public token0;2 address public token1; 复制1
此哈希值是这种交易类型的标识。 在这里,我们仅支持带有这些参数的 Permit
。
1 address public token0;2 address public token1; 复制2
接收人无法伪造数字签名。 但是,可以将同一笔交易发送两次(这是一种重放攻击)。 为防止发生这种情况,我们使用了随机数。 如果新 Permit
的随机数不是上次使用的随机数加一,我们认为它无效。
1 address public token0;2 address public token1; 复制3
这是获取链标识符的代码。 它使用一种名为 Yul 的以太坊虚拟机汇编语言。 请注意,在当前版本 Yul 中,必须使用 chainid()
,而非 chainid
。
1 address public token0;2 address public token1; 复制4
1 address public token0;2 address public token1; 复制5
这是实现批准功能的函数。 它接收相关字段作为参数,并将三个标量值(v、r 和 s)作为签名。
1 address public token0;2 address public token1; 复制6
截止日期后请勿接受交易。
1 address public token0;2 address public token1; 复制7
abi.encodePacked(...)
是我们预计将收到的信息。 我们知道随机数应该是什么,所以不需要将它作为一个参数。
以太坊签名算法预计获得 256 位用于签名,所以我们使用 keccak256
哈希函数。
1 address public token0;2 address public token1; 复制8
从摘要和签名中,我们可以用 ecrecover 函数计算出签名的地址。
1 address public token0;2 address public token1; 复制9
外围合约
外围合约是用于 Uniswap 的 API(应用程序接口)。 它们可用于其他合约或去中心化应用程序进行的外部调用。 你可以直接调用核心合约但更为复杂,如果你出错,则可能会损失价值。 核心合约只包含确保它们不会遭受欺骗的测试,不会对其他调用者进行健全性检查。 它们在外围,因此可以根据需要进行更新。
UniswapV2Router01.sol
本合约存在问题,不应该再使用。 幸运的是,外围合约无状态,也不拥有任何资产,弃用外围合约比较容易。建议使用 UniswapV2Router02
来替代。
UniswapV2Router02.sol
1 uint112 private reserve0; // uses single storage slot, accessible via getReserves2 uint112 private reserve1; // uses single storage slot, accessible via getReserves 复制0
其中大部分我们都曾遇到过,或相当明显。 一个例外是 IWETH.sol
。 Uniswapv2 允许兑换任意一对 ERC-20 代币,但以太币 (ETH) 本身并不是 ERC-20 代币。 它早于该标准出现,并采用独特的机制转换。 为了在适用于 ERC-20 代币的合约中使用以太币,人们制定出包装以太币 (WETH) 合约。 你发送以太币到该合约,它会为你铸造相同金额的包装以太币。 或者你可以销毁包装以太币,然后换回以太币。
1 uint112 private reserve0; // uses single storage slot, accessible via getReserves2 uint112 private reserve1; // uses single storage slot, accessible via getReserves 复制1
路由需要知道使用哪个工厂,以及对于需要包装以太币的交易,要使用什么包装以太币合约。 这些值是不可修改的,意味着它们只能在构造函数中设置。 这使得用户相信没有人能够改变这些值,让它们指向有风险的合约。
1 uint112 private reserve0; // uses single storage slot, accessible via getReserves2 uint112 private reserve1; // uses single storage slot, accessible via getReserves 复制2
此修改函数确保有时间限制的交易(如果可以,请在 Y 之前执行 X)不会在时限后发生。
1 uint112 private reserve0; // uses single storage slot, accessible via getReserves2 uint112 private reserve1; // uses single storage slot, accessible via getReserves 复制3
构造函数仅用于设置不可变的状态变量。
1 uint112 private reserve0; // uses single storage slot, accessible via getReserves2 uint112 private reserve1; // uses single storage slot, accessible via getReserves 复制4
当我们将代币从包装以太币合约换回以太币时,需要调用此函数。 只有我们使用的包装以太币合约才有权完成此操作。
增加流动资金
这些函数添加代币进行配对交易,从而增大了流动资金池。
1 uint112 private reserve0; // uses single storage slot, accessible via getReserves2 uint112 private reserve1; // uses single storage slot, accessible via getReserves 复制5
此函数用于计算应存入币对交易所的 A 代币和 B 代币的金额。
1 uint112 private reserve0; // uses single storage slot, accessible via getReserves2 uint112 private reserve1; // uses single storage slot, accessible via getReserves 复制6
这些是 ERC-20 代币合约的地址。
1 uint112 private reserve0; // uses single storage slot, accessible via getReserves2 uint112 private reserve1; // uses single storage slot, accessible via getReserves 复制7
这些是流动资金提供者想要存入的代币数额。 它们也是要存入的代币 A 和 B 的最大金额。
1 uint112 private reserve0; // uses single storage slot, accessible via getReserves2 uint112 private reserve1; // uses single storage slot, accessible via getReserves 复制8
这些是可接受的最低存款数额。 如果在达到最小金额或更高金额时交易无法完成,则会回滚交易。 如果不想要此功能,将它们设定为零即可。
流动性提供者指定最低金额,往往是因为他们想要限制交易汇率,使其在与当前汇率接近。 如果汇率波动太大,可能意味着基础价值可能发生改变,流动性提供者需要自己决定采取什么措施。
例如,想象汇率是一比一时,流动性提供者指定了以下值:
参数 | 值 |
---|---|
amountADesired | 1000 |
amountBDesired | 1000 |
amountAMin | 900 |
amountBMin | 800 |
只要汇率保持在 0.9 至 1.25 之间,交易就会进行。 如果汇率超出这个范围,交易将被取消。
这种预防措施的原因是交易不是即时的,你提交交易,最后验证者才会将它们包含在一个区块中(除非你的燃料价格非常低,在这种情况下你需要提交另一个具有相同随机数的交易以及更高的燃料价格来覆盖它)。 在提交交易和交易包含到区块中之间发生的事情是无法控制的。
1 uint112 private reserve0; // uses single storage slot, accessible via getReserves2 uint112 private reserve1; // uses single storage slot, accessible via getReserves 复制9
该函数返回流动性提供者应存入的金额,存入该金额是为了让比率等于当前储备金之间的比率。
1 uint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves 复制0
如果还没有此代币对的兑换交易,则创建一个。
1 uint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves 复制1
获取配对中的当前储备金。
1 uint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves 复制2
如果当前储备金为空,那么这是一笔新的配对交易。 存入的金额应与流动性提供者想要提供的金额完全相同。
1 uint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves 复制3
如果我们需要知道这些金额是多少,可以使用此函数获得最佳金额。 我们想要与当前储备相同的比率。
1 uint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves 复制4
如果 amountBOptimal
小于流动性提供者想要存入的金额,意味着代币 B 目前比流动性存款人所认为的价值更高,所以需要更少的金额。
1 uint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves 复制5
如果 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),但核心合约只是检查自己没有遭受欺骗。因此,如果汇率在提交交易至执行交易之间发生变化,你将面临损失资金价值的风险。 如果使用外围合约,它会计算你应该存入的金额并会立即存入,所以汇率不会改变,你不会有任何损失。
1 uint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves 复制6
此函数可以在交易中调用,用于存入流动资金。 大多数参数与上述 _addLiquidity
中相同,但有两个例外:
. to
是会获取新流动池代币的地址,这些代币铸造用于显示流动资金提供者在池中所占比率 deadline
是交易的时间限制
1 uint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves 复制7
我们计算实际存入的金额,然后找到流动资金池的帐户地址。 为了节省燃料,我们不是通过询问工厂执行此操作,而是使用库函数 pairFor
(参见如下程序库)
1 uint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves 复制8
将正确数额的代币从用户帐户转到配对交易。
1 uint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves 复制9
反过来,将流动资金池的部分所有权赋予 to
地址的流动性代币。 核心合约的 mint
函数查看合约有多少额外代币(与上次流动性发生变化时合约持有的金额比较),并相应地铸造流动性代币。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制00
当流动资金提供者想要向代币/以太币配对交易提供流动资金时,存在一些差别。 合约为流动性提供者处理以太币包装。 用户不需要指定想要存入多少以太币,因为用户直接通过交易发送以太币(金额在 msg.value
中)。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制01
为了将以太币存入合约,首先将其包装成包装以太币,然后将包装以太币转入配对。 请注意转账在 assert
中包装。 这意味着如果转账失败,此合约调用也会失败,因此包装不会真正发生。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制02
用户已经向我们发送了以太币,因此,如果还有任何额外以太币剩余(因为另一种代币比用户所认为的价值更低),我们需要发起退款。
撤回流动资金
下面的函数将撤回流动资金并还给流动资金提供者。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制03
最简单的流动资金撤回案例。 对于每种代币,都有一个流动性提供者同意接受的最低金额,必须在截止时间之前完成。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制04
核心合约的 burn
函数处理返还给用户的代币。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制05
某个函数返回多个值时,如果我们只对其中部分值感兴趣,以下便是我们只获取那些值的方式。 从消耗燃料的角度来说,这样比读取那些从来不用的值更加经济。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制06
将按从核心合约返回代币的路径(低位代币地址优先)调整为用户期望的方式(对应于 tokenA
和 tokenB
)。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制07
可以首先进行转账,然后再核实转账是否合法,因为如果不合法,我们可以回滚所有的状态更改。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制08
撤回以太币流动性的方式几乎是一样的,区别在于我们首先会收到包装以太币代币,然后将它们兑换成以太币并退还给流动性提供者。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制09
这些函数转发元交易,通过许可证机制使没有以太币的用户能够从资金池中提取资金。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制10
此函数可以用于在传输或存储时收取费用的代币。 当代币有这类费用时,我们无法依靠 removeLiquidity
函数来告诉我们可以撤回多少代币。因此,我们需要先提取然后查询余额。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制11
最后这个函数将存储费用计入元交易。
交易
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制12
公开给交易者的函数可以调用此函数以执行内部处理。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制13
在撰写此教程时,已有 388,160 个 ERC-20 代币。 如果每个代币对都有币对交易所,币对交易所将超过 1500 亿个。 目前,整个链上的帐户数量仅为该数量的 0.1%。 实际上,兑换函数支持路径这一概念。 交易者可以将 A 代币兑换成 B、B 代币兑换成 C、C 代币兑换成 D,因此不需要直接的 A-D 币对交易所。
这些市场上的价格往往是同步的,因为当价格不同步时,就会为套利创造机会。 设想一下,例如有三种代币 A、B 和 C。有三个币对交易所,每对代币一个。
初始情况
交易者出售 24.695 A 代币,获得 25.305 B 代币。
交易者卖出 24.695 个 B 代币得到 25.305 个 C 代币,大约获得 0.61 个 B 代币的利润。
随后,该交易者卖出 24.695 个 C 代币得到 25.305 个 A 代币,大约获得 0.61 个 C 代币的利润。 该交易者还多出了 0.61 个 A 代币(交易者最终拥有的 25.305 个A 代币,减去原始投资 24.695 个 A 代币)。
步骤 | A-B 兑换 | B-C 兑换 | A-C 兑换 |
---|---|---|---|
1 | A:1000 B:1050 A/B=1.05 | B:1000 C:1050 B/C=1.05 | A:1050 C:1000 C/A=1.05 |
2 | A:1024.695 B:1024.695 A/B=1 | B:1000 C:1050 B/C=1.05 | A:1050 C:1000 C/A=1.05 |
3 | A:1024.695 B:1024.695 A/B=1 | B:1024.695 C:1024.695 B/C=1 | A:1050 C:1000 C/A=1.05 |
4 | A:1024.695 B:1024.695 A/B=1 | B:1024.695 C:1024.695 B/C=1 | A:1024.695 C:1024.695 C/A=1 |
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制14
获取我们当前处理的配对,排序后(以便与配对一起使用)获得预期的输出金额。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制15
获得预期的金额后,按配对交易所需方式排序。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制16
这是最后一次兑换吗? 如果是,将收到用于交易的代币发送到目的地址。 如果不是,则将代币发送到下一个币对交易所。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制17
真正调用配对交易来兑换代币。 我们不需要回调函数来了解交易信息,因此没有在该字段中发送任何字节。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制18
交易者直接使用此函数来兑换代币。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制19
此参数包含 ERC-20 合约的地址。 如上文所述,此参数是一个数组,因为可能需要通过多个币对交易所将现有资产变为想要的资产。
Solidity 中的函数参数可以存入 memory
或者 calldata
。 如果此函数是合约的入口点,在由用户(通过交易)直接调用或从另一个合约调用时,那么参数的值可以直接从调用数据中获取。 如果函数是内部调用,如上述 _swap
函数,则参数必须存储在 memory
中。 从所调用合约的角度来看,calldata
为只读变量。
对于标量类型,如 uint
或 address
,编译器可以为我们处理存储选择,但对于数组,由于它们需要更多的存储空间也消耗更多的燃料,我们需要指定要使用的存储类型。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制20
返回值总是返回内存中。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制21
计算每次兑换时要购买的代币金额。 如果金额低于交易者愿意接受的最低金额,则回滚该交易。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制22
最后,将初始的 ERC-20 代币转到第一个配对交易的帐户中,然后调用 _swap
。 所有这些都发生在同一笔交易中,因此币对交易所知道任何意料之外的代币都是此次转账的一部分。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制23
前一个函数 swapTokensForTokens
,使交易者可以指定自己愿意提供的输入代币的准确数量和愿意接受的输出代币的最低数量。 此函数可以撤销兑换,使交易者能够指定想要的输出代币数量以及愿意支付的输入代币最大数量。
在这两种情况下,交易者必须首先给予此外围合约一定的额度,用于转账。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制24
这四种转换方式都涉及到以太币和代币之间的交易。 唯一不同的是,我们要么从交易者处收到以太币,并使用以太币铸造包装以太币,要么从路径上的最后一个交易所收到包装以太币并销毁,然后将产生的以太币再发送给交易者。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制25
此内部函数用于兑换有转账或存储费用的代币,以解决(此问题)。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制26
由于有转账费用,我们不能依靠 getAmountsOut
函数来告诉我们每次转账完成后的金额(调用原来的 _swap
函数之前可以这样做)。 相反,我们必须先完成转账然后再查看我们获得的代币数量。
注意:理论上我们可以使用此函数而非 _swap
,但在某些情况下(例如,如果因为在最后无法满足所需最低金额而导致转账回滚),最终会消耗更多燃料。 有转账费用的代币很少见,所以,尽管我们需要接纳它们,但不需要让所有的兑换都假定至少需要兑换一种需要收取转账费用的代币。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制27
这些方式与用于普通代币的相同,区别在于它们调用的是_swapSupportingFeeOnTransferTokens
。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制28
这些函数仅仅是调用 UniswapV2Library 函数的代理。
UniswapV2Migrator.sol
这个合约用于将交易从旧版 v1 迁移至 v2。 目前版本已经迁移,便不再相关。
程序库
SafeMath 库是一个文档很完备的程序库,这里便无需赘述了。
数学
此库包含一些 Solidity 代码通常不需要的数学函数,因而它们不是 Solidity 语言的一部分。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制29
首先赋予 x 一个大于平方根的估值(这是我们需要把 1-3 当作特殊情况处理的原因)。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制30
获取一个更接近的估值,即前一个估值与我们试图找到其方根值的数值的平均数除以前一个估值。 重复计算,直到新的估值不再低于现有估值。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制31
我们永远不需要零的平方根。 1、2 和 3 的平方根大致为 1(我们使用的是整数,所以忽略小数)。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制32
定点小数 (UQ112x112)
该库处理小数,这些小数通常不属于以太坊计算的一部分。 为此,它将数值编码x为 x*2^112。 这使我们能够使用原来的加法和减法操作码,无需更改。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制33
Q112
是 1 的编码。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制34
因为 y 是uint112
,所以最多可以是 2^112-1。 该数值还可以编码为 UQ112x112
。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制35
如果我们需要两个 UQ112x112
值相除,结果不需要再乘以 2^112。 因此,我们为分母取一个整数。 我们需要使用类似的技巧来做乘法,但不需要将 UQ112x112
的值相乘。
UniswapV2Library
此库仅被外围合约使用
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制36
按地址对这两个代币排序,所以我们将能够获得相应的配对交易地址。 这很有必要,否则就会出现两种可能性,一种是参数 A、B,而另一种是参数 B、A,这导致两次交易而非一次。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制37
此函数计算两种代币的配对交易地址。 此合约使用 CREATE2 操作码创建,如果我们知道它使用的参数,我们可以使用相同的算法计算地址。 这比查询工厂便宜得多,而且
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制38
此函数返回配对交易所拥有的两种代币的储备金。 请注意,它可以任意顺序接收代币并将代币排序,以便内部使用。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制39
如果不涉及交易费用的话,此函数将返回给你代币 A 兑换得到的代币 B。 此计算考虑到转账可能会改变汇率。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制40
如果使用配对交易没有手续费,上述 quote
函数非常有效。 然而,如果有 0.3% 的手续费,你实际得到的金额就会低于此值。 此函数可以计算缴纳交易费用后的金额。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制41
Solidity 本身不能进行小数计算,所以不能简单地将金额乘以 0.997。 作为替代方法,我们将分子乘以 997,分母乘以 1000,也能取得相同的效果。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制42
此函数大致完成相同的功能,但它会获取输出数额并提供输入代币的数量。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制43
在需要进行数次配对交易时,可以通过这两个函数获得相应数值。
转账帮助
此库添加了围绕 ERC-20 和以太坊转账的成功检查,并以同样的方式处理回滚和返回 false
值。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制44
我们可以通过以下两种方式调用不同的合约:
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制45
为了与之前的 ERC-20 标准创建的代币反向兼容,ERC-20 调用失败可能有两种情况:回退(在这种情况下 success
即是 false
),或者调用成功但返回 false
值(在这种情况下有输出数据,将其解码为布尔值,会得到 false
)。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制46
此函数实现了 ERC-20 的转账功能,可使一个帐户花掉由不同帐户所提供的额度。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制47
此函数实现了 ERC-20 的 transferFrom 功能,可使一个帐户花掉由不同帐户所提供的额度。
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制48
此函数将以太币转至一个帐户。 任何对不同合约的调用都可以尝试发送以太币。 因为我们实际上不需要调用任何函数,就不需要在调用中发送任何数据。
结论
本篇文章较长,约有 50 页。 如果你已读到此处,恭喜你! 希望你现在已经了解编写真实应用程序(相对于短小的示例程序)时的考虑因素,并且能够更好地为自己的用例编写合约。