手把手教你从0到1构建UniswapV2:part3

  • Louis
  • 发布于 4天前
  • 阅读 341

我们的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 代币的地址用于生成配对合约的地址,我们后面会说到这部分内容。接下来就是最核心的部分了,如何创建一个配对合约。

通过 create2 操作码部署合约

在以太坊中,合约可以部署合约。我们可以调用一个已经部署的合约的函数,该函数将部署另一个合约,并且我们无需从计算机层面来编译,只借助已经部署的合约就可以。

在 EVM 中,有两个部署合约的操作码:

CREATE 操作码:

CREATE从一开始就存在于 EVM 中。这个操作码会创建一个新帐户(以太坊地址),并在该地址部署合约代码。新地址是根据部署者合约的 nonce 计算得出的——这和你手动部署合约时确定合约地址的方式相同。Nonce 是地址成功交易的计数器:当你发送交易时,你的地址 nonce 就会增加。生成新帐户地址时对 nonce 的依赖使CREATE具有不确定性:地址取决于部署者地址的 nonce,而你无法控制它。部署者的地址我们都是清楚的,但是 nonce 在每次部署的时候都不相同。

CREATE2 操作码:

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 contract 路由合约

我们现在准备开启本系列的一个新的更大的篇章:我们开始研究 Router 路由合约。

Router 合约是一个高级合约,或者称之为上层合约,它可以作为大多数用户程序的入口点,这个合约让一些操作变的更加容易,比如:创建配对、添加流动性、删除流动性、执行交换。Router 合约适用于通过工厂合约部署的所有的配...

剩余50%的内容订阅专栏后可查看

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Louis
Louis
web3 developer,技术交流或者有工作机会可加VX: magicalLouis