OpenZeppelin Contracts ERC-4626 如何防御通胀攻击?虚拟份额与 offset 的作用
2026/9/13 10:22:53 网站建设 项目流程

OpenZeppelin Contracts ERC-4626 如何防御通胀攻击?虚拟份额与 offset 的作用

【免费下载链接】openzeppelin-contractsOpenZeppelin Contracts is a library for secure smart contract development.项目地址: https://gitcode.com/GitHub_Trending/op/openzeppelin-contracts

如果你用 OpenZeppelin Contracts 实现了一个 ERC-4626 金库(vault),上线时最常担心的安全问题之一就是“通胀攻击”(inflation attack):攻击者向空金库直接捐赠底层资产(donation),把份额价格抬高,再利用deposit份额数量向零取整的机制,让后来的用户存入的资产几乎全部被稀释。OpenZeppelin 从 v4.9 起在ERC4626中引入了“虚拟份额 + 虚拟资产 + 精度 offset”的组合防御,默认 offset 为 0,官方分析认为这已使攻击无利可图。这篇文章说明这套防御的运作方式、它在源码里的落点,以及如何用仓库自带测试验证它确实生效。

通胀攻击为什么成立

先建立文档给出的攻击模型(完整推导见 ERC-4626 指南的安全章节):

  • 用户存款得到的份额数按资产与当前兑换率计算后向零取整。兑换得越少,损失越大:存入不足 1 份额对应的资产时,用户直接得到 0 份额,相当于向金库捐赠。
  • 攻击流程:攻击者先向空金库deposit少量资产a_0,再把a_1资产直接转入金库地址。金库资产增加但份额总数不变,兑换率被拉向“右”,金库进入危险状态。
  • 数学结论:用户存款u只能换得u × a_0 / (a_0 + a_1)份额。只要满足u < 1 + a_1 / a_0,这笔存款就会被稀释到 0 份额。取a_0 = 1a_1 = u即可达成,攻击成本与攻击收益基本等价——这正是空金库让攻击者可图的原因。
  • 典型触发时机:攻击者盯住第一个做deposit的用户并抢跑(frontrun)其交易。

防御原理:虚拟份额、虚拟资产与 offset

文档给出的防御方案借鉴自 YieldBox,由两部分配合完成:

  1. 精度 offset:让份额的十进制精度比底层资产高offset位(用更多小数位表示份额)。更高的初始兑换率意味着同样存款换到更多份额,取整损失更小。
  2. 虚拟份额与虚拟资产:在换算公式里始终加入10^offset份虚拟份额和 1 份虚拟资产。它们的作用是在金库为空时锁定初始兑换率为10^offset,并且吞掉捐赠的一部分

关键在第二点的经济效果。带 offsetδ时,攻击者deposita_0后再捐赠a_1,攻击者只拥有a_0 / (1 + a_0)比例的份额,因此捐赠后只能收回a_1 × a_0 / (1 + a_0),剩余部分loss = a_1 / (1 + a_0)被金库的虚拟份额“没收”。用户u此时换到的份额为10^δ × u × (1 + a_0) / (1 + a_0 + a_1),要把它压到 0 份额需要10^δ × u ≤ loss。由此得到文档的两个结论:

  • offset = 0:攻击者损失至少等于用户存款,攻击在数学上不再盈利;
  • offset > 0:攻击成本比可抢走的价值高出10^offset倍量级,变得极其不划算。

下图展示 offset=3 时攻击者资金受限下的攻击效果(文档标注参数:δ=3,a_0=1,a_1=10^5):

防御在源码中的落点

防御逻辑集中在 ERC4626.sol 的两个内部换算函数,所有preview*与存取路径都经过它们:

function _convertToShares(uint256 assets, Math.Rounding rounding) internal view virtual returns (uint256) { return assets.mulDiv(totalSupply() + 10 ** _decimalsOffset(), totalAssets() + 1, rounding); } function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view virtual returns (uint256) { return shares.mulDiv(totalAssets() + 1, totalSupply() + 10 ** _decimalsOffset(), rounding); }
  • 分母里的+ 1就是虚拟资产,分子里的10 ** _decimalsOffset()就是虚拟份额;
  • decimals()也基于 offset 计算:_underlyingDecimals + _decimalsOffset(),即份额精度 = 资产精度 + offset。

offset 本身是可配置项,默认值为 0:

function _decimalsOffset() internal view virtual returns (uint8) { return 0; }

仓库提供了 ERC4626OffsetMock 演示如何自定义 offset(构造参数_offset直接覆盖_decimalsOffset())。如果你要实现自己的 vault,继承ERC4626并在构造函数传入底层资产即可默认获得该防御;需要更高安全余量时再覆盖_decimalsOffset返回更大的 offset。

ERC4626源码的 CAUTION 注释对防护边界有明确表述(见 ERC4626.sol 第 20-46 行):

  • 该机制不能完全阻止攻击,但默认 offset (0) 已使攻击无利可图——虚拟份额没收的捐赠部分与攻击者预期收益相抵;更大的 offset 让攻击成本比收益高出数量级。
  • 空(或接近空)金库的存款仍有被抢跑捐赠抬高价格的风险,文档建议 vault 部署者做一笔不小金额的初始存款,让价格操纵在成本上不可行。
  • 提现(withdraw)同样可能受滑点影响;用户侧可通过校验实际到账金额来保护自己,文档提到可用 ERC4626Router 一类做该检查的 wrapper。
  • 虚拟份额会“捕获”金库累积价值的一小部分;若金库发生亏损后用户集中赎回,虚拟份额会让先退出的人损失更小、后退出的人损失更大。
  • 想回退到 v4.9 之前的行为,只需覆盖_convertToShares_convertToAssets
  • 另一条硬性限制:任何不增加对应资产就铸造份额的机制都会破坏兑换率,本合约不能与ERC20FlashMint组合

用测试验证防御效果

仓库测试文件 ERC4626.test.js 中有一组针对“inflation attack: offset price by direct deposit of assets”的用例,可直接复现并核对防御效果。测试前置动作是向金库捐赠 1 单位资产来偏移价格(模拟攻击者 donation):

// Donate 1 token to the vault to offset the price await this.token.$_mint(this.vault, parseToken(1n));

随后deposit用例验证:即使价格已被捐赠偏移,previewDepositDeposit事件给出的份额仍按“实际资产 + 虚拟资产 / 实际份额 + 虚拟份额”的有效比率计算,用户不会拿到 0 份额。测试代码内嵌的表格记录了不同 offset 下,捐赠 1 单位后再存入 1 单位资产、最终可赎回的资产数(测试用例预期值):

offset存入资产可赎回资产
01.0000000000000000000.
61.0000000000000000000.999999000000000000
181.0000000000000000000.999999999999999999

并给出结论:攻击仍然可能发生,但被 offset 显著抬高成本——攻击要成功,捐赠规模必须比受害者存款大 10^offset 倍。另有mint用例说明用mint固定份额数可防通胀攻击,但会使 mint 非常昂贵。

在仓库内运行验证(仓库是只读参考环境,实际执行请在你本地 clone 后进行;npm install安装依赖,npm test运行 Hardhat 测试):

npm install npm test

package.json 中test脚本为. scripts/set-max-old-space-size.sh && hardhat test,也可用 Hardhat 的--grep参数只跑通胀攻击相关用例,例如npm test -- --grep "inflation attack"

另外两个值得注意的边界用例:

  • ERC4626.t.sol 中的testFuzzDecimalsOverflow表明:对 18 位精度的底层资产,当 offset 落在 238 到 255 区间时,decimals()会因uint8溢出而 panic(ARITHMETIC_UNDER_OR_OVERFLOW)。选择 offset 时要保证资产精度加 offset 不超过 255。
  • ERC4626.test.js 的decimals overflow用例以 offset 243/250/255 验证同一行为,$ERC4626OffsetMock即上文提到用于自定义 offset 的 mock。

边界与下一步

  • 这套防御的目标是“让攻击不盈利”,不是“阻止捐赠发生”;空金库上线时配合一笔初始存款,能进一步把价格操纵的成本抬到不可行。
  • 若你在定制 vault,任何对存取逻辑的覆盖都必须同步反映到preview*函数;文档明确提醒不要直接覆盖面向公共的deposit/mint/withdraw/redeem,否则会造成depositmint之间行为不一致。
  • 需要更高安全余量时,覆盖_decimalsOffset()返回大于 0 的值即可,换算逻辑无需改动;想恢复无虚拟份额的旧行为则覆盖两个_convertTo*函数。
  • 深入防御数学的完整推导(含 a_0、a_1、u 各阶段的资产/份额/兑换率表格)见 erc4626.adoc;收费扩展行为可参考文档中指向的 ERC4626Fees.sol 示例。

【免费下载链接】openzeppelin-contractsOpenZeppelin Contracts is a library for secure smart contract development.项目地址: https://gitcode.com/GitHub_Trending/op/openzeppelin-contracts

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询