高级教程

ERC721 / ERC1155 / ERC4626 标准详解

从 NFT 到多代币再到代币化金库,掌握以太坊三大核心代币标准

1. 什么是 ERC721

ERC721 是以太坊非同质化代币(NFT)标准。与 ERC20 不同,每个 ERC721 代币都是独一无二的,拥有独立的 tokenId 和元数据。这使其成为数字艺术、游戏资产、域名等不可互换资产的理想标准。

为什么需要 ERC721

  • 唯一性:每个代币都有独特的 ID 和属性
  • 所有权证明:链上记录每个代币的归属
  • 可转让性:标准化的转账接口
  • 可验证稀缺性:透明的发行和流通记录

核心特性

  • 每个代币有唯一的 tokenId(uint256)
  • ownerOf(tokenId) 查询所有者
  • balanceOf(owner) 查询持有数量
  • 支持单个代币授权和全局授权
  • safeTransferFrom 保护智能合约接收

2. ERC721 接口

ERC721 标准定义了一套完整的接口,包括所有权查询、转账和授权功能。以下是核心接口定义:

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

interface IERC721 {
    // Events
    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);

    // Balance & Ownership
    function balanceOf(address owner) external view returns (uint256 balance);
    function ownerOf(uint256 tokenId) external view returns (address owner);

    // Transfer
    function safeTransferFrom(address from, address to, uint256 tokenId) external;
    function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external;
    function transferFrom(address from, address to, uint256 tokenId) external;

    // Approval
    function approve(address to, uint256 tokenId) external;
    function getApproved(uint256 tokenId) external view returns (address operator);
    function setApprovalForAll(address operator, bool approved) external;
    function isApprovedForAll(address owner, address operator) external view returns (bool);
}
函数说明用途
balanceOf(address)查询地址持有的 NFT 数量显示用户资产数量
ownerOf(uint256)查询指定 tokenId 的所有者验证所有权
safeTransferFrom(from, to, tokenId)安全转账(检查接收者)避免转入不支持的合约
transferFrom(from, to, tokenId)基础转账(不检查接收者)节省 gas 的转账
approve(to, tokenId)授权单个代币给指定地址允许交易所/市场操作
setApprovalForAll(operator, approved)授权/取消授权所有代币一次性授权所有 NFT
getApproved(tokenId)查询代币的授权地址检查授权状态
isApprovedForAll(owner, operator)查询全局授权状态检查是否有全局权限

3. 存储结构

ERC721 的存储结构设计需要同时支持所有权追踪、余额统计和授权管理。以下是典型的状态变量设计:

// Token ownership mapping
mapping(uint256 => address) private _owners;

// Balance tracking
mapping(address => uint256) private _balances;

// Token approvals (single token)
mapping(uint256 => address) private _tokenApprovals;

// Operator approvals (all tokens)
mapping(address => mapping(address => bool)) private _operatorApprovals;

// Metadata
string private _name;
string private _symbol;
mapping(uint256 => string) private _tokenURIs;

关键要点

  • _owners 映射记录每个 tokenId 的所有者地址
  • _balances 记录每个地址持有的 NFT 数量
  • _tokenApprovals 记录单个代币的授权地址
  • _operatorApprovals 二维映射记录全局授权关系
  • _tokenURIs 存储每个代币的元数据 URI(可选)
  • name 和 symbol 是合约级别的标识符

4. 铸造 NFT

mint 函数用于创建新的 NFT。与 ERC20 不同,ERC721 的铸造必须指定唯一的 tokenId,且每个 tokenId 只能存在一个。

function mint(address to, uint256 tokenId) public {
    require(to != address(0), "mint to zero address");
    require(_owners[tokenId] == address(0), "token already minted");

    _balances[to] += 1;
    _owners[tokenId] = to;

    emit Transfer(address(0), to, tokenId);
}

安全检查

  • 接收地址不能为零地址
  • tokenId 不能已经存在(避免覆盖)
  • 检查 totalSupply 或 maxSupply 限制(可选)

执行步骤

  1. 增加接收者的 balance
  2. 记录 tokenId 的所有者
  3. 发出 Transfer(0x0, to, tokenId) 事件
  4. 可选:设置 tokenURI 元数据

5. 转账函数

ERC721 提供两种转账方式:基础的 transferFrom 和安全的 safeTransferFrom。主要区别在于是否检查接收者是否支持 ERC721。

基础 transferFrom

function transferFrom(address from, address to, uint256 tokenId) public {
    require(_isApprovedOrOwner(msg.sender, tokenId), "not owner nor approved");
    require(ownerOf(tokenId) == from, "from is not owner");
    require(to != address(0), "transfer to zero address");

    // Clear approvals
    delete _tokenApprovals[tokenId];

    // Update balances
    _balances[from] -= 1;
    _balances[to] += 1;

    // Update ownership
    _owners[tokenId] = to;

    emit Transfer(from, to, tokenId);
}

安全 safeTransferFrom

safeTransferFrom 会在转账后检查接收者是否实现了 IERC721Receiver 接口。如果接收者是合约但未实现接口,转账会回滚,防止 NFT 被锁死。

function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public {
    transferFrom(from, to, tokenId);
    require(_checkOnERC721Received(from, to, tokenId, data), "transfer to non ERC721Receiver");
}

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;
}

⚠️ 重要警告
使用 transferFrom 转入不支持的合约地址会导致 NFT 永久锁死无法取回!除非你确定接收者支持 ERC721,否则务必使用 safeTransferFrom。

6. ERC721 接收器

任何希望接收 ERC721 代币的智能合约都必须实现 IERC721Receiver 接口。这是 safeTransferFrom 的安全机制。

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

// Example implementation
contract NFTHolder is IERC721Receiver {
    function onERC721Received(
        address,
        address,
        uint256,
        bytes calldata
    ) external pure returns (bytes4) {
        return this.onERC721Received.selector;
    }
}

关键要点

  • 必须返回 onERC721Received.selector (0x150b7a02)
  • 返回错误值会导致转账回滚
  • 可以在此函数中添加额外逻辑(如记录、验证)
  • 四个参数:operator(调用者)、from(发送者)、tokenId、data(附加数据)
  • EOA(外部账户)不需要实现此接口

7. ERC721 应用场景

ERC721 的唯一性和可验证所有权使其成为多种 Web3 应用的基础。以下是主流应用场景:

数字艺术 & 收藏品

最广为人知的应用,每个 NFT 代表一件独特的数字艺术作品或收藏品。

  • CryptoPunks - 首批像素头像 NFT
  • Bored Ape Yacht Club - 高价值社区 NFT
  • Art Blocks - 生成艺术平台
  • OpenSea、Blur - NFT 交易市场

游戏资产

链游中的角色、装备、道具都可以用 ERC721 表示,实现真正的资产所有权。

  • Axie Infinity - 游戏角色和宠物
  • Decentraland - 虚拟土地地块
  • Gods Unchained - 游戏卡牌
  • The Sandbox - 游戏资产和地块

域名 & 身份

去中心化域名和数字身份系统,每个域名/身份都是独一无二的 NFT。

  • ENS(Ethereum Name Service)- .eth 域名
  • Unstoppable Domains - .crypto / .nft 域名
  • Lens Protocol - 社交身份 NFT
  • Proof of Attendance Protocol (POAP) - 活动证明

现实资产代币化(RWA)

将现实世界资产映射到链上,每个 NFT 代表一个独特的资产。

  • 房地产产权证明
  • 豪车、奢侈品所有权凭证
  • 专利、版权等知识产权
  • 门票、会员卡、资格证书

8. 什么是 ERC1155

ERC1155 是多代币标准(Multi-Token Standard),由 Enjin 团队提出。它允许一个合约同时管理多种 ERC20 和 ERC721 类型的代币,大幅降低 gas 成本和合约复杂度。

传统标准的问题

  • ERC721:每个 NFT 都需要单独转账,批量操作成本高
  • 游戏需要同时管理数百种道具,部署多个合约成本巨大
  • 同时持有 FT(同质化)和 NFT(非同质化)需要两套接口
  • 无法在一次交易中转移多种代币

ERC1155 的解决方案

  • 一个合约管理无限种代币(通过 uint256 id 区分)
  • 支持批量转账(safeBatchTransferFrom),节省 gas
  • 同时支持 FT 和 NFT:余额为 1 即 NFT,余额 > 1 即 FT
  • 统一的接口标准,简化集成

9. 核心概念

ERC1155 的核心创新是通过 id(uint256)区分不同代币,每个地址对每个 id 都有独立的余额。这种设计既支持同质化代币(余额 > 1),也支持非同质化代币(余额 = 1)。

ID类型示例余额特点
0FT(同质化)游戏金币可为任意数量
1FT(同质化)游戏钻石可为任意数量
1000NFT(非同质化)传奇武器 #1000每个地址最多 1
1001NFT(非同质化)传奇武器 #1001每个地址最多 1

余额结构:双层映射

mapping(address => mapping(uint256 => uint256)) private _balances;

  • 第一层:地址 => 第二层映射
  • 第二层:tokenId => 余额数量
  • 查询:balances[address][id] 返回该地址的该 id 代币数量
  • 高效:同一合约管理数千种代币,无需多次部署

10. ERC1155 接口

ERC1155 接口相比 ERC721 更简洁,但功能更强大。核心是支持批量操作和双层余额查询。

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

interface IERC1155 {
    // Events
    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);

    // Balance
    function balanceOf(address account, uint256 id) external view returns (uint256);
    function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids) external view returns (uint256[] memory);

    // Transfer
    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;

    // Approval
    function setApprovalForAll(address operator, bool approved) external;
    function isApprovedForAll(address account, address operator) external view returns (bool);
}
函数说明优势
balanceOf(account, id)查询单个代币余额支持 FT 和 NFT 统一查询
balanceOfBatch(accounts[], ids[])批量查询余额一次查询多个地址/代币
safeTransferFrom(from, to, id, amount, data)转账单个代币支持同质化和非同质化
safeBatchTransferFrom(from, to, ids[], amounts[], data)批量转账一次转移多种代币,节省 gas
setApprovalForAll(operator, approved)全局授权无需逐个代币授权
isApprovedForAll(account, operator)查询全局授权状态简化权限管理

11. 存储结构

ERC1155 的存储结构非常高效,使用双层映射存储所有代币余额,避免了 ERC721 中需要逐个记录 tokenId 所有者的问题。

// Double mapping: address => (tokenId => balance)
mapping(address => mapping(uint256 => uint256)) private _balances;

// Operator approvals
mapping(address => mapping(address => bool)) private _operatorApprovals;

// Token URIs
mapping(uint256 => string) private _uris;
string private _baseURI;

关键要点

  • _balances 是双层映射:address => (tokenId => balance)
  • 一个映射管理所有代币,无需分开存储
  • _operatorApprovals 只有全局授权,没有单个代币授权
  • _uris 可以为每个 id 设置独立的元数据 URI
  • _baseURI 作为所有 URI 的前缀(可选)
  • 相比 ERC721,存储结构更简洁高效

12. 铸造代币

ERC1155 支持两种铸造方式:单个铸造(mint)和批量铸造(mintBatch)。批量铸造可以在一次交易中创建多种代币,极大降低 gas 成本。

function mint(address to, uint256 id, uint256 amount, bytes memory data) public {
    require(to != address(0), "mint to zero address");

    _balances[to][id] += amount;

    emit TransferSingle(msg.sender, address(0), to, id, amount);

    _doSafeTransferAcceptanceCheck(msg.sender, address(0), to, id, amount, data);
}

function mintBatch(
    address to,
    uint256[] memory ids,
    uint256[] memory amounts,
    bytes memory data
) public {
    require(to != address(0), "mint to zero address");
    require(ids.length == amounts.length, "ids and amounts length mismatch");

    for (uint256 i = 0; i < ids.length; i++) {
        _balances[to][ids[i]] += amounts[i];
    }

    emit TransferBatch(msg.sender, address(0), to, ids, amounts);

    _doSafeBatchTransferAcceptanceCheck(msg.sender, address(0), to, ids, amounts, data);
}

单个铸造(mint)

  • 直接增加 _balances[to][id]
  • 发出 TransferSingle 事件
  • 调用接收者的 onERC1155Received 检查
  • 适合铸造单种代币

批量铸造(mintBatch)

  • 循环增加多个 id 的余额
  • ids 和 amounts 数组必须长度相等
  • 发出 TransferBatch 事件
  • 调用接收者的 onERC1155BatchReceived 检查
  • 适合游戏新手礼包、活动奖励等场景

13. 转账函数

ERC1155 的转账函数设计非常强大,既支持单个代币转账,也支持批量转账。批量转账是其相比 ERC721 的最大优势。

单个转账(safeTransferFrom)

function safeTransferFrom(
    address from,
    address to,
    uint256 id,
    uint256 amount,
    bytes memory data
) public {
    require(to != address(0), "transfer to zero address");
    require(
        from == msg.sender || isApprovedForAll(from, msg.sender),
        "caller is not owner nor approved"
    );

    uint256 fromBalance = _balances[from][id];
    require(fromBalance >= amount, "insufficient balance");

    unchecked {
        _balances[from][id] = fromBalance - amount;
    }
    _balances[to][id] += amount;

    emit TransferSingle(msg.sender, from, to, id, amount);

    _doSafeTransferAcceptanceCheck(msg.sender, from, to, id, amount, data);
}

批量转账(safeBatchTransferFrom)

批量转账允许在一次交易中转移多种代币,大幅降低 gas 成本。适合游戏道具交易、资产包转移等场景。

function safeBatchTransferFrom(
    address from,
    address to,
    uint256[] memory ids,
    uint256[] memory amounts,
    bytes memory data
) public {
    require(to != address(0), "transfer to zero address");
    require(
        from == msg.sender || isApprovedForAll(from, msg.sender),
        "caller is not owner nor approved"
    );
    require(ids.length == amounts.length, "ids and amounts length mismatch");

    for (uint256 i = 0; i < ids.length; i++) {
        uint256 id = ids[i];
        uint256 amount = amounts[i];

        uint256 fromBalance = _balances[from][id];
        require(fromBalance >= amount, "insufficient balance");

        unchecked {
            _balances[from][id] = fromBalance - amount;
        }
        _balances[to][id] += amount;
    }

    emit TransferBatch(msg.sender, from, to, ids, amounts);

    _doSafeBatchTransferAcceptanceCheck(msg.sender, from, to, ids, amounts, data);
}

批量转账的优势

  • Gas 效率:转 10 种代币的成本远低于调用 10 次单个转账
  • 原子性:要么全部成功,要么全部失败,避免部分转账问题
  • 用户体验:一次签名完成多种代币转移
  • 适合游戏/NFT 市场的复杂交易场景

14. ERC1155 接收器

与 ERC721 类似,任何希望接收 ERC1155 代币的智能合约都必须实现 IERC1155Receiver 接口。但 ERC1155 需要实现两个函数:单个接收和批量接收。

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);
}

关键要点

  • onERC1155Received - 处理单个代币转账
  • onERC1155BatchReceived - 处理批量代币转账
  • 必须返回正确的函数选择器,否则转账回滚
  • 四个参数:operator、from、id/ids、value/values、data
  • 可以在回调中实现业务逻辑(如记录、验证、触发其他操作)

15. ERC1155 应用场景

ERC1155 的多代币管理和批量操作能力使其成为游戏、虚拟世界和复杂 NFT 项目的首选标准。

区块链游戏

游戏通常需要管理数百种道具、货币和装备,ERC1155 是最佳选择。

  • Enjin - 跨游戏资产协议
  • Skyweaver - 链游卡牌
  • Illuvium - 3A 级 RPG 游戏
  • 游戏内同时包含金币(FT)和装备(NFT)

虚拟世界 & 元宇宙

虚拟世界需要管理大量不同类型的资产,ERC1155 提供高效的解决方案。

  • Decentraland - 可穿戴装备和土地
  • The Sandbox - 建筑材料和装饰品
  • 批量空投虚拟资产
  • 虚拟活动门票和纪念品

NFT 系列 & 版本化 NFT

相同设计的多份 NFT(如版画、限量商品)可以用同一个 id 表示。

  • 同一艺术作品的 100 份限量版(id=1, balance=100)
  • 活动门票(同一场次的多张票)
  • 音乐专辑的限量版 NFT
  • 盲盒/卡包系统

DeFi & 混合协议

需要同时处理 FT 和 NFT 的 DeFi 协议可以使用 ERC1155 简化设计。

  • 分数化 NFT(将一个 NFT 拆分成多份份额)
  • 期权/债券协议(同时管理代币和头寸 NFT)
  • 流动性挖矿奖励(代币 + NFT 徽章)
  • 复杂的质押/奖励系统

16. ERC20 / ERC721 / ERC1155 对比

选择合适的代币标准取决于你的应用场景。以下是三种主流标准的全面对比:

特性ERC20ERC721ERC1155
代币类型同质化(FT)非同质化(NFT)混合(FT + NFT)
唯一性无,所有代币相同每个 tokenId 唯一每个 id 可设置余额
余额查询balanceOf(address)balanceOf(address) 返回数量balanceOf(address, id)
转账transfer(to, amount)transferFrom(from, to, tokenId)safeTransferFrom(from, to, id, amount, data)
批量操作不支持不支持(需多次调用)safeBatchTransferFrom
授权方式approve(spender, amount)approve + setApprovalForAll仅 setApprovalForAll
元数据name, symbol, decimalstokenURI(tokenId)uri(id)
Gas 效率中等较高(逐个操作)最高(批量操作)
合约复杂度最简单中等较复杂
典型应用代币、货币、股份艺术品、域名、地块游戏、虚拟世界、混合资产

选择指南

  • ERC20:需要可互换的代币(如治理代币、稳定币、Memecoin)
  • ERC721:每个资产都是独一无二的(如数字艺术、域名、房产证)
  • ERC1155:需要管理多种代币或同时需要 FT + NFT(如游戏、元宇宙、复杂应用)
  • 成本考虑:如果需要大量批量操作,ERC1155 是最佳选择
  • 生态兼容:ERC721 在 NFT 市场(OpenSea、Blur)上支持最广泛

17. 什么是 ERC4626

ERC4626 是代币化金库标准(Tokenized Vault Standard),为 DeFi 协议的收益聚合器、借贷池、质押协议等提供统一的接口。它将底层资产(如 USDC)和金库份额(如 yvUSDC)的转换逻辑标准化。

传统金库协议的问题

在 ERC4626 出现之前,每个 DeFi 协议都有自己的金库实现方式:

  • Yearn 的 yToken、Aave 的 aToken、Compound 的 cToken 接口各不相同
  • 聚合器需要为每个协议编写适配器
  • 用户难以计算真实的资产价值
  • 无法通用地组合不同协议
  • 增加审计和开发成本

ERC4626 的解决方案

  • 统一的 deposit/withdraw/mint/redeem 接口
  • 标准化的资产/份额转换函数
  • 预览函数(preview*)让用户提前知道结果
  • 兼容 ERC20(金库份额本身是 ERC20 代币)
  • 简化 DeFi 协议之间的组合

18. 核心概念

ERC4626 的核心是资产(Asset)和份额(Share)之间的转换关系。理解这两个概念是掌握 ERC4626 的关键。

资产(Asset)

底层的 ERC20 代币,用户实际存入和取出的代币。

  • USDC - 稳定币
  • WETH - 包装 ETH
  • DAI - 去中心化稳定币
  • 任何 ERC20 代币

份额(Share)

金库发行的 ERC20 代币,代表用户在金库中的所有权比例。

  • yvUSDC - Yearn 的 USDC 金库份额
  • aUSDC - Aave 的 USDC 存款凭证
  • stETH - Lido 的质押 ETH 份额
  • 份额代币本身也是 ERC20,可转账和交易

汇率(Exchange Rate)

份额和资产之间的转换比率随时间变化(通常因收益增加而上升)。

份额价值 = (份额数量 × totalAssets) / totalSupply

例如:金库有 1000 USDC 资产,发行了 900 份额,每份额价值 = 1000 / 900 = 1.11 USDC

19. 四大核心操作

ERC4626 提供四种核心操作:deposit、mint、withdraw、redeem。看似冗余,实际上对应不同的使用场景。

函数指定目标使用场景
deposit(assets, receiver)指定存入的资产数量用户想存入固定数量的 USDC
mint(shares, receiver)指定铸造的份额数量用户想获得固定数量的份额代币
withdraw(assets, receiver, owner)指定取出的资产数量用户想取出固定数量的 USDC
redeem(shares, receiver, owner)指定赎回的份额数量用户想赎回所有份额代币

为什么需要两套接口?

deposit/mint 和 withdraw/redeem 看似重复,但实际上满足不同需求:用户可能想存入固定的资产数量(deposit),也可能想获得固定的份额数量(mint)。由于汇率波动,这两种操作的结果不同。预览函数(preview*)可以提前计算结果。

20. ERC4626 接口

ERC4626 继承 ERC20,并添加了资产管理相关的函数。接口分为几类:资产信息、转换、预览、限制、操作。

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

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

interface IERC4626 is IERC20 {
    // Events
    event Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares);
    event Withdraw(address indexed sender, address indexed receiver, address indexed owner, uint256 assets, uint256 shares);

    // Asset Info
    function asset() external view returns (address assetTokenAddress);
    function totalAssets() external view returns (uint256 totalManagedAssets);

    // Conversion
    function convertToShares(uint256 assets) external view returns (uint256 shares);
    function convertToAssets(uint256 shares) external view returns (uint256 assets);

    // Preview
    function previewDeposit(uint256 assets) external view returns (uint256 shares);
    function previewMint(uint256 shares) external view returns (uint256 assets);
    function previewWithdraw(uint256 assets) external view returns (uint256 shares);
    function previewRedeem(uint256 shares) external view returns (uint256 assets);

    // Limits
    function maxDeposit(address receiver) external view returns (uint256 maxAssets);
    function maxMint(address receiver) external view returns (uint256 maxShares);
    function maxWithdraw(address owner) external view returns (uint256 maxAssets);
    function maxRedeem(address owner) external view returns (uint256 maxShares);

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

接口分类

  • 资产信息:asset(), totalAssets()
  • 转换函数:convertToShares(), convertToAssets()
  • 预览函数:previewDeposit(), previewMint(), previewWithdraw(), previewRedeem()
  • 限制查询:maxDeposit(), maxMint(), maxWithdraw(), maxRedeem()
  • 核心操作:deposit(), mint(), withdraw(), redeem()
  • 事件:Deposit, Withdraw

21. 基础结构

实现 ERC4626 金库需要继承 ERC20(因为份额本身是 ERC20 代币),并引用底层资产代币。

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

contract MyVault is ERC20, IERC4626 {
    IERC20 private immutable _asset;

    constructor(IERC20 asset_, string memory name_, string memory symbol_)
        ERC20(name_, symbol_)
    {
        _asset = asset_;
    }

    function asset() public view returns (address) {
        return address(_asset);
    }

    function totalAssets() public view returns (uint256) {
        return _asset.balanceOf(address(this));
    }
}

关键要点

  • 金库继承 ERC20(份额代币)
  • _asset 是底层 ERC20 代币的引用
  • totalAssets() 返回金库持有的底层代币数量
  • totalSupply() 继承自 ERC20,返回已发行的份额数量
  • 金库本身不直接持有 ETH,只管理 ERC20 代币
  • 部署时需要指定底层资产地址

22. 转换函数

convertToShares 和 convertToAssets 是整个 ERC4626 的核心,负责计算资产和份额之间的转换比率。所有操作都依赖这两个函数。

function convertToShares(uint256 assets) public view returns (uint256) {
    uint256 supply = totalSupply();
    return (supply == 0) ? assets : assets * supply / totalAssets();
}

function convertToAssets(uint256 shares) public view returns (uint256) {
    uint256 supply = totalSupply();
    return (supply == 0) ? shares : shares * totalAssets() / supply;
}

关键要点

  • 首次存款时 supply = 0,份额 = 资产(1:1 汇率)
  • 后续存款按比例计算:份额 = 资产 × supply / totalAssets
  • 汇率随时间上涨(因为 totalAssets 增长,supply 不变)
  • convertToShares 向下取整,convertToAssets 也向下取整
  • 所有 preview* 函数都基于 convert* 函数

计算示例

假设金库状态:totalAssets = 1000 USDC,totalSupply = 900 份额

存入 100 USDC:
份额 = 100 × 900 / 1000 = 90 份额

赎回 90 份额:
资产 = 90 × 1000 / 900 = 100 USDC

23. 存入 & 铸造

deposit 和 mint 是用户向金库存入资产的两种方式。区别在于用户指定的是资产数量还是份额数量。

deposit - 指定资产数量

function deposit(uint256 assets, address receiver) public returns (uint256 shares) {
    require(assets <= maxDeposit(receiver), "deposit more than max");

    shares = previewDeposit(assets);

    // Transfer assets from sender to vault
    _asset.transferFrom(msg.sender, address(this), assets);

    // Mint shares to receiver
    _mint(receiver, shares);

    emit Deposit(msg.sender, receiver, assets, shares);
}

function previewDeposit(uint256 assets) public view returns (uint256) {
    return convertToShares(assets);
}

mint - 指定份额数量

function mint(uint256 shares, address receiver) public returns (uint256 assets) {
    require(shares <= maxMint(receiver), "mint more than max");

    // Round up to protect vault
    assets = _convertToAssets(shares, Math.Rounding.Up);

    _asset.transferFrom(msg.sender, address(this), assets);
    _mint(receiver, shares);

    emit Deposit(msg.sender, receiver, assets, shares);
}

deposit 要点

  • 用户指定存入的资产数量(assets)
  • 合约计算并铸造对应的份额数量(shares)
  • 资产从用户转移到金库
  • 份额铸造给 receiver(可以不是 msg.sender)
  • 发出 Deposit 事件

mint 要点

  • 用户指定铸造的份额数量(shares)
  • 合约计算需要的资产数量(assets)
  • 使用 Rounding.Up 向上取整保护金库
  • 适合用户想获得固定份额的场景
  • 同样发出 Deposit 事件

24. 取出 & 赎回

withdraw 和 redeem 是用户从金库取出资产的两种方式。支持三地址模式:owner(份额所有者)、receiver(资产接收者)、msg.sender(调用者)。

withdraw - 指定资产数量

function withdraw(uint256 assets, address receiver, address owner) public returns (uint256 shares) {
    require(assets <= maxWithdraw(owner), "withdraw more than max");

    // Round up to protect vault
    shares = _convertToShares(assets, Math.Rounding.Up);

    // If caller is not owner, check and spend allowance
    if (msg.sender != owner) {
        _spendAllowance(owner, msg.sender, shares);
    }

    // Burn shares from owner
    _burn(owner, shares);

    // Transfer assets to receiver
    _asset.transfer(receiver, assets);

    emit Withdraw(msg.sender, receiver, owner, assets, shares);
}

redeem - 指定份额数量

function redeem(uint256 shares, address receiver, address owner) public returns (uint256 assets) {
    require(shares <= maxRedeem(owner), "redeem more than max");

    assets = previewRedeem(shares);

    if (msg.sender != owner) {
        _spendAllowance(owner, msg.sender, shares);
    }

    _burn(owner, shares);
    _asset.transfer(receiver, assets);

    emit Withdraw(msg.sender, receiver, owner, assets, shares);
}

三地址模式

  • owner - 份额代币的所有者(从谁那里扣份额)
  • receiver - 资产代币的接收者(资产转给谁)
  • msg.sender - 调用者(需要有 owner 的授权)
  • 如果 msg.sender ≠ owner,需要先 approve
  • 允许第三方代为操作(如清算、路由合约)

25. 安全性考虑

ERC4626 金库面临多种安全风险,最著名的是通货膨胀攻击(Inflation Attack)。以下是核心安全问题和防护措施。

通货膨胀攻击(Inflation Attack)

攻击者利用首次存款时 supply = 0 的特性,通过直接转账操纵汇率,导致后续用户损失。

// Vulnerable to inflation attack
constructor() {
    // Empty - First depositor can manipulate exchange rate
}

// Protection: Mint initial virtual shares
constructor() {
    _mint(address(this), 1000);  // Lock initial shares in vault
}

// Or: Use a minimum deposit requirement
function deposit(uint256 assets, address receiver) public returns (uint256 shares) {
    require(assets >= MIN_DEPOSIT, "deposit too small");
    // ...
}

舍入保护

  • mint/withdraw 使用向上取整(保护金库)
  • deposit/redeem 使用向下取整(保护用户)
  • 确保金库永远不会损失资产
  • 用户可能损失极小的精度(可接受)

安全最佳实践

  • 部署时铸造初始虚拟份额(如 1000 wei)
  • 设置最小存款金额限制
  • 使用 OpenZeppelin 的 ERC4626 实现
  • 审计转换逻辑的舍入方向
  • 监控异常的大额存取操作
  • 测试边界情况(supply = 0, totalAssets = 0)

26. ERC4626 应用场景

ERC4626 已成为 DeFi 协议的标准,广泛应用于收益聚合、借贷、质押等场景。

收益聚合器(Yield Aggregator)

自动将资产分配到多个协议以获取最优收益。

  • Yearn Finance - 先驱的收益优化协议
  • Beefy Finance - 多链收益聚合
  • Harvest Finance - 自动复利策略
  • 用户存入稳定币,金库自动在 Aave、Compound 等协议间轮换

借贷协议(Lending Protocol)

将存款凭证代币化,方便转让和组合。

  • Aave v3 - aToken 采用 ERC4626 兼容设计
  • Euler Finance - 原生支持 ERC4626
  • Morpho - 借贷优化器
  • 存款凭证本身可以作为抵押品或交易

质押协议(Staking Protocol)

将质押份额代币化,实现流动性质押。

  • Lido(stETH)- 虽然早于 ERC4626,但概念类似
  • Frax(frxETH)- 采用 ERC4626 标准
  • Rocketpool(rETH)- 去中心化质押池
  • 用户质押 ETH,获得可流通的份额代币

资产管理 & 策略金库

专业团队管理的投资策略,用户买入份额参与。

  • Index Coop - 指数基金
  • Enzyme Finance - 链上资产管理
  • Set Protocol - 代币化投资组合
  • 自动化交易策略金库

27. 下一步学习

恭喜你完成 ERC721、ERC1155 和 ERC4626 的学习!以下是深入实践的建议:

实践项目

  • 用 Foundry 实现一个 ERC721 NFT 合约
  • 开发一个 ERC1155 游戏道具系统
  • 构建一个简单的 ERC4626 收益金库
  • 集成 OpenZeppelin 合约库
  • 部署到测试网并在 OpenSea 测试

进阶主题

  • ERC721A - Gas 优化的 NFT 批量铸造
  • ERC721Enumerable - 可枚举的 NFT 扩展
  • ERC2981 - NFT 版税标准
  • Account Abstraction - 智能合约钱包
  • 链上生成艺术(Generative Art)

💡 提示
建议从 ERC721 开始实践,它是最基础的标准。掌握后再学习 ERC1155 和 ERC4626,逐步理解复杂场景的设计思路。所有标准都建议使用 OpenZeppelin 的审计过的实现,避免重复造轮子和安全风险。