第四课:ERC20代币合约实战

好,咱们今天来聊聊ERC20代币合约。说实话,这个标准我闭着眼睛都能写出来——因为用得实在太多了。但真正写好一个生产级的代币合约,远不止是抄个接口那么简单。

我个人习惯把ERC20合约分成三个层次:基础实现、功能扩展、安全加固。今天咱们就从这三个维度,把ERC20彻底讲透。

4.1 完整ERC20代币合约编写

先看一个最基础的ERC20实现。嗯,这里要注意,虽然OpenZeppelin已经帮我们封装好了,但我还是建议你手写一遍核心逻辑——这样才能真正理解它的运作机制。

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

contract MyToken {
    string public name;
    string public symbol;
    uint8 public decimals = 18;
    uint256 public totalSupply;

    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);

    constructor(string memory _name, string memory _symbol, uint256 _initialSupply) {
        name = _name;
        symbol = _symbol;
        totalSupply = _initialSupply * 10 ** decimals;
        balanceOf[msg.sender] = totalSupply;
        emit Transfer(address(0), msg.sender, totalSupply);
    }

    function transfer(address to, uint256 amount) external returns (bool) {
        require(to != address(0), "ERC20: transfer to zero address");
        require(balanceOf[msg.sender] >= amount, "ERC20: insufficient balance");

        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
        emit Transfer(msg.sender, to, amount);
        return true;
    }

    function approve(address spender, uint256 amount) external returns (bool) {
        allowance[msg.sender][spender] = amount;
        emit Approval(msg.sender, spender, amount);
        return true;
    }

    function transferFrom(address from, address to, uint256 amount) external returns (bool) {
        require(from != address(0), "ERC20: transfer from zero address");
        require(to != address(0), "ERC20: transfer to zero address");
        require(balanceOf[from] >= amount, "ERC20: insufficient balance");
        require(allowance[from][msg.sender] >= amount, "ERC20: insufficient allowance");

        balanceOf[from] -= amount;
        balanceOf[to] += amount;
        allowance[from][msg.sender] -= amount;
        emit Transfer(from, to, amount);
        return true;
    }
}

这段代码虽然简单,但包含了ERC20的全部核心逻辑。你想想看,transfertransferFrom的区别在哪?说白了就是一个是直接转账,一个是授权转账——这是DeFi组合性的基础。

我的经验:transferFrom中,很多人会忘记减少授权额度。我曾经审计过一个项目,就是因为这个疏忽,导致用户可以无限次花别人的钱——嗯,后果很严重。

4.2 OpenZeppelin库集成

手写一遍是为了理解原理,但生产环境我强烈建议用OpenZeppelin。为什么?因为它的代码经过了无数次审计和实战检验,你踩过的坑它基本都帮你填平了。

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

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract MyToken is ERC20, Ownable {
    constructor(string memory name, string memory symbol)
        ERC20(name, symbol)
    {
        _mint(msg.sender, 1000000 * 10 ** decimals());
    }

    function mint(address to, uint256 amount) external onlyOwner {
        _mint(to, amount);
    }

    function burn(uint256 amount) external {
        _burn(msg.sender, amount);
    }
}

看到没?用OpenZeppelin,几十行代码就搞定了。而且它自动处理了decimals默认值、事件发射、安全检查等细节。我个人习惯在项目初期就用这个模板,省时省力。

关键点: OpenZeppelin的ERC20实现使用了_beforeTokenTransfer钩子函数,这为后续的功能扩展(比如快照、暂停)提供了完美的切入点。

4.3 代币增发与销毁

增发和销毁是代币经济学的核心操作。增发用于激励,销毁用于通缩。但这里有个坑——权限控制

我曾经见过一个项目,增发函数没有加onlyOwner,结果被黑客调用了一次,直接增发了10亿个代币...嗯,项目方当场崩溃。

// 增发
function mint(address to, uint256 amount) external onlyOwner {
    _mint(to, amount);
}

// 销毁
function burn(uint256 amount) external {
    _burn(msg.sender, amount);
}

// 从指定地址销毁(需要授权)
function burnFrom(address account, uint256 amount) external {
    uint256 currentAllowance = allowance(account, msg.sender);
    require(currentAllowance >= amount, "ERC20: burn amount exceeds allowance");
    _approve(account, msg.sender, currentAllowance - amount);
    _burn(account, amount);
}
操作 权限 典型场景
mint onlyOwner 挖矿奖励、空投
burn 任何人(销毁自己的) 用户主动销毁、手续费销毁
burnFrom 授权地址 DeFi协议自动销毁
注意: 增发时一定要考虑totalSupply的上限。虽然Solidity的uint256理论上不会溢出,但无限增发会稀释代币价值——这不仅是技术问题,更是经济模型问题。

4.4 代币快照功能

快照功能有什么用?说白了就是记录某个时间点所有人的余额。这在空投、治理投票、分红场景中非常常见。

OpenZeppelin提供了ERC20Snapshot扩展,实现起来很简单:

import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Snapshot.sol";

contract MyToken is ERC20, ERC20Snapshot, Ownable {
    constructor() ERC20("MyToken", "MTK") {
        _mint(msg.sender, 1000000 * 10 ** decimals());
    }

    function snapshot() external onlyOwner returns (uint256) {
        return _snapshot();
    }

    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 amount
    ) internal override(ERC20, ERC20Snapshot) {
        super._beforeTokenTransfer(from, to, amount);
    }
}

快照的原理其实不复杂:每次快照时,记录当前区块号,然后通过一个映射存储每个地址在不同快照ID下的余额。查询时,找到最近的快照ID,返回当时的余额。

避坑指南: 快照会消耗大量存储。我曾经在一个高频快照的项目中,发现gas费用暴涨——因为每次转账都要更新所有快照数据。建议控制快照频率,或者使用更高效的增量快照方案。

4.5 可暂停代币实现

可暂停功能,说白了就是一个紧急开关。当发现漏洞或异常时,管理员可以暂停所有转账操作,保护用户资产。

OpenZeppelin的Pausable结合ERC20,实现起来也很优雅:

import "@openzeppelin/contracts/security/Pausable.sol";

contract MyToken is ERC20, Pausable, Ownable {
    constructor() ERC20("MyToken", "MTK") {
        _mint(msg.sender, 1000000 * 10 ** decimals());
    }

    function pause() external onlyOwner {
        _pause();
    }

    function unpause() external onlyOwner {
        _unpause();
    }

    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 amount
    ) internal override whenNotPaused {
        super._beforeTokenTransfer(from, to, amount);
    }
}

这里的关键是whenNotPaused修饰符。它会在每次转账前检查合约是否暂停。如果暂停了,所有转账都会revert。

我的建议: 暂停功能是一把双刃剑。用得好可以救市,用不好会引发中心化争议。我个人建议在合约中加入时间锁或多签机制,避免管理员单方面滥用暂停权。

4.6 综合实战:一个完整的可暂停、可快照、可增发销毁的ERC20

好了,咱们把今天学的所有功能整合到一起。这才是生产级的代币合约:

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

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Snapshot.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract AdvancedToken is ERC20, ERC20Snapshot, Pausable, Ownable {
    constructor() ERC20("AdvancedToken", "ATK") {
        _mint(msg.sender, 1000000 * 10 ** decimals());
    }

    // 增发
    function mint(address to, uint256 amount) external onlyOwner {
        _mint(to, amount);
    }

    // 销毁
    function burn(uint256 amount) external {
        _burn(msg.sender, amount);
    }

    // 快照
    function snapshot() external onlyOwner returns (uint256) {
        return _snapshot();
    }

    // 暂停
    function pause() external onlyOwner {
        _pause();
    }

    function unpause() external onlyOwner {
        _unpause();
    }

    // 重写钩子函数
    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 amount
    ) internal override(ERC20, ERC20Snapshot) whenNotPaused {
        super._beforeTokenTransfer(from, to, amount);
    }
}

这个合约包含了我们今天讲的所有功能。你想想看,它是不是已经可以应对大部分DeFi场景了?

最后,我想说一句:写合约容易,写好合约难。每一个功能背后都有权衡——快照消耗gas,暂停影响用户体验,增发影响经济模型。作为开发者,你要做的不是堆砌功能,而是根据业务场景做出最合适的选择。

嗯,今天就到这里。下一章咱们会深入DeFi的核心——流动性池和AMM机制,那才是真正有意思的部分。