ERC20 同质化代币

122 阅读4分钟

ERC20

EIP(Ethereum Improvement Proposals)

ERC(Ethereum Request For Comments)

ERC20

ERC20 是以太坊上最早、最广泛使用的代币标准,由以太坊社区提出的 Ethereum Request for Comments #20 定义。它规定了一套智能合约接口,使得在以太坊网络上创建的代币具有统一的交互方式,从而能够被钱包、交易所、DApp 普遍支持。

  • 定义一套最小功能接口(即合约必须实现的函数和事件),确保不同代币之间的交互方式一致。
  • 让任何第三方只要遵守这套接口,就能无差别地与代币交互(如转账、授权、余额查询)。

ERC20合约标准

ERC20 Token Standard

Docs For Developers For ERC20

总结如下:

扩展要求

非强制但建议实现。

  • name() → 代币名称(如 "USD Coin")
  • symbol() → 代币符号(如 "USDC")
  • decimals() → 小数位数(如 18)
必须要求
  1. 函数(Methods)
函数签名返回值作用
totalSupply() view returns (uint256)总发行量返回代币总供应量
balanceOf(address _owner) view returns (uint256)余额查询某个地址的代币余额
transfer(address _to, uint256 _value) returns (bool)成功与否从调用者账户向指定地址转账
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success)成功与否从授权的账户转账(需要先 approve
function approve(address _spender, uint256 _value) public returns (bool success)成功与否授权某个地址可以支配自己账户中最多 _value 的代币
function allowance(address _owner, address _spender) public view returns (uint256 remaining)授权额度查询 _spender 还能使用 _owner 的多少代币
  1. 事件(Events)

ERC20 必须触发以下事件,以便链外应用监听:

事件参数用途
event Transfer(address indexed _from, address indexed _to, uint256 _value)_from, _to, _value记录代币转账
event Approval(address indexed _owner, address indexed _spender, uint256 _value)_owner, _spender, _value记录授权操作

意义

ERC20 的意义:

  1. 统一接口 → 钱包、交易所可以无缝支持任何 ERC20 代币。
  2. 简化开发 → 开发者可以快速创建新代币,而不必重新定义交互逻辑。
  3. 生态基础 → 稳定币(USDTUSDCDAI)、DeFi 项目代币(UNIAAVE)、治理代币等,几乎全部是 ERC20

Create ERC20 Token

Manually

一个简单的 ERC20 Token 合约代码:

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

contract TokenERC20 {
    string private s_name;
    string private s_symbol;
    uint8 private constant DECIMALS = 18;
    uint256 private immutable i_totalSupply;

    // mappings
    mapping(address => uint256) private s_balances; // address => amount
    mapping(address => mapping(address => uint256)) private s_allowances; // owner => (spender => amount)

    // evetns
    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
    ) {
        s_name = _name;
        s_symbol = _symbol;
        i_totalSupply = initialSupply * (10 ** uint256(DECIMALS));

        s_balances[msg.sender] = i_totalSupply;
        emit Transfer(address(0), msg.sender, i_totalSupply); // address(0) means token creation
    }

    // optional methods
    function name() public view returns (string memory) {
        return s_name;
    }

    function symbol() public view returns (string memory) {
        return s_symbol;
    }

    function decimals() public pure returns (uint8) {
        return DECIMALS;
    }

    function totalSupply() public view returns (uint256) {
        return i_totalSupply;
    }

    function balanceOf(address _owner) public view returns (uint256 balance) {
        require(
            _owner != address(0),
            "ERC20: balance query for the zero address"
        );
        // mapping 是 Solidity 的哈希表,任何 key 都存在, 未赋值的 key 对应的 value 默认是 0
        require(s_balances[_owner] > 0, "ERC20: has zero balance"); // every key in mapping has a default value of 0

        return s_balances[_owner];
    }

    function transfer(
        address _to,
        uint256 _value
    ) public returns (bool success) {
        require(_to != address(0), "ERC20: transfer to the zero address");
        require(
            s_balances[msg.sender] >= _value,
            "ERC20: transfer amount exceeds balance"
        );

        s_balances[msg.sender] -= _value;
        s_balances[_to] += _value;
        emit Transfer(msg.sender, _to, _value);
        return true;
    }

    function transferFrom(
        address _from,
        address _to,
        uint256 _value
    ) public returns (bool success) {
        require(_from != address(0), "ERC20: transfer from the zero address");
        require(_to != address(0), "ERC20: transfer to the zero address");
        require(
            s_balances[_from] >= _value,
            "ERC20: transfer amount exceeds balance"
        );
        require(
            s_allowances[_from][msg.sender] >= _value,
            "ERC20: transfer amount exceeds allowance"
        );

        s_balances[_from] -= _value;
        s_balances[_to] += _value;
        s_allowances[_from][msg.sender] -= _value;

        emit Transfer(_from, _to, _value);
        return true;
    }

    function approve(
        address _spender,
        uint256 _value
    ) public returns (bool success) {
        require(_spender != address(0), "ERC20: approve to the zero address");
        require(
            s_balances[msg.sender] >= _value,
            "ERC20: approve amount exceeds balance"
        );

        s_allowances[msg.sender][_spender] = _value;
        emit Approval(msg.sender, _spender, _value);
        return true;
    }

    function allowance(
        address _owner,
        address _spender
    ) public view returns (uint256 remaining) {
        require(
            _owner != address(0),
            "ERC20: allowance query for the zero address"
        );
        require(
            _spender != address(0),
            "ERC20: allowance query for the zero address"
        );

        return s_allowances[_owner][_spender];
    }
}

OpenZeppelin

OpenZeppelin 提供的 ERC20 实现就是业界公认的专业、安全的代币模板,生产级项目建议直接引用。

OpenZeppelin ERC20

引入 OpenZeppelin 库:

yarn add --dev @openzeppelin/contracts
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract OurToken is ERC20 {
    constructor(
        string memory name,
        string memory symbol,
        uint256 initialSupply
    ) ERC20(name, symbol) {
        _mint(msg.sender, initialSupply * (10 ** uint256(decimals())));
    }
}

OpenZeppelin 还提供了进阶模板:

  • ERC20Burnable → 支持销毁代币
  • ERC20Capped → 限制最大供应量
  • ERC20Mintable / AccessControl → 权限控制,可增发
  • ERC20Snapshot → 快照余额,常用于治理或分红

deploy

部署脚本:

// deploy.js
const { network } = require("hardhat")

const INITIAL_SUPPLY = "10000"
const TOKEN_NAME = "FilToken"
const TOKEN_SYMBOL = "FCC"

module.exports = async ({ getNamedAccounts, deployments }) => {
    const { deploy, log } = deployments
    const { deployer } = await getNamedAccounts()
    const ourToken = await deploy("OurToken", {
        from: deployer,
        args: [TOKEN_NAME, TOKEN_SYMBOL, INITIAL_SUPPLY],
        log: true,
        // we need to wait if on a live network so we can verify properly
        waitConfirmations: network.config.blockConfirmations || 1,
    })
    log(`ourToken deployed at ${ourToken.address}`)
}

module.exports.tags = ["all", "token"]