ERC721 / ERC1155 / ERC4626 标准详解
从 NFT 到多代币再到代币化金库,掌握以太坊三大核心代币标准
1. 什么是 ERC721
ERC721 是以太坊非同质化代币(NFT)标准。与 ERC20 不同,每个 ERC721 代币都是独一无二的,拥有独立的 tokenId 和元数据。这使其成为数字艺术、游戏资产、域名等不可互换资产的理想标准。
为什么需要 ERC721
- 唯一性:每个代币都有独特的 ID 和属性
- 所有权证明:链上记录每个代币的归属
- 可转让性:标准化的转账接口
- 可验证稀缺性:透明的发行和流通记录
核心特性
- 每个代币有唯一的 tokenId(uint256)
- ownerOf(tokenId) 查询所有者
- balanceOf(owner) 查询持有数量
- 支持单个代币授权和全局授权
- safeTransferFrom 保护智能合约接收
2. ERC721 接口
ERC721 标准定义了一套完整的接口,包括所有权查询、转账和授权功能。以下是核心接口定义:
// 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 限制(可选)
执行步骤
- 增加接收者的 balance
- 记录 tokenId 的所有者
- 发出 Transfer(0x0, to, tokenId) 事件
- 可选:设置 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 | 类型 | 示例 | 余额特点 |
|---|---|---|---|
0 | FT(同质化) | 游戏金币 | 可为任意数量 |
1 | FT(同质化) | 游戏钻石 | 可为任意数量 |
1000 | NFT(非同质化) | 传奇武器 #1000 | 每个地址最多 1 |
1001 | NFT(非同质化) | 传奇武器 #1001 | 每个地址最多 1 |
余额结构:双层映射
mapping(address => mapping(uint256 => uint256)) private _balances;
- 第一层:地址 => 第二层映射
- 第二层:tokenId => 余额数量
- 查询:balances[address][id] 返回该地址的该 id 代币数量
- 高效:同一合约管理数千种代币,无需多次部署
10. ERC1155 接口
ERC1155 接口相比 ERC721 更简洁,但功能更强大。核心是支持批量操作和双层余额查询。
// 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 对比
选择合适的代币标准取决于你的应用场景。以下是三种主流标准的全面对比:
| 特性 | ERC20 | ERC721 | ERC1155 |
|---|---|---|---|
| 代币类型 | 同质化(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, decimals | tokenURI(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,并添加了资产管理相关的函数。接口分为几类:资产信息、转换、预览、限制、操作。
// 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 USDC23. 存入 & 铸造
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 的审计过的实现,避免重复造轮子和安全风险。