@@ -15,6 +15,110 @@ web3初学者,做过一些学习项目,涉及defi,zkp,web3+ai,希望
1515## Notes
1616
1717<!-- Content_START -->
18+ # 2025-08-20
19+
20+ # 捐赠攻击学习笔记
21+
22+ ## 目录
23+ - 捐赠攻击的危害:为什么需要关注?
24+ - ERC-4626 股份膨胀:经典的捐赠攻击
25+ - 易受攻击的保险库示例
26+ - 结论
27+
28+ ## 捐赠攻击的危害:为什么需要关注?
29+ 在去中心化金融(DeFi)和智能合约领域,看似无害的行为可能被恶意利用。** 捐赠攻击(Donation Attacks)** 通过利用智能合约对代币余额的错误假设来实施攻击,通常源于对外部因素的错误依赖。
30+
31+ ### 核心问题
32+ 攻击者通过直接向智能合约发送代币(而不是通过协议的接口)来操纵合约的状态。这种“捐赠”行为可能严重干扰合约的逻辑,导致资产分配不公,损害合法用户的利益。如果协议仅依赖代币余额(` balanceOf ` )进行记账,而不考虑此类直接转账,攻击者就能通过操纵状态造成破坏。
33+
34+ ### Solodit 检查清单项:SOL-AM-DA-1
35+ > ** 问题** :协议是否依赖 ` balance ` 或 ` balanceOf ` 而不是内部记账?
36+ > ** 描述** :攻击者通过“捐赠”代币来操纵记账。
37+ > ** 修复方法** :实现内部记账,而不是直接依赖 ` balanceOf ` 。
38+
39+ 此检查项旨在通过确保协议不依赖外部函数(如 ` balanceOf ` 或 ` balance ` )进行记账,防止捐赠攻击。内部记账使用专用的状态变量来跟踪和管理余额,而不是依赖外部数据。
40+
41+ ### 为什么危险?
42+ 任何人可以直接向合约发送代币,而不受合约逻辑的限制。如果合约使用 ` token.balanceOf(address(this)) ` 来计算股份或取款金额,攻击者通过“捐赠”代币即可破坏系统,改变预期结果。
43+
44+ ## ERC-4626 股份膨胀:经典的捐赠攻击
45+ ### 什么是 ERC-4626?
46+ ERC-4626 是一种用于** 代币化保险库(Tokenized Vaults)** 的标准接口。保险库是一个智能合约,用户可存入特定底层资产(如 USDC 或 WETH),并获得代表其在保险库中资产份额的“股份”。这些股份通常会随着保险库的收益(例如通过借贷或挖矿策略)而增值。
47+
48+ ### 漏洞:捐赠与 ERC-4626 的碰撞
49+ 许多早期的 ERC-4626 实现根据保险库持有的底层资产总量(通过 ` asset.balanceOf(address(this)) ` 确定)来计算存款时应铸造的股份数量。其简化公式如下:
50+
51+ ```
52+ shares_to_mint = deposit_amount * total_shares / total_assets_in_vault
53+ ```
54+
55+ 这里的 ` total_assets_in_vault ` 通常由 ` asset.balanceOf(address(this)) ` 计算,而这正是捐赠攻击的切入点。
56+
57+ ### 股份膨胀攻击(Share Inflation Attack)
58+ 攻击通常针对新部署的 ERC-4626 保险库,尤其是在没有任何合法用户存款之前。攻击者通过操纵股份价格计算来窃取用户资金。以下是一个攻击示例:
59+
60+ 1 . ** 初始状态** :保险库为空,无股份,无 WETH。
61+ 2 . ** 攻击者首次存款** :攻击者通过存款函数存入 1 WEI(WETH)。由于保险库为空,设计上 1 WEI 存款铸造 1 股份。此时,股份价格为 1 WEI/股份。
62+ 3 . ** 操纵行为** :攻击者直接向保险库发送 1 WETH(1e18 WEI)。现在保险库持有约 1 WETH(1e18+1 WEI),但流通股份仍为 1 WEI。此时股份价格接近 1e18 WEI/股份(即“股份膨胀”)。
63+ 4 . ** 受害者损失** :普通用户尝试存入 0.5 WETH。保险库根据公式计算股份:` (0.5e18 * 1) / (1e18 + 1) ` ,结果四舍五入为 0 股份。用户存入 0.5 WETH 却未获得任何股份!
64+ 5 . ** 攻击者获利** :攻击者提取其 1 WEI 股份,拿走保险库中的全部资产(1.5 WETH)。
65+
66+ ### 为什么是捐赠攻击?
67+ 攻击者通过直接向合约发送资产(不增加股份总量)来操纵股份价格,造成数学陷阱,使合法用户的存款无法获得股份。
68+
69+ ### 缓解措施
70+ 核心问题在于处理首次存款和防止 ` totalSupply ` 为零或极小时的操纵。以下是一些解决方案:
71+ - ** 最低初始存款** :要求首次存款达到一定规模,但这会影响用户体验,且无法完全解决问题。
72+ - ** 内部余额跟踪** :完全忽略 ` asset.balanceOf ` ,通过内部变量精确跟踪存款和取款。这是理想方案,但会增加复杂性和 Gas 成本。
73+ - ** 虚拟股份/偏移** :通过假定保险库一开始就持有一定数量的资产和股份,锚定股份价格计算,防止初始状态被操纵。OpenZeppelin 的 ERC-4626 实现现已采用此方法。
74+
75+ ## 易受攻击的保险库示例
76+ 以下是一个简化的易受攻击的代币保险库代码:
77+
78+ <xaiArtifact artifact_id =" e8d44561-66f4-4b89-b721-9130ca3119f6 " artifact_version_id =" 431a5c98-e8b5-4e02-bfe6-b082a26b8462 " title =" Vulnerable_TokenVault.sol " contentType =" text/x-solidity " >
79+
80+ // SPDX-License-Identifier: MIT
81+ pragma solidity ^0.8.0;
82+
83+ import "@openzeppelin/contracts /token/ERC20/IERC20.sol";
84+
85+ contract TokenVault {
86+ IERC20 public token;
87+ mapping(address => uint256) public shares;
88+ uint256 public totalSupply;
89+
90+ constructor(IERC20 _token) {
91+ token = _token;
92+ }
93+
94+ function deposit(uint256 _amount) external {
95+ uint256 tokenBalance = token.balanceOf(address(this));
96+ uint256 shareAmount = 0;
97+
98+ if (totalSupply == 0) {
99+ shareAmount = _amount;
100+ } else {
101+ shareAmount = _amount * totalSupply / tokenBalance;
102+ }
103+
104+ shares[msg.sender] = shares[msg.sender] + shareAmount;
105+ totalSupply = totalSupply + shareAmount;
106+ token.transferFrom(msg.sender, address(this), _amount);
107+ }
108+
109+ function withdraw(uint256 _amount) external {
110+ uint256 shareAmount = _amount;
111+ require(shares[msg.sender] >= shareAmount, "Insufficient shares");
112+
113+ uint256 tokenBalance = token.balanceOf(address(this));
114+ uint256 amountToWithdraw = shareAmount * tokenBalance / totalSupply;
115+
116+ shares[msg.sender] = shares[msg.sender] - shareAmount;
117+ totalSupply = totalSupply - shareAmount;
118+ token.transfer(msg.sender, amountToWithdraw);
119+ }
120+ }
121+
18122# 2025-08-19
19123
20124# ERC-4626 通胀攻击学习笔记
0 commit comments