一、ERC20标准解析
1.1 标准背景
ERC20由Fabian Vogelsteller和Vitalik Buterin于2015年提出,于2017年正式成为以太坊改进提案(EIP-20)。该标准定义了代币合约必须实现的基本接口,使得不同代币能够在统一的方式下与DApp、钱包和交易所进行交互。
ERC20的核心价值在于互操作性。只要你的合约实现了标准接口,市场上任何支持ERC20的工具都能与你的代币无缝对接。
1.2 完整接口规范
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
interface IERC20 {
// 代币基本信息
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function decimals() external view returns (uint8);
// 供应量查询
function totalSupply() external view returns (uint256);
// 余额查询
function balanceOf(address account) external view returns (uint256);
// 转账功能
function transfer(address to, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
// 授权功能
function approve(address spender, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
// 事件通知
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
1.3 核心概念理解
decimals(精度):大多数代币使用18位精度,与ETH保持一致。这意味着1个完整代币在合约内部表示为10^18。如果你发行100万个代币,totalSupply应设置为1000000 * 10^18。
Transfer事件:任何代币转移都必须触发Transfer事件,包括铸币(从零地址转出)和销毁(转入零地址)。
approve + transferFrom模式:这种授权转账机制允许第三方在授权额度内代为转账,适用于去中心化交易所、借贷协议等场景。
二、最简实现
2.1 基础版ERC20
让我们从最基础但功能完整的实现开始:
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract BasicToken {
// 使用mapping存储余额
mapping(address => uint256) private _balances;
// 使用mapping存储授权额度
mapping(address => mapping(address => uint256)) private _allowances;
// 不可变的代币名称
string public immutable name;
// 不可变的代币符号
string public immutable symbol;
// 不可变的精度(通常为18)
uint8 public immutable decimals;
// 代币总供应量
uint256 private _totalSupply;
// 事件定义
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
constructor(
string memory tokenName,
string memory tokenSymbol,
uint8 tokenDecimals,
uint256 initialSupply
) {
name = tokenName;
symbol = tokenSymbol;
decimals = tokenDecimals;
// 初始供应量全部发送给合约部署者
_mint(msg.sender, initialSupply * 10 ** uint256(decimals));
}
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 virtual returns (bool) {
_transfer(msg.sender, to, amount);
return true;
}
function allowance(address owner, address spender) public view returns (uint256) {
return _allowances[owner][spender];
}
function approve(address spender, uint256 amount) public virtual returns (bool) {
_approve(msg.sender, spender, amount);
return true;
}
function transferFrom(
address from,
address to,
uint256 amount
) public virtual returns (bool) {
_spendAllowance(from, msg.sender, amount);
_transfer(from, to, amount);
return true;
}
// 内部函数
function _transfer(address from, address to, uint256 amount) internal virtual {
require(from != address(0), "Transfer from zero address");
require(to != address(0), "Transfer to zero address");
require(_balances[from] >= amount, "Insufficient balance");
_balances[from] -= amount;
_balances[to] += amount;
emit Transfer(from, to, amount);
}
function _mint(address account, uint256 amount) internal virtual {
require(account != address(0), "Mint to zero address");
_totalSupply += amount;
_balances[account] += amount;
emit Transfer(address(0), account, amount);
}
function _burn(address account, uint256 amount) internal virtual {
require(account != address(0), "Burn from zero address");
require(_balances[account] >= amount, "Burn amount exceeds balance");
_balances[account] -= amount;
_totalSupply -= amount;
emit Transfer(account, address(0), amount);
}
function _approve(
address owner,
address spender,
uint256 amount
) internal virtual {
require(owner != address(0), "Approve from zero address");
require(spender != address(0), "Approve to zero address");
_allowances[owner][spender] = amount;
emit Approval(owner, spender, amount);
}
function _spendAllowance(
address owner,
address spender,
uint256 amount
) internal virtual {
uint256 currentAllowance = _allowances[owner][spender];
if (currentAllowance != type(uint256).max) {
require(currentAllowance >= amount, "Insufficient allowance");
_approve(owner, spender, currentAllowance - amount);
}
}
}
2.2 增加所有者权限
在实际项目中,通常需要添加代币管理员角色:
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import "./BasicToken.sol";
contract TokenWithOwner is BasicToken {
address public owner;
modifier onlyOwner() {
require(msg.sender == owner, "Not the owner");
_;
}
constructor(
string memory tokenName,
string memory tokenSymbol,
uint8 tokenDecimals,
uint256 initialSupply
) BasicToken(tokenName, tokenSymbol, tokenDecimals, initialSupply) {
owner = msg.sender;
}
// 管理员铸币功能
function mint(address to, uint256 amount) external onlyOwner {
_mint(to, amount);
}
// 管理员销毁功能
function burn(uint256 amount) external {
_burn(msg.sender, amount);
}
// 管理员强制转账(用于处理错误发送的代币)
function rescueTokens(
address token,
address from,
address to,
uint256 amount
) external onlyOwner {
// 防止意外操作
require(token != address(this), "Cannot rescue own tokens");
IERC20(token).transferFrom(from, to, amount);
}
}
三、OpenZeppelin实现
OpenZeppelin是智能合约开发领域最受信赖的库,可通过npm安装:
bash
npm install @openzeppelin/contracts
最简实现只需几行代码:
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MyToken is ERC20 {
constructor(uint256 initialSupply) ERC20("My Token", "MTK") {
_mint(msg.sender, initialSupply);
}
}
OpenZeppelin会自动处理所有接口实现、事件触发和安全检查。
五、安全注意事项
5.1 approve的安全陷阱
ERC20的approve存在一个著名的安全问题:如果你先设置为X,再想改为Y,在两次交易之间可能存在竞态条件。
标准做法是先设置为0,再设置为目标值:
solidity
// 安全设置授权
function safeApprove(address spender, uint256 amount) external {
uint256 currentAllowance = allowance(msg.sender, spender);
// 如果要增加授权:先设为0,再设为新值
if (amount > currentAllowance) {
_approve(msg.sender, spender, 0);
_approve(msg.sender, spender, amount);
} else {
// 减少授权直接设置即可
_approve(msg.sender, spender, amount);
}
}
5.2 转账到零地址
务必检查目标地址:
solidity
// 低级错误示例
function unsafeTransfer(address to, uint256 amount) external {
_balances[msg.sender] -= amount;
_balances[to] += amount; // to可能为零地址!
emit Transfer(msg.sender, to, amount);
}
5.3 精度丢失
处理代币金额时要注意精度:
solidity
// 错误:可能导致精度丢失
uint256 fee = amount * 3 / 1000; // 如果amount很小,可能整除为0
// 正确:先乘后除,或使用高精度计算
uint256 fee = (amount * 3 * 1e18) / 1000 / 1e18;
六、完整测试示例
6.1 Foundry测试
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import "forge-std/Test.sol";
import "../src/MyToken.sol";
contract MyTokenTest is Test {
MyToken token;
address user1 = address(0x1);
address user2 = address(0x2);
function setUp() public {
token = new MyToken(1000000 * 10**18);
}
// 测试初始化
function testInitialSupply() public {
assertEq(token.totalSupply(), 1000000 * 10**18);
assertEq(token.balanceOf(address(this)), 1000000 * 10**18);
}
// 测试转账
function testTransfer() public {
token.transfer(user1, 100 * 10**18);
assertEq(token.balanceOf(user1), 100 * 10**18);
assertEq(token.balanceOf(address(this)), 999900 * 10**18);
}
// 测试余额不足
function testTransferInsufficientBalance() public {
vm.expectRevert();
token.transfer(user1, 2000000 * 10**18);
}
// 测试授权转账
function testApproveAndTransferFrom() public {
token.approve(user1, 500 * 10**18);
vm.prank(user1);
token.transferFrom(address(this), user2, 300 * 10**18);
assertEq(token.balanceOf(user2), 300 * 10**18);
assertEq(token.allowance(address(this), user1), 200 * 10**18);
}
// Fuzz测试
function testTransferFuzz(uint256 amount) public {
vm.assume(amount > 0 && amount <= token.balanceOf(address(this)));
uint256 initialBalance = token.balanceOf(address(this));
token.transfer(user1, amount);
assertEq(token.balanceOf(user1), amount);
assertEq(token.balanceOf(address(this)), initialBalance - amount);
}
}
6.2 部署脚本
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import "forge-std/Script.sol";
import "../src/MyToken.sol";
contract DeployToken is Script {
function run() external {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
// 发行1000万代币
MyToken token = new MyToken(10_000_000 * 10**18);
console.log("Token deployed at:", address(token));
console.log("Token name:", token.name());
console.log("Token symbol:", token.symbol());
console.log("Total supply:", token.totalSupply());
vm.stopBroadcast();
}
}
七、总结
ERC20代币开发看似简单,实则蕴含着丰富的工程考量。从基础实现到生产级应用,需要考虑安全、扩展性、Gas优化等多个维度。
对于大多数项目,直接使用OpenZeppelin是最佳选择。但理解其底层实现原理,能帮助你在遇到特殊需求时进行定制化开发,也能更好地理解其他基于ERC20的协议(如ERC777、ERC4626等)。
记住,代码安全永远是第一位的。一个经过充分测试的简单实现,远比一个功能丰富但未经充分审计的复杂系统更值得信赖。
常见问题
Q: ERC20代币可以添加黑名单功能吗?
A: 可以,但需要谨慎设计。建议使用模块化方式,如集成OpenZeppelin的AccessControl来管理黑名单权限。
Q: 如何实现代币的快照功能?
A: 使用OpenZeppelin的ERC20Snapshot扩展,可以记录特定时间点的余额快照,用于治理或分红场景。
Q: ERC20代币的decimals设置为多少合适?
A: 标准是18,与ETH保持一致。如果需要整数代币,可以设为0,但这会限制代币的可分性。
Q: 如何验证合约代码已正确实现ERC20?
A: 使用OpenZeppelin的ERC20.behavior.t.sol测试套件,或通过ERC-20接口检测工具进行验证。