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

  • Louis
  • 更新于 2024-07-22 18:42
  • 阅读 1111

在前面的两篇文章中,我们已经已经实现了Exchange合约的所有核心机制,包括定价功能、交换、LP代币和费用。看起来已经比较完善了,但是还缺少了一部分:工厂合约。本篇文章,我们就来实现它。

开篇介绍

在前面的两篇文章中,我们已经已经实现了Exchange合约的所有核心机制,包括定价功能、交换、LP代币和费用。看起来已经比较完善了,但是还缺少了一部分:工厂合约。本篇文章,我们就来实现它。

工厂合约的作用是什么

事实上工厂合约充当的角色是Exchange合约的注册表:每个新部署的Exchange合约都在工厂中注册。这是一个重要的机制:任何Exchange合约都可以通过查询注册表找到。通过注册表,当用户尝试将一个Token代币交换为另一个Token代币(而不是以太币)的时候,Exchange合约可以通过工厂合约找到其他的Exchange合约。

工厂合约提供的另一个比较实用的功能是能够方便的部署Exchange合约而无需处理代码、节点、部署脚本和任何其他开发工具。

Factory实现了一个功能,允许用户只需调用该函数即可创建和部署Exchange合约。所以,今天我们还将学习一个合约如何部署另一个合约。

Uniswap 只有一份工厂合约,因此只有一份 Uniswap 交易对的注册表。然而,没有什么可以阻止其他用户部署自己的工厂,甚至是未在官方工厂注册的Exchange合约。虽然这是可能的,但这样的交易所不会被 Uniswap 认可,也无法通过官方网站使用它们来交换代币。

让我们看看代码如何实现:

工厂合约的实现

工厂合约本质上是一个注册表,我们需要一个数据结构来存储Exchange合约,这应该是一个地址到地址的映射,它允许通过代币查找另外一个代币(1 个交换只能交换 1 个代币,还记得吗?)。

pragma solidity ^0.8.24;

import "./Exchange.sol";

contract Factory {
    mapping(address => address) public tokenToExchange;

    ...

接下来是 createExchange 函数,它允许通过Token地址来创建和部署Exchange合约:

function createExchange(address _tokenAddress) public returns (address) {
  require(_tokenAddress != address(0), "invalid token address");
  require(
    tokenToExchange[_tokenAddress] == address(0),
    "exchange already exists"
  );

  Exchange exchange = new Exchange(_tokenAddress);
  tokenToExchange[_tokenAddress] = address(exchange);

  return address(exchange);
}

让我们来做个简单的梳理:

这里有两项检查:

1、第一个确保令牌地址不是零地址( 0x0000000000000000000000000000000000000000 )。

2、下一步确保Token合约还没有被添加到注册表中(默认地址值为零地址)。我们的想法是,我们不希望同一个Token代币有不同的Exchange合约,因为我们不希望流动性分散在多个Exchange合约中。它应该更好地集中在一个Exchange合约,以减少滑点并提供更好的汇率。

Exchange exchange = new Exchange(_tokenAddress);

创建一个新的 Exchange 合约实例,并将 _tokenAddress 传递给该合约的构造函数。

我们使用提供的代币地址实例化 Exchange,这就是我们之前需要导入“Exchange.sol”的原因。这种实例化类似于OOP语言中的类实例化,但是,在Solidity中, new 操作符实际上会部署一个合约。返回的值具有Exchange合约的类型,并且每个合约都可以转换为地址 - 这就是我们在下一行中所做的,以获取新交易所的地址并将其保存到注册表中。

tokenToExchange[_tokenAddress] = address(exchange);
return address(exchange);

将新创建的 Exchange 合约地址存储在映射 tokenToExchange 中,以便以后可以通过代币地址查找到对应的兑换合约。返回新创建的 Exchange 合约的地址。

要完成合约,我们只需要再实现一个函数 - getExchange ,这将允许我们通过另一个合约的接口查询注册表:

function getExchange(address _tokenAddress) public view returns (address) {
  return tokenToExchange[_tokenAddress];
}

工厂就这样了!这真的很简单。

接下来,我们需要改进Exchange合约,以便它可以使用工厂来执行代币到代币的交换。

将Exchange合约和Factory合约做关联

首先,我们需要将Exchange合约和Factory做关联,因为每个Exchange合约都需要知道Factory合约的地址,并且我们并不期望硬编码,硬编码缺乏灵活性。如果想要将Exchange合约和Factory做关联,我们需要添加一个新的状态变量来存储工厂合约的地址,并且需要在构造函数中完成。

contract Exchange is ERC20 {
    address public tokenAddress;
    address public factoryAddress; // <--- new line

    constructor(address _token) ERC20("Suniswap-V1", "SUNI-V1") {
        require(_token != address(0), "invalid token address");

        tokenAddress = _token;
        factoryAddress = msg.sender;  // <--- new line

    }
    ...
}

就是这样。现在已准备好进行Token代币到另一个Token代币的交换。让我们来实现它。

代币到代币互换

当我们有两个通过注册表链接的Exchange合约时,我们如何将一个Token代币交换为另一个Token代币呢?也许是这样的:

1、将一个标准ERC20 Token和以太币进行兑换;

2、不要将以太币发送给用户,而是要将目标Token的地址通过映射找出来。

3、如果Exchange合约存在,用以太币发送到对应的合约兑换为那个对应的Token代币。

4、将兑换出来的Token代币发送给用户。

这个想法看起来不错,我们尝试实现这个想法,我们会创建一个函数名字就叫做tokenToTokenSwap

// Exchange.sol

function tokenToTokenSwap(
    uint256 _tokensSold,
    uint256 _minTokensBought,
    address _tokenAddress
) public {
    ...

该函数接受三个参数:要出售的代币数量、要交换的最小代币数量、要交换已售代币的代币地址。

我们首先检查用户提供的代币地址是否存在兑换。如果没有,就会抛出错误。

address exchangeAddress = IFactory(factoryAddress).getExchange(
    _tokenAddress
);
require(
    exchangeAddress != address(this) && exchangeAddress != address(0),
    "invalid exchange address"
);

我们使用 IFactory ,它是工厂合约的接口。与其他合约(或 OOP 中的类)交互时使用接口是一个很好的实践。然而,接口不允许访问状态变量,这就是我们在工厂合约中实现 getExchange 函数的原因——这样我们就可以通过接口调用合约中的函数从而拿到状态变量。

interface IFactory {
  function getExchange(address _tokenAddress) external returns (address);
}

接下来,我们使用当前的Exchange合约将Token代币交换为以太币,将用户的代币转移到Exchange合约。

uint256 tokenReserve = getReserve();
uint256 ethBought = getAmount(
    _tokensSold,
    tokenReserve,
    address(this).balance
);

IERC20(tokenAddress).transferFrom(
    msg.sender,
    address(this),
    _tokensSold
);

该函数的最后一步是使用其他Exchange合约将以太币交换为代币:

IExchange(exchangeAddress).ethToTokenSwap{value: ethBought}(
    _minTokensBought
);

我们就完成了!

实际上并非如此。你能看出问题吗?让我们看一下 etherToTokenSwap 的最后一行:

IERC20(tokenAddress).transfer(msg.sender, tokensBought);

它将购买的代币发送到 msg.sender 。在 Solidity 中, msg.sender 是动态的,而不是静态的,它指向发起当前调用的人(或者在合约的情况下是什么)。当用户调用合约函数时,它会指向用户的地址。但是当一个合约调用另一个合约时, msg.sender 就是调用合约的地址!

因此, tokenToTokenSwap 会将代币发送到第一个Exchange合约的地址!但这不是问题,因为我们可以调用 ERC20(_tokenAddress).transfer(...) 将这些Token发送给用户。然而,有一个 getter 解决方案:让我们节省一些 Gas 并将代币直接发送给用户。为此,我们需要将 etherToTokenSwap 函数拆分为两个函数:

function ethToToken(uint256 _minTokens, address recipient) private {
  uint256 tokenReserve = getReserve();
  uint256 tokensBought = getAmount(
    msg.value,
    address(this).balance - msg.value,
    tokenReserve
  );

  require(tokensBought >= _minTokens, "insufficient output amount");

  IERC20(tokenAddress).transfer(recipient, tokensBought);
}

function ethToTokenSwap(uint256 _minTokens) public payable {
  ethToToken(_minTokens, msg.sender);
}

ethToToken 是私有函数,所有 ethToTokenSwap 都用于执行此操作,只有一个区别:它需要一个Token接收者的地址,这使我们可以灵活地选择要将Token发送给谁。 ethToTokenSwap 现在只是 ethToToken 的包装函数,始终将 msg.sender 作为接收者传递。

现在,我们需要另一个函数来将Token发送给自定义接收者。我们可以使用 ethToToken 来实现这一点,但让我们将其保留为私有且nopayable的。

function ethToTokenTransfer(uint256 _minTokens, address _recipient)
  public
  payable
{
  ethToToken(_minTokens, _recipient);
}

这只是 ethToTokenSwap 的副本,允许将Token发送给自定接收者。我们现在可以在 tokenToTokenSwap 函数中使用它:

    ...

    IExchange(exchangeAddress).ethToTokenTransfer{value: ethBought}(
        _minTokensBought,
        msg.sender
    );
}

我们将向发起交换的人发送Token代币。现在,我们完成了!

总结:

我们自己的 Uniswap V1 现已完成。如果您对如何改进有任何想法,请尝试一下!例如,交易所中有一个函数可以计算代币互换中代币的输出量。如果您在理解某些东西的工作原理时遇到任何问题,请随时检查测试。我已经介绍了所有功能,包括 tokenToTokenSwap

下次我们将开始学习Uniswap V2。虽然它们基本上是相同的东西,相同的集合或核心原则,但它提供了一些新的强大功能。

源码地址

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

0 条评论

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