发行 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: MITpragma 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 可视化配置,自动生成代码。
部署脚本
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,});