token

Cyber-GE

第 6 章 · 约 25 分钟阅读

代币标准

Token Standards

第 6 章:代币标准

代币标准定义了智能合约之间的通用接口。理解 ERC-20、ERC-721 和 ERC-1155 的设计,是构建 DeFi 和 NFT 应用的基础。


ERC-20:同质化代币

ERC-20 是以太坊上最广泛使用的代币标准。USDT、USDC、LINK、UNI 等主流代币都遵循这个标准。"同质化"意味着每个代币单位是完全相同的——1 个 USDC 和另 1 个 USDC 没有区别。

核心接口

ERC-20 定义了 6 个必须实现的函数和 2 个事件:

interface IERC20 {
    // 查询函数
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function allowance(address owner, address spender) external view returns (uint256);
    
    // 转账函数
    function transfer(address to, uint256 amount) external returns (bool);
    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);
}

transfer 用于直接转账,approve + transferFrom 组合用于授权第三方(如 DEX)代为转账。这种两步授权模式是 DeFi 交互的基础。

ERC-20 授权流程

基础实现

一个最小的 ERC-20 实现:

contract SimpleToken is IERC20 {
    string public name;
    string public symbol;
    uint8 public decimals = 18;
    
    uint256 private _totalSupply;
    mapping(address => uint256) private _balances;
    mapping(address => mapping(address => uint256)) private _allowances;
    
    constructor(string memory _name, string memory _symbol, uint256 initialSupply) {
        name = _name;
        symbol = _symbol;
        _mint(msg.sender, initialSupply);
    }
    
    function totalSupply() external view override returns (uint256) {
        return _totalSupply;
    }
    
    function balanceOf(address account) external view override returns (uint256) {
        return _balances[account];
    }
    
    function allowance(address owner, address spender) external view override returns (uint256) {
        return _allowances[owner][spender];
    }
    
    function transfer(address to, uint256 amount) external override returns (bool) {
        _transfer(msg.sender, to, amount);
        return true;
    }
    
    function approve(address spender, uint256 amount) external override returns (bool) {
        _allowances[msg.sender][spender] = amount;
        emit Approval(msg.sender, spender, amount);
        return true;
    }
    
    function transferFrom(address from, address to, uint256 amount) external override returns (bool) {
        uint256 currentAllowance = _allowances[from][msg.sender];
        require(currentAllowance >= amount, "ERC20: insufficient allowance");
        
        unchecked {
            _allowances[from][msg.sender] = currentAllowance - amount;
        }
        
        _transfer(from, to, amount);
        return true;
    }
    
    function _transfer(address from, address to, uint256 amount) internal {
        require(from != address(0), "ERC20: transfer from zero address");
        require(to != address(0), "ERC20: transfer to zero address");
        require(_balances[from] >= amount, "ERC20: insufficient balance");
        
        unchecked {
            _balances[from] -= amount;
            _balances[to] += amount;
        }
        
        emit Transfer(from, to, amount);
    }
    
    function _mint(address to, uint256 amount) internal {
        require(to != address(0), "ERC20: mint to zero address");
        
        _totalSupply += amount;
        _balances[to] += amount;
        
        emit Transfer(address(0), to, amount);
    }
}

decimals 的含义

decimals 表示代币的小数位数。以太坊不支持浮点数,所以 1 USDC 实际上存储为 1000000(6 位小数),1 ETH 存储为 1000000000000000000(18 位小数)。

// 转账 1.5 USDC(6 decimals)
usdc.transfer(recipient, 1_500_000);

// 转账 1.5 DAI(18 decimals)
dai.transfer(recipient, 1_500_000_000_000_000_000);

不同代币的 decimals 不同,计算时必须注意单位转换。

常见扩展

可销毁代币

function burn(uint256 amount) external {
    _balances[msg.sender] -= amount;
    _totalSupply -= amount;
    emit Transfer(msg.sender, address(0), amount);
}

带上限的代币

uint256 public constant MAX_SUPPLY = 1_000_000_000 * 10**18;

function _mint(address to, uint256 amount) internal override {
    require(_totalSupply + amount <= MAX_SUPPLY, "Exceeds max supply");
    super._mint(to, amount);
}

快照功能(用于治理投票):

mapping(uint256 => mapping(address => uint256)) private _snapshotBalances;
uint256 private _currentSnapshotId;

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

function balanceOfAt(address account, uint256 snapshotId) external view returns (uint256) {
    return _snapshotBalances[snapshotId][account];
}

ERC-721:非同质化代币

ERC-721 定义了 NFT(Non-Fungible Token)标准。每个代币都有唯一的 tokenId,代表独一无二的资产——可以是数字艺术品、游戏道具、域名或任何需要唯一性的东西。

核心接口

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) 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);
    
    // 事件
    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);
}

与 ERC-20 不同,ERC-721 的转账是针对特定 tokenId 的。safeTransferFrom 会检查接收方是否能处理 NFT,防止代币被永久锁定在不支持的合约中。

元数据扩展

大多数 NFT 需要关联元数据(图片、属性等)。ERC-721 Metadata 扩展定义了获取元数据的接口:

interface IERC721Metadata is IERC721 {
    function name() external view returns (string memory);
    function symbol() external view returns (string memory);
    function tokenURI(uint256 tokenId) external view returns (string memory);
}

tokenURI 返回一个 URL,指向 JSON 格式的元数据:

{
    "name": "CryptoPunk #3100",
    "description": "One of 9 Alien punks",
    "image": "ipfs://QmXxx.../3100.png",
    "attributes": [
        {"trait_type": "Type", "value": "Alien"},
        {"trait_type": "Accessory", "value": "Headband"}
    ]
}

基础实现

contract SimpleNFT is IERC721, IERC721Metadata {
    string public override name;
    string public override symbol;
    string private _baseURI;
    
    mapping(uint256 => address) private _owners;
    mapping(address => uint256) private _balances;
    mapping(uint256 => address) private _tokenApprovals;
    mapping(address => mapping(address => bool)) private _operatorApprovals;
    
    uint256 private _tokenIdCounter;
    
    constructor(string memory _name, string memory _symbol, string memory baseURI_) {
        name = _name;
        symbol = _symbol;
        _baseURI = baseURI_;
    }
    
    function balanceOf(address owner) external view override returns (uint256) {
        require(owner != address(0), "ERC721: zero address");
        return _balances[owner];
    }
    
    function ownerOf(uint256 tokenId) public view override returns (address) {
        address owner = _owners[tokenId];
        require(owner != address(0), "ERC721: nonexistent token");
        return owner;
    }
    
    function tokenURI(uint256 tokenId) external view override returns (string memory) {
        require(_owners[tokenId] != address(0), "ERC721: nonexistent token");
        return string(abi.encodePacked(_baseURI, toString(tokenId), ".json"));
    }
    
    function approve(address to, uint256 tokenId) external override {
        address owner = ownerOf(tokenId);
        require(msg.sender == owner || _operatorApprovals[owner][msg.sender], "ERC721: not authorized");
        _tokenApprovals[tokenId] = to;
        emit Approval(owner, to, tokenId);
    }
    
    function setApprovalForAll(address operator, bool approved) external override {
        _operatorApprovals[msg.sender][operator] = approved;
        emit ApprovalForAll(msg.sender, operator, approved);
    }
    
    function getApproved(uint256 tokenId) external view override returns (address) {
        require(_owners[tokenId] != address(0), "ERC721: nonexistent token");
        return _tokenApprovals[tokenId];
    }
    
    function isApprovedForAll(address owner, address operator) public view override returns (bool) {
        return _operatorApprovals[owner][operator];
    }
    
    function transferFrom(address from, address to, uint256 tokenId) public override {
        require(_isApprovedOrOwner(msg.sender, tokenId), "ERC721: not authorized");
        _transfer(from, to, tokenId);
    }
    
    function safeTransferFrom(address from, address to, uint256 tokenId) external override {
        safeTransferFrom(from, to, tokenId, "");
    }
    
    function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public override {
        transferFrom(from, to, tokenId);
        require(_checkOnERC721Received(from, to, tokenId, data), "ERC721: non-receiver");
    }
    
    function mint(address to) external returns (uint256) {
        uint256 tokenId = _tokenIdCounter++;
        _balances[to]++;
        _owners[tokenId] = to;
        emit Transfer(address(0), to, tokenId);
        return tokenId;
    }
    
    function _transfer(address from, address to, uint256 tokenId) internal {
        require(ownerOf(tokenId) == from, "ERC721: wrong owner");
        require(to != address(0), "ERC721: zero address");
        
        delete _tokenApprovals[tokenId];
        _balances[from]--;
        _balances[to]++;
        _owners[tokenId] = to;
        
        emit Transfer(from, to, tokenId);
    }
    
    function _isApprovedOrOwner(address spender, uint256 tokenId) internal view returns (bool) {
        address owner = ownerOf(tokenId);
        return (spender == owner || 
                _tokenApprovals[tokenId] == spender || 
                _operatorApprovals[owner][spender]);
    }
    
    function _checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory data) private returns (bool) {
        if (to.code.length > 0) {
            try IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, data) returns (bytes4 retval) {
                return retval == IERC721Receiver.onERC721Received.selector;
            } catch {
                return false;
            }
        }
        return true;
    }
    
    function toString(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--;
            buffer[digits] = bytes1(uint8(48 + value % 10));
            value /= 10;
        }
        return string(buffer);
    }
}

interface IERC721Receiver {
    function onERC721Received(
        address operator,
        address from,
        uint256 tokenId,
        bytes calldata data
    ) external returns (bytes4);
}

枚举扩展

ERC-721 Enumerable 扩展允许遍历所有代币和某个地址拥有的代币:

interface IERC721Enumerable is IERC721 {
    function totalSupply() external view returns (uint256);
    function tokenByIndex(uint256 index) external view returns (uint256);
    function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256);
}

这个扩展增加了存储成本,但对于需要在链上遍历 NFT 的应用很有用。


ERC-1155:多代币标准

ERC-1155 是一个更灵活的标准,允许在单个合约中管理多种代币类型——既可以是同质化的(如游戏金币),也可以是非同质化的(如游戏装备)。

设计动机

在游戏场景中,可能需要管理数百种不同的物品。如果每种物品都部署一个 ERC-20 或 ERC-721 合约,Gas 成本会非常高。ERC-1155 允许在一个合约中管理所有物品,并支持批量操作,大幅降低 Gas 成本。

核心接口

interface IERC1155 {
    // 查询函数
    function balanceOf(address account, uint256 id) external view returns (uint256);
    function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids) external view returns (uint256[] memory);
    
    // 授权函数
    function setApprovalForAll(address operator, bool approved) external;
    function isApprovedForAll(address account, address operator) external view returns (bool);
    
    // 转账函数
    function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes calldata data) external;
    function safeBatchTransferFrom(address from, address to, uint256[] calldata ids, uint256[] calldata amounts, bytes calldata data) external;
    
    // 事件
    event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value);
    event TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values);
    event ApprovalForAll(address indexed account, address indexed operator, bool approved);
    event URI(string value, uint256 indexed id);
}

注意 balanceOf 需要两个参数:地址和代币 ID。批量操作(balanceOfBatchsafeBatchTransferFrom)可以在一次交易中处理多种代币。

基础实现

contract GameItems is IERC1155 {
    // id => account => balance
    mapping(uint256 => mapping(address => uint256)) private _balances;
    mapping(address => mapping(address => bool)) private _operatorApprovals;
    
    string private _uri;
    
    // 代币类型常量
    uint256 public constant GOLD = 0;      // 同质化:金币
    uint256 public constant SILVER = 1;    // 同质化:银币
    uint256 public constant SWORD = 2;     // 非同质化:剑(每把唯一)
    uint256 public constant SHIELD = 3;    // 非同质化:盾
    
    constructor(string memory uri_) {
        _uri = uri_;
    }
    
    function uri(uint256 id) external view returns (string memory) {
        return string(abi.encodePacked(_uri, toString(id), ".json"));
    }
    
    function balanceOf(address account, uint256 id) public view override returns (uint256) {
        require(account != address(0), "ERC1155: zero address");
        return _balances[id][account];
    }
    
    function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids) 
        external view override returns (uint256[] memory) 
    {
        require(accounts.length == ids.length, "ERC1155: length mismatch");
        
        uint256[] memory batchBalances = new uint256[](accounts.length);
        for (uint256 i = 0; i < accounts.length; i++) {
            batchBalances[i] = balanceOf(accounts[i], ids[i]);
        }
        return batchBalances;
    }
    
    function setApprovalForAll(address operator, bool approved) external override {
        _operatorApprovals[msg.sender][operator] = approved;
        emit ApprovalForAll(msg.sender, operator, approved);
    }
    
    function isApprovedForAll(address account, address operator) public view override returns (bool) {
        return _operatorApprovals[account][operator];
    }
    
    function safeTransferFrom(
        address from, address to, uint256 id, uint256 amount, bytes calldata data
    ) external override {
        require(from == msg.sender || isApprovedForAll(from, msg.sender), "ERC1155: not authorized");
        require(to != address(0), "ERC1155: zero address");
        
        _balances[id][from] -= amount;
        _balances[id][to] += amount;
        
        emit TransferSingle(msg.sender, from, to, id, amount);
        
        _doSafeTransferAcceptanceCheck(msg.sender, from, to, id, amount, data);
    }
    
    function safeBatchTransferFrom(
        address from, address to, uint256[] calldata ids, uint256[] calldata amounts, bytes calldata data
    ) external override {
        require(from == msg.sender || isApprovedForAll(from, msg.sender), "ERC1155: not authorized");
        require(to != address(0), "ERC1155: zero address");
        require(ids.length == amounts.length, "ERC1155: length mismatch");
        
        for (uint256 i = 0; i < ids.length; i++) {
            _balances[ids[i]][from] -= amounts[i];
            _balances[ids[i]][to] += amounts[i];
        }
        
        emit TransferBatch(msg.sender, from, to, ids, amounts);
        
        _doSafeBatchTransferAcceptanceCheck(msg.sender, from, to, ids, amounts, data);
    }
    
    function mint(address to, uint256 id, uint256 amount) external {
        _balances[id][to] += amount;
        emit TransferSingle(msg.sender, address(0), to, id, amount);
    }
    
    function mintBatch(address to, uint256[] calldata ids, uint256[] calldata amounts) external {
        require(ids.length == amounts.length, "ERC1155: length mismatch");
        
        for (uint256 i = 0; i < ids.length; i++) {
            _balances[ids[i]][to] += amounts[i];
        }
        
        emit TransferBatch(msg.sender, address(0), to, ids, amounts);
    }
    
    function _doSafeTransferAcceptanceCheck(
        address operator, address from, address to, uint256 id, uint256 amount, bytes memory data
    ) private {
        if (to.code.length > 0) {
            try IERC1155Receiver(to).onERC1155Received(operator, from, id, amount, data) returns (bytes4 response) {
                require(response == IERC1155Receiver.onERC1155Received.selector, "ERC1155: rejected");
            } catch {
                revert("ERC1155: non-receiver");
            }
        }
    }
    
    function _doSafeBatchTransferAcceptanceCheck(
        address operator, address from, address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data
    ) private {
        if (to.code.length > 0) {
            try IERC1155Receiver(to).onERC1155BatchReceived(operator, from, ids, amounts, data) returns (bytes4 response) {
                require(response == IERC1155Receiver.onERC1155BatchReceived.selector, "ERC1155: rejected");
            } catch {
                revert("ERC1155: non-receiver");
            }
        }
    }
    
    function toString(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--; buffer[digits] = bytes1(uint8(48 + value % 10)); value /= 10; }
        return string(buffer);
    }
}

interface IERC1155Receiver {
    function onERC1155Received(address operator, address from, uint256 id, uint256 value, bytes calldata data) external returns (bytes4);
    function onERC1155BatchReceived(address operator, address from, uint256[] calldata ids, uint256[] calldata values, bytes calldata data) external returns (bytes4);
}

ERC-20 扩展:Permit

传统的 ERC-20 授权需要两笔交易:先 approve,再让 DEX 调用 transferFrom。ERC-2612(Permit)允许用户通过签名授权,将两笔交易合并为一笔。

interface IERC20Permit {
    function permit(
        address owner,
        address spender,
        uint256 value,
        uint256 deadline,
        uint8 v, bytes32 r, bytes32 s
    ) external;
    
    function nonces(address owner) external view returns (uint256);
    function DOMAIN_SEPARATOR() external view returns (bytes32);
}

用户签署一个包含授权信息的消息,DEX 在调用 transferFrom 之前先调用 permit 验证签名并设置授权。整个过程只需要一笔交易。

abstract contract ERC20Permit is ERC20, IERC20Permit {
    mapping(address => uint256) public override nonces;
    
    bytes32 public override DOMAIN_SEPARATOR;
    bytes32 private constant PERMIT_TYPEHASH = keccak256(
        "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
    );
    
    constructor() {
        DOMAIN_SEPARATOR = keccak256(abi.encode(
            keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
            keccak256(bytes(name)),
            keccak256(bytes("1")),
            block.chainid,
            address(this)
        ));
    }
    
    function permit(
        address owner, address spender, uint256 value, uint256 deadline,
        uint8 v, bytes32 r, bytes32 s
    ) external override {
        require(block.timestamp <= deadline, "ERC20Permit: expired");
        
        bytes32 structHash = keccak256(abi.encode(
            PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline
        ));
        
        bytes32 hash = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash));
        address signer = ecrecover(hash, v, r, s);
        
        require(signer != address(0) && signer == owner, "ERC20Permit: invalid signature");
        
        _approve(owner, spender, value);
    }
}

ERC-4626:金库标准

ERC-4626 是 DeFi 中收益金库的标准化接口。Yearn、Aave、Compound 等协议的收益代币都可以用这个标准来描述。它让不同协议的金库可以互相组合,大大简化了 DeFi 的可组合性。

核心概念

金库的基本模式是:用户存入底层资产(如 USDC),获得份额代币(如 yUSDC)。金库用这些资产去赚取收益。当用户赎回时,用份额代币换回底层资产加上收益。

份额计算的核心公式:

shares = assets × totalShares / totalAssets
assets = shares × totalAssets / totalShares

收益来自 totalAssets 增加而 totalShares 不变。如果金库通过投资让资产从 1000 增长到 1100,而份额总量保持 1000 不变,那么每份额的价值就从 1 变成了 1.1。

核心接口

interface IERC4626 is IERC20 {
    function asset() external view returns (address);
    function totalAssets() external view returns (uint256);
    
    function deposit(uint256 assets, address receiver) external returns (uint256 shares);
    function mint(uint256 shares, address receiver) external returns (uint256 assets);
    function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares);
    function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets);
    
    function convertToShares(uint256 assets) external view returns (uint256);
    function convertToAssets(uint256 shares) external view returns (uint256);
    
    function previewDeposit(uint256 assets) external view returns (uint256);
    function previewMint(uint256 shares) external view returns (uint256);
    function previewWithdraw(uint256 assets) external view returns (uint256);
    function previewRedeem(uint256 shares) external view returns (uint256);
}

depositmint 都是存款,区别在于指定的是资产数量还是份额数量。withdrawredeem 同理。preview 函数用于预估操作结果,convert 函数用于纯粹的数学转换。

通胀攻击

ERC-4626 最重要的安全问题是通胀攻击。攻击者作为第一个存款人,存入极少量资产(如 1 wei),然后直接向金库转入大量资产(不通过 deposit)。这会让后续存款人因为舍入误差而获得 0 份额。

攻击流程:
1. 攻击者存入 1 wei,获得 1 份额
2. 攻击者直接转入 1000 USDC(不通过 deposit)
3. 此时 totalAssets = 1000 USDC + 1 wei,totalShares = 1
4. 受害者存入 500 USDC
5. shares = 500 × 1 / 1000 = 0(舍入到 0)

防护方法是使用虚拟份额。OpenZeppelin 的 ERC4626 实现已经内置了这个防护:

function _decimalsOffset() internal pure virtual returns (uint8) {
    return 0;  // 可以覆盖为更大的值
}

function convertToShares(uint256 assets) public view returns (uint256) {
    return assets.mulDiv(
        totalSupply() + 10 ** _decimalsOffset(),
        totalAssets() + 1,
        Math.Rounding.Down
    );
}

标准选择指南

代币标准对比

选择代币标准的核心问题是:资产是否可互换?ERC-20 适合完全同质化的资产,每个单位没有区别;ERC-721 适合每个资产都独一无二的场景;ERC-1155 则是两者的结合,特别适合需要批量操作的游戏场景。

对于治理代币,通常选择 ERC-20 并添加快照功能,以便在投票时追溯历史余额。对于门票或会员卡,如果需要批量发行且每张票没有本质区别,ERC-1155 更高效;如果每张票需要独特属性,则选择 ERC-721。


总结

代币标准是以太坊生态系统的基础设施。ERC-20 的简洁设计让 DeFi 成为可能,ERC-721 开创了 NFT 市场,ERC-1155 为游戏和复杂应用提供了灵活性。

理解这些标准的接口和实现细节,不仅有助于开发自己的代币,也有助于理解与其他协议的交互方式。下一章将讨论合约升级模式,解决智能合约"部署后不可修改"的限制。


参考文献

Qián - The Creative

卦辞 · Judgment

"元亨利贞。"

象曰 · Image

天行健,君子以自强不息。

今日启示 · Insight

创造力与领导力的时刻。保持正直,大事可成。