第十五章 · 第一课 / ERC standard system

一枚代币,如何被全世界的钱包与协议读懂?

ERC-20 不是一种币,而是一份共同接口: 它让彼此从未见过的钱包、交易所与合约,知道怎样查询余额、转移代币与授权使用。

目录主题:ERC-20:同质化代币,例如稳定币与 DeFi 代币

  • 阅读约 48 分钟
  • 2 个互动实验
  • 14 组结构图
  • 6 题自测
20TOKEN
CONTRACT
WALLET
DEX
LENDING
TREASURY
一份接口,多种资产,无数接入方。

01 / LEDGER

代币在合约里,不在钱包里

钱包只是读取某个代币合约中“地址 → 余额”的记录,并代你签署调用。

02 / INTERFACE

标准统一动作,不统一价值

ERC-20 规定怎样读写,不规定价格、抵押、增发权限、锚定或治理含义。

03 / COMPOSABILITY

授权机制是 DeFi 的接口铰链

approve 与 transferFrom 让另一份合约在限定额度内组合你的代币。

01 / Orientation

先把“币、代币、标准”拆成三层

多数误解都来自把经济对象、链上程序与技术接口混成一件事。

你在钱包中看到的 “100 USDC” 不是 100 枚数字硬币。 更准确地说,它是某个合约在自己的账本中,为你的地址记下的一个整数。

ETH 是以太坊协议原生资产,余额直接存在以太坊账户状态中; ERC-20 代币则由各自的智能合约维护。转一笔 ETH 和转一笔 ERC-20, 虽然钱包界面都写“发送”,底层交易结构并不相同。

“ERC-20”指的是一种标准接口。只要合约按照约定暴露核心函数并发出事件, 钱包就能显示它,DEX 可以尝试交换它,借贷协议可以尝试接收它。 这是一种语法兼容,不是对资产质量、安全性或价格的背书。

Economic object

资产与承诺

它代表美元、治理权、积分、协议份额,还是没有外部承诺的记账单位?

Onchain program

代币合约

一个具体地址上的代码与状态:余额、总量、权限和自定义规则都在这里。

Shared interface

ERC-20

其他程序如何查询和调用这份账本的共同语言;它不替代前两层审查。

02 / Contract ledger

钱包没有“装着”代币,它只是问合约:这个地址有多少?

先看状态模型,再看小数位。掌握这一步,后面的函数会自然连起来。

一个典型 ERC-20 实现至少维护两类数字:全局的 totalSupply, 以及按地址索引的 balanceOf。概念上可以把它想成 mapping(address => uint256),但标准只规定外部可观察行为, 不强制合约内部必须用哪一种存储布局。

balances[address]

addressbase units
0xA11c…Alice12,000,000
0xB0b…Bob3,000,000
0xP00l…Pool5,000,000

canonical invariant

Σ balances20,000,000= totalSupply

这里展示常规实现的守恒模型。遇到 rebasing、桥接包装或特殊记账方式时,要阅读具体实现。

合约只处理整数。钱包里看到的小数,是界面根据可选的 decimals() 元数据换算后的展示值。小数位不会提高计算精度,也不会改变供应量; 它只是告诉界面小数点放在哪里。

人类显示数量 × 10decimals = 链上整数

若 decimals = 6:12.5 个代币 = 12,500,000 个基础单位。若 decimals = 18:1 个代币 = 1018 个基础单位。

03 / Interface anatomy

六个核心函数、两个事件,构成最小共同语言

EIP-20 的力量不在函数很多,而在所有接入方都能预先知道这些入口的含义。

Read / 链下 eth_call

观察状态

  • totalSupply()当前总供应量
  • balanceOf(owner)某地址的余额
  • allowance(owner, spender)剩余可代扣额度
  • name / symbol / decimals常见但属于可选 view 元数据

Write / 需要交易

改变状态

  • transfer(to, value)调用者直接发送
  • approve(spender, value)设定代扣额度
  • transferFrom(from, to, value)使用已授予额度代扣

Logs / 供索引器观察

公开通知

  • Transfer(from, to, value)余额移动;零额也应发出
  • Approval(owner, spender, value)approve 成功后的新额度

钱包通过 RPC 发起链下 eth_call 读取 view 方法时,不会创建链上交易,也不会消耗用户的链上 Gas; 但同一个 view 方法若被另一份合约在交易中调用,执行仍计入 Gas,RPC 服务本身也可能有配额或费用。

成员 类型 它回答的问题 关键边界
totalSupply() required 有多少基础单位处于供应中? 不告诉你谁能增发,也不保证固定供应。
balanceOf(owner) required 某地址当前有多少基础单位? 地址可以是人控制的 EOA,也可以是合约。
transfer(to, value) required 调用者怎样直接转出自己的代币? 接收方是合约时,ERC-20 本身不要求回调确认。
approve(spender, value) required 怎样给另一个地址设置代扣上限? 再次调用会覆盖标准额度,不会转走余额。
transferFrom(from, to, value) required 被授权者怎样代表 owner 转移? 通常同时受余额与 allowance 两个约束。
allowance(owner, spender) required 还剩多少可代扣额度? 额度不是托管,余额不足时仍无法成功。
name / symbol / decimals optional 界面如何显示名称、符号与小数位? 接入方不能把它们存在当成绝对前提。

04 / Direct transfer

你给 Bob 转代币时,交易真正发给了谁?

答案不是 Bob,而是代币合约。Bob 与数量被编码在 calldata 里。

原生 ETH 转账中,交易的 to 通常就是收款地址,原生 value 携带金额。ERC-20 转账中,交易的 to代币合约地址,原生 value 通常为 0, calldata 表达“调用 transfer(Bob, amount)”。

这仍是一笔需要被网络执行的交易,因此 Gas 通常用该网络的原生资产支付: 在 Ethereum 主网上是 ETH。持有 ERC-20 余额,不自动意味着你有足够 ETH 支付转账或 approve 的 Gas。

Native transfer

发送 ETH

tx.toBob
tx.value1 ETH
calldata通常为空
改写协议账户余额

Contract call

发送 ERC-20

tx.toToken Contract
tx.value0 ETH
calldatatransfer(Bob, amount)
改写合约内部余额状态

STEP 01

签名调用

Alice 的钱包签署一笔发往代币合约的交易。

STEP 02

校验余额

合约判断 Alice 是否有足够余额,并应用自定义限制。

STEP 03

更新两端

Alice 减少、Bob 增加;普通转账不改变 totalSupply。

STEP 04

发出事件

Transfer 日志让钱包、浏览器和索引器重建历史。

Interactive model 01

直接转账状态机

固定本地算例 · 不连接钱包 · 不发送交易

默认算例:Alice 发送 25 LWD 后,余额从 120/30 变为 95/55,总供应量仍为 200,并产生一条 Transfer 事件。零额转账也应当产生事件。

等待调用。普通 transfer 只重分配余额,不改变总供应量。

  1. 尚无新事件。

05 / Allowance

approve 不是付款,而是给另一份合约一把有限额的钥匙

DeFi 的常见两步体验——先授权,再交换——来自 ERC-20 的双账本设计。

假设 Alice 要用 DEX Router 交换 100 个代币。Router 不能伪装成 Alice 调用 transfer,所以 Alice 先调用代币合约的 approve(Router, 100)。随后 Router 才能调用 transferFrom(Alice, Pool, 100)

授权记录是 owner × spender × token contract 三者的关系。 approve 不冻结余额,也不把代币交给 Router;Alice 仍可以自己花掉余额。 因此 transferFrom 成功必须同时满足:余额足够、额度足够、代币自定义规则允许。

OWNERAlice
SPENDERDEX Router
RECEIVERLiquidity Pool
approve(Router, 100)
只改额度
transferFrom(Alice, Pool, 100)
额度 + 余额一起检查
balanceOf(Alice)
资金仍属 Alice

Ledger A / ownership

余额账本

balances[Alice] = 500
balances[Pool] = 0

Ledger B / permission

授权账本

allowances[Alice][Router] = 100

Interactive model 02

授权双账本实验

模拟常见 OpenZeppelin 行为 · EIP-20 未规定无限额度优化

默认算例:approve(100) 不改变 Alice 的 500 LWD;Router 代扣 30 后,Alice 为 470、Pool 为 30、剩余额度为 70。

尚未授权。Router 现在不能从 Alice 账户代扣。

  1. 尚无新事件。

06 / Permit extension

不先发送 approve 交易,也能建立额度吗?

ERC-2612 把“授权意图”变成可由别人提交的 EIP-712 签名,但它没有消灭 allowance 风险。

permit 不是 ERC-20 核心接口。支持 ERC-2612 的代币额外暴露 permit(owner, spender, value, deadline, v, r, s)nonces(owner)DOMAIN_SEPARATOR()

owner 在链下签署包含 owner、spender、value、当前 nonce 与 deadline 的结构化消息; 任意地址都可以把签名提交给代币合约。提交成功后,合约设置 allowance、递增 owner 的 nonce, 并发出 Approval。离线签名本身不改链上状态,也不移动代币。

STEP 01

签名授权

owner 核对 token、spender、value、nonce、deadline 与签名域后离线签名。

STEP 02

任意人提交

DApp 或中继者把签名上链并支付 Gas;提交者不必等于 owner。

STEP 03

合约校验

验证签名、owner、当前 nonce、deadline 以及绑定链与验证合约的 domain。

STEP 04

额度生效

allowance 更新、nonce 递增并发 Approval;之后仍需 transferFrom 才会移动代币。

字段或机制 它解决什么 它不解决什么
nonce 每次成功 permit 后递增,使同一签名不能再次使用。 不能判断 spender 是否可信,也不能限制它怎样使用额度。
deadline 规定签名最晚可在何时提交;过期 permit 必须失败。 在有效期内,中继者仍可选择立即提交、延后提交或不提交。
DOMAIN_SEPARATOR 把签名隔离到特定签名域;常见域包含名称、版本、chainId 与 verifyingContract。 EIP-712 本身不固定每个应用的域字段,也不解释授权的商业含义。
签名校验 owner 非零、签名有效、nonce 正确且未过期时才可生效;无效 permit 必须 revert。 不能消除 approve 的额度覆盖竞态,也不能替代对代理和权限的审查。

07 / State & logs

余额在状态里,转账历史在日志里;两者不是同一种东西

理解 storage 与 event 的分工,才能正确阅读区块浏览器和索引数据。

对普通的非零、不同地址间转账,EVM 会更新合约状态中的可观察余额; 对所有成功的 transfertransferFrom,EIP-20 都要求把 Transfer 事件写入交易收据日志——即使数量为零。 合约今后的 balanceOf 读取状态;钱包历史页和数据索引器往往扫描日志。

Persistent state

权威当前值

balanceOf 与 allowance 面向“现在”。状态会被后续交易继续改写,节点必须保存其可验证结果。

ONE TRANSACTION

Receipt log

可索引历史信号

Transfer 与 Approval 面向“发生过什么”。日志便于过滤,但不能单独替代当前状态读取。

event Transfer(address indexed from, address indexed to, uint256 value)

fromto 被标记为 indexed,索引器可以高效按地址过滤; value 位于日志 data 中。EIP-20 还要求零额转账也发出 Transfer。 新币创建时发出 Transfer(0x0, to, value) 是 EIP-20 的 SHOULD 建议; burn 时使用 Transfer(from, 0x0, value) 是 OpenZeppelin 等实现的常见惯例, 但不是 EIP-20 核心规范单独规定的 burn 接口。

区块浏览器里的 “Token Transfers” 通常是从日志解释出来的视图, 不是一类独立的以太坊交易。一次交易可以产生零条、一条或多条代币转账事件; 例如路由交换常穿过多个池与多种代币。

事件也不保证能独立重建所有当前值。例如常见 OpenZeppelin 5.x 实现消耗 allowance 时默认不再发一条新的 Approval;rebase 或指数型余额还可能在没有普通 Transfer 的情况下变化。 因此关键判断应重新读取合约状态,而不是只累加历史日志。

08 / Implementation

标准没有规定怎样造币;它只规定造出来以后怎样被共同使用

安全实现通常复用经过广泛审查的库,再把供应与权限作为明确的业务决策。

OpenZeppelin 的 ERC20 实现刻意不决定供应机制。派生合约可以在构造时一次性 _mint,也可以把 mint 暴露给受控角色,或加入 cap、pause、votes、 permit 等扩展。“符合 ERC-20”从来不等于“固定供应”

FixedLessonToken.solteaching example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {ERC20} from
  "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract FixedLessonToken is ERC20 {
  constructor()
    ERC20("Loreword", "LWD")
  {
    // decimals() 默认返回 18;供应策略由本合约选择。
    _mint(msg.sender, 1_000_000 * 10 ** decimals());
  }
}

ERC-20 defines

共同接口

  • 余额与供应量怎样被查询
  • 直接转账与授权代扣的外部语义
  • Transfer 与 Approval 事件

Token chooses

经济与控制规则

  • 谁能 mint、burn、pause、upgrade 或 blacklist
  • 是否封顶、收税、rebase、冻结或跨链
  • 价格、抵押、赎回、治理与法律承诺

在常见 OpenZeppelin 5.x 实现中,转账、铸造与销毁最终统一经过内部 _update(from, to, value);零地址区分 mint 与 burn。 这便于扩展,但也意味着覆盖该钩子时必须理解所有路径,而不是只盯着公开的 transfer。

09 / Stablecoins & DeFi

为什么一种稳定币能进入钱包、DEX、借贷与金库,而无需逐一重写接口?

因为每个协议面对的是同一组函数形状;资产差异被保留在合约与经济层。

ERC-20COMMON
SOCKET
WALLETbalanceOf · transfer
DEX ROUTERapprove · transferFrom
LENDINGdeposit · collateral
VAULTassets in · shares out

STEP 01

建立额度

用户用 approve 或 permit 给 Vault 精确额度;此时资产仍在用户余额中。

STEP 02

调用 deposit

用户进入 Vault 的业务入口;deposit 不是 ERC-20 核心函数,而是协议自己的接口。

STEP 03

执行 transferFrom

Vault 作为 spender 从用户地址拉取资产,同时检查余额、额度与代币自定义规则。

STEP 04

按实账铸份额

协议比较调用前后余额差;fee-on-transfer 时实际到账可能小于参数 amount。

这条链解释了为什么“接口兼容”仍需要业务层核算:若 Vault 按用户传入的 100 铸造份额, 但税费代币实际只到账 98,系统会凭空产生 2 的缺口。稳健实现应按明确支持的代币语义, 使用实际余额增量计算份额,或直接拒绝 fee-on-transfer、rebase 等不受支持的资产。

稳定币与 DeFi 代币说明了“接口相同、含义不同”。Ethereum 上的 USDC 可以作为 ERC-20 被转账和授权,但其美元锚定、储备、发行与冻结能力不由 ERC-20 决定; DAI 的外部余额也通过 ERC-20 合约呈现,但其供应来自另一套抵押债务系统。

UNI 是 ERC-20,同时可承载协议治理语义;流动性池份额或 ERC-4626 金库份额也常以 ERC-20 表示。接口让它们可被钱包与其他合约复用,具体价值仍取决于底层资产、 赎回规则、权限和市场。

Stablecoin

稳定币

ERC-20 负责可转移性;锚定与赎回来自发行方储备、抵押系统或其他机制。

Governance

治理代币

余额可进一步映射为投票权,但快照、委托和治理执行需要额外标准与合约。

Receipt / share

凭证与份额

一个 ERC-20 单位可能代表池份额、金库份额或包装资产,而不是固定数量的美元。

10 / Semantic variance

同样都叫 ERC-20,为什么集成结果仍可能完全不同?

标准提供最小公分母;真实代币可能在这个边界之外加入影响余额与转账的语义。

01 / Plain

普通账本

转多少到多少,余额变化与 value 一致,最容易集成。

02 / Controlled

可暂停或冻结

接口仍在,但管理员权限可能阻止特定转账或地址。

03 / Dynamic

税费或 rebase

接收增量可能不等于参数 value,余额也可能无显式转账而变化。

04 / Upgradeable

代理与可变逻辑

合约地址不变,但实现、权限或行为可能通过升级改变。

Return values

有的旧代币不返回 bool

直接 ABI 解码可能失败;SafeERC20 通过低级调用兼容无返回值但未 revert 的实现。

Balance delta

收到的未必等于 value

fee-on-transfer 代币会扣税。依赖精确到账的协议应比较调用前后余额,而非盲信参数。

Approvals

额度更新可能有特殊要求

某些代币要求先归零再设非零值;集成库提供 forceApprove 等兼容工具。

Callbacks

“普通 ERC-20”也可能带钩子

扩展或恶意实现可能在转账路径执行外部逻辑。协议仍需按不受信任合约交互来设计。

11 / Security frame

面对一个代币,不要只问“是不是 ERC-20”,要画出谁能改什么

用户与开发者的风险入口不同,但都应从地址、权限、语义与授权四条线检查。

Identity layer

你面对的是哪个合约?

核对网络和地址;不要依赖名称、符号、图标或搜索结果。确认是否为代理以及当前实现。

Control layer

谁能增发、冻结或升级?

查 owner、roles、多签、时间锁和代理管理员;权限集中并不等于骗局,但必须进入风险模型。

Permission layer

谁还能从你的余额代扣?

allowance 可能在你离开 DApp 后继续有效。精确授权、核对 spender、及时撤销。

  1. 01 网络、合约地址与官方来源一致吗? 把名称和图标视为展示,不视为身份。
  2. 02 name、symbol、decimals 与基础单位怎样显示? 始终用整数运算,显示时再换算。
  3. 03 供应从哪里来,谁有 mint 或 burn 权限? 检查角色、上限、代理和治理执行路径。
  4. 04 转账是否可能暂停、冻结、收税或 rebase? 不要把 value 自动当作实际余额增量。
  5. 05 approve 或 permit 的 spender 到底是谁? 核对地址与升级权;签名还要核对 value、deadline、chainId 与 verifyingContract。
  6. 06 集成是否正确处理 false、无返回值与 revert? 使用成熟封装,并测试非标准代币行为。

12 / Recap

把整课压缩成一个可验证的心智模型

看到任何 ERC-20,先定位合约账本,再区分余额、授权、调用、事件与经济规则。

合约地址确定账本身份
+ balanceOf回答“谁有多少”
+ transfer移动调用者余额
+ approve → transferFrom建立受限代扣
+ Transfer / Approval公开发生过什么
≠ 价格、锚定、供应政策与安全背书

Q01钱包显示 Alice 有 100 个 ERC-20,最准确的底层解释是什么?

答案:B。钱包只是读取并展示合约状态。

Q02Alice 给 Bob 转 ERC-20 时,交易的 tx.to 通常是谁?

答案:C。Bob 被编码为 transfer 的参数。

Q03decimals = 6 最主要表示什么?

答案:A。小数点是展示约定,合约运算仍是整数。

Q04Alice 执行 approve(Router, 100) 后,立即发生了什么?

答案:B。授权与余额是两本账。

Q05两个合约都实现 ERC-20,可以直接推出什么?

答案:C。接口兼容不是经济或安全等价。

Q06balanceOf 与 Transfer 事件的关系,哪项正确?

答案:A。日志与状态相互关联,但用途不同。

13 / Glossary & primary sources

术语与一手资料

标准以 EIP 原文为准;实现与扩展以维护方当前文档为准。下面的链接均指向规范或官方资料。

ERC / EIP
ERC 是应用层标准类别;广为人知的 ERC-20 规范正式页面是 EIP-20。
Base unit
合约存储和运算的最小整数单位;界面结合 decimals 转成人类可读数量。
Owner / Spender
owner 持有余额;spender 是获得 allowance、可调用 transferFrom 的地址。
Allowance
某 owner 允许某 spender 在特定代币合约中代扣的剩余额度。
Revert
调用失败并回滚本次状态修改;已消耗的 Gas 通常不会返还。
Composability
不同合约因共同接口而能像积木一样调用、持有或组合彼此的资产与功能。
  1. 01 EIP-20 · ERC-20 Token Standard 六个核心函数、可选元数据与两个事件
  2. 02 Ethereum.org · Token standards 标准与可组合性的官方概览
  3. 03 Ethereum.org · Understand the ERC-20 token smart contract 接口与实现入门
  4. 04 OpenZeppelin Contracts 5.x · ERC20 API 实现、扩展与 SafeERC20
  5. 05 OpenZeppelin · Creating ERC-20 Supply 供应机制不属于标准核心
  6. 06 ERC-2612 · Permit Extension for EIP-20 签名授权、nonce、deadline 与重放防护
  7. 07 EIP-712 · Typed structured data hashing and signing Permit 使用的结构化签名基础
  8. 08 ERC-4626 · Tokenized Vaults 以 ERC-20 表示金库份额
  9. 09 Ethereum.org · Wrapped ether 原生 ETH 与 ERC-20 形式 WETH 的差别
  10. 10 Circle Developers · What is USDC? 稳定币发行与应用语义实例
  11. 11 Sky Protocol Docs · Dai DAI 外部 ERC-20 账本与系统关系
  12. 12 Uniswap Developers · Permit2 overview 签名控制与时限授权的进阶实例
  13. 13 EIP-20 discussion · allowance ordering mitigation 先归零再改额度的背景
  14. 14 ERC-6093 · Custom Errors for Common Token Standards ERC-20 等代币标准的 custom error 词汇
  15. 15 Solidity Docs · Events 事件日志、indexed 参数与过滤

Lesson 15.01 / conclusion

ERC-20 的伟大,不是创造了某一种代币,而是让不同代币获得了同一种可被调用的语法。

下一课将转向 ERC-721:当每个资产需要独立身份时,为什么“余额”不再足够。