深入认识 ERC-20 代币标准:开发者必读的知识点与实践指南

·

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?

以下示例用不到 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 的安全瑕疵:抢跑攻击

常见攻击路径:

  1. 用户 A 授权合约 C 额度 1000;
  2. 用户 A 把额度改到 500,交易在 mempool 中排队;
  3. MEV 机器人发现队列,提前用掉 1000 代币;
  4. 额度修改为 500 的交易成功,继续收割 500。

解决方案对比表(简明文字版):

开发者视角建议:直接使用社区维护的 OpenZeppelin 实现并开启 Permit 延展,可在不牺牲复杂度的情况下,大幅提升安全性与用户体验。👇

👉 一步完成代币授权与交易?免手续费的路由设置范例就在这。

2. 授权需两次交易,体验割裂

用户给 DEX 加流动性时,往往需要先做授权,再做 swap。ERC-2612 让签名消息替代链上 approve,把两笔交易压缩成一笔,Gas 消耗降低 30%–50%。

实现步骤:

  1. 前端取用户签名:permit(owner, spender, value, deadline, v, r, s)
  2. 合约同时调用:permit + transferFrom
  3. 一笔交易完成授权和资金划转。

3. 操作代币必须拥有 ETH —— meta-transaction

钱包里有 100 USDT,却没 ETH 付 Gas?Meta-transaction 允许“别人代付,用户用 USDT 返现”。

流程分成三步:

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/tokenswallet-standard.org 的字典即可快速比对 symbol、decimals、explorer 是否符合通用格式。

Q5:发行代币后如何直接进入 Uniswap?
A:在工厂合约新建交易对,往池子注入基础代币+新币,设置初始价格即可。前期可先测试网反复跑一次,确认无黑线逻辑。

Q6:合约审计阶段,常见高危点有哪些?
A:1) 无限铸币权限控制;2) 黑名单绕过;3) permit 消息重放;4) 回退函数误开 payable。

结语与行动建议

通过本文梳理,你是否已经具备从 0 发布一个安全、兼容且用户体验友好的 ERC-20 代币的全部要点?下一步可以:

把知识落地,就是现在。祝你编码顺利,代码审计零漏洞!