ERC-4626 是 ERC-20 的扩展,为持有单一底层 ERC-20 资产的代币化金库定义了标准 API。金库本身是一种代币,其份额代表对金库资产的比例索取权,该标准固定了集成方可依赖的存款、铸造、取款、赎回和转换函数。本文解释了份额/资产的心智模型,逐步介绍一个可运行的基于 OpenZeppelin 的金库,展示如何通过 JSON-RPC 验证部署,并涵盖通胀攻击以及处理异步和多资产场景的扩展标准(ERC-7540、ERC-7575)。它将规范要求与您必须在自己部署上测量的内容区分开来。
ERC-4626 标准化了什么,又留给了你什么
ERC-4626 最终确定为 EIP-4626,是用于代币化金库的标准 API,这些金库代表单一底层 ERC-20 代币的份额。金库合约本身必须实现 ERC-20,因此金库的单位——称为份额——是可转让的代币,其持有者对金库的底层持仓拥有索取权。该标准定义了存入和取出资产、铸造和赎回份额以及读取两者之间转换率的函数。
该标准没有定义的是策略。一个合规的金库可以借贷到货币市场、持有代币化国债,或者简单地闲置;ERC-4626 只固定了接口以及围绕 totalAssets、totalSupply 和转换函数的会计语义。费用、上限、访问控制和收益来源是实现选择。这就是为什么两个具有相同接口的 ERC-4626 金库在压力下可能表现截然不同。
本指南假设您能阅读 Solidity、对 ERC-20 授权有基本了解,并且能够访问 EVM JSON-RPC 端点。关于端点选择而非接口设计,以太坊 RPC API 概览和 RPC 端点指南涵盖了连接性;本页聚焦于金库合约本身。
- 金库必须实现 ERC-20 及其可选元数据扩展(name、symbol、decimals)。
- 金库恰好管理一个底层
asset()代币。 - 转换函数在调用时必须与
totalAssets()和totalSupply()保持一致。 - 策略、费用、上限和暂停行为不在标准范围内。
份额与资产的心智模型
将金库想象为链上的共同基金份额类别。存款人投入资产并获得份额。汇率是管理资产与流通份额的比率:如果一个金库持有 100 资产支持 200 份额,每份价值 0.5 资产。收益通过增加 totalAssets 而 totalSupply 保持不变来累积,因此每份资产价值上升,而非份额数量增加。
该标准通过两个转换函数和两个预览函数暴露这种关系。convertToShares(assets) 和 convertToAssets(shares) 报告当前状态下的汇率。previewDeposit、previewMint、previewWithdraw 和 previewRedeem 报告特定操作将执行的结果,包括舍入。集成方应使用预览函数进行面向用户的报价,因为它们考虑了金库应用的舍入方向。
舍入很重要。金库应朝有利于金库而非用户的方向舍入,这样重复的小额操作无法抽走价值。规范要求 previewDeposit 和 previewMint 向下舍入发行的份额,而 previewWithdraw 和 previewRedeem 向上舍入所需的资产。在集成审查中,朝相反方向舍入的金库是一个危险信号。
// Read-only checks any integrator can run against a deployed vault
interface IERC4626View {
function asset() external view returns (address);
function totalAssets() external view returns (uint256);
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 maxDeposit(address receiver) external view returns (uint256);
}一次通读完整接口
该标准将其函数分为四个写入路径和一组读取路径。存在四个写入路径是因为用户可以指定资产金额或份额金额,并且调用者和接收者可以不同。deposit(assets, receiver) 拉取固定数量的资产并向 receiver 发行份额。mint(shares, receiver) 发行固定数量的份额并拉取所需的任何资产。withdraw(assets, receiver, owner) 从 owner 销毁足够的份额以释放固定的资产数量。redeem(shares, receiver, owner) 销毁固定数量的份额并释放相应的资产。
每个写入路径都有一个匹配的 max* 视图——maxDeposit、maxMint、maxWithdraw、maxRedeem——报告给定地址的限制。集成方必须在提交交易前检查这些,因为金库可能限制存款、暂停取款或限制某些地址。忽略 maxDeposit 的交易将回滚并浪费 gas。
读取路径包括 asset、totalAssets、两个转换函数和四个预览函数。该标准还定义了 Deposit 和 Withdraw 事件,索引器和会计系统使用这些事件来重建份额所有权,而无需重放每笔转账。
- 写入路径:deposit、mint、withdraw、redeem。
- 限制视图:maxDeposit、maxMint、maxWithdraw、maxRedeem。
- 汇率视图:convertToShares、convertToAssets。
- 预览视图:previewDeposit、previewMint、previewWithdraw、previewRedeem。
- 事件:Deposit(sender, owner, assets, shares) 和 Withdraw(sender, receiver, owner, assets, shares)。
使用 OpenZeppelin 构建最小合规金库
OpenZeppelin Contracts 提供了一个 ERC4626 基类,它在 ERC20 和 SafeERC20 之上实现了该接口。该基类处理份额数学、预览函数以及针对通胀攻击的虚拟偏移防御。一个最小金库只需要提供底层资产地址以及份额的名称和符号。
下面的示例是一个起点,而非生产金库。它闲置持有资产;真实金库会添加一个随时间增加 totalAssets 的策略,并会覆盖 totalAssets() 以报告策略的价值。它还继承了 OpenZeppelin 的默认虚拟偏移,这是后面讨论的推荐基线防御。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {ERC4626} from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract SimpleVault is ERC4626 {
constructor(IERC20 asset_)
ERC20("Simple Vault Share", "svSHARE")
ERC4626(asset_)
{}
// Idle vault: totalAssets() defaults to asset.balanceOf(address(this)).
// Override totalAssets() when a strategy holds the assets elsewhere.
}通过 JSON-RPC 验证部署
在集成金库之前,确认其报告的状态与链上一致。以下过程使用 eth_call 针对金库地址和已知持有者。将 RPC_URL 替换为您的提供商端点,将 VAULT_ADDRESS 替换为已部署的金库。显示的选择器是 asset()、totalAssets() 和 totalSupply() 的标准四字节选择器;您可以使用 cast sig 或任何 ABI 编码器重新生成它们。
按顺序运行调用并将结果记录在表格中。预期的验证是 asset() 返回您期望的底层代币,totalAssets() 至少等于您能核算的资产总和,并且 convertToShares(totalAssets()) 在舍入范围内接近 totalSupply()。两者之间的巨大不匹配是存款前检查金库会计覆盖的信号。
curl -s RPC_URL \
-H 'content-type: application/json' \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_call",
"params": [
{"to": "VAULT_ADDRESS", "data": "0x38d52e0f"},
"latest"
]
}'
# 0x38d52e0f = asset()
# 0x01e1d114 = totalAssets()
# 0x18160ddd = totalSupply()记录集成测量
接口合规并不能告诉你金库是否安全或具有流动性。构建一个小型测量表,并从实时调用而非文档中填充它。下表是一个模板;每个值必须来自您自己的 eth_call 结果或您信任的索引器,并且您应该在关心的每个区块重新运行它。
| 检查项 | 方法 | 预期结果 |
|---|---|---|
| 底层资产 | asset() | 与您打算存入的代币匹配 |
| 管理资产 | totalAssets() | 与您能验证的策略余额一致 |
| 份额供应 | totalSupply() | 首次存款后非零 |
| 存款报价 | previewDeposit(amount) | 份额向下舍入,非零 |
| 赎回报价 | previewRedeem(shares) | 资产向下舍入,最多 totalAssets() |
| 存款限制 | maxDeposit(you) | 至少为您打算的金额 |
| 取款限制 | maxWithdraw(you) | 至少为您打算的金额 |
| 往返损失 | 存款然后赎回预览 | 小且可由舍入和费用解释 |
往返检查是最有用的单一数字。预览一笔现实规模的存款,然后预览赎回它将发行的份额,并将返回的资产与存入的资产进行比较。如果差距很大且无法由记录的费用或舍入解释,则应暂停。不要假设一个值;在您将进行交易的区块测量它。
通胀攻击与虚拟偏移防御
最著名的 ERC-4626 陷阱是通胀攻击,也称为捐赠攻击。攻击者存入少量资金以获得一份份额,然后直接向金库捐赠大量底层资产,从而抬高每份份额的资产价值。随后存入的存款人如果其资产小于膨胀的份额价格,可能会被向下舍入为零份额,攻击者则赎回捐赠加上受害者的存款。
OpenZeppelin 推荐的防御是虚拟偏移:份额数学在分子和分母中加入少量虚拟资产和少量虚拟份额,因此第一个存款人无法使一份份额任意昂贵。基类合约默认应用此防御。覆盖转换内部实现的金库必须保留等效防御,或记录为什么其设计使攻击无利可图。
该攻击并非唯一的陷阱。收费转账的底层代币破坏了金库收到恰好存入金额的假设。Rebasing 代币在没有转账的情况下改变 totalAssets。报告策略价值的金库必须处理策略损失,而不让早期赎回者以旧汇率退出。这些都是阅读金库覆盖而非仅信任接口的理由。
- 除非有经过审查的替代方案,否则保留虚拟偏移。
- 拒绝或特殊处理收费转账和 rebasing 底层代币。
- 在策略和会计经过审计之前限制存款。
- 在分叉测试中显式测试第一个存款人路径。
异步与多资产扩展
ERC-4626 假设原子存款和赎回在限制范围内。这一假设在现实世界资产协议、跨链借贷、具有解绑队列的流动性质押以及结算延迟的保险模块中不成立。ERC-7540 扩展了异步金库的标准:请求被提交,然后稍后领取,同时现有的 deposit、mint、withdraw 和 redeem 函数被重用来领取已结算的请求。
ERC-7575 解决了具有多个资产或入口点的金库,例如流动性提供者代币。由于 ERC-4626 要求金库本身是 ERC-20,多资产金库在基础标准下很别扭或不合规;ERC-7575 将份额代币从金库实现中外部化,使金库可以管理多个资产,同时仍然暴露兼容的表面。
如果您正在集成一个声称 ERC-4626 但异步结算的金库,请检查它是否实现了 ERC-7540,并在围绕它构建 UI 之前阅读请求生命周期。将异步金库视为原子金库将产生错误的报价和失败的领取。
排查常见集成失败
大多数 ERC-4626 集成错误属于几种模式。存款回滚是因为调用者未授权金库使用资产金额,或授权了错误的支出者。取款回滚是因为由于暂停或上限导致 maxWithdraw 为零,或者因为调用者传递了他们不控制的 owner 且没有授权。报价错误是因为集成方使用了 convertToShares 而不是 previewDeposit,忽略了舍入和费用。
一个更微妙的失败是状态过时。在一个区块计算的报价在交易落地时可能无效,尤其是在 totalAssets 随借贷利率变动的金库中。在金库支持的情况下使用截止时间或滑点界限,并在提交前立即重新检查限制。如果金库的 totalAssets 在没有相应的 Deposit 或 Withdraw 事件的情况下跳变,在将汇率视为可靠之前调查捐赠或策略报告。
当调用在 RPC 层而非合约中失败时,确认端点已同步且链 ID 与部署匹配。来自一个网络的金库地址在另一个网络上将返回空数据。网络目录列出了支持的链,当您需要在许多金库上一致地读取状态时,专用节点访问值得考虑。
- 在每次存款或铸造前检查授权和支出者。
- 在与交易相同的区块读取 max* 限制。
- 报价时优先使用预览函数而非原始转换。
- 在调试合约逻辑之前确认链 ID 和端点同步。
标准的局限性与权衡
ERC-4626 以将策略风险隐藏在统一接口背后为代价换取可组合性。两个具有相同 ABI 的金库可能具有完全不同的风险特征,而标准没有给集成方表达这种差异的方式。跨多个金库路由的聚合器必须在接口之上添加自己的风险评分、上限和监控。
该标准还假设单一底层 ERC-20 和原子结算。需要多个资产或延迟结算的项目必须采用扩展,而具有非标准代币的项目必须自行处理边缘情况。这些都不是标准的缺陷;这是最小接口所能承诺的边界。
最后,合规是必要的但不充分。一个金库可以通过所有接口检查,但仍然在经济上不安全。将接口视为入场券,将测量表视为真正的尽职调查。
构建者和集成方的后续步骤
如果您正在构建金库,请从 OpenZeppelin 基类开始,保留虚拟偏移,添加具有显式 totalAssets 覆盖的策略,并为第一个存款人路径、收费转账资产和策略损失编写分叉测试。如果您正在集成,请将测量表构建到您的监控中,使其在您报价的每个区块运行,并对 totalAssets 中无法解释的变化发出警报。
对于基础设施方面,查看 RPC 定价以确定您的读取量,并在需要托管访问而非运行自己的节点时使用 API 服务。对于更广泛的协议背景,学习库涵盖了相关标准和操作指南。具体的端点和提供商选择最好由 RPC 助手处理,它将简短、最新的建议与本实现指南分开。