本篇是系列的第八篇,将完成 UniswapV1 克隆版的最后一个重要组件:工厂合约。
本系列文章详细介绍 UniswapV1 的核心机制和实现原理,通过从零开始构建去中心化交易所,深入理解 AMM(自动做市商)机制。本篇是系列的第八篇,将完成 UniswapV1 克隆版的最后一个重要组件:工厂合约。
经过前面七篇文章的学习,我们已经实现了 Exchange 合约的所有核心功能,包括定价算法、代币兑换、流动性代币(LP tokens)以及手续费机制。现在我们的 UniswapV1 克隆版本已经接近完成,但还缺少一个关键组件:工厂合约(Factory Contract)。
工厂合约在 Uniswap 生态系统中扮演着至关重要的角色,它不仅充当所有交易所的注册中心,还提供了便捷的交易所部署功能。本篇文章将带您深入了解工厂合约的设计理念和实现细节。
工厂合约充当所有交易所的中央注册表,每个新部署的 Exchange 合约都会在工厂中进行注册。这种机制提供了以下重要功能:
工厂合约提供了无需编程技能即可部署交易所的能力:
通过限制每个代币只能有一个官方交易所,工厂合约确保:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
import "./Exchange.sol";
/**
* @title Factory
* @notice UniswapV1 工厂合约,负责管理和部署交易所
* @dev 实现交易所注册表和自动化部署功能
*/
contract Factory {
// @notice 代币地址到交易所地址的映射
// @dev 每个代币只能对应一个交易所
mapping(address => address) public tokenToExchange;
// @notice 交易所创建事件
// @param token 代币地址
// @param exchange 交易所地址
event ExchangeCreated(address indexed token, address indexed exchange);
}
/**
* @notice 为指定代币创建新的交易所
* @param _tokenAddress 要创建交易所的代币地址
* @return exchange 新创建的交易所地址
* @dev 每个代币只能创建一个交易所,避免流动性分散
*/
function createExchange(address _tokenAddress)
public
returns (address exchange)
{
// 验证代币地址不能为零地址
require(_tokenAddress != address(0), "Factory: invalid token address");
// 确保该代币尚未创建交易所
require(
tokenToExchange[_tokenAddress] == address(0),
"Factory: exchange already exists"
);
// 部署新的交易所合约
Exchange newExchange = new Exchange(_tokenAddress);
exchange = address(newExchange);
// 注册到映射表中
tokenToExchange[_tokenAddress] = exchange;
// 触发事件通知
emit ExchangeCreated(_tokenAddress, exchange);
return exchange;
}
实现要点说明:
new
操作符部署新的 Exchange 合约/**
* @notice 根据代币地址查询对应的交易所地址
* @param _tokenAddress 代币地址
* @return exchange 交易所地址,如果不存在则返回零地址
*/
function getExchange(address _tokenAddress)
public
view
returns (address exchange)
{
return tokenToExchange[_tokenAddress];
}
这个函数提供了通过接口访问注册表的标准方式,其他合约可以通过此函数查找特定代币的交易所。
为了支持代币间兑换功能,我们需要将 Exchange 合约与 Factory 合约关联:
contract Exchange is ERC20 {
// @notice 关联的代币合约地址
address public tokenAddress;
// @notice 工厂合约地址
address public factoryAddress;
/**
* @notice 构造函数
* @param _token 要交易的代币地址
* @dev 工厂地址自动设置为部署者(工厂合约)
*/
constructor(address _token) ERC20("Zuniswap-V1", "ZUNI-V1") {
require(_token != address(0), "Exchange: invalid token address");
tokenAddress = _token;
factoryAddress = msg.sender; // 工厂合约作为部署者
}
}
为了在 Exchange 合约中调用 Factory 的功能,我们需要定义接口:
/**
* @title IFactory
* @notice 工厂合约接口
*/
interface IFactory {
/**
* @notice 获取指定代币的交易所地址
* @param _tokenAddress 代币地址
* @return 交易所地址
*/
function getExchange(address _tokenAddress) external view returns (address);
}
代币间兑换(如 DAI → USDC)在 UniswapV1 中需要通过两步完成:
这种设计利用 ETH 作为中间媒介,简化了系统架构。
/**
* @notice 代币间兑换功能
* @param _tokensSold 出售的代币数量
* @param _minTokensBought 期望获得的最少代币数量
* @param _tokenAddress 目标代币地址
*/
function tokenToTokenSwap(
uint256 _tokensSold,
uint256 _minTokensBought,
address _tokenAddress
) public {
// 查找目标代币的交易所
address exchangeAddress = IFactory(factoryAddress).getExchange(_tokenAddress);
require(
exchangeAddress != address(this) && exchangeAddress != address(0),
"Exchange: 无效的交易所地址"
);
// 第一步:将用户代币兑换为 ETH
uint256 tokenReserve = getReserve();
uint256 ethBought = getAmount(
_tokensSold,
tokenReserve,
address(this).balance
);
// 转移用户代币到当前交易所
IERC20(tokenAddress).transferFrom(
msg.sender,
address(this),
_tokensSold
);
// 第二步:在目标交易所将 ETH 兑换为目标代币
IExchange(exchangeAddress).ethToTokenTransfer{value: ethBought}(
_minTokensBought,
msg.sender
);
}
/**
* @title IExchange
* @notice 交易所合约接口
*/
interface IExchange {
/**
* @notice 将 ETH 兑换为代币并发送给指定接收者
* @param _minTokens 最少获得的代币数量
* @param _recipient 代币接收者地址
*/
function ethToTokenTransfer(uint256 _minTokens, address _recipient)
external
payable;
}
为支持代币间兑换,我们需要重构原有的 ethToTokenSwap
函数:
/**
* @notice 内部函数:ETH 兑换代币的核心逻辑
* @param _minTokens 最少获得的代币数量
* @param recipient 代币接收者地址
*/
function ethToToken(uint256 _minTokens, address recipient) private {
uint256 tokenReserve = getReserve();
uint256 tokensBought = getAmount(
msg.value,
address(this).balance - msg.value,
tokenReserve
);
require(tokensBought >= _minTokens, "Exchange: insufficient output amount");
IERC20(tokenAddress).transfer(recipient, tokensBought);
}
/**
* @notice 用户调用的 ETH 兑换代币接口
* @param _minTokens 最少获得的代币数量
*/
function ethToTokenSwap(uint256 _minTokens) public payable {
ethToToken(_minTokens, msg.sender);
}
/**
* @notice 支持自定义接收者的 ETH 兑换代币接口
* @param _minTokens 最少获得的代币数量
* @param _recipient 代币接收者地址
*/
function ethToTokenTransfer(uint256 _minTokens, address _recipient)
public
payable
{
ethToToken(_minTokens, _recipient);
}
在 Solidity 中,new
操作符不仅仅是创建对象实例,它实际上会在区块链上部署一个新的合约:
在代币间兑换中,msg.sender
的值会发生变化:
msg.sender
是用户地址msg.sender
是调用合约的地址这种特性要求我们在设计跨合约调用时特别注意接收者地址的处理。
使用接口而非直接合约调用的优势:
// 避免零地址
require(_tokenAddress != address(0), "无效地址");
// 避免自引用
require(exchangeAddress != address(this), "不能是自己");
在代币间兑换中,确保状态更新在外部调用之前完成:
// 好的实践:先更新状态,再进行外部调用
tokenBalance -= _tokensSold;
IERC20(token).transfer(recipient, amount);
require(tokensBought >= _minTokens, "Exchange: insufficient output amount");
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
import "forge-std/Test.sol";
import "../src/Factory.sol";
import "../src/Exchange.sol";
import "../src/Token.sol";
contract FactoryTest is Test {
Factory factory;
Token tokenA;
Token tokenB;
address user = makeAddr("user");
address liquidityProvider = makeAddr("liquidityProvider");
function setUp() public {
factory = new Factory();
tokenA = new Token("Token A", "TKNA", 1000000 * 10**18);
tokenB = new Token("Token B", "TKNB", 1000000 * 10**18);
}
}
function testCreateExchange() public {
// 创建交易所
address exchangeAddress = factory.createExchange(address(tokenA));
// 验证交易所地址不为零
assertTrue(exchangeAddress != address(0));
// 验证映射关系正确
assertEq(factory.getExchange(address(tokenA)), exchangeAddress);
}
function testCannotCreateDuplicateExchange() public {
// 创建第一个交易所
factory.createExchange(address(tokenA));
// 尝试创建重复交易所,应该失败
vm.expectRevert("Factory: exchange already exists");
factory.createExchange(address(tokenA));
}
function testCannotCreateExchangeWithZeroAddress() public {
// 尝试使用零地址创建交易所,应该失败
vm.expectRevert("Factory: invalid token address");
factory.createExchange(address(0));
}
function testTokenToTokenSwap() public {
// 创建两个交易所
address exchangeAAddress = factory.createExchange(address(tokenA));
address exchangeBAddress = factory.createExchange(address(tokenB));
Exchange exchangeA = Exchange(exchangeAAddress);
Exchange exchangeB = Exchange(exchangeBAddress);
// 为两个交易所添加流动性
vm.startPrank(liquidityProvider);
vm.deal(liquidityProvider, 20 ether);
tokenA.mint(liquidityProvider, 1000 * 10**18);
tokenB.mint(liquidityProvider, 1000 * 10**18);
tokenA.approve(exchangeAAddress, 1000 * 10**18);
tokenB.approve(exchangeBAddress, 1000 * 10**18);
exchangeA.addLiquidity{value: 10 ether}(1000 * 10**18);
exchangeB.addLiquidity{value: 10 ether}(1000 * 10**18);
vm.stopPrank();
// 用户进行代币间兑换
vm.startPrank(user);
tokenA.mint(user, 10 * 10**18);
tokenA.approve(exchangeAAddress, 10 * 10**18);
uint256 balanceBefore = tokenB.balanceOf(user);
exchangeA.tokenToTokenSwap(
10 * 10**18, // 出售 10 个 tokenA
1, // 最少获得 1 个 tokenB
address(tokenB)
);
uint256 balanceAfter = tokenB.balanceOf(user);
// 验证用户获得了 tokenB
assertTrue(balanceAfter > balanceBefore);
vm.stopPrank();
}
# 运行所有测试
forge test
# 运行特定测试文件
forge test --match-contract FactoryTest
# 详细输出
forge test -vvv
# 生成 Gas 报告
forge test --gas-report
通过实现工厂合约,我们完成了 UniswapV1 克隆版的所有核心功能。工厂合约作为系统的注册中心和部署工具,提供了以下关键价值:
完整的项目代码已托管在 GitHub 上,包含所有合约实现、详细测试和部署脚本。建议读者克隆代码进行实践学习,通过动手操作加深对 UniswapV1 机制的理解。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!