ERC-4626 通胀攻击:DeFi 金库的"捐款陷阱"

前言

2023年,某 DeFi 金库刚上线,攻击者只花了1块钱,就把整个金库的钱全卷走了。这不是科幻,这就是 ERC-4626 金库的"捐款陷阱"------通胀攻击(Inflation Attack)。

这个漏洞利用了金库在初始阶段的设计缺陷,让攻击者几乎零成本稀释其他用户的份额,最终把整个金库的资金卷走。本文会用通俗的语言讲清楚这个攻击是怎么回事,同时也会给开发者提供技术细节和防御方案。

ERC-4626 是什么

ERC-4626 是以太坊上一种标准化金库合约(Vault)。工作原理很简单:

  • 你存入底层资产(比如 USDT、ETH)
  • 金库给你份额(shares,也叫 vault token)
  • 以后你可以用份额赎回资产,还能赚取收益

核心公式:

scss 复制代码
你得到的份额 = (你存的钱 × 总份额) ÷ 总资产

关键点 :这是整数除法,会向下取整(小数直接砍掉)。这就是漏洞的根源。

ERC-4626 存款流程

graph LR A[用户存入资产
如 USDT] --> B[金库计算份额
shares = assets × totalShares ÷ totalAssets] B --> C[用户获得份额
vault token] C --> D[后续可赎回
按比例取回资产] style A fill:#d3f9d8,stroke:#2f9e44,stroke-width:2px style B fill:#e5dbff,stroke:#5f3dc4,stroke-width:2px style C fill:#ffe8cc,stroke:#d9480f,stroke-width:2px style D fill:#c5f6fa,stroke:#0c8599,stroke-width:2px

通胀攻击原理

通俗解释

想象一下:你买10000张彩票,结果系统把它四舍五入成0张......这就是通胀攻击的本质。

攻击者通过"捐款"把金库的总资产拉得很大,而总份额很小,导致后续用户存钱时,计算出来的份额因为向下取整变成0。用户的钱进了金库,但什么都没拿到,最后攻击者把所有钱全拿走。

攻击流程(4步)

graph TD A[步骤1:攻击者存入 1 个 T] --> B[获得 1 个份额
totalAssets=1, totalShares=1] B --> C[步骤2:攻击者捐款 100000 个 T
不通过 deposit,直接转账] C --> D[totalAssets=100001
totalShares=1 不变] D --> E[步骤3:受害者存入 10000 个 T] E --> F[计算份额
10000 × 1 ÷ 100001 ≈ 0.09999] F --> G[向下取整 = 0 份额
受害者什么都没得到!] G --> H[步骤4:攻击者赎回 1 个份额] H --> I[卷走全部 110001 个 T
攻击成功!] style A fill:#d3f9d8,stroke:#2f9e44,stroke-width:2px style C fill:#ffe3e3,stroke:#c92a2a,stroke-width:2px style E fill:#fff4e6,stroke:#e67700,stroke-width:2px style G fill:#ffe3e3,stroke:#c92a2a,stroke-width:3px style I fill:#ffe3e3,stroke:#c92a2a,stroke-width:3px

攻击成本 :几乎为0(第一次的1个T最后也拿回来了) 攻击获利:受害者的全部存款

技术深入:数学根源

ERC-4626 的 convertToShares 函数使用了整数除法:

solidity 复制代码
function convertToShares(uint256 assets) public view returns (uint256) {
    if (totalAssets() == 0) {
        return assets;
    }
    return totalSupply() * assets / totalAssets();  // 向下取整
}

totalAssets 远大于 totalSupply 时,assets * totalSupply 还没除,就因为整数除法被抹掉小数。攻击者通过 donate 把 totalAssets 拉到极大,让任何后续 deposit 的分子都被"稀释"成0。

正常情况 vs 攻击后对比

graph LR subgraph normal["正常情况"] N1[totalAssets = 100
totalShares = 100] N2[用户存入 10 个资产] N3[计算:10 × 100 ÷ 100 = 10] N4[获得 10 个份额 ✓] N1 --> N2 --> N3 --> N4 end subgraph attack["攻击后情况"] A1[totalAssets = 100001
totalShares = 1] A2[用户存入 10000 个资产] A3[计算:10000 × 1 ÷ 100001 ≈ 0.09999] A4[向下取整 = 0 份额 ✗] A1 --> A2 --> A3 --> A4 end style N1 fill:#d3f9d8,strok9e44,stroke-width:2px style N4 fill:#d3f9d8,stroke:#2f9e44,stroke-width:2px style A1 fill:#ffe3e3,stroke:#c92a2a,stroke-width:2px style A4 fill:#ffe3e3,stroke:#c92a2a,stroke-width:3px

真实案例

时间线与影响

  • 2023-2025年:多个知名 DeFi 项目早期金库都被这样攻击过
  • 涉及金额:数百万美元的潜在风险交易(幸运的是大部分未被利用)
  • 高危时期:新金库刚上线、TVL(总锁仓价值)接近0的时候

实际案例

Morpho DAO

  • 防御方式:在金库初始化时存入资产,对应的份额铸造给金库自身(作为非操作地址)
  • 效果:初始存入的资产越多,攻击难度越大
  • 缺点:这些资产实际上被锁死了

YieldBox

  • 防御方式:使用虚拟份额和虚拟资产
  • 效果:不需要烧掉任何代币就能达到防御效果
  • 实现:设置 asset offset 为1,supply offset 为1e8

防御方案

用户层面:如何保护自己

如果你是 DeFi 用户,记住这几点:

  1. 不要急着冲新上线的金库 - 等 TVL 起来、审计报告出来再参与
  2. 观察项目是否使用了防御措施 - 查看项目文档或审计报告
  3. 小额测试 - 第一次存款用小额测试,确认能正常获得份额

开发者层面:技术防御

目前主流有3种成熟防护方案:

1. 虚拟份额 + 小数偏移量(最推荐)

这是 OpenZeppelin 官方推荐的方案:

solidity 复制代码
uint256 private constant VIRTUAL_SHARES = 1e6;
uint256 private constant VIRTUAL_ASSETS = 1e6;

// 修改计算公式
totalAssets = realAssets + VIRTUAL_ASSETS;
totalShares = realShares + VIRTUAL_SHARES;

工作原理

graph TD A[OpenZeppelin 防御方案] --> B[虚拟份额 + 小数偏移量] B --> C[增加虚拟资产
VIRTUAL_ASSETS = 1e6] B --> D[增加虚拟份额
VIHARES = 1e6] C --> E[totalAssets = realAssets + VIRTUAL_ASSETS] D --> F[totalShares = realShares + VIRTUAL_SHARES] E --> G[限制汇率操纵能力] F --> G G --> H[攻击者捐款被虚拟资产稀释] H --> I[攻击无利可图 ✓] style A fill:#e7f5ff,stroke:#1971c2,stroke-width:2px style B fill:#e5dbff,stroke:#5f3dc4,stroke-width:2px style G fill:#ffe8cc,stroke:#d9480f,stroke-width:2px style I fill:#d3f9d8,stroke:#2f9e44,stroke-width:3px

效果

  • 即使 totalAssets 被捐到100000,也不会让新用户的 shares 直接变成0
  • 虚拟资产和份额会捕获部分捐款,使攻击无利可图
  • 偏移量越大,攻击者损失越多,安全性越高

2. 最小存款检查

deposit 前加入最小金额检查

solidity 复制代码
require(assets >= MIN_DEPOSIT, "Deposit too small");

优点 :实现简单 缺点:提高了使用门槛,对小额用户不友好

3. 使用 OpenZeppelin ERC4626 最新版

OpenZeppelin 已在 v5+ 版本中默认加入虚拟份额保护,直接继承使用即可:

solidity 复制代码
import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";

contract MyVault is ERC4626 {
    constructor(IERC20 asset) ERC4626(asset) ERC20("MyVault", "MVT") {}
}

总结

一句话记住这个漏洞

攻击者捐了一大笔钱,把金库的总资产吹得很大,后续存钱的人就什么份额都拿不到,最后捐款的人把所有钱全拿走。

给用户的建议

  • 新金库别急着冲,等 TVL 起来再说
  • 观察项目是否有审计报告和防御措施

给开发者的建议

  • 使用 OpenZeppelin 最新版 ERC4626
  • 或实现虚拟份额 + 小数偏移量防护
  • 在金库初始化时进行充分测试

技术漏洞可以修复,但安全意识要时刻保持。DeFi 的世界很精彩,但也充满风险,保护好自己的资产才是第一位的。


参考资料

相关推荐
UrbanJazzerati1 小时前
非常友好的Vue 3 生命周期详解
前端·面试
AAA阿giao1 小时前
从零构建一个现代登录页:深入解析 Tailwind CSS + Vite + Lucide React 的完整技术栈
前端·css·react.js
毅航2 小时前
自然语言处理发展史:从规则、统计到深度学习
人工智能·后端
JxWang052 小时前
Task04:字符串
后端
兆子龙2 小时前
像 React Hook 一样「自动触发」:用 Git Hook 拦住忘删的测试代码与其它翻车现场
前端·架构
树獭叔叔3 小时前
10-让模型更小更聪明,学而不忘:知识蒸馏与持续学习
后端·aigc·openai
JxWang053 小时前
Task02:链表
后端
兆子龙3 小时前
用 Auto.js 实现挂机脚本:从找图点击到循环自动化
前端·架构
SuperEugene3 小时前
表单最佳实践:从 v-model 到自定义表单组件(含校验)
前端·javascript·vue.js