在前面的两篇文章中,我们已经已经实现了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做关联,我们需要添加一个新的状态变量来存储工厂合约的地址,并且需要在构造函数中完成。
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。虽然它们基本上是相同的东西,相同的集合或核心原则,但它提供了一些新的强大功能。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!