ERC20代币标准详解

ERC20,说白了就是以太坊上代币的"通用语言"。我刚开始接触智能合约时,觉得这玩意儿不就是个接口规范吗?后来踩过坑才明白——没有标准,DeFi生态根本玩不转。

ERC20接口规范

ERC20标准定义了一套接口,所有代币合约都得遵守。就像USB接口一样,不管里面是什么电路,插上去就能用。

核心接口就这几个:

  • totalSupply() — 返回总供应量
  • balanceOf(address) — 查询某个地址的余额
  • transfer(address, uint256) — 转账
  • approve(address, uint256) — 授权
  • allowance(address, address) — 查询授权额度
  • transferFrom(address, address, uint256) — 代转账

嗯,这里要注意:接口里没有构造函数,没有mapping,只有函数签名。你想想看,这其实给了开发者很大的自由度——只要实现了这些函数,内部怎么存数据都行。

代币合约实现

我个人习惯用OpenZeppelin的模板,但为了讲清楚原理,咱们手写一个精简版。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract MyToken {
    string public name = "MyToken";
    string public symbol = "MTK";
    uint8 public decimals = 18;
    uint256 private _totalSupply;
    
    mapping(address => uint256) private _balances;
    mapping(address => mapping(address => uint256)) private _allowances;
    
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
    
    constructor(uint256 initialSupply) {
        _totalSupply = initialSupply * 10 ** decimals;
        _balances[msg.sender] = _totalSupply;
        emit Transfer(address(0), msg.sender, _totalSupply);
    }
    
    function totalSupply() public view returns (uint256) {
        return _totalSupply;
    }
    
    function balanceOf(address account) public view returns (uint256) {
        return _balances[account];
    }
    
    function transfer(address to, uint256 amount) public returns (bool) {
        require(to != address(0), "ERC20: transfer to zero address");
        require(_balances[msg.sender] >= amount, "ERC20: insufficient balance");
        
        _balances[msg.sender] -= amount;
        _balances[to] += amount;
        emit Transfer(msg.sender, to, amount);
        return true;
    }
    
    function approve(address spender, uint256 amount) public returns (bool) {
        _allowances[msg.sender][spender] = amount;
        emit Approval(msg.sender, spender, amount);
        return true;
    }
    
    function allowance(address owner, address spender) public view returns (uint256) {
        return _allowances[owner][spender];
    }
    
    function transferFrom(address from, address to, uint256 amount) public returns (bool) {
        require(from != address(0), "ERC20: transfer from zero address");
        require(to != address(0), "ERC20: transfer to zero address");
        require(_balances[from] >= amount, "ERC20: insufficient balance");
        require(_allowances[from][msg.sender] >= amount, "ERC20: insufficient allowance");
        
        _balances[from] -= amount;
        _balances[to] += amount;
        _allowances[from][msg.sender] -= amount;
        emit Transfer(from, to, amount);
        return true;
    }
}

关键点:构造函数里铸造了所有代币给部署者。实际项目中,你可能需要更复杂的铸造逻辑,比如时间锁、多签控制。

总供应量与余额管理

总供应量是个固定值,但余额是动态变化的。我在项目中遇到过一个问题:有人试图通过反复调用transfer来制造假交易量。怎么防?其实ERC20标准本身不关心这个,但你可以加个交易限额或者冷却时间。

余额管理说白了就是加减法:

  • 转账时,发送方余额减少,接收方余额增加
  • 铸造时,总供应量增加,接收方余额增加
  • 销毁时,总供应量减少,发送方余额减少

你想想看,如果余额管理出bug,比如溢出,那整个代币就废了。所以Solidity 0.8+默认带溢出检查,但老版本你得自己用SafeMath。

转账与授权机制

转账分两种:

  1. 直接转账 — 你直接给别人打钱
  2. 授权转账 — 你授权别人帮你打钱

授权机制是DeFi的基石。比如你在Uniswap上交易,其实是先授权Uniswap合约花你的代币,然后它调用transferFrom帮你转出去。

我曾经踩过一个坑:授权额度没有清零。如果你授权了100个代币给某个合约,它只花了50个,那剩下的50个它随时可以花。所以很多DeFi协议会先授权0,再授权新额度,防止重入攻击。

授权流程是这样的:

  1. 用户A调用approve(合约地址, 100)
  2. 合约调用transferFrom(A, B, 50)
  3. A的授权额度从100变成50
  4. 合约可以继续花剩下的50

事件(Event)的使用

事件是合约和前端之间的桥梁。没有事件,前端根本不知道链上发生了什么。

ERC20标准定义了两个事件:

事件名 参数 说明
Transfer from, to, value 任何转账都会触发
Approval owner, spender, value 授权额度变化时触发

我个人习惯把indexed关键字加在from和to上,这样前端可以快速过滤。比如查询某个地址的所有转账记录:

// 前端代码示例(ethers.js)
const filter = contract.filters.Transfer(myAddress);
contract.on(filter, (from, to, value) => {
    console.log(`${from} 转了 ${value} 给 ${to}`);
});

小技巧:事件参数里的indexed最多三个。如果你需要更多索引字段,可以用多个事件,或者把额外数据塞到data里。

嗯,这里还要注意:事件不是必须的,但强烈建议加上。没有事件的代币合约,就像没有仪表盘的汽车——能开,但你不知道发生了什么。

我记得有一次调试一个跨链桥,发现代币转过去了但前端没显示。查了半天,原来是事件没emit。从那以后,我写合约第一件事就是检查事件有没有漏掉。

总结一下:ERC20标准看似简单,但每个细节都关乎安全。授权机制要防重入,余额管理要防溢出,事件要完整记录。把这些搞明白了,DeFi开发就入门了。