Web3交易所(合约篇)

1,089 阅读10分钟

Web3交易所(合约篇)

在本文中,我们将探索如何使用Solidity编写符合ERC20标准的代币合约和交易所合约,并通过React和Wagmi框架构建前端页面。如果你是Web3开发新手,建议先学习这篇文章构建Web3以太坊应用:Hardhat、Wagmi与Next.js集成指南

ERC20代币合约实现

编写以太坊代币通常有两种方法:一是手动实现ERC20协议的接口,二是继承OpenZeppelin提供的合约。为了更深入地理解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
        );
        // 发出完成交易事件。
    }
}

测试编写

测试是我们开发过程中的重要部分。我们将为YexiyueTokenExchange合约编写测试:

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仓库。在这个仓库中,你将找到所有相关的Solidity合约代码、测试脚本。

下一篇Web3交易所(前端篇) - 掘金 (juejin.cn)