ERC-20 代币标准自 2015 年提出以来,已成为以太坊生态最重要的基石之一。它简单、清晰,却在多年的演变中衍生出大量安全、效率与用户体验的进步。本文用一线开发者视角,重新梳理 ERC-20 的核心接口、示范实现、常见陷阱以及解決方案,帮助你快速在智能合约与 DApp 工程中落地。
ERC-20 接口定义
ERC-20 只规定 6 个 必须实现 的核心函数和 2 个事件,省略“可选元数据”依然合规。
| Solidity 代码 | 说明 |
|---|---|
totalSupply | 当前全网代币总量 |
balanceOf(account) | 查询某地址余额 |
transfer(to, amount) | 普通转账 |
approve(spender, amount) | 授权他人额度 |
allowance(owner, spender) | 查询已授权额度 |
transferFrom(from, to, amount) | 代表他人执行转账 |
Transfer(from, to, value) | 转账事件 |
Approval(owner, spender, value) | 授权事件 |
除了必须实现的 6 个接口,大多数项目还会补充 3 个 可选方法,用于钱包或区块浏览器展示元数据:
function name() external view returns (string memory); // 代币名称
function symbol() external view returns (string memory); // 代币符号
function decimals() external view returns (uint8); // 小数位当你钱包里看到 “USDT 6 位小数、符号 = USDT” 时,就是靠这些方法读取。
最简 ERC-20 合约示例
核心数据为何只需要两个 mapping?
_balances[address]: 普通余额_allowances[owner][spender]: 授权额度
以下示例用不到 60 行代码就完成一个发行 10 000 枚的 DemoToken:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract DemoToken {
string public name = "DemoToken";
string public symbol = "DT";
uint8 public decimals = 18;
uint256 public totalSupply = 10_000 * 10 ** 18;
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() {
_balances[msg.sender] = totalSupply;
}
function balanceOf(address account) external view returns (uint256) {
return _balances[account];
}
function allowance(address owner, address spender) external view returns (uint256) {
return _allowances[owner][spender];
}
function approve(address spender, uint256 amount) external returns (bool) {
_allowances[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function transfer(address to, uint256 amount) external returns (bool) {
_balances[msg.sender] -= amount;
_balances[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}
function transferFrom(address from, address to, uint256 amount) external returns (bool) {
require(_allowances[from][msg.sender] >= amount, "insufficient allowance");
_allowances[from][msg.sender] -= amount;
_balances[from] -= amount;
_balances[to] += amount;
emit Transfer(from, to, amount);
return true;
}
}把合约编译部署,你就拥有了自己的 ERC-20 代币环境,可继续往 DEX、借贷或桥接协议里集成测试。
👉 想让代币直接在浏览器钱包即开即用?这里有一套验证兼容性的完整玩法。
三大会痛点及升级方案
1. approve / transferFrom 的安全瑕疵:抢跑攻击
常见攻击路径:
- 用户 A 授权合约 C 额度 1000;
- 用户 A 把额度改到 500,交易在 mempool 中排队;
- MEV 机器人发现队列,提前用掉 1000 代币;
- 额度修改为 500 的交易成功,继续收割 500。
解决方案对比表(简明文字版):
- 两次 approve
先 approve 0,再 approve 新值。缺点是两倍 Gas。 - OpenZeppelin 的
increaseAllowance/decreaseAllowance
引入安全增量接口。 - ERC-2612(permit)
采用链下签名:用户签完“授权消息”后,DApp 可把授权+转账在 1 笔交易完成。 - approve 新增 currentValue 校验
需要改 ABI,兼容性差,实际采用率低。
开发者视角建议:直接使用社区维护的 OpenZeppelin 实现并开启 Permit 延展,可在不牺牲复杂度的情况下,大幅提升安全性与用户体验。👇
2. 授权需两次交易,体验割裂
用户给 DEX 加流动性时,往往需要先做授权,再做 swap。ERC-2612 让签名消息替代链上 approve,把两笔交易压缩成一笔,Gas 消耗降低 30%–50%。
实现步骤:
- 前端取用户签名:
permit(owner, spender, value, deadline, v, r, s) - 合约同时调用:
permit+transferFrom。 - 一笔交易完成授权和资金划转。
3. 操作代币必须拥有 ETH —— meta-transaction
钱包里有 100 USDT,却没 ETH 付 Gas?Meta-transaction 允许“别人代付,用户用 USDT 返现”。
流程分成三步:
- 用户线下签名转账意图;
- Relayer(第三方)垫付 ETH;
- 合约把转账手续费从成功转账的 USDT 中扣除并返给 Relayer。
OpenGSN 提供了相对成熟的链下去中心化中继网络;OpenZeppelin Defender 支持中心化管理。上线成本低,适合已有用户体量的 DApp 快速接入。
实战答疑(FAQ)
Q1:累积允许授权额度太高会导致资产风险吗?
A:是的。建议用户日常授权只用必须的最小额度,DApp 前端可给出“授权当前数量”一键按钮,而非无限授权。
Q2:为什么部分代币无法直接转账而要特殊处理?
A:有的代币在转账函数中加入了黑白名单或收税逻辑,破坏了 ERC-20 抽象,需要阅读 token contract 源码再决定是否兼容。
Q3:ERC-20 与 ERC-777 谁更安全?
A:ERC-777 引入了 hook 机制,降低 approve/transferFrom 痛点,但同时也带来新的重入攻击场景。新项目如无跨合约回调需求,仍推荐先用 ERC-20 + Permit。
Q4:验证新合约是否与所有钱包兼容,有现成工具吗?
A:用 ethereum-lists/tokens 或 wallet-standard.org 的字典即可快速比对 symbol、decimals、explorer 是否符合通用格式。
Q5:发行代币后如何直接进入 Uniswap?
A:在工厂合约新建交易对,往池子注入基础代币+新币,设置初始价格即可。前期可先测试网反复跑一次,确认无黑线逻辑。
Q6:合约审计阶段,常见高危点有哪些?
A:1) 无限铸币权限控制;2) 黑名单绕过;3) permit 消息重放;4) 回退函数误开 payable。
结语与行动建议
通过本文梳理,你是否已经具备从 0 发布一个安全、兼容且用户体验友好的 ERC-20 代币的全部要点?下一步可以:
- Fork OpenZeppelin 合约模板,本地 Remix 编译部署;
- 集成 permit 封装脚本,减少用户授权步骤;
- 部署到测试网,做一次真实的跨链桥用户体验测试。
把知识落地,就是现在。祝你编码顺利,代码审计零漏洞!