ERC-20 代币实战:从铸造到转账的完整开发指南

1. 引言:什么是 ERC-20?

ERC-20 是以太坊上最流行的代币标准,它定义了一套统一的接口规则,使得任何遵循该标准的代币都能与钱包、交易所和去中心化应用(DApp)无缝交互。简单来说,ERC-20 为代币的创建和管理提供了"通用说明书"。

本文将带你从零开始,实战开发一个具备铸造(Mint)转账(Transfer) 功能的 ERC-20 代币。你将学习到:

  • ERC-20 标准的核心接口。
  • 如何使用 Solidity 和 Hardhat 编写安全的代币合约。
  • 如何实现铸造功能,为特定地址生成新代币。
  • 如何实现并测试转账功能。
  • 将合约部署到测试网并进行验证。

2. 环境与工具准备

在开始编码前,请确保你的开发环境已就绪。

2.1 安装 Node.js 和 npm

访问 Node.js 官网 下载并安装 LTS 版本。安装完成后,在终端运行以下命令验证:

bash 复制代码
node --version
npm --version

2.2 初始化 Hardhat 项目

我们将使用 Hardhat 作为开发、测试和部署框架。

bash 复制代码
mkdir erc20-tutorial && cd erc20-tutorial
npm init -y
npm install --save-dev hardhat
npx hardhat init

在初始化向导中,选择 "Create a JavaScript project",并安装所有推荐的依赖。

2.3 安装 OpenZeppelin 合约库

OpenZeppelin 提供了经过严格审计、安全可靠的合约实现,我们将基于它来构建我们的代币。

bash 复制代码
npm install @openzeppelin/contracts

3. ERC-20 标准核心接口解析

ERC-20 标准规定了代币合约必须实现的最小接口。理解这些接口是开发的基础。

3.1 必须实现的函数

  1. totalSupply(): 返回代币的总供应量。
  2. balanceOf(address account) : 返回指定 account 地址的代币余额。
  3. transfer(address recipient, uint256 amount) : 从调用者地址向 recipient 地址转账 amount 数量的代币。
  4. allowance(address owner, address spender) : 返回 owner 授权给 spender 使用的代币数量。
  5. approve(address spender, uint256 amount) : 授权 spender 可以从调用者地址转出最多 amount 数量的代币。
  6. transferFrom(address sender, address recipient, uint256 amount) : 在授权额度内,从 sender 地址向 recipient 地址转账 amount 数量的代币。

3.2 必须触发的事件

  • Transfer(address indexed from, address indexed to, uint256 value): 当代币被转账(包括铸造和销毁)时触发。
  • Approval(address indexed owner, address indexed spender, uint256 value): 当授权成功时触发。

4. 编写我们的代币合约:MyToken

我们将创建一个名为 MyToken 的可铸造 ERC-20 代币。

4.1 创建合约文件

contracts/ 目录下创建 MyToken.sol 文件。

4.2 合约代码详解

solidity 复制代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

// 导入 OpenZeppelin 的 ERC-20 实现和拥有者权限控制
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

/**
 * @title MyToken - 一个可铸造的 ERC-20 代币
 * @dev 继承自 ERC20 和 Ownable,只有合约所有者可以铸造新代币。
 */
contract MyToken is ERC20, Ownable {
    /**
     * @dev 构造函数,初始化代币名称和符号,并将合约部署者设为所有者。
     * @param initialOwner 初始合约所有者地址,拥有铸造权限。
     */
    constructor(address initialOwner)
        ERC20("MyToken", "MTK")
        Ownable(initialOwner)
    {
        // 构造函数体可以为空,或者执行一些初始设置
        // 例如,可以在这里进行初始铸造:_mint(initialOwner, 1000000 * 10 ** decimals());
    }

    /**
     * @dev 铸造新代币的函数,仅所有者可调用。
     * @param to 接收新铸造代币的地址。
     * @param amount 要铸造的代币数量(以最小单位计,例如 wei)。
     */
    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
    }
}

代码解析:

  • ERC20("MyToken", "MTK") : 继承 OpenZeppelin 的 ERC20 合约,并传入代币名称和符号。
  • Ownable(initialOwner) : 继承 Ownable 合约,实现所有权管理。initialOwner 在部署时被设为所有者。
  • onlyOwner : 这是一个函数修饰器(modifier),确保只有合约所有者才能调用 mint 函数。
  • _mint(to, amount) : 调用 ERC-20 内部函数,安全地向地址 to 铸造 amount 数量的代币,并自动触发 Transfer 事件。

5. 编译与本地测试

编写测试是确保合约安全可靠的关键步骤。

5.1 编写测试脚本

test/ 目录下创建 MyToken.test.js 文件。

javascript 复制代码
const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("MyToken", function () {
  let MyToken;
  let myToken;
  let owner;
  let addr1;
  let addr2;

  beforeEach(async function () {
    // 获取合约工厂
    MyToken = await ethers.getContractFactory("MyToken");
    // 获取测试账户
    [owner, addr1, addr2] = await ethers.getSigners();
    // 部署合约,将部署者(owner)设为初始所有者
    myToken = await MyToken.deploy(owner.address);
    await myToken.waitForDeployment();
  });

  describe("部署与初始化", function () {
    it("应该设置正确的名称和符号", async function () {
      expect(await myToken.name()).to.equal("MyToken");
      expect(await myToken.symbol()).to.equal("MTK");
    });

    it("应该将部署者设为所有者", async function () {
      expect(await myToken.owner()).to.equal(owner.address);
    });
  });

  describe("铸造功能", function () {
    it("只有所有者可以铸造代币", async function () {
      // 所有者铸造成功
      await expect(myToken.connect(owner).mint(addr1.address, 1000))
        .to.emit(myToken, "Transfer")
        .withArgs(ethers.ZeroAddress, addr1.address, 1000);

      expect(await myToken.balanceOf(addr1.address)).to.equal(1000);

      // 非所有者铸造失败
      await expect(
        myToken.connect(addr1).mint(addr2.address, 500)
      ).to.be.revertedWithCustomError(myToken, "OwnableUnauthorizedAccount");
    });
  });

  describe("转账功能", function () {
    beforeEach(async function () {
      // 先给 addr1 铸造一些代币用于测试转账
      await myToken.connect(owner).mint(addr1.address, 1000);
    });

    it("应该允许用户转账代币", async function () {
      await expect(myToken.connect(addr1).transfer(addr2.address, 300))
        .to.emit(myToken, "Transfer")
        .withArgs(addr1.address, addr2.address, 300);

      expect(await myToken.balanceOf(addr1.address)).to.equal(700);
      expect(await myToken.balanceOf(addr2.address)).to.equal(300);
    });

    it("转账金额不能超过余额", async function () {
      await expect(
        myToken.connect(addr1).transfer(addr2.address, 1500)
      ).to.be.revertedWithCustomError(myToken, "ERC20InsufficientBalance");
    });
  });
});

5.2 运行测试

在项目根目录下运行:

bash 复制代码
npx hardhat test

如果一切正常,你将看到所有测试用例通过。

6. 部署到测试网

让我们将合约部署到 Sepolia 测试网。

6.1 配置网络和私钥

  1. hardhat.config.js 中添加 Sepolia 网络配置。
  2. 从 Alchemy 或 Infura 获取一个 Sepolia RPC URL。
  3. 将你的测试账户私钥(切勿使用主网私钥!)添加到环境变量或配置中。

一个简化的 hardhat.config.js 示例:

javascript 复制代码
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();

/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
  solidity: "0.8.20",
  networks: {
    sepolia: {
      url: process.env.SEPOLIA_RPC_URL || "",
      accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
    },
  },
};

6.2 编写部署脚本

scripts/ 目录下创建 deploy.js

javascript 复制代码
async function main() {
  const [deployer] = await ethers.getSigners();
  console.log("使用账户部署合约:", deployer.address);

  const MyToken = await ethers.getContractFactory("MyToken");
  // 部署时将部署者地址作为 initialOwner 传入
  const myToken = await MyToken.deploy(deployer.address);

  await myToken.waitForDeployment();

  console.log("MyToken 合约已部署到地址:", await myToken.getAddress());
  console.log("代币名称:", await myToken.name());
  console.log("代币符号:", await myToken.symbol());
  console.log("合约所有者:", await myToken.owner());
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

6.3 执行部署

运行以下命令进行部署:

bash 复制代码
npx hardhat run scripts/deploy.js --network sepolia

部署成功后,控制台会输出合约地址。你可以使用 Etherscan Sepolia 查看你的合约。

7. 与合约交互

部署后,你可以通过 Hardhat Console 或编写前端 DApp 与合约交互。

7.1 使用 Hardhat Console 铸造和转账

bash 复制代码
npx hardhat console --network sepolia

在控制台中:

javascript 复制代码
// 连接到已部署的合约
const MyToken = await ethers.getContractFactory("MyToken");
const myToken = await MyToken.attach("YOUR_DEPLOYED_CONTRACT_ADDRESS");

// 获取当前账户(部署者)
const [owner, addr1] = await ethers.getSigners();

// 所有者铸造 1000 MTK 给 addr1
const tx = await myToken.mint(addr1.address, ethers.parseUnits("1000", 18));
await tx.wait();
console.log("铸造成功!");

// 检查 addr1 的余额
const balance = await myToken.balanceOf(addr1.address);
console.log("addr1 余额:", ethers.formatUnits(balance, 18), "MTK");

// addr1 向 owner 转账 500 MTK
const tx2 = await myToken.connect(addr1).transfer(owner.address, ethers.parseUnits("500", 18));
await tx2.wait();
console.log("转账成功!");

7.2 前端集成(简要)

你可以使用 ethers.js 或 web3.js 库在网页中连接用户钱包(如 MetaMask),并调用合约的 minttransfer 函数。核心逻辑与上述控制台脚本类似。

8. 安全注意事项与最佳实践

  1. 使用经过审计的库:始终优先使用 OpenZeppelin 等成熟库,避免重复造轮子。
  2. 权限控制 :像 mint 这样的敏感操作必须严格限制访问权限(如 onlyOwner)。
  3. 正确处理小数 :ERC-20 使用 decimals 变量处理小数,默认是 18。在交互时,注意单位转换(如 parseUnits, formatUnits)。
  4. 充分测试:覆盖正常路径、边界情况和异常情况(如余额不足、未授权访问)。
  5. 考虑升级性:如果未来可能需要修复 bug 或添加功能,可以考虑使用可升级合约模式(如 OpenZeppelin Upgrades)。
  6. 主网部署前:务必在测试网上进行全面测试,并考虑进行第三方审计。

9. 总结

通过本文,你已完成了一个具备铸造和转账功能的完整 ERC-20 代币项目。你学会了:

  • ERC-20 标准的核心。
  • 基于 OpenZeppelin 编写安全合约。
  • 使用 Hardhat 进行开发、测试和部署。
  • 实现并测试了核心的铸造与转账逻辑。

下一步,你可以尝试为代币添加更多功能,如销毁(Burn)、暂停(Pausable)、快照(Snapshot)或实现代币销售(ICO)逻辑。区块链开发的世界广阔而有趣,祝你探索愉快!

相关资源:

相关推荐
醉颜凉1 天前
“三把钥匙开一把锁”:多签钱包深度解析与资产安全提升指南
安全·区块链
cmes_love2 天前
期货Level2与股票逐笔行情数据有什么,如何下载
数据库·区块链
小果因子实验室2 天前
量化研究--编写第一个回测算法策略研究
大数据·区块链
IvorySQL3 天前
PG 日报|SQL/PGQ 图查询基于联接重写机制实现
数据库·人工智能·sql·postgresql·区块链·ivorysql
磐链科技4 天前
技术服务:交易所从流量竞争到生态竞争的核心变量
区块链
Web3李李5 天前
DApp核心安全漏洞与项目方全方位防护指南
web3·区块链·智能合约·软件开发·dapp开发·安全审计
双缝观察者5 天前
消费场景中,多层级资金流转异常模式识别与溯源方法
区块链
磐链科技5 天前
从性能到智能:区块链交易所前沿技术全景洞察
区块链
汇策研习社5 天前
改良版ADX指标实战指南:告别传统ADX滞后、震荡误判难题
大数据·经验分享·金融·区块链·fastbull