本文主要介绍了以太坊智能合约中一种隐蔽的攻击方式:捐赠攻击(Donation Attacks)。攻击者通过直接向合约地址发送以太币,绕过合约预期的入口点,导致合约内部状态变量与实际余额不一致,从而引发会计错误、拒绝服务等问题。文章分析了攻击原理、展示了易受攻击的合约代码,并提供了安全的合约实现方案,强调应明确追踪以太币流入、拒绝意外存款,以及使用熔断器等机制。
想象一下,你正在以太坊上运行一个众筹 dApp。你的社区热闹非凡,支持者纷纷发送 ETH,你的活动也越来越受欢迎。一切似乎都很完美 — 直到攻击者在没有使用你的捐赠函数的情况下,将 ETH 偷偷地塞进你的合约。突然间,你的内部账目乱了,资金分配等关键功能失败,用户也被锁在了资金之外。对你平台的信任度下降,你的项目也陷入停顿。这就是捐赠攻击 — 一种微妙的漏洞利用方式,它不会窃取资金,而是通过注入不需要的 ETH 来扰乱你合约的逻辑。
欢迎来到智能合约安全:Solodit CheckList 系列的第三期,我们将探讨 Solodit CheckList 中定义的 SOL-AM-DonationAttack:意外的 ETH 存款。捐赠攻击利用了以太坊的无需许可的特性,允许任何人通过直接转账或 selfdestruct
函数向合约发送 ETH。这些攻击可能导致会计错误,触发拒绝服务 (DoS) 条件,或锁定资金,所有这些都在暗中进行。截至 2025 年 7 月,以太坊的区块 gas 限制约为 3000 万 gas,这些漏洞甚至可能削弱精心设计的合约。
在这个引人入胜、以开发者为中心的指南中,我们将探讨捐赠攻击如何运作,分析一个易受攻击的众筹合约,并提供一个具有强大保护的安全实现。我们将包括详细的代码片段、视觉上吸引人的工作流程图和最佳实践,以确保你的合约保持弹性。无论你是构建 DeFi 协议、众筹平台还是 NFT 市场,本文都将为你提供抵御这些沉默破坏者并保持你的 dApp 平稳运行的工具。
智能合约通常依赖于内部状态变量(例如,totalRaised
)来跟踪资金和执行逻辑。然而,以太坊的开放架构允许任何人将 ETH 发送到合约的地址,绕过预期的入口点。这会在合约的实际余额(address(this).balance
)与其内部记录之间造成不匹配,从而导致:
Solodit CheckList 的 SOL-AM-DonationAttack 强调显式的 ETH 管理和拒绝意外存款。让我们深入了解这些攻击是如何展开的,以及如何阻止它们。
捐赠攻击利用了以太坊向任何地址(包括合约)发送 ETH 而不触发其逻辑的能力。攻击者使用两种主要方法:
address(contract).transfer(amount)
)或没有数据的钱包转账发送 ETH,绕过像 contribute()
这样的 payable 函数。selfdestruct(payable(contract))
的合约,强制 ETH 进入目标合约。这种方法特别阴险,因为它绕过了 receive()
或 fallback()
函数。这些攻击会扰乱那些假设 address(this).balance
与内部状态变量匹配的合约。例如,如果未跟踪的 ETH 使其逻辑倾斜,众筹合约可能无法分配资金或允许不正确的支付,从而可能导致 DoS 或财务错误。
2017 年,Parity Multisig Wallet 遭受了一次灾难性的漏洞利用,导致 1.5 亿美元的 ETH 被冻结。虽然主要是一个访问控制和重入问题,但该事件因对意外 ETH 流入处理不当而加剧。这凸显了智能合约中强大 ETH 管理的关键需求,这一教训在 2025 年仍然适用。
让我们检查一个跟踪捐款但对捐赠攻击完全开放的脆弱众筹合约:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;
contract VulnerableCrowdfunding {
mapping(address => uint256) public contributions;
uint256 public totalRaised;
// Users contribute ETH
function contribute() external payable {
contributions[msg.sender] += msg.value;
totalRaised += msg.value;
}
// Distribute funds to project
function distributeFunds(address payable recipient) external {
require(address(this).balance >= totalRaised, "Insufficient funds");
(bool success, ) = recipient.call{value: totalRaised}("");
require(success, "Distribution failed");
}
}
此合约假定 address(this).balance
与 totalRaised
匹配,因此容易受到以下攻击:
address(contract).transfer(1 ether)
)。address(this).balance
,而不会更新 contributions
或 totalRaised
。distributeFunds
函数通过 require
检查,但使用不正确的会计进行操作,可能会导致过度分配或逻辑错误。2. Selfdestruct 攻击:
contract MaliciousContract {
constructor(address payable target) payable {
selfdestruct(target); // Forces ETH into target contract
}
}
selfdestruct(payable(vulnerableCrowdfunding))
,将 1 ETH 发送到众筹合约。address(this).balance
,而不会更新 totalRaised
,从而扰乱逻辑或在更严格的情况下导致 DoS。该合约假定所有 ETH 都通过 contribute()
进入,因此它对外部存款毫无防御能力。这可能导致会计错误、DoS 条件或依赖余额一致性的函数中的意外行为。
捐赠攻击利用了以太坊的基本属性:任何人都可以将 ETH 发送到任何地址,包括智能合约。这种开放性创造了两个主要的攻击媒介:
该图说明了捐赠攻击如何在合约的实际余额与其内部状态跟踪之间造成严重的不匹配。当攻击者直接发送 ETH 或通过 selfdestruct 强制发送 ETH 时,合约的余额会增加,而其状态变量保持不变。这种差异会导致关键功能失败,因为它们依赖于实际余额和跟踪金额之间的一致核算。
让我们检查一个易受攻击的众筹合约,该合约演示了捐赠攻击如何破坏功能:
安全实施图演示了正确的 ETH 处理如何防止捐赠攻击。实心箭头表示成功的操作,而虚线箭头表示恢复的尝试。当用户通过预期函数贡献时,所有状态变量都会一致地更新。即使攻击者尝试直接转账或 selfdestruct,合约也会通过依赖跟踪余额而不是实际余额来保持准确的会计。
Solodit CheckList 建议拒绝意外的 ETH 存款并依赖状态变量进行会计处理。以下是众筹合约的安全版本:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;
contract FixedCrowdfunding {
mapping(address => uint256) public contributions;
mapping(address => uint256) public pendingWithdrawals;
uint256 public totalRaised;
address public admin;
bool public paused;
modifier onlyAdmin() {
require(msg.sender == admin, "Not admin");
_;
}
modifier whenNotPaused() {
require(!paused, "Contract paused");
_;
}
constructor() {
admin = msg.sender;
}
// Users contribute ETH
function contribute() external payable whenNotPaused {
require(msg.value > 0, "No ETH sent");
contributions[msg.sender] += msg.value;
totalRaised += msg.value;
pendingWithdrawals[msg.sender] += msg.value; // Track for refunds
emit Contributed(msg.sender, msg.value);
}
// Reject unexpected ETH deposits
receive() external payable {
revert("Direct ETH deposits not allowed");
}
// Distribute funds to project
function distributeFunds(address payable recipient) external onlyAdmin whenNotPaused {
require(totalRaised > 0, "No funds to distribute");
require(address(this).balance >= totalRaised, "Insufficient funds");
uint256 amount = totalRaised;
totalRaised = 0; // Prevent reentrancy
(bool success, ) = recipient.call{value: amount}("");
require(success, "Distribution failed");
emit FundsDistributed(recipient, amount);
}
// Allow users to withdraw refunds (pull-over-push)
function withdraw() external whenNotPaused {
uint256 amount = pendingWithdrawals[msg.sender];
require(amount > 0, "No funds to withdraw");
pendingWithdrawals[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Withdrawal failed");
emit Withdrawn(msg.sender, amount);
}
// Pause contract in case of attack
function pause() external onlyAdmin {
paused = true;
emit Paused();
}
// Unpause contract
function unpause() external onlyAdmin {
paused = false;
emit Unpaused();
}
event Contributed(address indexed user, uint256 amount);
event FundsDistributed(address indexed recipient, uint256 amount);
event Withdrawn(address indexed user, uint256 amount);
event Paused();
event Unpaused();
}
contribute
函数更新 contributions
、totalRaised
和 pendingWithdrawals
,确保跟踪所有 ETH 流入。receive
函数恢复直接 ETH 转账,防止未跟踪的流入。虽然 selfdestruct
绕过了 receive
,但合约的逻辑依赖于 totalRaised
,而不是 address(this).balance
。withdraw
函数允许用户声明退款,分配 gas 成本并隔离故障。paused
标志和 whenNotPaused
修饰符在攻击期间停止操作。onlyAdmin
修饰符将敏感函数限制为管理员。distributeFunds
中的外部调用之前重置 totalRaised
可以防止重入。这是安全合约的工作流程,展示了它如何缓解捐赠攻击:
此图说明了安全合约如何拒绝直接转账,显式跟踪捐款,并忽略来自 selfdestruct
的未跟踪 ETH,从而确保稳健的操作。
contribute
跟踪,防止不匹配。矿工可以在 coinbase 交易中包含 ETH 转账,从而强制将 ETH 送入合约,而无需触发 receive()
。
缓解措施:
totalRaised
)进行逻辑,而不是 address(this).balance
。function checkBalanceConsistency() external view onlyAdmin returns (bool) {
return address(this).balance >= totalRaised;
}
未跟踪的 ETH 可能会扰乱具有多个依赖于余额的函数(例如,支付、退款)的合约。
缓解措施:
function withdraw() external whenNotPaused {
uint256 amount = pendingWithdrawals[msg.sender];
require(amount > 0, "No funds to withdraw");
pendingWithdrawals[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Withdrawal failed");
emit Withdrawn(msg.sender, amount);
}
在基于代理的合约中,selfdestruct
可以将 ETH 存入代理中,从而扰乱依赖于余额的逻辑。
缓解措施:
selfdestruct
。function getProxyBalance() external view onlyAdmin returns (uint256) {
return address(this).balance;
}
为了符合 Solodit 的 SOL-AM-DonationAttack 和 2025 行业标准:
receive
函数,该函数恢复直接转账:receive() external payable {
revert("Direct ETH deposits not allowed");
}
totalRaised
、pendingWithdrawals
)进行会计处理。function pause() external onlyAdmin {
paused = true;
emit Paused();
}
onlyAdmin
限制敏感函数。slither --detect selfdestruct
以识别漏洞。selfdestruct
攻击。截至 2025 年 7 月,这些工具增强了捐赠攻击预防:
selfdestruct
和依赖于余额的逻辑(slither --detect selfdestruct
)。通过以下方式确保你的合约能够承受攻击:
it("rejects direct ETH deposits", async () => {
await expect(
web3.eth.sendTransaction({
from: accounts[0],
to: crowdfunding.address,
value: ethers.utils.parseEther("1"),
})
).to.be.revertedWith("Direct ETH deposits not allowed");
});
it("handles selfdestruct attacks", async () => {
const MaliciousContract = await ethers.getContractFactory("MaliciousContract");
await MaliciousContract.deploy(crowdfunding.address, {
value: ethers.utils.parseEther("1"),
});
expect(await ethers.provider.getBalance(crowdfunding.address)).to.equal(
ethers.utils.parseEther("1")
);
await expect(crowdfunding.distributeFunds(accounts[1])).to.be.revertedWith("No funds to distribute");
});
2023 年,一个 DeFi 众筹平台遭受了一次捐赠攻击,当时一名攻击者使用 selfdestruct
强制将 10 ETH 送入合约。该平台的退款逻辑依赖于 address(this).balance
,由于未跟踪的 ETH 而失败,导致用户资金被锁定数周。该团队通过部署一个具有显式 ETH 跟踪和 pull-over-push 提款的新合约来缓解这种情况,但该事件导致了严重的用户流失和声誉损害。
捐赠攻击可以放大其他漏洞:
require
检查在 gas 密集型循环中失败,从而加剧 DoS 条件。为了对此进行反击,请结合:
withdraw
函数通过分配 gas 成本来与 DoS 缓解措施保持一致。MAX_CONTRIBUTIONS
常量限制贡献垃圾邮件。捐赠攻击是隐秘的破坏者,它们利用以太坊的开放性来破坏合约逻辑。通过遵循 Solodit 的 SOL-AM-DonationAttack 指南 — 拒绝意外 ETH、显式跟踪资金,以及使用稳健的pull-over-push 和断路器等模式 — 你可以构建防捐赠合约。像 Slither、MythX 和 Foundry 这样的工具,加上严格的测试,可确保在攻击下的弹性。
本文是智能合约安全:Solodit CheckList 系列的一部分。接下来,我们将探讨抢先交易攻击,剖析基于 mempool 的漏洞利用,并实施诸如提交-披露方案之类的保护措施。无论你是构建众筹平台、DeFi 协议还是 NFT 市场,掌握这些防御措施对于安全、可扩展和无需信任的系统至关重要。
行动号召:在评论中分享你的想法!你在你的项目中遇到过捐赠攻击吗?关注该系列以获取有关保护智能合约的更多见解,并查看 Foundry 和 Forta 等工具,以在漏洞之前保持领先地位。
- 原文链接: medium.com/@ankitacode11...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!