第五部分: 当心 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 的逻辑:具有 requirerevert 语句的功能,这些语句可以预测地失败(例如,投票限制)。
  • 外部调用:与可以被操纵为 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);
}

攻击场景:阻塞投票

以下是攻击者如何利用此合约:

  1. 合法投票:用户调用 vote(candidate),递增 votes[candidate]。当候选人接近限制时(例如,votes[candidate] = 99),合约变得容易受到攻击。
  2. 攻击者的举动:攻击者监视合约(例如,通过区块链事件或 mempool 分析)并识别出 votes[candidate] = 99 的候选人。他们重复调用 vote(candidate) 并降低 gas 费用(例如,5 gwei),因为他们知道每次调用都会由于 require 检查而 revert。
  3. 结果
  • 每次失败的调用都会消耗约 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 修饰符限制了敏感函数(finalizeVotingpauseunpause)。
  • 事件日志记录:事件为承诺、投票和状态更改提供了 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。通过 hasVotedMAX_VOTESCOMMIT_WINDOW 检查,如果有效则成功。
  • 攻击者:尝试垃圾邮件发送 commitVoterevealVotevoteBatch,但由于 hasVotedMAX_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 清单和行业标准保持一致,请采用以下做法:

  1. 限制用户操作
  • 限制每个地址的操作一次:require(!hasVoted[msg.sender], "Already voted");
  1. 尽早验证输入
  • 在状态更新之前检查条件:require(candidate != address(0), "Invalid candidate");
  1. 使用 Gas 高效的事件
  • 使用事件记录操作:emit Voted(msg.sender, candidate);
  1. 实施断路器
  • 在攻击期间暂停操作:function pause() external onlyAdmin { paused = true; }
  1. 限制状态增长
  • 使用常量:uint256 public constant MAX_VOTES = 100;
  1. 利用时间窗口
  • 限制操作:require(block.timestamp <= votingStart + VOTING_WINDOW, "Voting closed");
  1. 避免在关键路径中进行外部调用
  • 尽量减少对外部合约的依赖:votes[candidate]++;
  1. 使用批量处理
  • 有效地处理多个操作:voteBatch 跳过无效输入。
  1. 监控 Mempool 活动
  • 使用 Forta 或自定义脚本来检测垃圾邮件模式。
  1. 全面测试
  • 使用 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) 场景以模拟恶意破坏的影响。

恶意破坏弹性的测试

为了确保你的合约抵抗恶意破坏:

  1. 单元测试(使用 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);
});
  1. 模糊测试:使用 Echidna 模拟随机投票垃圾邮件和无效输入。
  2. Mempool 模拟:使用 Foundry 用失败的交易填充 mempool 并测试合约性能。
  3. 主网分叉:使用 Hardhat 分叉以太坊主网以测试 gas 动态和 mempool 行为。
  4. 监控:使用 Forta 检测异常交易模式,例如重复 revert。

结论:构建防恶意破坏的智能合约

恶意破坏攻击的目的不是抢劫,而是破坏。通过阻塞 mempool 和浪费 gas,攻击者会降低信任和功能。但是通过 Solodit 清单的 SOL-AM-Griefing 最佳实践——提交-揭示方案、早期验证、批量处理和断路器——你可以构建能够抵御这些破坏企图的 dApp。

继续通过本系列的见解加强你的合约。在第 6 部分中,我们将探讨矿工操纵、MEV 风险以及如何使用 Chainlink VRF 等随机性工具来保护时间敏感的操作。

在那之前——彻底审计,防御性编码,并自信地构建无需信任的应用

  • 原文链接: medium.com/@ankitacode11...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
ankitacode11
ankitacode11
江湖只有他的大名,没有他的介绍。