第五部分: 当心 gas 消耗 ——保护以太坊智能合约免受恶意破坏攻击
本文深入探讨了以太坊智能合约中一种隐蔽但极具破坏性的漏洞:恶意破坏攻击(Griefing Attacks)。文章解释了破坏攻击的原理,分析了易受攻击的代码,并通过真实案例展示了攻击流程。此外,文章还提供了一个安全、高效、抗恶意破坏的智能合约示例,并讨论了相关的防御策略、测试方法和高级工具。
欢迎来到智能合约安全:Solodit 清单系列的第五部分,我们将在这里解决一个微妙但极具破坏性的漏洞:恶意破坏攻击(Griefing Attacks),在 Solodit 清单中也称为 SOL-AM-Griefing。
想象一下,你已经为 DAO 部署了一个去中心化投票系统。一切运行顺利——直到一个恶意行为者开始用旨在失败的交易淹没你的智能合约。这些行为不会窃取资金或改变结果,但它们会浪费 gas,堵塞以太坊的 mempool,并扰乱合法使用。这就是恶意破坏。
恶意破坏攻击利用以太坊的 gas 机制来造成拒绝服务的情况,增加交易成本,并侵蚀用户信任。对于攻击者来说,它们成本低廉,但对于用户和开发者来说却代价高昂。
在本指南中,我们将:
- 解释恶意破坏攻击是如何运作的。
- 分析易受攻击的代码,并演练真实世界的攻击工作流程。
- 构建一个安全的、gas 高效的、抗恶意破坏的智能合约。
- 将恶意破坏与之前部分中的相关漏洞联系起来。
- 分享测试策略和高级工具以进行缓解。
为什么恶意破坏攻击是一种无声的威胁
恶意破坏攻击是以太坊生态系统的破坏者。与重入攻击或价格操纵不同,它们的目的不是直接窃取资金或利用逻辑。它们的目标是破坏,针对以太坊的 gas 模型以:
- 浪费 Gas:强迫用户在失败的交易中燃烧 ETH,抬高成本并阻止交互。
- 造成延迟:用失败的交易堵塞 mempool,延迟合法的交易,并迫使用户支付更高的 gas 费用。
- 扰乱功能:触发拒绝服务 (DoS) 情况,阻止关键功能,如投票或提款。
- 侵蚀信任:正如历史事件中看到的那样,让用户感到沮丧,损害 dApp 的声誉和用户信心。
Solodit 清单的 SOL-AM-Griefing 强调 gas 高效的设计、早期输入验证和受限的用户操作,以减轻这些风险。恶意破坏特别阴险,因为对于攻击者来说成本很低——失败的交易仍然会消耗 revert 之前的操作的 gas,对用户的影响大于攻击者,而攻击者可以负担得起垃圾邮件。
恶意破坏攻击是如何运作的
恶意破坏攻击利用以太坊的透明 mempool 和 gas 机制,通过提交故意失败的交易,消耗 gas 并扰乱合约操作。攻击者针对具有以下特征的合约:
- 容易 Revert 的逻辑:具有
require或revert语句的功能,这些语句可以预测地失败(例如,投票限制)。 - 外部调用:与可以被操纵为 revert 或燃烧过多 gas 的合约进行交互。
- 宽松的输入验证:允许不受限制的重复调用的功能,从而实现垃圾邮件。
常见的恶意破坏策略

攻击机制
- Gas 消耗:失败的交易会消耗 revert 之前执行的操作的 gas(例如,读取状态、评估条件)。
- Mempool 动态:攻击者提交低 gas 交易,这些交易在 mempool 中停留,迫使用户以更高的费用出价(例如,100 gwei 与 20 gwei)。
- 影响:延迟关键操作(例如,DAO 投票),增加成本,并可能导致用户放弃 dApp。
真实世界的例子:2020 年 Uniswap Mempool 阻塞
2020 年,机器人用低 gas、容易 revert 的交易垃圾邮件阻塞 Uniswap 的 mempool。合法用户不得不支付更高的费用或忍受延迟。这次攻击暴露了开放 mempool 和大量 revert 路径中的弱点。Uniswap 后来引入了:
- Gas 优化
- 批量处理
- 链下模拟 (Uniswap V3)
其他协议(如 Compound)也在其治理系统中面临恶意破坏尝试,其中失败的提案延迟了 DAO 投票。
易受攻击的代码:一个适合恶意破坏的投票合约
让我们检查一个简单的投票合约,用户可以为候选人投票,每个候选人限制为 100 票。它缺乏限制使其成为恶意破坏的主要目标。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;
contract VulnerableVoting {
mapping(address => uint256) public votes;
// Vote for a candidate
function vote(address candidate) external {
require(votes[candidate] < 100, "Candidate reached vote limit");
votes[candidate]++;
emit Voted(msg.sender, candidate);
}
event Voted(address indexed voter, address indexed candidate);
}
攻击场景:阻塞投票
以下是攻击者如何利用此合约:
- 合法投票:用户调用
vote(candidate),递增votes[candidate]。当候选人接近限制时(例如,votes[candidate] = 99),合约变得容易受到攻击。 - 攻击者的举动:攻击者监视合约(例如,通过区块链事件或 mempool 分析)并识别出
votes[candidate] = 99的候选人。他们重复调用vote(candidate)并降低 gas 费用(例如,5 gwei),因为他们知道每次调用都会由于require检查而 revert。 - 结果:
- 每次失败的调用都会消耗约 20,000 gas 来读取
votes[candidate]并评估require语句。 - mempool 中充满了这些失败的交易,从而延迟了合法的投票。
- 用户必须支付更高的 gas 费用(例如,100 gwei 而不是 20 gwei)才能超过攻击者的交易或等待 mempool 清除。
4. 影响:投票过程变慢,用户面临更高的成本,并且有些人放弃投票。在治理 DAO 中,这可能会扭曲结果或延迟关键决策。
为什么它容易受到攻击
- 不受限制的调用:合约允许每个地址无限次调用
vote,从而实现垃圾邮件。 - 容易 Revert 的逻辑:
require(votes[candidate] < 100)检查是必要的,但会成为攻击者触发 gas 浪费型 revert 的武器。 - Mempool 暴露:以太坊的公共 mempool 使攻击者可以监视和定位接近投票限制的候选人。
易受攻击的工作流程
这是工作流程,用 Mermaid 可视化:

补救措施:Gas 高效且抗恶意破坏的设计
Solodit 清单建议限制用户操作,尽早验证输入,并使用 gas 高效的模式来防止恶意破坏。以下是一个安全的投票合约,其中包含这些原则,以及高级功能,如提交-揭示、批量投票和断路器。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;
contract FixedVoting {
mapping(address => uint256) public votes; // Tracks votes per candidate
mapping(address => bool) public hasVoted; // Tracks if user has voted
mapping(address => bytes32) public commitments; // For commit-reveal
address public admin;
bool public paused;
uint256 public constant MAX_VOTES = 100; // Max votes per candidate
uint256 public constant VOTING_WINDOW = 7 days; // Voting period
uint256 public constant COMMIT_WINDOW = 1 hours; // Commit-reveal window
uint256 public votingStart;
modifier onlyAdmin() {
require(msg.sender == admin, "Not admin");
_;
}
modifier whenNotPaused() {
require(!paused, "Contract paused");
_;
}
modifier withinVotingWindow() {
require(block.timestamp <= votingStart + VOTING_WINDOW, "Voting closed");
_;
}
constructor() {
admin = msg.sender;
votingStart = block.timestamp;
}
// Commit a hashed vote intention
function commitVote(bytes32 commitment) external whenNotPaused withinVotingWindow {
require(!hasVoted[msg.sender], "Already committed");
hasVoted[msg.sender] = true;
commitments[msg.sender] = commitment;
emit VoteCommitted(msg.sender, commitment);
}
// Reveal and record vote
function revealVote(address candidate, uint256 nonce) external whenNotPaused withinVotingWindow {
require(commitments[msg.sender] != bytes32(0), "No commitment");
require(block.timestamp <= votingStart + COMMIT_WINDOW, "Commit window expired");
require(keccak256(abi.encodePacked(msg.sender, candidate, nonce)) == commitments[msg.sender], "Invalid commitment");
require(candidate != address(0), "Invalid candidate");
require(votes[candidate] < MAX_VOTES, "Candidate reached vote limit");
delete commitments[msg.sender];
votes[candidate]++;
emit Voted(msg.sender, candidate);
}
// Batch vote for multiple candidates
function voteBatch(address[] calldata candidates) external whenNotPaused withinVotingWindow {
require(!hasVoted[msg.sender], "Already voted");
require(candidates.length <= 10, "Too many candidates");
hasVoted[msg.sender] = true;
for (uint256 i = 0; i < candidates.length; i++) {
if (candidates[i] != address(0) && votes[candidates[i]] < MAX_VOTES) {
votes[candidates[i]]++;
emit Voted(msg.sender, candidates[i]);
}
}
}
// Finalize voting
function finalizeVoting() external onlyAdmin {
require(block.timestamp > votingStart + VOTING_WINDOW, "Voting still active");
paused = true;
emit VotingFinalized();
}
// 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 VoteCommitted(address indexed voter, bytes32 commitment);
event Voted(address indexed voter, address indexed candidate);
event VotingFinalized();
event Paused();
event Unpaused();
}
它是如何运作的
- 提交-揭示方案:用户提交一个哈希投票 (
commitVote) 以隐藏 mempool 中的详细信息,然后在 1 小时的时间窗口内揭示它 (revealVote)。这可以防止抢先交易(第 4 部分)并减少恶意破坏的机会。 - 受限制的投票:
hasVoted映射确保每个地址仅投票一次,从而限制垃圾邮件。 - 早期验证:诸如
require(!hasVoted[msg.sender])和require(candidate != address(0))之类的检查会尽早失败,从而最大限度地减少 gas 浪费。 - 批量投票:
voteBatch函数可以有效地处理多个投票,跳过无效的候选人而不会 revert。 - 投票限制:
MAX_VOTES常量 (100) 限制了每个候选人的投票数,从而防止状态膨胀(第 2 部分)。 - 时间窗口:
VOTING_WINDOW(7 天)和COMMIT_WINDOW(1 小时)限制了攻击面。 - 断路器:
paused标志和whenNotPaused修饰符会在攻击期间停止操作。 - 访问控制:
onlyAdmin修饰符限制了敏感函数(finalizeVoting、pause、unpause)。 - 事件日志记录:事件为承诺、投票和状态更改提供了 gas 高效的透明度。
安全优势
- 减少垃圾邮件:每个地址一票和提交-揭示限制了恶意破坏的机会。
- Gas 效率:早期验证和批量处理最大限度地减少了 revert 时 gas 的浪费(早期 revert 的 gas 约为 5,000,而易受攻击的版本中约为 20,000)。
- DoS 保护:断路器和时间窗口限制了攻击的影响和持续时间。
- 状态控制:
MAX_VOTES上限可以防止状态膨胀,与第 2 部分保持一致。 - Mempool 隐私:提交-揭示隐藏了投票详细信息,从而降低了抢先交易的风险(第 4 部分)。
- 稳健的设计:访问控制、高效的事件和故障隔离可确保可靠性。
用户和攻击者工作流程比较
易受攻击的投票工作流程
- 合法用户:调用
vote(candidate),如果votes[candidate] < 100,则期望成功。 - 攻击者:针对达到限制的候选人垃圾邮件发送
vote(candidate),从而触发 revert 并每次 revert 消耗约 20,000 gas。用失败的交易填充 mempool,从而延迟合法的投票并增加 gas 成本(例如,从 20 gwei 到 100 gwei)。 - 结果:用户面临延迟、更高的费用或失败的投票,从而扰乱了投票过程。
安全的投票工作流程
- 合法用户:通过
commitVote提交哈希投票,然后通过revealVote揭示它或使用voteBatch。通过hasVoted、MAX_VOTES和COMMIT_WINDOW检查,如果有效则成功。 - 攻击者:尝试垃圾邮件发送
commitVote、revealVote或voteBatch,但由于hasVoted或MAX_VOTES而提前 revert。voteBatch中的无效候选人将被跳过而不会 revert。失败的成本很低(约 5,000 gas)。 - Mempool:保持清洁,因为失败不会累积。
- 管理员:如果在检测到垃圾邮件,可以暂停合约以保护用户。
- 结果:合法的投票可以顺利进行,并且攻击者可以被最小的破坏中和。
工作流程可视化
以下是一个流程图,比较了易受攻击和安全的实现:

高级防御:Gas 退款模型
为了进一步减轻恶意破坏,请考虑为受垃圾邮件影响的合法用户提供 gas 退款模型,尤其是在高风险的 dApp 中,如治理或 NFT 铸造:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;
contract RefundableVoting is FixedVoting {
mapping(address => uint256) public gasRefunds;
function claimGasRefund() external whenNotPaused {
uint256 amount = gasRefunds[msg.sender];
require(amount > 0, "No refund");
gasRefunds[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Refund failed");
emit GasRefunded(msg.sender, amount);
}
function recordGasRefund(address user, uint256 amount) external onlyAdmin {
gasRefunds[user] += amount;
emit GasRefundRecorded(user, amount);
}
event GasRefunded(address indexed user, uint256 amount);
event GasRefundRecorded(address indexed user, uint256 amount);
}
- 它是如何运作的:管理员监控失败的交易(例如,通过 Forta 等链下工具)并为受影响的用户记录退款。用户通过
claimGasRefund索取退款,使用 pull-over-push 模式(第 1 部分)。 - 好处:抵消用户成本,通过减少其影响来阻止恶意破坏,并维持信任。
- 注意事项:需要链下监视和管理员干预,从而增加了复杂性。
与之前部分的集成:整体防御
恶意破坏攻击可能会放大本系列早期部分的漏洞:
- 抢先交易(第 4 部分):恶意破坏者垃圾邮件发送失败的交易以阻塞 mempool,从而延迟合法的投票并启用其他操作的抢先交易(例如,价格更新)。
- 捐赠攻击(第 3 部分):强制 ETH 存款可能会扰乱依赖于余额的逻辑,而恶意破坏垃圾邮件会延迟恢复操作。
- 状态膨胀(第 2 部分):恶意破坏可能会用失败的交易膨胀映射(例如,
votes),从而增加合法操作的 gas 成本。 - 无界循环(第 1 部分):循环中的恶意破坏(例如,处理投票)可能会触发 gas 限制失败,从而放大 DoS 风险。
为了应对这种情况,请结合防御:
- Pull-Over-Push(第 1 部分):使用
claimGasRefund来隔离失败并分配 gas 成本。 - 状态上限(第 2 部分):应用
MAX_VOTES来限制状态增长,从而防止恶意破坏垃圾邮件膨胀。 - 显式 ETH 跟踪(第 3 部分):拒绝意外的 ETH 以避免余额操纵:
receive() external payable {
revert("Direct ETH deposits not allowed");
}
- 提交-揭示(第 4 部分):在
commitVote中使用哈希承诺来隐藏投票详细信息,从而减少基于 mempool 的恶意破坏和抢先交易。
例如,voteBatch 函数与第 2 部分的批量处理保持一致:
function voteBatch(address[] calldata candidates) external whenNotPaused withinVotingWindow {
require(!hasVoted[msg.sender], "Already voted");
require(candidates.length <= 10, "Too many candidates");
hasVoted[msg.sender] = true;
for (uint256 i = 0; i < candidates.length; i++) {
if (candidates[i] != address(0) && votes[candidates[i]] < MAX_VOTES) {
votes[candidates[i]]++;
emit Voted(msg.sender, candidates[i]);
}
}
}
这会跳过无效的候选人而不会 revert,从而减少 gas 浪费和 DoS 风险。
防止恶意破坏攻击的最佳实践
为了与 Solodit 清单和行业标准保持一致,请采用以下做法:
- 限制用户操作:
- 限制每个地址的操作一次:
require(!hasVoted[msg.sender], "Already voted");
- 尽早验证输入:
- 在状态更新之前检查条件:
require(candidate != address(0), "Invalid candidate");
- 使用 Gas 高效的事件:
- 使用事件记录操作:
emit Voted(msg.sender, candidate);
- 实施断路器:
- 在攻击期间暂停操作:
function pause() external onlyAdmin { paused = true; }
- 限制状态增长:
- 使用常量:
uint256 public constant MAX_VOTES = 100;
- 利用时间窗口:
- 限制操作:
require(block.timestamp <= votingStart + VOTING_WINDOW, "Voting closed");
- 避免在关键路径中进行外部调用:
- 尽量减少对外部合约的依赖:
votes[candidate]++;
- 使用批量处理:
- 有效地处理多个操作:
voteBatch跳过无效输入。
- 监控 Mempool 活动:
- 使用 Forta 或自定义脚本来检测垃圾邮件模式。
- 全面测试:
- 使用 Foundry 模拟恶意破坏,使用 Echidna 进行模糊测试,并使用 Hardhat 分叉主网。
恶意破坏预防的高级工具(2025 年更新)
截至 2025 年 7 月,这些工具增强了恶意破坏攻击的预防:
- Slither (0.10.x):检测 gas 密集型功能和容易 revert 的逻辑 (
slither --detect high-gas)。 - MythX:分析交易失败路径和 mempool 漏洞。
- Foundry:通过 mempool 操纵和差异模糊测试来模拟恶意破坏。
- Forta:实时监控 mempool 中垃圾邮件或失败的交易模式。
- OpenZeppelin Defender:自动响应可疑的交易活动。
- Tenderly:模拟大量 revert 的场景并可视化 gas 使用情况。
- MEV-Geth:测试矿工可提取价值 (MEV) 场景以模拟恶意破坏的影响。
恶意破坏弹性的测试
为了确保你的合约抵抗恶意破坏:
- 单元测试(使用 Foundry):
it("prevents griefing via repeated votes", async () => {
await voting.vote(candidate.address, { from: user });
await expect(voting.vote(candidate.address, { from: user }))
.to.be.revertedWith("Already voted");
});
it("handles vote limit gracefully", async () => {
for (let i = 0; i < 100; i++) {
await voting.vote(candidate.address, { from: accounts[i] });
}
await expect(voting.vote(candidate.address, { from: accounts[101] }))
.to.be.revertedWith("Candidate reached vote limit");
});
it("processes batch votes efficiently", async () => {
const candidates = [candidate1.address, candidate2.address, invalidCandidate.address];
await voting.voteBatch(candidates, { from: user });
expect(await voting.votes(candidate1.address)).to.equal(1);
expect(await voting.votes(candidate2.address)).to.equal(1);
expect(await voting.votes(invalidCandidate.address)).to.equal(0);
});
- 模糊测试:使用 Echidna 模拟随机投票垃圾邮件和无效输入。
- Mempool 模拟:使用 Foundry 用失败的交易填充 mempool 并测试合约性能。
- 主网分叉:使用 Hardhat 分叉以太坊主网以测试 gas 动态和 mempool 行为。
- 监控:使用 Forta 检测异常交易模式,例如重复 revert。
结论:构建防恶意破坏的智能合约
恶意破坏攻击的目的不是抢劫,而是破坏。通过阻塞 mempool 和浪费 gas,攻击者会降低信任和功能。但是通过 Solodit 清单的 SOL-AM-Griefing 最佳实践——提交-揭示方案、早期验证、批量处理和断路器——你可以构建能够抵御这些破坏企图的 dApp。
继续通过本系列的见解加强你的合约。在第 6 部分中,我们将探讨矿工操纵、MEV 风险以及如何使用 Chainlink VRF 等随机性工具来保护时间敏感的操作。
在那之前——彻底审计,防御性编码,并自信地构建无需信任的应用。
- 原文链接: medium.com/@ankitacode11...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~