第3章 ERC20标准:代币标准详解、接口定义、实现一个ERC20代币、代币发行与销毁
聊到DeFi,绕不开的就是ERC20。说实话,ERC20就是以太坊上代币的“普通话”。没有它,各个项目发的币就是各说各话,钱包、交易所根本没法统一处理。今天咱们就把这个标准彻底掰开揉碎。
3.1 ERC20标准详解
ERC20,全称是“Ethereum Request for Comments 20”,由Vitalik Buterin和Fabian Vogelsteller在2015年提出。它定义了一套通用的代币接口。任何遵循这个接口的代币,都能被钱包、交易所、DeFi协议无缝支持。
我个人习惯把ERC20理解成“代币界的USB接口”。你想想看,不管U盘里面是什么芯片,只要接口是USB,插上就能用。ERC20也一样,不管代币的经济模型多复杂,只要实现了标准接口,就能被MetaMask识别,能在Uniswap上交易。
核心要点:ERC20不是代码,是一套规则。它规定了代币必须有哪些函数和事件。
3.2 接口定义:你必须实现的6+2个函数
ERC20标准定义了6个必须实现的函数和2个必须触发的事件。我当年刚学的时候,总觉得事件可有可无,直到在项目中排查一笔丢失的转账,才发现没有事件日志,链下服务根本不知道发生了什么。
3.2.1 核心函数
| 函数 | 功能描述 | 返回值 |
|---|---|---|
totalSupply() |
返回代币总供应量 | uint256 |
balanceOf(address) |
查询某个地址的代币余额 | uint256 |
transfer(address, uint256) |
向指定地址转账 | bool |
allowance(address, address) |
查询授权额度(owner给spender的额度) | uint256 |
approve(address, uint256) |
授权某个地址可以花你的代币 | bool |
transferFrom(address, address, uint256) |
从授权地址转出代币 | bool |
3.2.2 必须触发的事件
- Transfer事件:任何转账操作(包括mint和burn)都必须触发。
- Approval事件:调用
approve()时触发。
我的小技巧:写事件的时候,记得把indexed关键字加上。比如event Transfer(address indexed from, address indexed to, uint256 value)。加了indexed的参数可以被链下服务高效检索,不然查个转账记录得全量扫描,慢得你想哭。
3.3 实现一个ERC20代币:从零开始写代码
好,理论说完了,咱们直接上手写代码。我用Solidity 0.8.x版本,因为0.8之后内置了溢出检查,省得我们手动加SafeMath了。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MyToken {
// 状态变量
string public name = "MyToken";
string public symbol = "MTK";
uint8 public decimals = 18;
uint256 private _totalSupply;
// 映射:地址 -> 余额
mapping(address => uint256) private _balances;
// 映射:所有者 -> (授权地址 -> 额度)
mapping(address => mapping(address => uint256)) private _allowances;
// 事件
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
// 构造函数:初始化总供应量
constructor(uint256 initialSupply) {
_totalSupply = initialSupply * 10 ** decimals;
_balances[msg.sender] = _totalSupply;
emit Transfer(address(0), msg.sender, _totalSupply);
}
// 查询总供应量
function totalSupply() public view returns (uint256) {
return _totalSupply;
}
// 查询余额
function balanceOf(address account) public view returns (uint256) {
return _balances[account];
}
// 转账
function transfer(address to, uint256 amount) public returns (bool) {
require(to != address(0), "ERC20: transfer to the zero address");
require(_balances[msg.sender] >= amount, "ERC20: insufficient balance");
_balances[msg.sender] -= amount;
_balances[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}
// 授权
function approve(address spender, uint256 amount) public returns (bool) {
require(spender != address(0), "ERC20: approve to the zero address");
_allowances[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
// 查询授权额度
function allowance(address owner, address spender) public view returns (uint256) {
return _allowances[owner][spender];
}
// 授权转账
function transferFrom(address from, address to, uint256 amount) public returns (bool) {
require(from != address(0), "ERC20: transfer from the zero address");
require(to != address(0), "ERC20: transfer to the zero address");
require(_balances[from] >= amount, "ERC20: insufficient balance");
require(_allowances[from][msg.sender] >= amount, "ERC20: insufficient allowance");
_balances[from] -= amount;
_balances[to] += amount;
_allowances[from][msg.sender] -= amount;
emit Transfer(from, to, amount);
return true;
}
}
避坑指南:我曾经在transferFrom里忘记扣减授权额度,结果用户授权一次就能无限次转走代币。这个bug在审计时被揪出来,差点导致项目延期。记住:transferFrom必须同时更新余额和授权额度。
3.4 代币发行与销毁:Mint和Burn
标准ERC20接口里没有mint和burn函数。为什么?因为标准只管“代币怎么转”,不管“代币怎么生怎么灭”。但实际项目中,这两个功能几乎必用。
3.4.1 发行(Mint)
Mint就是增发代币。我习惯在合约里加一个onlyOwner修饰符,防止随便谁都能印钞。
// 修饰符:只有合约所有者能调用
modifier onlyOwner() {
require(msg.sender == owner, "Not the owner");
_;
}
// 发行函数
function mint(address to, uint256 amount) public onlyOwner {
require(to != address(0), "ERC20: mint to the zero address");
_totalSupply += amount;
_balances[to] += amount;
emit Transfer(address(0), to, amount);
}
注意看,mint操作触发了Transfer(address(0), to, amount)。from地址是0x0,表示“从虚无中创造”。链下服务通过监听这个事件,就能知道代币被增发了。
3.4.2 销毁(Burn)
Burn就是销毁代币,减少总供应量。销毁通常由用户自己发起,比如你想退出某个质押池,把凭证代币销毁换回本金。
// 销毁函数
function burn(uint256 amount) public {
require(_balances[msg.sender] >= amount, "ERC20: burn amount exceeds balance");
_balances[msg.sender] -= amount;
_totalSupply -= amount;
emit Transfer(msg.sender, address(0), amount);
}
销毁时,to地址是0x0,表示“把代币送进黑洞”。一旦销毁,这些代币就永远无法恢复了。嗯,这里要注意:销毁前一定要确认用户真的想好了,不然哭都来不及。
3.4.3 发行与销毁的常见场景
| 场景 | 操作 | 说明 |
|---|---|---|
| 项目启动 | Mint | 在构造函数中一次性铸造所有代币 |
| 流动性挖矿 | Mint | 按区块逐步释放奖励代币 |
| 回购销毁 | Burn | 项目方用利润回购代币并销毁,通缩模型 |
| 跨链桥 | Mint + Burn | 源链销毁,目标链铸造,保持总供应量不变 |
我的经验:如果你做的是DeFi协议,建议把mint权限交给时间锁合约,而不是直接放在部署者地址。我曾经见过一个项目,部署者私钥泄露,黑客直接mint了10亿个代币砸盘。用时间锁,至少给社区一个反应时间。
3.5 总结
ERC20标准看似简单,但坑不少。我见过太多“标准”实现,结果在transferFrom的授权扣减逻辑上翻车。记住三点:
- 事件一定要触发,尤其是mint和burn时的Transfer事件
- 授权额度在
transferFrom中必须扣减 - mint和burn不是标准接口,但实际项目几乎必用
下一章,咱们聊聊ERC20的进阶玩法——带快照的投票代币,以及如何防止闪电贷攻击。到时候你会发现,标准只是起点,真正的工程挑战在后面。