3、ERC20代币标准:接口详解、发行自己的代币、代币的转账与授权

好,咱们今天来聊聊ERC20。这个标准,可以说是以太坊生态里最基础、最常用的东西了。你想想看,不管是发项目、做DeFi,还是搞DAO,几乎都绕不开它。我当年刚接触Solidity时,第一个实战项目就是发一个自己的ERC20代币。嗯,那会儿踩了不少坑,今天我把这些经验都揉碎了讲给你听。

3.1 ERC20接口详解:标准到底规定了什么?

ERC20说白了,就是一套大家都遵守的规则。它规定了代币合约必须实现哪些函数和事件。为什么要有标准?因为钱包、交易所、DApp都需要用统一的方式来跟你的代币交互。没有标准,那可就乱套了。

我个人习惯把ERC20接口分成三块来看:

  • 查询类函数totalSupply()balanceOf()allowance()
  • 操作类函数transfer()approve()transferFrom()
  • 事件TransferApproval

先看查询类。 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,再授权为目标值。或者用 increaseAllowancedecreaseAllowance 函数。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是相通的,但又有它独特的地方。咱们下章见。