我们的UniswapV2已经实现了最关键的部分——配对合约。不过我们还没有算上一些协议交易的费用(Uniswap从每笔流动性存款中收取的费用),这篇文章,我们会实现这部分内容,
文章写到现在,我们的UniswapV2已经实现了最关键的部分——配对合约。不过我们还没有算上一些协议交易的费用(Uniswap 从每笔流动性存款中收取的费用),这篇文章,我们会实现这部分内容,虽然这不是用户交易的关键部分,但是也很重要。
这篇文章,我们会继续完善工厂合约,它作为已经部署的配对合约的注册表,起到一个关键作用,我们还会在基础合约的上层实现一些高级合约,这些合约可以方便的和用户进行交互,提升Uniswap的易用性。
工厂合约是所有已经部署的配对合约的注册表,这个合约是很有必要的。因为我们并不希望拥有多个相同的交易对,因为如果存在多个相同的代币合约对,流动性就会被分散。并且,工厂合约简化了配对合约的部署,我们无需手动部署配对合约,只需要调用工厂合约的方法即可。
Uniswap 中只部署了一个工厂合约,该合约是Uniswap 官方的交易对注册表,这个注册表在代币发现方面很有用,人们可以通过查询合约中代币地址找到相应的货币对,除此之外,还可以扫描合约事件的历史记录来查找所有已经部署的合约。当然我们也是可以自己手动部署货币对合约的。
我们来看看代码应该如何实现:
contract ZuniswapV2Factory {
error IdenticalAddresses();
error PairExists();
error ZeroAddress();
event PairCreated(
address indexed token0,
address indexed token1,
address pair,
uint256
);
mapping(address => mapping(address => address)) public pairs;
address[] public allPairs;
...
从上面的代码结构来看,工厂合约是比较简单的:它仅在创建一个配对合约的时候发出一个PairCreated
事件,并且存储所有创建的配对合约的映射。
但是创建配对合约并不是一件容易的事情,我们先来看代码实现:
function createPair(address tokenA, address tokenB)
public
returns (address pair)
{
if (tokenA == tokenB) revert IdenticalAddresses();
(address token0, address token1) = tokenA < tokenB
? (tokenA, tokenB)
: (tokenB, tokenA);
if (token0 == address(0)) revert ZeroAddress();
if (pairs[token0][token1] != address(0)) revert PairExists();
bytes memory bytecode = type(ZuniswapV2Pair).creationCode;
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
assembly {
pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
}
IZuniswapV2Pair(pair).initialize(token0, token1);
pairs[token0][token1] = pair;
pairs[token1][token0] = pair;
allPairs.push(pair);
emit PairCreated(token0, token1, pair, allPairs.length);
}
首先,我们不允许传入的两个代币是一样的,请注意,这里我们并不会检查代币合约是否是真实存在的,这是用户的行为。工厂合约并不关心。
接下来,我们对代币进行排序,这个步骤非常重要,它可以避免重复(配对合约允许双向交换),此外,配对 token 代币的地址用于生成配对合约的地址,我们后面会说到这部分内容。接下来就是最核心的部分了,如何创建一个配对合约。
在以太坊中,合约可以部署合约。我们可以调用一个已经部署的合约的函数,该函数将部署另一个合约,并且我们无需从计算机层面来编译,只借助已经部署的合约就可以。
在 EVM 中,有两个部署合约的操作码:
CREATE从一开始就存在于 EVM 中。这个操作码会创建一个新帐户(以太坊地址),并在该地址部署合约代码。新地址是根据部署者合约的 nonce 计算得出的——这和你手动部署合约时确定合约地址的方式相同。Nonce 是地址成功交易的计数器:当你发送交易时,你的地址 nonce 就会增加。生成新帐户地址时对 nonce 的依赖使CREATE
具有不确定性:地址取决于部署者地址的 nonce,而你无法控制它。部署者的地址我们都是清楚的,但是 nonce 在每次部署的时候都不相同。
CREATE2 ,在EIP-1014中添加。此操作码的作用与CREATE
完全相同,但它允许确定性地生成新合约的地址CREATE2
不使用外部状态(如其他合约的随机数)来生成合约地址,并让我们完全控制如何 地址生成完毕,你不需要知道nonce
,你只需要知道部署的合约字节码(静态的)和 salt (由你选择的字节序列)。
让我们回到代码层面来看下:
...
bytes memory bytecode = type(ZuniswapV2Pair).creationCode;
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
assembly {
pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
}
...
第一行我们得到了ZuniswapV2Pair
合约的创建字节码,创建字节码是真正的智能合约字节码,它包括:
1、构造函数的逻辑,这部分负责智能合约的初始化和部署,并不会存储在区块链上。
2、运行时的字节码,即合约的实际业务逻辑,这部分的字节码是存储在以太坊区块链上的。
我们想在这里使用完整的字节码。
下一行创建 salt,用于确定性地生成新合约地址的字节序列。这里,我们对 token 对的地址进行哈希处理创建 salt,这意味着每个唯一的代币对只会产生唯一的 salt,并且每个代币对合约生成的地址也是唯一的。
最后一行我们调用 create2 操作码:
1、使用 bytecode + salt 确定性地创建一个新的地址
2、部署新的 ZuniswapV2Pair 合约。
3、最终得到了配对合约的地址。
createPair 的其余部分还是比较清晰的:
1、 部署完毕一个配对合约后,我们需要对其进行初始化,也就是设置其代币:
// ZuniswapV2Pair.sol
function initialize(address token0_, address token1_) public {
if (token0 != address(0) || token1 != address(0))
revert AlreadyInitialized();
token0 = token0_;
token1 = token1_;
}
2、 然后,新的配对会被存储到allPairs
数组中。
3、 最后,我们可以触发 PairCreated 事件。
我们现在准备开启本系列的一个新的更大的篇章:我们开始研究 Router 路由合约。
Router 合约是一个高级合约,或者称之为上层合约,它可以作为大多数用户程序的入口点,这个合约让一些操作变的更加容易,比如:创建配对、添加流动性、删除流动性、执行交换。Router 合约适用于通过工厂合约部署的所有的配...
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!