ERC-4626: 代币化金库会计标准 (Tokenized Vault Standard)
ERC-4626 是以太坊针对**产生收益的代币化金库(Yield-bearing Vaults)**制定的统一会计接口标准。
在 ERC-4626 之前,各大借贷与质押协议(如 Compound 的 cToken、Aave 的 aToken、Yearn 的 yVault)各自维护了一套私有的份额(Shares)与底层资产(Assets)计算公式。接口割裂导致聚合器、机枪池与借贷衍生品必须为每个协议编写高度耦合的适配器代码。ERC-4626 确立了统一的存款、铸币、提取、赎回状态机与精度换算范式。
核心数学模型:资产与份额的动态换算
金库通常持有某种底层代币(asset),并向存入资产的用户按比例铸造代表金库索赔权的凭证代币(shares)。
1. 基础换算比例公式
$$ \text{shares} = \text{assets} \times \frac{\text{totalSupply}}{\text{totalAssets}} $$
$$ \text{assets} = \text{shares} \times \frac{\text{totalAssets}}{\text{totalSupply}} $$
totalAssets():当前金库所管理并拥有控制权的底层资产物理总量。totalSupply():当前已发行的金库凭证份额总量。
2. 整数除法截断与舍入方向约束(Rounding Direction)
EVM 仅支持向零截断的整数算术。ERC-4626 明确规定了所有计算的舍入偏向,必须始终偏向保护金库账本完整性、惩罚提取套利:
| 操作接口 | 核心算式 | 规范舍入方向 (Rounding) | 经济学目的 |
|---|---|---|---|
previewDeposit(assets) / deposit | 计算获得的 shares | 向下取整(Round Down) | 避免用户存入资产时多得份额,防止凭空印钞 |
previewMint(shares) / mint | 计算需付出的 assets | 向上取整(Round Up) | 确保用户存足全额资产才可获得指定份额 |
previewWithdraw(assets) / withdraw | 计算需扣除的 shares | 向上取整(Round Up) | 提取资产时多销毁微量份额,保护剩余存款人 |
previewRedeem(shares) / redeem | 计算返还的 assets | 向下取整(Round Down) | 销毁份额时少给微量底层资产,收益沉淀于池中 |
核心状态机与标准接口
ERC-4626 扩展了 ERC-20 标准,包含四组对称的存款与提取方法:
interface IERC4626 is IERC20 {
// 底层资产地址
function asset() external view returns (address assetTokenAddress);
// 金库总管理资产数
function totalAssets() external view returns (uint256 totalManagedAssets);
// 1. 基于资产数存入 (Deposit: 指定资产,换取份额)
function deposit(uint256 assets, address receiver) external returns (uint256 shares);
// 2. 基于份额数存入 (Mint: 指定目标份额,支付资产)
function mint(uint256 shares, address receiver) external returns (uint256 assets);
// 3. 基于资产数提取 (Withdraw: 指定提取资产数,销毁份额)
function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares);
// 4. 基于份额数赎回 (Redeem: 指定销毁份额数,返还资产)
function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets);
// 查询与模拟接口 (无状态开销)
function convertToShares(uint256 assets) external view returns (uint256 shares);
function convertToAssets(uint256 shares) external view returns (uint256 assets);
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);
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);
}
初始存款通胀攻击与虚拟偏移量防御 (Virtual Offset)
1. 攻击根因复盘
在初始空池(\(\text{totalSupply} = 0\))状态下,攻击者先存入 1 wei 资产获得 1 wei 份额,随后通过直接 transfer 向金库地址强制捐赠 \(10^6\) 个代币拉升每股净值。后续真实用户存入资金时,由于向下截断,计算出的 shares 为 0,资金被全额吞并。
2. 虚拟资产与虚拟份额偏移量解法 (OpenZeppelin 方案)
OpenZeppelin ERC4626 实现中引入了虚拟偏移量(如增加 \(10^{\text{decimals}}\)):
$$ \text{shares} = \text{assets} \times \frac{\text{totalSupply} + 10^a}{\text{totalAssets} + 1} $$
$$ \text{assets} = \text{shares} \times \frac{\text{totalAssets} + 1}{\text{totalSupply} + 10^a} $$
通过人为在分母分子中注入虚拟底数,即使在极端空池状态下,1 wei 的小额建仓也无法将单位净值拉升至发生严重截断的区间。
生产级安全检查清单
- 严格对齐舍入方向:所有换算函数必须严格遵循“存入少给份额,提取多扣份额”的原则,严禁使用原生除法默认向零截断造成方向颠倒。
- 防范初始通胀与捐赠攻击:必须配置虚拟偏移量机制,或在初始化阶段铸造并死锁最小基础份额(如向
address(0)锁定 1000 shares)。 - 防范零份额铸造与充值:在
deposit与mint逻辑中加入require(shares > 0)断言,防止用户资产被无偿吞没。 - 脱钩直接余额物理依赖:避免单纯依赖
asset.balanceOf(address(this))计算totalAssets(),采用内部记账抵御闪电贷或直接转账对汇率的单区块瞬态冲击。