3、ERC20代币标准:接口详解、发行自己的代币、代币的转账与授权
好,咱们今天来聊聊ERC20。这个标准,可以说是以太坊生态里最基础、最常用的东西了。你想想看,不管是发项目、做DeFi,还是搞DAO,几乎都绕不开它。我当年刚接触Solidity时,第一个实战项目就是发一个自己的ERC20代币。嗯,那会儿踩了不少坑,今天我把这些经验都揉碎了讲给你听。
3.1 ERC20接口详解:标准到底规定了什么?
ERC20说白了,就是一套大家都遵守的规则。它规定了代币合约必须实现哪些函数和事件。为什么要有标准?因为钱包、交易所、DApp都需要用统一的方式来跟你的代币交互。没有标准,那可就乱套了。
我个人习惯把ERC20接口分成三块来看:
- 查询类函数:
totalSupply()、balanceOf()、allowance() - 操作类函数:
transfer()、approve()、transferFrom() - 事件:
Transfer、Approval
先看查询类。 totalSupply() 返回代币总供应量,balanceOf(address) 查某个地址的余额。这两个函数都是 view 类型的,不花Gas。我在项目中遇到过有人把 balanceOf 写成 public 变量,虽然也能用,但不符合标准接口,钱包就不认了。
操作类函数是核心。 transfer 是从你账户转给别人,approve 是授权别人花你的钱,transferFrom 是别人花你授权的钱。这个授权机制,是ERC20最巧妙也最容易出问题的地方。
核心接口速览表
| 函数 | 说明 | 返回值 |
|---|---|---|
| totalSupply() | 总供应量 | uint256 |
| balanceOf(account) | 查询余额 | uint256 |
| transfer(recipient, amount) | 转账 | bool |
| approve(spender, amount) | 授权 | bool |
| transferFrom(sender, recipient, amount) | 代转账 | bool |
| allowance(owner, spender) | 查询授权额度 | uint256 |
3.2 发行自己的代币:从零开始写一个ERC20合约
好,理论讲完了,咱们直接上手写代码。我建议你跟着我一起敲,别光看。
先引入OpenZeppelin的ERC20标准实现。为什么用现成的?因为安全。我自己早期手写过ERC20,结果在 transferFrom 里漏了个检查,差点出大问题。从那以后,能用审计过的库,我绝不自己造轮子。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MyToken is ERC20 {
constructor(uint256 initialSupply) ERC20("MyToken", "MTK") {
_mint(msg.sender, initialSupply * 10 ** decimals());
}
}
这段代码看着简单,但有几个关键点我得提醒你:
- 构造函数:传了代币名称和符号。我习惯用全称和缩写,比如"蓝海币"和"LHB"。
- 精度:
decimals()默认是18,跟ETH一样。你想想看,如果精度设成0,那最小单位就是1个币,没法分。所以一般不动它。 - 铸币:
_mint是内部函数,直接把初始供应量打给合约部署者。注意这里乘了10 ** decimals(),因为合约内部存储的是最小单位。
我的小技巧:部署时如果传 1000000 作为初始供应量,实际发行的是100万 * 10^18个最小单位。前端显示时记得除以精度,不然数字会大得吓人。
3.3 代币的转账与授权:实战中的坑与解法
转账和授权,是ERC20最常用的功能。但这里面的坑,我踩过不止一次。
3.3.1 转账:transfer 的正确姿势
transfer 函数很简单,就是从你的地址转到目标地址。但要注意,它返回一个 bool。我曾经见过有人不检查返回值,结果转账失败也不知道。嗯,这很危险。
// 正确做法:检查返回值
bool success = token.transfer(recipient, amount);
require(success, "Transfer failed.");
// 或者用 SafeERC20 的 safeTransfer
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
using SafeERC20 for IERC20;
token.safeTransfer(recipient, amount);
我个人强烈推荐用 SafeERC20。它帮你处理了返回值检查,还兼容那些不返回bool的老代币。比如USDT,它的 transfer 就不返回值,直接用 transfer 会报错。用 safeTransfer 就没事。
3.3.2 授权:approve 的双重调用问题
授权这块,有个经典坑:双重调用攻击。说白了,就是用户先授权A给B 100个币,然后B花掉了50个。这时候用户想改成授权80个,就发起一笔新的授权交易,把额度改成80。但如果在第一笔交易还没确认时,B抢先花掉了剩下的50个,那用户的授权额度就变成了80+50=130个,比原来还多。
避坑指南:我曾经在写一个DEX聚合器时遇到过这个问题。解决方案是:先授权为0,再授权为目标值。或者用 increaseAllowance 和 decreaseAllowance 函数。OpenZeppelin的ERC20已经内置了这两个函数,直接用就行。
// 安全授权方式
// 先清零
token.approve(spender, 0);
// 再设置新值
token.approve(spender, newAmount);
// 或者用增量函数
token.increaseAllowance(spender, addedValue);
token.decreaseAllowance(spender, subtractedValue);
3.3.3 transferFrom:三方交互的典型场景
transferFrom 是DApp的核心。比如在去中心化交易所里,用户先授权合约花他的代币,然后合约调用 transferFrom 把代币从用户账户转走。这个流程,你想想看,是不是很常见?
但要注意,transferFrom 的调用者必须是已经被授权的地址。而且它也会返回 bool,同样建议用 safeTransferFrom。
// 完整的三方交互示例
// 1. 用户调用 approve(contractAddress, 1000)
// 2. 合约调用 transferFrom(userAddress, contractAddress, 500)
// 合约内部代码
function deposit(uint256 amount) external {
require(amount > 0, "Amount must be > 0");
// 从用户账户转代币到合约
token.safeTransferFrom(msg.sender, address(this), amount);
// 更新用户存款记录
balances[msg.sender] += amount;
emit Deposited(msg.sender, amount);
}
总结一下今天的重点:
- ERC20接口有6个函数和2个事件,必须全部实现
- 发行代币用OpenZeppelin的模板,安全省心
- 转账和授权一定要检查返回值,推荐用SafeERC20
- 授权时注意双重调用问题,用增量函数或先清零再授权
好了,这一章的内容就到这儿。下一章我们会讲ERC721,也就是NFT的标准。到时候你会发现,很多思路跟ERC20是相通的,但又有它独特的地方。咱们下章见。