大白话解析 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等于最大值),就不扣减 }}