Openzeppelin 学习:ERC20源码分析
ERC20源码解析ERC20是以太坊上的一种代币标准,定义了一组规则,任何实现它的智能合约都可以发行一个“可替代代币”(FungibleToken,FT)可替代”是一个经济学术语,意思是:每一个单位的代币都是一样的、等价可互换的比如:你有一个1USDT,我也有一个1U
ERC20 源码解析
ERC20 是以太坊上的一种代币标准,定义了一组规则,任何实现它的智能合约都可以发行一个“可替代代币”(Fungible Token, FT)
可替代”是一个经济学术语,意思是: 每一个单位的代币都是一样的、等价可互换的
比如:
- 你有一个 1 USDT,我也有一个 1 USDT,它们没有任何区别,随时可以交换。
- ERC20 代币的余额是一个整数,就像银行账户一样:你可以持有
100个单位
? 核心区别总结:FT vs NFT
| 特性 | FT(ERC20) | NFT(ERC721/ERC1155) |
|---|---|---|
| 替代性 | 可替代(同质化) | 不可替代(非同质化) |
| 单位表示 | 数字余额(如 100) | 唯一 ID(如 tokenId = 1, 2, 3) |
| 使用场景 | 金融资产、积分、投票权等 | 数字收藏品、门票、产权等 |
| 常见标准 | ERC20 | ERC721 / ERC1155 |
| 是否可以批量 | 可以转账任意数量 | 通常一次转一个(ERC721),ERC1155 可批量 |
** 导入依赖接口与工具类**
import {IERC20} from "./IERC20.sol";
import {IERC20Metadata} from "./extensions/IERC20Metadata.sol";
import {Context} from "../../utils/Context.sol";
import {IERC20Errors} from "../../interfaces/draft-IERC6093.sol";
继承的核心模块:
IERC20: ERC20 标准接口定义(包括transfer,approve等)。IERC20Metadata: 补充接口,定义如name(),symbol(),decimals()。Context: 提供_msgSender()方法,方便 meta-transaction 支持。IERC20Errors: 标准错误定义(ERC-6093 草案)用于 revert 时的错误格式(比如ERC20InvalidSender等)。
合约声明与继承
abstract contract ERC20 is Context, IERC20, IERC20Metadata, IERC20Errors {
abstract表示这是一个抽象合约,不能直接部署。- 继承了上述接口和工具类。
存储变量定义
mapping(address account => uint256) private _balances;
mapping(address account => mapping(address spender => uint256)) private _allowances;
uint256 private _totalSupply;
string private _name;
string private _symbol;
_balances: 每个地址的代币余额。_allowances: 授权机制(某地址允许另一个地址最多能转多少 token)。_totalSupply: 总供应量。_name,_symbol: 代币的名称与符号
构造函数
constructor(string memory name_, string memory symbol_) {
_name = name_;
_symbol = symbol_;
}
只在部署合约时调用,用来初始化代币的名称和符号。
MetaData 获取
function name() public view virtual returns (string memory) {
return _name;
}
function symbol() public view virtual returns (string memory) {
return _symbol;
}
function decimals() public view virtual returns (uint8) {
return 18;
}
decimals()默认返回 18(模拟以太单位)。- 所有 Metadata 接口均为
view且可virtual以供 override
标准接口函数实现
1. totalSupply() 和 balanceOf()
function totalSupply() public view virtual returns (uint256) {
return _totalSupply;
}
function balanceOf(address account) public view virtual returns (uint256) {
return _balances[account];
}
- 分别返回全局供应量和单一地址余额。
2. transfer()
- transfer -> _transfer -> _update
function transfer(address to, uint256 value) public virtual returns (bool) {
address owner = _msgSender();
_transfer(owner, to, value);
return true;
}
- 将当前调用者的余额转账至
to。 - 实际调用
_transfer(owner, to, value)实现转账逻辑。
3. approve() 与 allowance()
- approve -> _approve -> _approve重载
function approve(address spender, uint256 value) public virtual returns (bool) {
address owner = _msgSender();
_approve(owner, spender, value);
return true;
}
function allowance(address owner, address spender) public view virtual returns (uint256) {
return _allowances[owner][spender];
}
approve()设置授权额度(包含uint256.max的无限授权逻辑)。allowance()读取授权额度。
4. transferFrom()
function transferFrom(address from, address to, uint256 value) public virtual returns (bool) {
address spender = _msgSender();
_spendAllowance(from, spender, value);
_transfer(from, to, value);
}
- 从
from向to发送value代币,调用_spendAllowance()和_transfer()
核心内部函数
1. _transfer()
function _transfer(address from, address to, uint256 value) internal {
// from 不能为零地址
if (from == address(0)) {
revert ERC20InvalidSender(address(0));
}
// to 不能为零地址
if (to == address(0)) {
revert ERC20InvalidReceiver(address(0));
}
_update(from, to, value);
}
- 检查合法地址 → 触发
_update()进行实际操作。
2. _update()
function _update(address from, address to, uint256 value) internal virtual {
//场景:token铸造
if (from == address(0)) {
_totalSupply += value;
} else {
// 场景:正常转账- from账户减少token
uint256 fromBalance = _balances[from];
if (fromBalance < value) {
revert ERC20InsufficientBalance(from, fromBalance, value);
}
unchecked {
_balances[from] = fromBalance - value;
}
}
//场景:token 销毁
if (to == address(0)) {
unchecked {
_totalSupply -= value;
}
} else {
// 场景:正常转账- to账户增加token
unchecked
_balances[to] += value;
}
}
emit Transfer(from, to, value);
}
-
设计亮点:该函数统一处理三种操作:
- 正常转账:from ≠ 0 且 to ≠ 0
- 铸造(mint):from == 0
- 销毁(burn):to == 0
-
所有余额和总量变化都统一经由
_update()完成,有助于自定义逻辑拓展。
3. _mint() 和 _burn()
function _mint(address account, uint256 value) internal {
if (account == address(0)) {
revert ERC20InvalidReceiver(address(0));
}
_update(address(0), account, value);
}
function _burn(address account, uint256 value) internal {
if (account == address(0)) {
revert ERC20InvalidSender(address(0));
}
_update(account, address(0), value);
}
- 都是
_update()的封装,分别传入 from/to 为 zero address。
授权操作函数
1. _approve()(两个版本)
function _approve(address owner, address spender, uint256 value) internal {
_approve(owner, spender, value, true);
}
// 增加:事件触发逻辑
function _approve(address owner, address spender, uint256 value, bool emitEvent) internal virtual {
if (owner == address(0)) {
revert ERC20InvalidApprover(address(0));
}
if (spender == address(0)) {
revert ERC20InvalidSpender(address(0));
}
_allowances[owner][spender] = value;
if (emitEvent) {
emit Approval(owner, spender, value);
}
}
- 第二个版本提供了是否触发
Approval事件的开关。
2. _spendAllowance()
function _spendAllowance(address owner, address spender, uint256 value) internal virtual {
uint256 currentAllowance = allowance(owner, spender); // 当前授权额度
if (currentAllowance < type(uint256).max) {
if (currentAllowance < value) {
revert ERC20InsufficientAllowance(spender, currentAllowance, value);
}
unchecked {
_approve(owner, spender, currentAllowance - value, false);
}
}
}
- 该方法是
transferFrom的关键一环,用于消耗spender对owner的授权额度。 - 如果授权额度是最大值
type(uint256).max,说明是“无限授权”,就不会减少额度(节省 gas)。 - 否则,进行额度减少处理。
- 使用
unchecked可以省 gas,因为已经做过越界检查。
错误处理(EIP-6093)
合约中大量使用:
revert ERC20InvalidSender(address(0));
revert ERC20InsufficientBalance(from, balance, required);
这些错误类型定义于 IERC20Errors,相比使用 require(...) 加 string 更节省 gas。
ERC20 Demo
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyCustomToken is ERC20, Ownable {
constructor(
string memory name_,
string memory symbol_,
uint256 initialSupply
) ERC20(name_, symbol_) Ownable(msg.sender) {
// 初始发行全部给合约部署者
_mint(msg.sender, initialSupply);
}
/// @notice 增发代币,只允许合约拥有者调用
function mint(address to, uint256 amount) external onlyOwner {
_mint(to, amount);
}
/// @notice 销毁自己账户的代币
function burn(uint256 amount) external {
_burn(msg.sender, amount);
}
}
ERC20 扩展模块
OpenZeppelin v5 中围绕 ERC20 提供了 多个扩展模块(extensions),快速构建功能强大的代币系统,适配各种业务场景,比如可销毁、可暂停、快照、投票、许可等
| 模块 | 说明 | 常用场景 |
|---|---|---|
ERC20Burnable |
添加了 burn 和 burnFrom 功能,可销毁自己的代币或允许销毁被批准的代币 |
通缩模型、销毁代币机制 |
ERC20Capped |
设置最大总供应量,mint 超过 cap 将报错 |
限量代币、预挖、ICO 总量限制 |
ERC20Pausable |
添加 pause() / unpause(),可暂停转账、授权操作 |
应急响应、合规控制 |
ERC20Permit |
支持 EIP-2612 permit() 签名授权转账,无需先调用 approve() |
提升用户体验、Gasless 交易 |
ERC20Votes |
引入治理功能,通过代币权重进行投票 | DAO 治理、链上投票 |
ERC20FlashMint |
支持无抵押的闪电贷(必须在同一交易内归还) | 闪电贷、DeFi 套利、流动性操作 |
?️ 示例组合使用(实际场景)
-
DAO 代币:
ERC20Votes+ERC20Permit+ERC20Snapshot→ 投票、Gasless 授权、治理快照
-
预挖代币:
ERC20Capped+ERC20Mintable(自定义) → 限制供应、按需增发
-
应急控制:
ERC20Pausable→ 突发事件可暂停转账,例如攻击检测时冻结功能
-
可销毁代币:
ERC20Burnable→ 用户主动销毁代币或通过机制回收销毁
-
DeFi 支持代币:
ERC20Permit+ERC20FlashMint→ 支持 Gasless 授权 + 闪电贷策略