介绍
Web3 交易所(合约篇)
# Web3 交易所(合约篇)
在本文中,我们将探索如何使用 Solidity 编写符合 ERC20 标准的代币合约和交易所合约,并通过 React 和 Wagmi 框架构建前端页面。如果你是 Web3 开发新手,建议先学习这篇文章构建 Web3 以太坊应用:Hardhat、Wagmi 与 Next.js 集成指南 (opens new window)。
# ERC20 代币合约实现
编写以太坊代币通常有两种方法:一是手动实现ERC20 (opens new window)协议的接口,二是继承OpenZeppelin (opens new window)提供的合约。为了更深入地理解 ERC20 协议,本文将采用第一种方法。
首先,在项目的contracts
文件夹下创建一个新的 Solidity 文件YexiyueToken.sol
:
// 指定许可证类型,这里是UNLICENSED,意味着无许可协议。
// SPDX-License-Identifier: UNLICENSED
// 设置合约使用Solidity版本,这里使用的是^0.8.24。
pragma solidity ^0.8.24;
// 创建一个名为YexiyueToken的合约。
contract YexiyueToken {
// 公开变量,储存代币名称。
string public name = "YexiyueToken";
// 代币拥有者的地址。
address public owner;
// 代币符号。
string public symbol = "YXT";
// 小数点精度,这里设置为18位小数。
uint256 public decimals = 18;
// 代币总供应量。
uint256 public totalSupply;
// 映射每个地址的代币余额。
mapping(address => uint256) public balanceOf;
// 映射允许他人从某地址转移代币的额度。
mapping(address => mapping(address => uint256)) public allowance;
// 构造函数,初始化代币总供应并分配给合约部署者。
constructor() payable {
totalSupply = 1000000 * (10 ** decimals);
owner = payable(msg.sender);
balanceOf[msg.sender] = totalSupply;
}
// 定义一个Transfer事件,用于记录转账行为。
event Transfer(address indexed from, address indexed to, uint256 value);
// 转账函数,允许一个地址向另一个地址转移代币。
function transfer(address _to, uint256 _value) public returns (bool) {
_transfer(msg.sender, _to, _value);
return true;
}
// 内部函数,处理实际的代币转移逻辑。
function _transfer(address _from, address _to, uint256 _value) internal {
// 检查目标地址和发送地址的有效性,以及发送者余额是否充足。
require(_to != address(0), "Invalid recipient address");
require(_from != address(0), "Invalid sender address");
require(balanceOf[_from] >= _value, "Insufficient balance");
// 更新余额,并触发Transfer事件。
balanceOf[_from] -= _value;
balanceOf[_to] += _value;
emit Transfer(_from, _to, _value);
}
// 定义Approval事件,记录代币授权行为。
event Approval(
address indexed _owner,
address indexed _spender,
uint256 _value
);
// 授权函数,允许一个地址(_spender)从另一个地址(_owner)提取一定额度的代币。
function approve(
address _spender,
uint256 _value
) public returns (bool success) {
require(_spender != address(0), "Invalid spender address");
// 要求_value大于0
require(_value > 0, "Approval value must be greater than 0");
allowance[msg.sender][_spender] = _value;
emit Approval(msg.sender, _spender, _value);
return true;
}
// 根据之前授权,从一个地址转移代币到另一个地址。
function transferFrom(
address _from,
address _to,
uint256 _value
) public returns (bool success) {
// 检查转出地址的余额和授权额度是否足够。
require(balanceOf[_from] >= _value, "Insufficient balance");
require(
allowance[_from][msg.sender] >= _value,
"Insufficient allowance"
);
// 减少授权额度并执行转移。
allowance[_from][msg.sender] -= _value;
_transfer(_from, _to, _value);
return true;
}
}
# 交易所合约开发
接下来,我们将编写交易所合约Exchange.sol
:
// SPDX-License-Identifier: UNLICENSED
// 指定智能合约的许可证标识,这里使用的是无许可证(UNLICENSED),表示代码没有许可证限制。
pragma solidity ^0.8.24;
// 指定编译器的版本,这里要求至少是0.8.24版本。
import "./YexiyueToken.sol";
// 导入本地路径下的YexiyueToken合约。
contract Exchange {
// 定义Exchange合约。
address public feeAccount;
// 交易费用账户的地址。
uint256 public feePercent;
// 交易费用百分比。
address constant ETHER = address(0);
// 定义一个常量,代表以太币的地址。
mapping(address => mapping(address => uint256)) public tokens;
// 定义一个二维映射,用于存储每个用户在每种代币中的余额。
event Deposit(
address indexed token,
address indexed user,
uint256 amount,
uint256 balance
);
// Deposit事件,记录存款操作。
event Withdraw(
address indexed token,
address indexed user,
uint256 amount,
uint256 balance
);
// Withdraw事件,记录提款操作。
struct Token {
address token;
uint256 amount;
}
// Token结构体,包含代币地址和数量。
struct _Order {
uint256 id;
address user;
Token getToken;
Token giveToken;
uint256 timestamp;
}
// _Order结构体,包含订单的详细信息。
mapping(uint256 => _Order) public orders;
// 订单映射,通过订单ID可以查询到订单详情。
uint256 public orderCount;
// 订单计数器。
mapping(uint256 => bool) public orderCancel;
// 订单取消状态映射。
mapping(uint256 => bool) public orderFill;
// 订单完成状态映射。
event Order(
uint256 indexed id,
address indexed user,
Token getToken,
Token giveToken,
uint256 timestamp
);
// Order事件,记录创建订单的操作。
event Cancel(
uint256 indexed id,
address indexed user,
Token getToken,
Token giveToken,
uint256 timestamp
);
// Cancel事件,记录取消订单的操作。
event Trade(
uint256 indexed id,
address indexed user,
Token getToken,
Token giveToken,
uint256 timestamp
);
// Trade事件,记录完成交易的操作。
constructor(address _feeAccount, uint256 _feePercent) payable {
// 构造函数,初始化费用账户和费用百分比。
feeAccount = _feeAccount;
feePercent = _feePercent;
}
function depositToken(address _token, uint256 _amount) public {
// 允许用户向合约存入指定的代币。
require(_token != ETHER);
// 确保不是以太币。
require(_amount > 0, "Amount must be greater than 0");
// 确保存入的数量大于0。
require(
YexiyueToken(_token).transferFrom(
msg.sender,
address(this),
_amount
)
);
// 调用YexiyueToken合约的transferFrom函数,从用户地址转移到合约地址。
tokens[_token][msg.sender] += _amount;
// 更新用户在指定代币中的余额。
emit Deposit(_token, msg.sender, _amount, tokens[_token][msg.sender]);
// 发出存款事件。
}
function depositEther() public payable {
// 允许用户向合约存入以太币。
tokens[ETHER][msg.sender] += msg.value;
// 更新用户在以太币中的余额。
emit Deposit(ETHER, msg.sender, msg.value, tokens[ETHER][msg.sender]);
// 发出存款事件。
}
function withdrawEther(uint256 _amount) public {
// 允许用户从合约提取以太币。
require(tokens[ETHER][msg.sender] >= _amount);
// 确保用户的余额足够。
tokens[ETHER][msg.sender] -= _amount;
// 更新用户的余额。
payable(msg.sender).transfer(_amount);
// 将指定数量的以太币转账给用户。
emit Withdraw(ETHER, msg.sender, _amount, tokens[ETHER][msg.sender]);
// 发出行提款事件。
}
function withdraw(address _token, uint256 _amount) public {
// 允许用户从合约提取指定的代币。
require(_token != ETHER);
// 确保不是以太币。
require(tokens[_token][msg.sender] >= _amount, "Insufficient balance");
// 确保用户的余额足够。
require(YexiyueToken(_token).transfer(msg.sender, _amount));
// 调用YexiyueToken合约的transfer函数,从合约地址转移到用户地址。
tokens[_token][msg.sender] -= _amount;
// 更新用户在指定代币中的余额。
emit Withdraw(_token, msg.sender, _amount, tokens[_token][msg.sender]);
// 发出行提款事件。
}
function balanceOf(address _token, address _user) public view returns (uint256) {
// 查看指定用户的代币余额。
return tokens[_token][_user];
}
function makeOrder(Token memory getToken, Token memory giveToken) public {
// 创建一个新的交易订单。
require(
tokens[giveToken.token][msg.sender] >= giveToken.amount,
"You don't have enough tokens"
);
// 确保用户有足够的代币进行交易。
orderCount += 1;
// 增加订单计数器。
orders[orderCount] = _Order(
orderCount,
msg.sender,
getToken,
giveToken,
block.timestamp
);
// 存储订单信息。
emit Order(
orderCount,
msg.sender,
getToken,
giveToken,
block.timestamp
);
// 发出创建订单事件。
}
function cancelOrder(uint256 _orderId) public {
// 取消一个已经创建的订单。
_Order memory order = orders[_orderId];
// 获取订单信息。
require(
order.user == msg.sender,
"You are not the owner of this order"
);
// 确保订单属于调用者。
orderCancel[_orderId] = true;
// 标记订单为取消。
emit Cancel(
_orderId,
msg.sender,
order.getToken,
order.giveToken,
block.timestamp
);
// 发出取消订单事件。
}
function fillOrder(uint256 _orderId) public {
// 完成一个交易订单。
_Order memory order = orders[_orderId];
// 获取订单信息。
require(order.user != msg.sender);
// 不能完成自己的订单。
require(
tokens[order.giveToken.token][order.user] >= order.giveToken.amount,
"Not enough token"
);
// 确保卖方有足够的代币。
uint256 feeAmount = (order.getToken.amount * feePercent) / 100;
// 计算交易费用。
require(
tokens[order.getToken.token][msg.sender] >=
(order.getToken.amount + feeAmount),
"Insufficient balance"
);
// 确保买方有足够的代币和交易费用。
// 完成代币转移和费用支付。
tokens[order.getToken.token][msg.sender] -= (order.getToken.amount + feeAmount);
tokens[order.getToken.token][feeAccount] += feeAmount;
tokens[order.getToken.token][order.user] += order.getToken.amount;
tokens[order.giveToken.token][msg.sender] += order.giveToken.amount;
tokens[order.giveToken.token][order.user] -= order.giveToken.amount;
orderFill[_orderId] = true;
// 标记订单为完成。
emit Trade(
_orderId,
order.user,
order.getToken,
order.giveToken,
block.timestamp
);
// 发出完成交易事件。
}
}
# 测试编写
测试是我们开发过程中的重要部分。我们将为YexiyueToken
和Exchange
合约编写测试:
test/YexiyueToken.ts
import { expect } from "chai";
import hre from "hardhat";
import { parseEther } from "viem";
describe("YexiyueToken", function () {
// 测试YexiyueToken合约的部署和基本功能。
it("Deployment", async function () {
// 测试合约的部署。
const contract = await hre.viem.deployContract("YexiyueToken", [], {
value: parseEther("1"),
});
// 部署YexiyueToken合约,并向其发送1个以太币。
const [owner, otherWallet] = await hre.viem.getWalletClients();
// 获取两个钱包客户端,一个作为合约所有者,另一个作为其他用户。
const publicClient = await hre.viem.getPublicClient();
// 获取公共客户端,用于读取合约状态。
const res = await publicClient.readContract({
abi: contract.abi,
address: contract.address,
functionName: "balanceOf",
args: [owner.account.address],
});
// 读取合约所有者的余额。
expect(res).to.equal(parseEther("1000000"));
// 期望合约所有者的余额为1000000个代币。
const balance2 = await publicClient.readContract({
abi: contract.abi,
address: contract.address,
functionName: "balanceOf",
args: [otherWallet.account.address],
});
// 读取其他用户的余额。
expect(balance2).to.equal(parseEther("0"));
// 期望其他用户的余额为0。
await owner.writeContract({
abi: contract.abi,
address: contract.address,
functionName: "transfer",
args: [otherWallet.account.address, parseEther("100")],
});
// 合约所有者向其他用户转账100个代币。
expect(
await publicClient.readContract({
abi: contract.abi,
address: contract.address,
functionName: "balanceOf",
args: [otherWallet.account.address],
})
).to.equal(parseEther("100"));
// 期望转账后其他用户的余额为100个代币。
});
it("exchange", async () => {
// 测试Exchange合约与YexiyueToken合约的交互。
const [owner, otherWallet] = await hre.viem.getWalletClients();
// 获取两个钱包客户端。
const yexiyueToken = await hre.viem.deployContract("YexiyueToken", [], {
value: parseEther("1"),
});
// 部署YexiyueToken合约。
const exchange = await hre.viem.deployContract(
"Exchange",
[owner.account.address, 10n],
{
value: parseEther("1"),
}
);
// 部署Exchange合约,并设置交易费用账户和费用百分比。
// 先授权
await yexiyueToken.write.approve([exchange.address, 1000n], {
account: owner.account.address,
});
// 合约所有者授权Exchange合约从其账户中转移1000个代币。
// 再存钱
await exchange.write.depositToken([yexiyueToken.address, 1000n], {
account: owner.account.address,
});
// 合约所有者向Exchange合约存入1000个代币。
expect(
await exchange.read.tokens([yexiyueToken.address, owner.account.address])
).equal(1000n);
// 期望Exchange合约记录的合约所有者的代币余额为1000。
await exchange.write.withdraw([yexiyueToken.address, 100n], {
account: owner.account.address,
});
// 合约所有者从Exchange合约提取100个代币。
expect(
await exchange.read.tokens([yexiyueToken.address, owner.account.address])
).equal(900n);
// 期望提取后Exchange合约记录的合约所有者的代币余额为900。
});
});
# 交易所测试
test/Exchange.ts
import { expect } from "chai";
import hre from "hardhat";
import { parseEther } from "viem";
// 定义以太坊地址常量
const ETHER_ADDRESS = "0x0000000000000000000000000000000000000000";
// 描述交易所测试套件
describe("Exchange", function () {
// 初始化函数,用于部署合约和准备测试环境
async function init() {
// 获取钱包客户端
const [owner, first, second, third] = await hre.viem.getWalletClients();
// 部署 YexiyueToken 合约
const yexiyueToken = await hre.viem.deployContract("YexiyueToken");
// 部署 Exchange 合约,指定交易所的管理员地址和初始ETH余额
const exchange = await hre.viem.deployContract("Exchange", [
third.account.address,
10n,
]);
// 获取公共客户端
const publicClient = await hre.viem.getPublicClient();
// 返回部署的合约和钱包实例
return {
owner,
first,
second,
yexiyueToken,
exchange,
publicClient,
};
}
// 测试部署合约是否成功
it("should deploy", async () => {
expect(init()).fulfilled;
});
// 测试转账和授权功能,以及在交易所进行存款和创建订单的操作
it("should transfer", async () => {
// 初始化合约和钱包实例
const { owner, first, second, yexiyueToken, exchange, publicClient } =
await init();
// 转账 YXT 给 first 和 second 账户
await owner.writeContract({
address: yexiyueToken.address,
abi: yexiyueToken.abi,
functionName: "transfer",
args: [first.account.address, parseEther("200")],
});
await owner.writeContract({
address: yexiyueToken.address,
abi: yexiyueToken.abi,
functionName: "transfer",
args: [second.account.address, parseEther("200")],
});
// first 账户授权 exchange 合约操作其 YXT
await first.writeContract({
address: yexiyueToken.address,
abi: yexiyueToken.abi,
functionName: "approve",
args: [exchange.address, parseEther("100")],
});
// first 账户向 exchange 存入 YXT
await first.writeContract({
address: exchange.address,
abi: exchange.abi,
functionName: "depositToken",
args: [yexiyueToken.address, parseEther("100")],
});
// first 和 second 账户向 exchange 存入 ETH
await first.writeContract({
address: exchange.address,
abi: exchange.abi,
functionName: "depositEther",
args: [],
value: parseEther("100"),
});
await second.writeContract({
address: exchange.address,
abi: exchange.abi,
functionName: "depositEther",
args: [],
value: parseEther("100"),
});
// 验证余额是否正确
expect(
await publicClient.readContract({
address: exchange.address,
abi: exchange.abi,
functionName: "balanceOf",
args: [yexiyueToken.address, first.account.address],
})
).equal(parseEther("100"));
expect(
await publicClient.readContract({
address: exchange.address,
abi: exchange.abi,
functionName: "balanceOf",
args: [ETHER_ADDRESS, second.account.address],
})
).equal(parseEther("100"));
// first 账户创建订单
await first.writeContract({
address: exchange.address,
abi: exchange.abi,
functionName: "makeOrder",
args: [
{ token: ETHER_ADDRESS, amount: parseEther("10") },
{ token: yexiyueToken.address, amount: parseEther("10") },
],
});
// 验证订单数量
expect(
await publicClient.readContract({
address: exchange.address,
abi: exchange.abi,
functionName: "orderCount",
args: [],
})
).equal(1n);
// 尝试取消订单(非订单创建者)
expect(
second.writeContract({
address: exchange.address,
abi: exchange.abi,
functionName: "cancelOrder",
args: [1n],
})
).rejected;
// 订单创建者成功取消订单
expect(
first.writeContract({
address: exchange.address,
abi: exchange.abi,
functionName: "cancelOrder",
args: [1n],
})
).fulfilled;
// second 账户填充订单
await second.writeContract({
address: exchange.address,
abi: exchange.abi,
functionName: "fillOrder",
args: [1n],
});
// 验证 second 账户的 ETH 余额是否正确
expect(
await publicClient.readContract({
address: exchange.address,
abi: exchange.abi,
functionName: "balanceOf",
args: [ETHER_ADDRESS, second.account.address],
})
).equal(parseEther("89"));
});
});
# 智能合约部署
我们将使用 Hardhat 工具链将智能合约部署到本地网络:
pnpm hardhat ignition deploy ./ignition/modules/Exchange.ts --network localhost --reset
pnpm hardhat ignition deploy ./ignition/modules/YexiyueToken.ts --network localhost
# 最后
如果你对本文内容感兴趣,想要获取完整的代码实现和进一步探索,可以访问Yexiyue-Token GitHub 仓库 (opens new window)。在这个仓库中,你将找到所有相关的 Solidity 合约代码、测试脚本。