ERC20代币合约开发实战从入门到精通

金色六边形ERC20代币图标与区块链网络节点交织的紫色调封面

一、ERC20标准解析

1.1 标准背景

ERC20由Fabian Vogelsteller和Vitalik Buterin于2015年提出,于2017年正式成为以太坊改进提案(EIP-20)。该标准定义了代币合约必须实现的基本接口,使得不同代币能够在统一的方式下与DApp、钱包和交易所进行交互。

ERC20的核心价值在于互操作性。只要你的合约实现了标准接口,市场上任何支持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接口检测工具进行验证。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注