ERC-20 与 ERC-721 标准详解

深度解析以太坊两大核心代币标准:ERC-20(同质化代币)和 ERC-721(非同质化代币)。涵盖标准接口、完整实现代码、元数据扩展、版税机制与 OpenZeppelin 最佳实践。

导语:标准化让创新成为可能

以太坊上的代币(Token)不是内置功能,而是通过智能合约标准实现的。

在 ERC-20 之前,每个项目都发明自己的代币合约,钱包和交易所不得不为每种代币写适配代码。ERC-20 的出现统一了接口,使得任何兼容的钱包都能立即支持新代币——这是标准化带来的巨大价值。

ERC-721 则将这一思想延伸到独特资产上——每一枚代币都是独一无二的,这就是 NFT(Non-Fungible Token)。

一句话总结:ERC-20 让"货币"标准化,ERC-721 让"独特资产"标准化,二者奠定了以太坊数字经济的基础设施。


1. ERC-20:同质化代币标准

1.1 标准接口

interface IERC20 {
    // === 基础信息 ===
    function name() external view returns (string memory);
    function symbol() external view returns (string memory);
    function decimals() external view returns (uint8);  // 通常 18
    function totalSupply() external view returns (uint256);

    // === 余额与转账 ===
    function balanceOf(address account) external view returns (uint256);
    function transfer(address to, uint256 amount) external returns (bool);

    // === 授权与代理转账 ===
    function allowance(address owner, address spender) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address from, address to, uint256 amount) external returns (bool);

    // === 事件 ===
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
}

1.2 完整实现(安全版)

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

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/Context.sol";

contract MyToken is Context, IERC20 {
    mapping(address => uint256) private _balances;
    mapping(address => mapping(address => uint256)) private _allowances;

    uint256 private _totalSupply;
    string private _name;
    string private _symbol;
    uint8 private constant _decimals = 18;

    constructor(string memory name_, string memory symbol_, uint256 initialSupply) {
        _name = name_;
        _symbol = symbol_;
        _mint(_msgSender(), initialSupply * 10 ** _decimals);
    }

    function name() public view returns (string memory) {
        return _name;
    }

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

    function decimals() public pure returns (uint8) {
        return _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 returns (bool) {
        address owner = _msgSender();
        _transfer(owner, 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 returns (bool) {
        address owner = _msgSender();
        _approve(owner, spender, amount);
        return true;
    }

    function transferFrom(address from, address to, uint256 amount) public returns (bool) {
        address spender = _msgSender();
        _spendAllowance(from, spender, amount);
        _transfer(from, to, amount);
        return true;
    }

    // === 内部函数 ===
    function _transfer(address from, address to, uint256 amount) internal {
        require(from != address(0), "ERC20: transfer from zero");
        require(to != address(0), "ERC20: transfer to zero");

        uint256 fromBalance = _balances[from];
        require(fromBalance >= amount, "ERC20: insufficient balance");
    unchecked {
        _balances[from] = fromBalance - amount;
        _balances[to] += amount;
    }
        emit Transfer(from, to, amount);
    }

    function _mint(address account, uint256 amount) internal {
        require(account != address(0), "ERC20: mint to zero");
        _totalSupply += amount;
    unchecked {
        _balances[account] += amount;
    }
        emit Transfer(address(0), account, amount);
    }

    function _approve(address owner, address spender, uint256 amount) internal {
        require(owner != address(0), "ERC20: approve from zero");
        require(spender != address(0), "ERC20: approve to zero");
        _allowances[owner][spender] = amount;
        emit Approval(owner, spender, amount);
    }

    function _spendAllowance(address owner, address spender, uint256 amount) internal {
        uint256 currentAllowance = allowance(owner, spender);
        if (currentAllowance != type(uint256).max) {
            require(currentAllowance >= amount, "ERC20: insufficient allowance");
        unchecked {
            _approve(owner, spender, currentAllowance - amount);
        }
        }
    }
}

1.3 approve + transferFrom 模式

这个设计是 DeFi 协议的基石:

// 1. 用户授权 DEX 使用 1000 个代币
usdc.approve(dexAddress, 1000 * 10**18);

// 2. DEX 从用户账户转走代币(无需用户私钥)
usdc.transferFrom(userAddress, dexLiquidityPool, 1000 * 10**18);
风险防护
授权额度无限大用户使用完后主动 approve(0)
重放攻击每次授权新额度前先设为 0
前端钓鱼仔细核对 spender 地址

一句话总结:ERC-20 的核心是 balance + allowance 双层模型 —— transfer 直接转账,approve + transferFrom 让第三方代表你操作。


2. ERC-721:非同质化代币(NFT)

2.1 标准接口

interface IERC721 {
    // === 所有权 ===
    function balanceOf(address owner) external view returns (uint256);
    function ownerOf(uint256 tokenId) external view returns (address);

    // === 转移 ===
    function transferFrom(address from, address to, uint256 tokenId) external;
    function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external;
    function approve(address to, uint256 tokenId) external;
    function setApprovalForAll(address operator, bool approved) external;
    function getApproved(uint256 tokenId) external view returns (address);
    function isApprovedForAll(address owner, address operator) external view returns (bool);

    // === 元数据(可选扩展) ===
    function tokenURI(uint256 tokenId) external view returns (string memory);

    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
}

2.2 核心差异:每个 tokenId 都是唯一的

特性ERC-20ERC-721
同质化✅ 所有代币完全等价❌ 每个代币唯一
余额模型balanceOf(addr) 返回总量balanceOf(addr) 返回持有的 NFT 数量
转账参数transfer(to, amount)transferFrom(from, to, tokenId)
元数据tokenURI() 指向 JSON 元数据

2.3 NFT 元数据标准

{
  "name": "Bored Ape #1234",
  "description": "A unique digital collectible",
  "image": "ipfs://Qm.../image.png",
  "attributes": [
    { "trait_type": "Background", "value": "Blue" },
    { "trait_type": "Eyes", "value": "Laser" },
    { "trait_type": "Rarity Score", "value": 95, "display_type": "number" }
  ]
}

存储方案对比:

方案去中心化持久性成本适用
IPFS + Pinning⚠️ 需节点保持主流方案
Arweave✅ 永久一次性付费高价值资产
中心化服务器不推荐

2.4 完整 NFT 合约实现

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

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract MyNFT is ERC721, ERC721URIStorage, Ownable {
    uint256 private _tokenIdCounter;
    string private _baseTokenURI;
    uint256 public constant MINT_PRICE = 0.01 ether;
    uint256 public constant MAX_SUPPLY = 10000;

    constructor(string memory baseURI) ERC721("MyNFT", "MNFT") Ownable(msg.sender) {
        _baseTokenURI = baseURI;
    }

    function mint() public payable returns (uint256) {
        require(msg.value >= MINT_PRICE, "Insufficient payment");
        require(_tokenIdCounter < MAX_SUPPLY, "Sold out");

        uint256 tokenId = _tokenIdCounter;
        _tokenIdCounter++;

        _safeMint(msg.sender, tokenId);
        _setTokenURI(tokenId, string(abi.encodePacked(_baseTokenURI, _uintToString(tokenId), ".json")));

        return tokenId;
    }

    function withdraw() public onlyOwner {
        payable(owner()).transfer(address(this).balance);
    }

    function supportsInterface(bytes4 interfaceId)
        public
        view
        override(ERC721, ERC721URIStorage)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }

    // 覆盖 tokenURI
    function tokenURI(uint256 tokenId)
        public
        view
        override(ERC721, ERC721URIStorage)
        returns (string memory)
    {
        return super.tokenURI(tokenId);
    }

    // 辅助:uint to string
    function _uintToString(uint256 value) internal pure returns (string memory) {
        if (value == 0) return "0";
        uint256 temp = value;
        uint256 digits;
        while (temp != 0) { digits++; temp /= 10; }
        bytes memory buffer = new bytes(digits);
        while (value != 0) { digits -= 1; buffer[digits] = bytes1(uint8(48 + uint256(value % 10))); value /= 10; }
        return string(buffer);
    }
}

一句话总结:ERC-721 的核心是每个 tokenId 映射到唯一的所有者和元数据 URI,这让数字艺术品、游戏道具、身份凭证的链上确权成为可能。


3. ERC-2981:NFT 版税标准

interface IERC2981 {
    function royaltyInfo(uint256 tokenId, uint256 salePrice)
        external
        view
        returns (address receiver, uint256 royaltyAmount);
}

实际应用:

function royaltyInfo(uint256, uint256 salePrice)
    public
    view
    override
    returns (address receiver, uint256 royaltyAmount)
{
    return (owner(), (salePrice * 500) / 10000);  // 5% 版税
}

注意:链上版税是可选项——执行依赖于 NFT 市场的合约代码遵守。Opensea、Blur 等市场的版税执行策略不同。

一句话总结:ERC-2981 为创作者提供了一种标准化的链上版税声明方式,但实际执行仍依赖市场合约的自愿遵守。


4. OpenZeppelin 最佳实践

生产环境不要从零写标准合约,应使用 OpenZeppelin 的审计通过版本:

npm install @openzeppelin/contracts
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

OpenZeppelin 提供的安全功能:

工具用途
ReentrancyGuard防止重入攻击
Ownable / AccessControl权限管理
Pausable紧急暂停
SafeMath (0.8 前)防溢出(0.8 后内置)
Counters安全的计数器
ERC20Permitgasless approve(EIP-2612)

一句话总结:OpenZeppelin 合约库经过行业审计和数十亿美元 TVL 的考验,是生产级代币合约的唯一理智选择。


5. ERC-1155:多代币标准

在 ERC-20 和 ERC-721 之间,还存在一个中间地带:半同质化代币。游戏道具是最典型的场景——同一类型的装备(如"铁剑")是可互换的,但不同稀有度或附魔的同一装备又是唯一的。

interface IERC1155 {
    // 批量余额查询
    function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids)
        external view returns (uint256[] memory);
    
    // 批量转移
    function safeBatchTransferFrom(
        address from, address to, uint256[] calldata ids, uint256[] calldata amounts, bytes calldata data
    ) external;
    
    // 单一转移
    function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes calldata data)
        external;
}

ERC-1155 的核心设计是tokenId 区分类型,用 amount 表示数量

场景tokenId=1 (铁剑)tokenId=2 (史诗铁剑 #42)
ERC-20 风格amount=100(100 把铁剑)不适用
ERC-721 风格不适用amount=1(唯一的 NFT)
ERC-1155amount=100(100 把铁剑)amount=1(唯一的史诗铁剑)

这种灵活性让 ERC-1155 成为链游和元宇宙项目的首选,OpenSea、Rarible 等主流 NFT 市场均支持该标准。

一句话总结:ERC-1155 一个合约管理多种代币类型,兼具 ERC-20 的批量效率和 ERC-721 的唯一性,是游戏和元宇宙资产的标准选择。


5. 总结

标准含义核心接口
ERC-20同质化代币(货币)transfer, approve, balanceOf
ERC-721非同质化代币(唯一资产)transferFrom, ownerOf, tokenURI
ERC-2981NFT 版税royaltyInfo
ERC-1155多代币标准(半同质化)同一合约管理多种代币类型

理解这些标准是进入 DeFi 和 NFT 开发的基础:

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「blockchain」更多文章

  1. 智能合约安全审计与常见漏洞
  2. Hardhat 与 Foundry 开发工具链
  3. DeFi 核心协议与流动性挖矿