W3
合约开发中级预计 40-50 分钟9 个步骤35 次学习

实战编写 ERC-20:基于 OpenZeppelin 发一个代币合约

亲手完成一个可部署的 ERC-20 代币合约:拆解 EIP-20 标准规定的 6 个必选函数(totalSupply / balanceOf / transfer / allowance / approve / transferFrom)与 Transfer、Approval 两个事件;安装并理解 OpenZeppelin Contracts 库为什么是行业标配;写出继承 ERC20 的 MyToken,用构造函数设置名称符号并初始 mint;讲透 _mint / _burn / approve / allowance / transferFrom 的授权扣款链路;分析加「转账征税」等自定义逻辑时最容易引入的漏洞;编译后检查 artifacts 中的 ABI 与字节码;再扩展到 ERC20Burnable、Pausable、ERC20Permit、ERC20Votes 与 ERC-721/1155 的区别。

实战编写 ERC-20:基于 OpenZeppelin 发一个代币合约
⚠️
操作前必读

本章代码只在本地与测试网部署。真实发币涉及法律合规与资金安全:自定义 transfer 逻辑(征税、黑名单、反射奖励)是归零与漏洞重灾区,无数代币因 _update 钩子写错而卡死转账或被套利;无限 approve 也是用户资产被盗的高频入口。学习阶段请完整保留 OpenZeppelin 的标准实现,不要随意魔改。

01
1. 标准规范与依赖准备

ERC-20 标准到底规定了什么

登录记录进度

ERC-20(Ethereum Request for Comments 20)是同质化代币的统一接口标准——「同质化」指每个代币完全等价、可任意拆分互换,如 USDT、USDC。标准规定了 6 个必选函数:totalSupply()(总供应量)、balanceOf(account)(查余额)、transfer(to, amount)(直接转账)、allowance(owner, spender)(查询授权额度)、approve(spender, amount)(授权他人动用)、transferFrom(from, to, amount)(被授权方代转);外加两个必须发出的事件 TransferApproval;可选的 name()symbol()decimals() 元数据。

标准最大的价值是互操作性:钱包、交易所、DeFi 协议只要认这一个接口,就能自动支持成千上万种代币,而不需要为每个代币单独适配。

ERC-20 标准到底规定了什么
ERC-20 标准到底规定了什么|界面示意 · docs.openzeppelin.com · 采集于 2026-09-18
💡
小技巧:

decimals 只是显示层约定(通常为 18),合约内部永远以最小整数单位记账;写金额计算时脑子里要始终保持「原始单位」思维。

02
1. 标准规范与依赖准备

安装 OpenZeppelin Contracts 库

登录记录进度

OpenZeppelin Contracts 是经过行业十余年实战检验、由顶级安全团队维护的标准合约库,ERC-20、ERC-721、AccessControl、安全数学工具等都有官方实现。

在 Hardhat 项目中安装:npm install @openzeppelin/contracts(Foundry 用 forge install OpenZeppelin/openzeppelin-contracts)。

然后在合约里 import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 即可继承使用。

原则:能复用经过审计的标准实现,就绝不手写核心逻辑。自己实现的 ERC20 几乎必然遗漏边界条件(零地址、授权变更 race condition、溢出处理),而 OpenZeppelin 的每个版本都被数千个项目与审计师反复审查过。要学原理可以读它的源码,但生产环境请继承它。

安装 OpenZeppelin Contracts 库
安装 OpenZeppelin Contracts 库|界面示意 · docs.openzeppelin.com · 采集于 2026-09-18
🚫
避坑提醒:

从 npm 安装时核对包名是否被「仿冒包」抢注(typosquatting 攻击常见);开源依赖也应固定版本号并留意官方安全公告。

03
2. 写出代币合约

写出第一个代币合约 MyToken

登录记录进度

contracts/MyToken.sol 中写入:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract MyToken is ERC20, Ownable {
    constructor() ERC20("My Token", "MTK") Ownable(msg.sender) {}

    function mint(address to, uint256 amount) external onlyOwner {
        _mint(to, amount);
    }
}

逐行理解:is ERC20, Ownable 同时继承标准代币与所有权;构造函数里 ERC20("My Token", "MTK") 设定代币全名与代码;Ownable(msg.sender) 把部署者设为初始 owner;自定义 mint 函数用 onlyOwner 限制只有管理员能增发,内部调用以下划线开头的 _mint(OZ 的内部函数,外部不可直接调用)。执行 npx hardhat compile 编译通过,第一个代币合约就完成了。

写出第一个代币合约 MyToken
写出第一个代币合约 MyToken|界面示意 · docs.openzeppelin.com · 采集于 2026-09-18
💡
小技巧:

文件名第一行的 SPDX-License-Identifier 不是注释摆设,主网验证源码时编译器与浏览器会读取它,开源合约建议用 MIT。

04
2. 写出代币合约

理解授权扣款链路:approve + allowance + transferFrom

登录记录进度

这是 ERC-20 最容易绕晕初学者的设计,也是 DeFi 的基石。场景:你要让一个 DEX 合约用你的 USDT 买币,你不能把私钥给它,于是走「授权—代转」两步: 1. 你调用 USDT.approve(dexAddress, 100e6):授权 DEX 最多动用你 100 USDT,此时 allowance(你, DEX) 变为 100。

  1. 你在 DEX 上下单,DEX 调用 USDT.transferFrom(你, DEX, 100e6):合约检查 allowance 足够后扣款,并把额度减为 0,同时发出 Transfer 事件。

关键点:transferFrom 的调用者是 DEX 合约而不是你;每次代转都会扣减 allowance;balanceOf 永远记录真实余额,allowance 只是「允许别人花的上限」,不锁定资金。OZ 还提供 increaseAllowance / decreaseAllowance 解决直接改 approve 额度时的历史竞争问题。

理解授权扣款链路:approve + allowance + transferFrom
理解授权扣款链路:approve + allowance + transferFrom|界面示意 · docs.openzeppelin.com · 采集于 2026-09-18
🚫
避坑提醒:

无限授权(approve 无限额)意味着该合约一旦有漏洞或作恶,可以转走你这种代币的全部余额。投入新协议前,在区块浏览器或 revoke 工具里检查并及时撤销授权。

05
2. 写出代币合约

_mint、_burn 与自定义钩子 _update

登录记录进度

OpenZeppelin 内部函数的命名约定:下划线开头的是内部函数,包含实际逻辑但不做外部权限校验,权限由你在外层函数控制:_mint(account, amount) 铸造新代币并增加 totalSupply;_burn(account, amount) 销毁并减少总量;_transfer(from, to, amount) 执行真正的转账。

新版 OZ 把转账的核心逻辑收敛到一个内部钩子 _update(from, to, value)——无论 mint、burn 还是 transfer 最终都经过它。

如果你想加「转账收 1% 税」「黑名单地址不能转账」「快照记账」,官方推荐重写 _update,但这恰恰是最高危的自定义点:钩子写错会导致所有转账永久卡死、税款去向错误或与 ERC20Votes/快照扩展不兼容。

_mint、_burn 与自定义钩子 _update
_mint、_burn 与自定义钩子 _update|界面示意 · docs.openzeppelin.com · 采集于 2026-09-18
🚫
避坑提醒:

魔改 _update 加税收/反射的代币是审计重灾区:必须处理好 from 或 to 为零地址(mint/burn)、内部转账免税白名单、与 gas token 的兼容,新手不要在主网项目中尝试。

06
3. 编译产物与扩展能力

编译产物:读懂 ABI 与字节码

登录记录进度

编译后打开 artifacts/contracts/MyToken.sol/MyToken.json,里面两个字段最重要: - bytecode(字节码):部署时真正写进链上的 EVM 机器码,是一串十六进制。

合约创建交易的 data 字段就是「字节码 + 构造参数编码」。 - abi(Application Binary Interface):一个 JSON 数组,逐项列出每个函数的名字、输入参数类型、输出、是否 payable,以及事件定义。

前端 ethers.js 正是靠 new ethers.Contract(address, abi, signer) 才知道该如何给你的合约编码调用、解析返回值。

理解两者分工:字节码决定「合约实际能做什么」,ABI 只是告诉外部「该怎么和它说话」。区块浏览器「验证合约(Verify)」做的事,就是让你上传源码,由浏览器重新编译并比对链上字节码是否一致——一致才会显示绿色对勾与公开源码。

编译产物:读懂 ABI 与字节码
编译产物:读懂 ABI 与字节码|界面示意 · docs.soliditylang.org · 采集于 2026-09-18
💡
小技巧:

ABI 不包含函数实现,泄露 ABI 没有安全风险;真正要保护的是私钥。开源 ABI 反而能方便他人集成你的合约。

07
3. 编译产物与扩展能力

常用扩展:Burnable、Pausable、Permit 与 Votes

登录记录进度

OpenZeppelin 提供即插即用的扩展模块: - ERC20Burnable:增加标准的 burn / burnFrom,支持持币者销毁自己的代币(通缩模型常用)。

  • Pausable:增加暂停开关,紧急情况下 owner 可 pause() 冻结所有转账,用于安全响应;但也意味着中心化权力,需配合多签使用。 - ERC20Permit:EIP-2612 离线签名授权,用户用签名代替单独发一笔 approve 交易,省 Gas 且改善体验(USDC 已支持)。

  • ERC20Votes:记录历史投票权余额快照,是 DAO 治理代币(Compound/Uniswap 模式)的标准底座,与代币转账自动保持快照一致。

选择扩展的原则:只引入真正需要的。每多一个 owner 可调的开关,就多一份中心化风险,也多一处审计面。

常用扩展:Burnable、Pausable、Permit 与 Votes
常用扩展:Burnable、Pausable、Permit 与 Votes|界面示意 · docs.openzeppelin.com · 采集于 2026-09-18
🚫
避坑提醒:

Pausable、黑名单这类能力在主网项目中必须由多签或 Timelock 持有,单个 EOA 持有关停权会被市场视为中心化隐患。

08
4. 其他代币标准与本章验收

ERC-20 之外:NFT 标准 ERC-721 与 ERC-1155

登录记录进度

写完同质化代币,顺带建立对另外两个标准的认知: - ERC-721(NFT)非同质化代币,每个 tokenId 独一无二、不可互换,每个 ID 独立映射 owner 与 metadata URI,适用于数字藏品、会员凭证、游戏装备、RWA 资产凭证。

核心函数是 ownerOf(tokenId)safeTransferFromtokenURI,事件是 Transfer。 - ERC-1155(多代币标准):一个合约同时管理多种 tokenId,每个 ID 既可以是「非同质化的单件」也可以是「同质化的一批」(带余额),并支持批量转账(一次交易转多种资产,省 Gas),游戏道具、票务、市场组合订单常用。

选择依据:资产之间完全等价用 20;每个都独一无二用 721;合约内存在多种资产类型或要批量操作选 1155。

ERC-20 之外:NFT 标准 ERC-721 与 ERC-1155
ERC-20 之外:NFT 标准 ERC-721 与 ERC-1155|界面示意 · docs.openzeppelin.com · 采集于 2026-09-18
💡
小技巧:

ERC-721 的 receiver 安全回调 onERC721Received、ERC-1155 的 onERC1155Received 是为了防止 NFT 被打进无法识别的合约永久锁死,接入时别漏掉。

09
4. 其他代币标准与本章验收

本章验收:画出你的合约数据流

登录记录进度

合上书,尝试独立完成以下验收任务: 1. 从零新建 Hardhat 项目,安装 OpenZeppelin,写出带 mint(onlyOwner)的 ERC-20 并编译成功。 2. 在纸上或白板画出三个角色(用户、USDT 合约、DEX 合约)之间 approve 与 transferFrom 的调用箭头,标注每一步 allowance 与余额如何变化。

  1. 打开编译产物,指出哪部分是字节码、哪部分是 ABI,并说出前端需要其中哪一个。 4. 用自己的话回答:为什么不推荐新手重写 _update 加转账税?Pausable 给了 owner 什么权力、应该由谁持有?

能完整答出这四点,说明你已经具备了「读得懂、改得对、部署得出」标准代币合约的能力。下一章我们将学习专业开发中最关键的一环——自动化测试。

本章验收:画出你的合约数据流
本章验收:画出你的合约数据流|界面示意 · docs.soliditylang.org · 采集于 2026-09-18
💡
小技巧:

建议把本章的 MyToken 保留在 GitHub 私有仓库中,后续测试章和部署章都会在它之上继续迭代。

来源与时效

教程记录平台 docs.openzeppelin.com · 客户端 Web / App · 版本 v1。各平台界面会随版本更新变化,请以官方当前界面为准。

全部步骤已完成

操作完成后建议再核对一次到账金额与手续费,并保留交易哈希作为凭证。

相关教程