01 / LEDGER
代币在合约里,不在钱包里
钱包只是读取某个代币合约中“地址 → 余额”的记录,并代你签署调用。
Ethereum learning path
Chapter 15 · Lesson 01
第十五章 · 第一课 / ERC standard system
ERC-20 不是一种币,而是一份共同接口: 它让彼此从未见过的钱包、交易所与合约,知道怎样查询余额、转移代币与授权使用。
目录主题:ERC-20:同质化代币,例如稳定币与 DeFi 代币
01 / LEDGER
钱包只是读取某个代币合约中“地址 → 余额”的记录,并代你签署调用。
02 / INTERFACE
ERC-20 规定怎样读写,不规定价格、抵押、增发权限、锚定或治理含义。
03 / COMPOSABILITY
approve 与 transferFrom 让另一份合约在限定额度内组合你的代币。
01 / Orientation
多数误解都来自把经济对象、链上程序与技术接口混成一件事。
你在钱包中看到的 “100 USDC” 不是 100 枚数字硬币。 更准确地说,它是某个合约在自己的账本中,为你的地址记下的一个整数。
ETH 是以太坊协议原生资产,余额直接存在以太坊账户状态中; ERC-20 代币则由各自的智能合约维护。转一笔 ETH 和转一笔 ERC-20, 虽然钱包界面都写“发送”,底层交易结构并不相同。
“ERC-20”指的是一种标准接口。只要合约按照约定暴露核心函数并发出事件, 钱包就能显示它,DEX 可以尝试交换它,借贷协议可以尝试接收它。 这是一种语法兼容,不是对资产质量、安全性或价格的背书。
Economic object
它代表美元、治理权、积分、协议份额,还是没有外部承诺的记账单位?
Onchain program
一个具体地址上的代码与状态:余额、总量、权限和自定义规则都在这里。
Shared interface
其他程序如何查询和调用这份账本的共同语言;它不替代前两层审查。
02 / Contract ledger
先看状态模型,再看小数位。掌握这一步,后面的函数会自然连起来。
一个典型 ERC-20 实现至少维护两类数字:全局的 totalSupply,
以及按地址索引的 balanceOf。概念上可以把它想成
mapping(address => uint256),但标准只规定外部可观察行为,
不强制合约内部必须用哪一种存储布局。
balances[address]
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
Write / 需要交易
Logs / 供索引器观察
钱包通过 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 与数量被编码在 calldata 里。
原生 ETH 转账中,交易的 to 通常就是收款地址,原生
value 携带金额。ERC-20 转账中,交易的 to
是代币合约地址,原生 value 通常为 0,
calldata 表达“调用 transfer(Bob, amount)”。
这仍是一笔需要被网络执行的交易,因此 Gas 通常用该网络的原生资产支付: 在 Ethereum 主网上是 ETH。持有 ERC-20 余额,不自动意味着你有足够 ETH 支付转账或 approve 的 Gas。
Native transfer
Contract call
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 只重分配余额,不改变总供应量。
05 / Allowance
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 成功必须同时满足:余额足够、额度足够、代币自定义规则允许。
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 账户代扣。
06 / Permit extension
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 会更新合约状态中的可观察余额;
对所有成功的 transfer 与 transferFrom,EIP-20 都要求把
Transfer 事件写入交易收据日志——即使数量为零。
合约今后的 balanceOf 读取状态;钱包历史页和数据索引器往往扫描日志。
Persistent state
balanceOf 与 allowance 面向“现在”。状态会被后续交易继续改写,节点必须保存其可验证结果。
Receipt log
Transfer 与 Approval 面向“发生过什么”。日志便于过滤,但不能单独替代当前状态读取。
from 与 to 被标记为 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”从来不等于“固定供应”。
// 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
Token chooses
在常见 OpenZeppelin 5.x 实现中,转账、铸造与销毁最终统一经过内部
_update(from, to, value);零地址区分 mint 与 burn。
这便于扩展,但也意味着覆盖该钩子时必须理解所有路径,而不是只盯着公开的 transfer。
09 / Stablecoins & DeFi
因为每个协议面对的是同一组函数形状;资产差异被保留在合约与经济层。
STEP 01
用户用 approve 或 permit 给 Vault 精确额度;此时资产仍在用户余额中。
STEP 02
用户进入 Vault 的业务入口;deposit 不是 ERC-20 核心函数,而是协议自己的接口。
STEP 03
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
标准提供最小公分母;真实代币可能在这个边界之外加入影响余额与转账的语义。
01 / Plain
转多少到多少,余额变化与 value 一致,最容易集成。
02 / Controlled
接口仍在,但管理员权限可能阻止特定转账或地址。
03 / Dynamic
接收增量可能不等于参数 value,余额也可能无显式转账而变化。
04 / Upgradeable
合约地址不变,但实现、权限或行为可能通过升级改变。
Return values
直接 ABI 解码可能失败;SafeERC20 通过低级调用兼容无返回值但未 revert 的实现。
Balance delta
fee-on-transfer 代币会扣税。依赖精确到账的协议应比较调用前后余额,而非盲信参数。
Approvals
某些代币要求先归零再设非零值;集成库提供 forceApprove 等兼容工具。
Callbacks
扩展或恶意实现可能在转账路径执行外部逻辑。协议仍需按不受信任合约交互来设计。
11 / Security frame
用户与开发者的风险入口不同,但都应从地址、权限、语义与授权四条线检查。
Identity layer
核对网络和地址;不要依赖名称、符号、图标或搜索结果。确认是否为代理以及当前实现。
Control layer
查 owner、roles、多签、时间锁和代理管理员;权限集中并不等于骗局,但必须进入风险模型。
Permission layer
allowance 可能在你离开 DApp 后继续有效。精确授权、核对 spender、及时撤销。
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 原文为准;实现与扩展以维护方当前文档为准。下面的链接均指向规范或官方资料。
Lesson 15.01 / conclusion
ERC-20 的伟大,不是创造了某一种代币,而是让不同代币获得了同一种可被调用的语法。
下一课将转向 ERC-721:当每个资产需要独立身份时,为什么“余额”不再足够。