跳转到内容

发行 ERC-20 Token

发行 ERC-20 Token

本页内容正在整理中,欢迎贡献

概述

本教程带你发行一个功能完整的 ERC-20 代币:设置代币名称、符号、供应量,支持铸造和销毁,并添加 Owner 权限控制。最终部署到 Sepolia 并在 Etherscan 上验证。

主要内容

需求设计

Token 规格:
名称:My Token
符号:MTK
精度:18 位
初始供应量:100,000,000 MTK(1 亿)
最大供应量:1,000,000,000 MTK(10 亿上限)
功能:
✅ ERC-20 基础功能(transfer, approve, transferFrom)
✅ Owner 可铸造代币
✅ 持有者可销毁自己的代币
✅ EIP-2612 Permit(无 Gas 授权)
✅ 暂停功能(紧急情况)

合约实现

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Pausable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyToken is ERC20, ERC20Burnable, ERC20Permit, ERC20Pausable, Ownable {
uint256 public constant MAX_SUPPLY = 1_000_000_000 * 10 ** 18; // 10 亿
event TokensMinted(address indexed to, uint256 amount);
constructor(address initialOwner)
ERC20("My Token", "MTK")
ERC20Permit("My Token")
Ownable(initialOwner)
{
// 初始铸造 1 亿代币给 Owner
_mint(initialOwner, 100_000_000 * 10 ** 18);
}
// Owner 铸造(受最大供应量限制)
function mint(address to, uint256 amount) external onlyOwner {
require(totalSupply() + amount <= MAX_SUPPLY, "Exceeds max supply");
_mint(to, amount);
emit TokensMinted(to, amount);
}
// 暂停/恢复(紧急情况)
function pause() external onlyOwner { _pause(); }
function unpause() external onlyOwner { _unpause(); }
// 必须覆盖的函数(Pausable + ERC20 冲突)
function _update(address from, address to, uint256 value)
internal override(ERC20, ERC20Pausable)
{
super._update(from, to, value);
}
}

使用 OpenZeppelin Wizard 生成

访问 wizard.openzeppelin.com 可视化配置,自动生成代码。

部署脚本

scripts/deploy.ts
import { ethers, run } from "hardhat";
async function main() {
const [deployer] = await ethers.getSigners();
console.log("部署账户:", deployer.address);
const MyToken = await ethers.getContractFactory("MyToken");
const token = await MyToken.deploy(deployer.address);
await token.waitForDeployment();
const tokenAddress = await token.getAddress();
console.log("MyToken 部署地址:", tokenAddress);
console.log("初始供应量:", ethers.formatEther(await token.totalSupply()), "MTK");
// 等待确认后验证
await token.deploymentTransaction()?.wait(5);
await run("verify:verify", {
address: tokenAddress,
constructorArguments: [deployer.address],
});
}
main().catch(console.error);

测试

import { expect } from "chai";
import { ethers } from "hardhat";
import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers";
describe("MyToken", function () {
async function deployFixture() {
const [owner, alice, bob] = await ethers.getSigners();
const MyToken = await ethers.getContractFactory("MyToken");
const token = await MyToken.deploy(owner.address);
return { token, owner, alice, bob };
}
it("初始配置正确", async function () {
const { token, owner } = await loadFixture(deployFixture);
expect(await token.name()).to.equal("My Token");
expect(await token.symbol()).to.equal("MTK");
expect(await token.decimals()).to.equal(18);
expect(await token.totalSupply()).to.equal(ethers.parseEther("100000000"));
expect(await token.balanceOf(owner.address)).to.equal(ethers.parseEther("100000000"));
});
it("Owner 可以铸造", async function () {
const { token, owner, alice } = await loadFixture(deployFixture);
const amount = ethers.parseEther("1000");
await expect(token.mint(alice.address, amount))
.to.emit(token, "TokensMinted")
.withArgs(alice.address, amount);
expect(await token.balanceOf(alice.address)).to.equal(amount);
});
it("非 Owner 不能铸造", async function () {
const { token, alice } = await loadFixture(deployFixture);
await expect(token.connect(alice).mint(alice.address, 1n))
.to.be.revertedWithCustomError(token, "OwnableUnauthorizedAccount");
});
it("超过最大供应量时 revert", async function () {
const { token, owner, alice } = await loadFixture(deployFixture);
const overMax = ethers.parseEther("900000001"); // 超过剩余上限
await expect(token.mint(alice.address, overMax))
.to.be.revertedWith("Exceeds max supply");
});
it("持有者可以销毁代币", async function () {
const { token, owner } = await loadFixture(deployFixture);
const burnAmount = ethers.parseEther("1000");
await token.burn(burnAmount);
expect(await token.totalSupply()).to.equal(ethers.parseEther("100000000") - burnAmount);
});
});

与代币交互

// 使用 viem 查询代币信息
import { createPublicClient, http, formatUnits } from "viem";
import { mainnet } from "viem/chains";
import { erc20Abi } from "viem";
const client = createPublicClient({ chain: mainnet, transport: http() });
const [name, symbol, decimals, totalSupply, balance] = await client.multicall({
contracts: [
{ address: TOKEN_ADDRESS, abi: erc20Abi, functionName: "name" },
{ address: TOKEN_ADDRESS, abi: erc20Abi, functionName: "symbol" },
{ address: TOKEN_ADDRESS, abi: erc20Abi, functionName: "decimals" },
{ address: TOKEN_ADDRESS, abi: erc20Abi, functionName: "totalSupply" },
{ address: TOKEN_ADDRESS, abi: erc20Abi, functionName: "balanceOf", args: [userAddress] },
],
allowFailure: false,
});

深入阅读