> 技术文档 > 大白话解析 OpenZeppelin 的 ERC20 合约(附源代码)

大白话解析 OpenZeppelin 的 ERC20 合约(附源代码)


一、先搞明白:什么是 ERC20 代币

你可以把 ​​ERC20 代币​​想象成在以太坊区块链上发行的一种“​​数字积分/代币​​”,跟游戏里的金币、Q币有点像,但它是去中心化的,运行在区块链上。

比如:

  • USDT、UNI、SHIB 这些币,其实都是 ERC20 代币

  • 你可以在钱包里持有它,转账给别人,用它买东西等等

而 ​​ERC20​​ 就是一个​​标准规则​​,规定了这种代币应该有哪些功能,比如:

  • 怎么转账(transfer)

  • 怎么授权别人用你的币(approve)

  • 怎么查看余额(balanceOf)

  • 代币叫什么名字、符号是什么、有几位小数(name, symbol, decimals)

​OpenZeppelin 的 ERC20 合约​,就是有人(OpenZeppelin 团队)按照这个标准,用 Solidity 写好了一个​​安全、可靠、大家都在用的代码模板​​,咱们开发者可以直接拿来用,或者在其基础上改。


二、OpenZeppelin ERC20 合约是啥?

简单说,它就是一个​​智能合约代码​​,实现了 ERC20 标准要求的所有功能,包括:

功能

说明

举个例子

transfer

把代币从自己转到别人

我给你 10 个币

approve

允许别人用我的代币

我允许 Binance 帮我卖 100 个币

transferFrom

别人用我的授权去转我的币

Binance 帮我卖那 100 个币

balanceOf

查某个人有多少代币

我有多少币?

totalSupply

查这个代币总共发了多少

这个项目一共发了多少币?

name / symbol / decimals

代币的名字、符号和小数位数

比如叫“MyToken”,符号“MTK”,小数位 18

它是一个​​基础模板​​,你可以在这个基础上​​加功能​​,比如:

  • 让只有项目方能发新币(mint)

  • 让某些人不能转币(黑名单)

  • 收手续费

  • 做空投等等


三、为啥要用 OpenZeppelin 的 ERC20 合约?自己写不行吗?

你当然可以自己写一个 ERC20 合约,但​​自己写容易出错,不安全!​

OpenZeppelin 是一个非常出名的、专门写智能合约“​​安全组件​​”的团队,他们的代码:

✅ ​​经过大量审计和实战检验​​,很少有漏洞

✅ ​​大家都在用​​,兼容性好

✅ ​​模块化设计​​,你只管用,不用重复造轮子

✅ ​​安全第一​​,帮你处理各种边界情况(比如转给零地址、余额不够等)

所以,​​绝大多数项目(包括 Uniswap、Aave 等大项目)都直接用 OpenZeppelin 的 ERC20 合约作为基础​​,而不是自己从头写。


四、用大白话讲讲它是怎么工作的

我们把几个核心功能用简单的话说一下:


1. ​​部署代币(创建代币)​

当你部署这个合约时,你需要给它起个名字和符号,比如:

new MyToken(\"我的代币\", \"MTK\")

这就创建了一个叫“我的代币”、符号是“MTK”的新代币,总供应量一开始是 0,你可能还需要后续“铸造”出来。


2. ​​铸造代币(Mint)—— 发行新币​

默认情况下,OpenZeppelin 的合约里​​没有直接让大家随便铸造代币的功能​​,因为太危险了!谁都能发币的话,那币就不值钱了。

但如果你想让项目方(比如你)能发新币,你可以​​继承这个合约并添加一个 mint 函数​​,并且加上权限控制(只允许你自己调用)。

比如:项目方可以调用 mint 给用户发奖励,或者空投。


3. ​​转账(Transfer)—— 把币给别人​

比如,你钱包里有 100 个 MTK,你想给朋友转 10 个,你调用的就是 transfer(朋友的地址, 10)

合约会检查:

  • 你不能转给“零地址”(即不能瞎转)

  • 你得有足够的币

  • 然后把你的余额减少 10,你朋友的余额增加 10


4. ​​授权(Approve)—— 让别人帮你操作你的币​

有时候你不想直接把币给别人,但你想让某个平台(比如交易所、DeFi 协议)帮你卖币、转账等,你就可以用 approve功能:

approve(平台的地址, 100)

意思是:我允许这个平台最多动用我账户里的 100 个币。

然后平台就可以用 transferFrom来帮你操作这些币了。


5. ​​代理转账(TransferFrom)—— 别人帮你转你授权过的币​

比如你授权了 Binance 可以动用你 100 个币,那么 Binance 就可以调用 transferFrom(你的地址, 别人的地址, 10),把你的币转给别人。


6. ​​查看余额、名字等(BalanceOf / Name / Symbol)​

  • balanceOf(某地址)→ 查他有多少币

  • name()→ 查代币叫啥名,比如“我的代币”

  • symbol()→ 查代币符号,比如“MTK”

  • decimals()→ 查精度,通常是 18(跟 ETH 一样)


五、OpenZeppelin ERC20 合约有哪些“高级隐藏功能”?

虽然上面那些是你最常用到的功能,但这个合约还设计得非常灵活和强大,比如:


✅ 可扩展性强

它用了很多 internal(内部)函数,比如:

  • _transfer():真正处理转账逻辑

  • _mint():真正处理发币逻辑

  • _burn():真正处理销毁币逻辑

  • _approve():处理授权逻辑

这些函数都是 ​​internal​​,意味着你可以继承这个合约,然后​​重写它们​​,加入你自己的逻辑,比如:

  • 扣手续费

  • 黑名单控制(某些地址不能转币)

  • 只允许特定时间转账

  • 做一些自动化的操作


✅ 安全性高

它考虑了各种异常情况,比如:

  • 不能转给 0 地址(零地址)

  • 不能从 0 地址收币

  • 余额不足不能转

  • 授权不够不能用别人的币

  • 错误会用标准的错误提示,而不是模糊的失败

这些都让合约更加安全可靠。


✅ 符合最新标准

它遵循了最新的 ERC20 标准以及相关的 EIP(比如 EIP-2612、EIP-6093),跟其他 DeFi 协议、钱包、浏览器兼容性更好。


六、总结:OpenZeppelin ERC20 合约到底有啥用?

用一句话来说:

OpenZeppelin 的 ERC20 合约,就是一个​​安全、可靠、标准、灵活的代币代码模板​​,你只需要简单改改,就能发行属于自己的代币,或者在其基础上开发更复杂的功能。


七、源代码

// SPDX-License-Identifier: MIT// 这行是许可证声明,说明这个代码遵循MIT协议,可以自由使用和修改// 下面是OpenZeppelin团队开发的ERC20代币标准实现,最新更新到v5.4.0版本// 适用的Solidity编程语言版本是0.8.20及以上pragma solidity ^0.8.20;// 导入必要的\"零件\":// IERC20.sol:定义了ERC20代币必须实现的基本功能(比如转账、查询余额)import {IERC20} from \"./IERC20.sol\";// IERC20Metadata.sol:扩展功能,定义了代币名称、符号、小数位等元信息import {IERC20Metadata} from \"./extensions/IERC20Metadata.sol\";// Context.sol:提供工具,用于获取\"谁在调用这个合约\"的地址import {Context} from \"../../utils/Context.sol\";// IERC20Errors.sol:定义了各种错误情况(比如余额不足时该怎么提示)import {IERC20Errors} from \"../../interfaces/draft-IERC6093.sol\";/** * @title ERC20 * @dev 这是一个最常用的代币标准实现(就像\"模板\") * 你可以基于它创建自己的代币,比如比特币、以太币的代币都类似这样实现 * 举个例子:如果你想创建\"狗狗币\",就可以复制这个模板改改名字和符号 */abstract contract ERC20 is Context, IERC20, IERC20Metadata, IERC20Errors { // 【核心存储1】记录每个地址有多少代币 // 类似银行的账本:key是用户地址,value是余额 // 比如:_balances[\"0x123...\"] = 100 表示这个地址有100个代币 mapping(address account => uint256) private _balances; // 【核心存储2】记录授权关系 // 比如:你允许朋友花你10个代币,这里就会记录 // _allowances[你的地址][朋友的地址] = 10 mapping(address account => mapping(address spender => uint256)) private _allowances; // 【核心存储3】代币的总数量 // 比如比特币总共有2100万枚,这里就存2100万 uint256 private _totalSupply; // 代币的全名,比如\"Ethereum\" string private _name; // 代币的简称,比如\"ETH\" string private _symbol; /** * @dev 构造函数:创建代币时自动执行,用来设置代币的基本信息 * @param name_ 代币的全名,比如\"My First Token\" * @param symbol_ 代币的简称,比如\"MFT\" * 举个例子:部署时传入(\"张三币\", \"ZS\"),就创建了一个叫张三币的代币 */ constructor(string memory name_, string memory symbol_) { _name = name_; // 保存代币名称 _symbol = symbol_; // 保存代币简称 } /** * @dev 查看代币的全名 * @return 返回代币名称的文字 * 比如刚才创建的\"张三币\",调用这个函数就会显示\"张三币\" */ function name() public view virtual returns (string memory) { return _name; } /** * @dev 查看代币的简称 * @return 返回代币简称的文字 * 比如刚才的例子,会显示\"ZS\" */ function symbol() public view virtual returns (string memory) { return _symbol; } /** * @dev 查看代币的小数位数(默认18位,和以太币一样) * @return 小数位数,固定返回18 * 为什么需要这个?比如1个代币,实际在电脑里存的是1000000000000000000(18个0) * 就像1元=100分,这里1个代币=10^18个\"最小单位\" */ function decimals() public view virtual returns (uint8) { return 18; } /** * @dev 查看代币的总数量 * @return 总数量(包含18位小数,需要除以10^18才是实际显示的数量) * 比如总供应量是1000个代币,这里会显示1000000000000000000000 */ function totalSupply() public view virtual returns (uint256) { return _totalSupply; } /** * @dev 查看某个地址有多少代币 * @param account 要查询的地址(比如\"0x123...\") * @return 该地址的余额(同样包含18位小数) * 比如查询你的地址,返回50000000000000000000,就表示你有50个代币 */ function balanceOf(address account) public view virtual returns (uint256) { return _balances[account]; } /** * @dev 给别人转账代币 * @param to 接收人的地址(不能是0地址,就像不能转账给\"空气\") * @param value 转账数量(注意要包含18位小数,比如转1个代币就要传1000000000000000000) * @return 永远返回true(如果失败会直接报错,不会返回false) * 例子:你调用transfer(\"0x456...\", 1000000000000000000),就是给0x456地址转1个代币 */ function transfer(address to, uint256 value) public virtual returns (bool) { address owner = _msgSender(); // 先找到\"谁在调用这个函数\"(也就是你自己) _transfer(owner, to, value); // 调用内部函数实际执行转账 return true; } /** * @dev 查看你授权给别人多少代币可以花 * @param owner 你的地址 * @param spender 被授权人的地址 * @return 还能花的数量 * 例子:你授权朋友花5个代币,朋友已经花了2个,这里会显示3*10^18 */ function allowance(address owner, address spender) public view virtual returns (uint256) { return _allowances[owner][spender]; } /** * @dev 授权别人可以花你的代币 * @param spender 被授权人的地址(比如你的朋友) * @param value 允许花的数量(包含18位小数) * @return 永远返回true(失败会报错) * 例子:approve(\"朋友的地址\", 5000000000000000000) 表示允许朋友花5个你的代币 */ function approve(address spender, uint256 value) public virtual returns (bool) { address owner = _msgSender(); // 你自己的地址 _approve(owner, spender, value); // 内部函数执行授权 return true; } /** * @dev 花别人授权给你的代币(从别人地址转到第三方) * @param from 被花代币的人的地址 * @param to 接收代币的地址 * @param value 要花的数量 * @return 永远返回true * 例子:朋友授权你花5个代币,你调用transferFrom(朋友地址, 商店地址, 2*10^18) * 就表示用朋友的2个代币给商店付款 */ function transferFrom(address from, address to, uint256 value) public virtual returns (bool) { address spender = _msgSender(); // 你自己的地址(被授权人) _spendAllowance(from, spender, value); // 先扣减授权额度 _transfer(from, to, value); // 再执行转账 return true; } /** * @dev 内部转账函数(不对外开放,仅供合约内部使用) * 负责检查转账的合法性,然后调用_update实际执行 */ function _transfer(address from, address to, uint256 value) internal { if (from == address(0)) { // 如果从0地址转账,就报错(0地址相当于\"空气\",不能作为转出方) revert ERC20InvalidSender(address(0)); } if (to == address(0)) { // 如果转到0地址,也报错(相当于把钱扔了,不允许) revert ERC20InvalidReceiver(address(0)); } _update(from, to, value); // 调用核心更新函数 } /** * @dev 核心更新函数(最底层的操作,处理所有代币变动) * 三种功能: * 1. 普通转账:from和to都是正常地址(比如A转给B) * 2. 铸造代币:from是0地址(相当于\"无中生有\",给to地址新增代币) * 3. 销毁代币:to是0地址(相当于\"把代币烧掉\",减少总供应量) */ function _update(address from, address to, uint256 value) internal virtual { if (from == address(0)) { // 铸造代币:from是0地址,说明要新增代币 _totalSupply += value; // 总供应量增加 } else { // 普通转账或销毁:先检查from地址有没有足够的代币 uint256 fromBalance = _balances[from]; if (fromBalance < value) { // 如果余额不足,就报错 revert ERC20InsufficientBalance(from, fromBalance, value); } // 安全地减少from地址的余额(用unchecked是为了节省gas) unchecked { _balances[from] = fromBalance - value; } } if (to == address(0)) { // 销毁代币:to是0地址,减少总供应量 unchecked { _totalSupply -= value; } } else { // 普通转账:增加to地址的余额 unchecked { _balances[to] += value; } } // 触发转账事件(告诉区块链:发生了一笔转账,方便钱包等工具显示) emit Transfer(from, to, value); } /** * @dev 铸造代币(内部函数,用于创建新代币) * @param account 接收新代币的地址 * @param value 铸造的数量 * 例子:_mint(\"你的地址\", 1000*10^18) 表示给你创建1000个新代币 */ function _mint(address account, uint256 value) internal { if (account == address(0)) { // 不能铸造给0地址,否则代币就\"消失\"了 revert ERC20InvalidReceiver(address(0)); } // 调用_update,from设为0地址表示铸造 _update(address(0), account, value); } /** * @dev 销毁代币(内部函数,用于减少代币总量) * @param account 要销毁代币的地址 * @param value 销毁的数量 * 例子:_burn(\"你的地址\", 50*10^18) 表示从你的地址销毁50个代币 */ function _burn(address account, uint256 value) internal { if (account == address(0)) { // 不能从0地址销毁(本来就没有代币) revert ERC20InvalidSender(address(0)); } // 调用_update,to设为0地址表示销毁 _update(account, address(0), value); } /** * @dev 内部授权函数(默认触发事件) */ function _approve(address owner, address spender, uint256 value) internal { // 第三个参数true表示要触发事件,让外界知道授权发生了 _approve(owner, spender, value, true); } /** * @dev 授权的底层实现(可以控制是否触发事件) * 触发事件会消耗更多gas,内部操作时可以关掉 */ function _approve(address owner, address spender, uint256 value, bool emitEvent) internal virtual { if (owner == address(0)) { // 0地址不能作为授权人(没有代币可以授权) revert ERC20InvalidApprover(address(0)); } if (spender == address(0)) { // 不能授权给0地址(相当于授权给空气,没意义) revert ERC20InvalidSpender(address(0)); } // 保存授权额度 _allowances[owner][spender] = value; if (emitEvent) { // 触发授权事件,告诉外界授权发生了 emit Approval(owner, spender, value); } } /** * @dev 消耗授权额度(比如transferFrom时会调用) */ function _spendAllowance(address owner, address spender, uint256 value) internal virtual { uint256 currentAllowance = allowance(owner, spender); if (currentAllowance < type(uint256).max) { // 如果不是无限授权(max表示无限),就需要扣减额度 if (currentAllowance < value) { // 授权额度不够时报错 revert ERC20InsufficientAllowance(spender, currentAllowance, value); } // 扣减授权额度,并且不触发事件(节省gas) unchecked { _approve(owner, spender, currentAllowance - value, false); } } // 如果是无限授权(currentAllowance等于最大值),就不扣减 }}