ERC-20
ERC-20 本身不是一个代币,也不是一段代码,而是一套“技术标准”或“接口规范”。它为所有基于以太坊的“同质化代币”(Fungible Tokens)定义了一套通用的、必须遵守的规则。“同质化”意味着每个代币之间都是完全相同、可以互换的,就像你钱包里的一元硬币和我钱包里的一元硬币一样,没有区别。
“ERC”是“Ethereum Request for Comments”的缩写,意为“以太坊意见征求稿”,而“20”是这份提案的编号。该标准规定,任何希望被视为ERC-20代币的智能合约,必须实现以下一套函数和事件(Events)。
核心函数与事件接口
函数/事件 | 描述 | 作用与解释 |
---|---|---|
name() |
(可选) 返回代币的名称,如 "Tether USD"。 | 方便用户界面展示。 |
symbol() |
(可选) 返回代币的符号,如 "USDT"。 | 方便用户界面展示。 |
decimals() |
(可选) 返回代币支持的小数位数。 | 8 意味着1,00000000 个最小单位等于1个代币。USDT是6,ETH是18。 |
totalSupply() |
(必须) 返回代币的总供应量。 | 提供了代币总量的透明度。 |
balanceOf(address _owner) |
(必须) 返回指定地址的代币余额。 | 查询任何人的持币数量。 |
transfer(address _to, uint256 _value) |
(必须) 从消息发送者(msg.sender )的账户向 _to 地址发送 _value 数量的代币。 |
这是最基础的点对点转账功能。 |
approve(address _spender, uint256 _value) |
(必须) 授权 _spender 地址可以从你的账户中提取不超过 _value 数量的代币。 |
这是实现合约交互的关键。你授权一个DEX(去中心化交易所)可以动用你100个USDT。 |
allowance(address _owner, address _spender) |
(必须) 查询 _spender 地址仍然被授权可以从 _owner 地址提取的代币数量。 |
查询授权余额。 |
transferFrom(address _from, address _to, uint256 _value) |
(必须) 由 _spender 地址调用,从 _from 地址向 _to 地址转移 _value 数量的代币。 |
在 approve 之后,被授权的DEX调用此函数,来实际执行你授权给它的那笔转账。 |
Transfer(address indexed _from, address indexed _to, uint256 _value) |
(必须事件) 在代币被转移时必须触发的事件。 | 方便链下应用(如钱包、浏览器)追踪代币流转历史。 |
Approval(address indexed _owner, address indexed _spender, uint256 _value) |
(必须事件) 在 approve 函数被成功调用时必须触发的事件。 |
方便追踪授权记录。 |
approve
+ transferFrom
的工作流程
这是理解ERC-20与DApp交互的核心。你不能直接“发送”代币给一个智能合约,因为合约无法知道这笔钱是干嘛的。正确的流程是“授权”:
- 你 (用户) 想在Uniswap(一个DEX合约)上用100个USDT兑换ETH。
- 你首先需要调用USDT合约的
approve()
函数,授权Uniswap的合约地址可以动用你账户里最多100个USDT。 - 然后,你再调用Uniswap的
swap()
函数。 - Uniswap的
swap()
函数在执行时,会它自己去调用USDT合约的transferFrom()
函数,把经过你授权的100个USDT从你的地址转移到它自己的地址,然后完成后续的兑换操作。
ERC-20标准的重要性
- 互操作性 (Interoperability) 这是最大的优点。因为所有ERC-20代币都遵循同一套规则,任何钱包(MetaMask, Trust Wallet)、交易所(Uniswap, Curve)、借贷协议(Aave, Compound)都可以无需定制开发,直接支持任何一个新的ERC-20代币。这创造了一个无需许可、可自由组合的“金融乐高”世界。
- 降低开发成本与风险 开发者无需为每个新代币重新设计底层逻辑。他们可以使用经过千锤百炼、由社区审计过的标准模板(例如 OpenZeppelin 的ERC-20实现)。这极大地减少了犯错的可能性。像本节下文讨论的美链(BEC)事件中的整数溢出漏洞,在使用标准的、安全的模板下是不会发生的。
- 催生生态繁荣 正是因为ERC-20的标准化,才使得2017年的ICO(首次代币发行)热潮成为可能,并为后来的DeFi(去中心化金融)和GameFi的爆发奠定了基础。它为以太坊上的价值表示和流转提供了通用语言。
现实中的例子:我们熟知的稳定币 USDT
、USDC
,去中心化交易所代币 UNI
,以及各种Meme币如 SHIB
等,绝大多数都是以太坊上的ERC-20代币。
美链(Beauty Chain, BEC)
事件概述
- 时间:2018年4月22日
- 主角:一个名为“美链(BEC)”的ERC-20代币。
- 事件:一名或多名攻击者利用了BEC智能合约中的一个**整数溢出(Integer Overflow)**漏洞,成功地调用了一个函数,从无到有地“凭空创造”了天量(具体是 2^255 * 2,一个近乎无穷大的数字)的BEC代币,并将其转入自己的两个地址。
- 后果:
- 当这笔异常巨大的转账记录出现在区块链上时,立刻被市场上的监控工具捕捉到。
- 市场迅速反应,意识到BEC代币的总量已经失控,其价值基础不复存在。
- 各大交易所(如OKEx)紧急暂停了BEC的充值和交易。
- 在短短几个小时内,BEC代币的价格暴跌超过99.5%,几乎归零。一个市值一度高达数亿美元的项目,瞬间灰飞烟灭。
这次攻击来自于合约中一个名为 batchTransfer
的批量转账函数里,一行极其简单的乘法代码。
// 这是BEC合约中存在漏洞的批量转账函数
function batchTransfer(address[] _receivers, uint256 _value) public returns (bool) {
// 1. 获取要转账的地址数量
uint cnt = _receivers.length;
// 2. 检查:要求接收者数量在1到20之间
require(cnt > 0 && cnt <= 20);
// 3. 计算总转账金额
// !!! 致命的漏洞就在下面这一行 !!!
uint256 amount = uint256(cnt) * _value;
// 4. 检查:要求调用者的余额必须大于或等于总转账金额
require(_value > 0 && balances[msg.sender] >= amount);
// 5. 从调用者账户中减去总额
balances[msg.sender] -= amount;
// 6. 循环给每个接收者转入_value数量的代币
for (uint i = 0; i < cnt; i++) {
balances[_receivers[i]] += _value;
Transfer(msg.sender, _receivers[i], _value);
}
return true;
}
攻击者的目标是绕过第4步的余额检查 balances[msg.sender] >= amount
。他需要让 amount
这个值变得非常小(最好是0),但同时,在第6步的循环中,他又希望转给自己的 _value
是一个巨大的数字。
利用整数溢出,他完美地做到了这一点:
- 构造参数:攻击者调用
batchTransfer
函数,并传入两个精心构造的参数:_receivers
: 一个包含两个他自己控制的地址的数组。因此cnt = 2
。_value
: 一个极其巨大的数字,例如 2^255 (大约是uint256
最大值的一半)。
- 触发溢出:在第3步计算总金额时,合约执行
amount = 2 * (2^255)
。- 在数学上,结果是 2256。
- 但在
uint256
类型中,这个数字超过了它的最大表示范围,于是发生了溢出,amount
的值“翻转”变为了0
。
- 绕过检查:在第4步进行余额检查时,
require(balances[msg.sender] >= 0)
这个条件永远为真。攻击者几乎不需要任何BEC余额就能通过检查。 - 凭空印钞:
- 在第5步,合约从攻击者余额中减去
amount
(也就是减0),攻击者的余额不变。 - 在第6步,循环执行两次。每一次,都将那个巨大的
_value
(即 2^255)转入攻击者的地址。 - 最终,攻击者成功地给自己控制的两个地址,每个地址转入了 2^255 个BEC代币,而他自己的余额几乎没有减少。
- 在第5步,合约从攻击者余额中减去
后果
美链事件是所有智能合约开发者的警钟,它带来了深刻的教训:
- 安全审计的绝对必要性:这是一个非常基础的漏洞,任何一个合格的智能合约审计师都能轻易发现。这表明,项目在上线前进行专业的第三方安全审计是不可或缺的。
- 安全数学库的重要性:在此事件之后,社区更加重视使用经过安全审计的数学库,例如当时广泛使用的 OpenZeppelin 的
SafeMath
库。这个库会重写加减乘除运算,在发生溢出时直接抛出错误(revert
),而不是允许其“翻转归零”。 - 语言设计的演进:这个漏洞是推动Solidity语言自身进化的重要动力之一。为了从根本上解决这个问题,自Solidity 0.8.0版本起,语言已经默认内置了对整数溢出和下溢的检查。现在,除非开发者使用一个特殊的
unchecked { ... }
块,否则任何会导致溢出的算术运算都会直接revert
。