智胜Mempool掠夺者:对抗以太坊智能合约中的抢跑交易和恶意干扰攻击

本文深入探讨了以太坊智能合约中抢跑交易和恶意干扰攻击的原理、风险及应对措施。文章通过分析实际案例和提供代码示例,展示了如何利用承诺-揭示方案、时间锁、访问控制和熔断机制等技术手段,来保护智能合约免受攻击,确保交易的公平性和合约的稳定性。

智胜交易池掠夺者:对抗以太坊智能合约中的抢先交易和恶意破坏攻击

智能合约安全:Solodit 清单系列 的第 4 部分

简介:以太坊交易池的狂野西部

想象一下你正在一个以太坊驱动的拍卖会上,准备抢购一个稀有的 NFT。 你提交了你的竞标,确信它会被接受。 但在它执行之前,攻击者监视公共交易池,提交一个更高 gas 费的交易,并超过了你的出价。 或者更糟的是,有人向你的投票 dApp 发送垃圾交易,浪费 gas 并冻结功能。 欢迎来到 抢先交易 (front-running)恶意破坏 (griefing) 攻击领域,以太坊的透明性变成了一把双刃剑。

本期 智能合约安全:Solodit 清单系列 的第四部分侧重于:

  • SOL-AM-FrontRunning: 交易顺序操纵
  • SOL-AM-Griefing: 恶意交易失败

这些攻击威胁金融安全、合约完整性和用户信任。 随着以太坊的区块 gas 限制约为 3000 万(截至 2025 年 6 月),即使只有少数不良行为者也可以阻塞 dApp 或操纵订单流。 本指南探讨了这些攻击是如何运作的,以及如何使用代码示例、工作流程和最佳实践来防御它们。

为什么抢先交易和恶意破坏攻击很重要

以太坊的交易池是一个公共队列,交易等待被挖矿。 矿工优先考虑更高的 gas 费用,这为攻击者创造了机会:

  • 抢先交易 (Front-run): 通过支付更多的 gas 来跳过队列。
  • 恶意破坏 (Grief): 通过故意使交易失败来浪费 gas。

主要风险:

  • 经济损失: 抢先交易迫使用户支付过高的价格,或在市场或拍卖中错失机会。
  • 拒绝服务 (DoS): 恶意破坏阻塞合约功能,阻止合法用户。
  • 声誉损害: 饱受这些攻击困扰的 dApp,如 2019 年的 Bancor 抢先交易事件,会失去用户信任。

Solodit 清单的 SOL-AM-FrontRunningSOL-AM-Griefing 强调交易隐私、gas 高效设计和强大的验证,以减轻这些风险。 让我们探讨每种攻击以及如何应对它们。

第 1 节:抢先交易 —— 窃取交易池中的领先地位

抢先交易如何运作

抢先交易利用以太坊透明的交易池,其中待处理的交易对所有人(包括矿工和机器人)可见。 攻击者监视交易池中价值高的交易(例如,DEX 中低价购买),并提交 gas 费更高的竞争交易以首先执行。 常见的策略包括:

  • 价格操纵: 在用户交易之前更新市场价格。
  • 订单劫持: 抢夺购买订单或拍卖竞标等机会。
  • 交易替换: 超过用户的交易以取代他们的位置。

尽管以太坊在 2021 年的伦敦升级 (EIP-1559) 使 gas 定价更可预测,但由于矿工有能力优先处理高优先级的交易,抢先交易仍然存在。

真实案例:2019 年 Bancor 抢先交易

2019 年,Bancor 去中心化交易所成为抢先交易机器人的目标,这些机器人监控交易池中的大额交易。 通过提交更高 gas 费的交易,机器人在用户之前执行交易,从价格波动中获利。 这突出了对提交-揭示方案等交易隐私机制的需求。

易受攻击的代码:一个容易被利用的交易所

以下是一个去中心化交易所合约,由于其依赖公共交易数据,因此容易受到抢先交易的攻击。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract VulnerableExchange {
    uint256 public price;

    constructor(uint256 initialPrice) {
        price = initialPrice;
    }

    function buy(uint256 amount) external payable {
        require(msg.value >= amount * price, "Insufficient payment");
        // Process purchase: transfer tokens
        // 处理购买:转移代币
        emit Purchase(msg.sender, amount, price);
    }

    function updatePrice(uint256 newPrice) external {
        price = newPrice;
        emit PriceUpdated(newPrice);
    }

    event Purchase(address indexed buyer, uint256 amount, uint256 price);
    event PriceUpdated(uint256 newPrice);
}

攻击场景:抢先交易的行动

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

  1. 用户操作: 用户以 price = 1 ETH 提交 buy(100)(总计:100 ETH),并支付标准 gas 费。
  2. 攻击者的举动: 机器人发现交易池中的交易,并提交两个 gas 费更高的交易:
  • 调用 updatePrice(2 ETH) 将价格提高一倍。
  • 调用 buy(100) 以原始价格 (1 ETH) 购买。

3. 结果:

  • 矿工优先处理攻击者的交易。
  • updatePrice 首先执行,设置 price = 2 ETH
  • 攻击者的 buy 以 1 ETH 执行,抢夺了这笔交易。
  • 用户的 buy 要么失败(付款不足),要么以 2 ETH (200 ETH) 执行。

4. 影响: 攻击者获利,而用户支付过高的费用或失去机会。

易受攻击的工作流程:交易池陷阱

  • 用户: 以 price = 1 ETH 提交 buy(100),期望支付 100 ETH。
  • 攻击者: 监控交易池,提交 updatePrice(2 ETH)buy(100),并支付更高的 gas 费。
  • 矿工: 优先处理攻击者的交易,首先更新价格并执行他们的购买。
  • 结果: 用户的交易失败或以更差的价格执行。

补救:提交-揭示和 Gas 保护

为了应对抢先交易,我们使用 提交-揭示方案 来隐藏交易详细信息,并添加时间锁和熔断器等保护措施。 以下是交易所的安全版本。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract FixedExchange {
    uint256 public price;
    address public admin;
    bool public paused;
    mapping(address => bytes32) public commitments;
    mapping(address => uint256) public commitTimestamps;
    uint256 public constant COMMIT_WINDOW = 1 hours;

    modifier onlyAdmin() {
        require(msg.sender == admin, "Not admin");
        _;
    }

    modifier whenNotPaused() {
        require(!paused, "Contract paused");
        _;
    }

    constructor(uint256 initialPrice) {
        admin = msg.sender;
        price = initialPrice;
    }

    function commitBuy(bytes32 commitment) external payable whenNotPaused {
        require(commitments[msg.sender] == bytes32(0), "Commitment exists");
        require(msg.value > 0, "No ETH sent");
        commitments[msg.sender] = commitment;
        commitTimestamps[msg.sender] = block.timestamp;
        emit BuyCommitted(msg.sender, commitment);
    }

    function revealBuy(uint256 amount, uint256 nonce) external whenNotPaused {
        require(commitments[msg.sender] != bytes32(0), "No commitment");
        require(block.timestamp <= commitTimestamps[msg.sender] + COMMIT_WINDOW, "Commit window expired");
        require(keccak256(abi.encodePacked(msg.sender, amount, nonce)) == commitments[msg.sender], "Invalid commitment");
        require(msg.value >= amount * price, "Insufficient payment");
        delete commitments[msg.sender];
        delete commitTimestamps[msg.sender];
        // Process purchase: transfer tokens
        // 处理购买:转移代币
        emit Purchase(msg.sender, amount, price);
    }

    function updatePrice(uint256 newPrice) external onlyAdmin whenNotPaused {
        price = newPrice;
        emit PriceUpdated(newPrice);
    }

    function pause() external onlyAdmin {
        paused = true;
        emit Paused();
    }

    function unpause() external onlyAdmin {
        paused = false;
        emit Unpaused();
    }

    event BuyCommitted(address indexed buyer, bytes32 commitment);
    event Purchase(address indexed buyer, uint256 amount, uint256 price);
    event PriceUpdated(uint256 newPrice);
    event Paused();
    event Unpaused();
}

它是如何工作的

  • 提交-揭示方案: 用户提交一个带有购买详细信息(地址、金额、nonce)的哈希承诺( commitBuy),将其从交易池中隐藏。 他们稍后揭示详细信息( revealBuy),经验证后执行。
  • 时间锁: COMMIT_WINDOW(1 小时)确保及时揭示,防止无限期的承诺。
  • 访问控制: 只有管理员可以更新价格,从而降低操纵风险。
  • 熔断器: paused 标志和 whenNotPaused 修饰符在攻击期间暂停操作。
  • 事件日志: 事件确保承诺、购买和状态更改的透明度。

安全的工作流程:阻止抢先交易者

用户:

  1. 使用哈希承诺(例如,keccak256(abi.encodePacked(msg.sender, amount, nonce)))调用 commitBuy,并发送 ETH。
  2. 等待交易被挖矿,隐藏详细信息。
  3. 使用金额和 nonce 调用 revealBuy,如果有效,则执行购买。

攻击者:

  1. 看到承诺,但无法解码金额或 nonce。
  2. 无法抢先交易,因为价格更新不会影响已承诺的交易。
  3. 垃圾承诺的成本很高,并且受到 commitTimestamps 的限制。
  • 结果: 用户的购买以预期的价格执行,并且攻击者的干扰被最小化。

安全优势

  • 交易隐私: 提交-揭示将购买细节从交易池中隐藏。
  • 受控更新: 仅管理员的价格更新减少了操纵。
  • DoS 保护: 时间锁和熔断器限制了垃圾邮件和攻击的影响。
  • Gas 效率: 有效的承诺验证隔离了故障。

第 2 节:恶意破坏攻击 —— 浪费 Gas 来破坏逻辑

恶意破坏攻击如何运作

恶意破坏攻击涉及提交旨在失败的交易,浪费 gas 并破坏合约逻辑。 攻击者针对具有外部调用或用户输入的合约,利用 require 条件来触发回滚。 常见的策略包括:

  • 重复失败: 调用在特定条件下回滚的函数(例如,投票限制)。
  • 外部调用操作: 部署可预测性失败的合约,从而使调用者停滞。
  • 交易池阻塞: 用失败的交易淹没交易池,以延迟合法的交易。

恶意破坏不会窃取资金,但会通过增加成本和阻止功能来导致 DoS。

真实案例:2020 年 Uniswap 恶意破坏

在 2020 年,Uniswap 面临恶意破坏攻击,机器人提交失败的交易来阻塞交易池,从而延迟了交易。 后来版本中的 gas 优化缓解了这一点,突出了对强大输入验证的需求。

易受攻击的代码:一场投票合约围攻

以下是一个投票合约,由于未经检查的用户操作,因此很容易受到恶意破坏的攻击。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract VulnerableVoting {
    mapping(address => uint256) public votes;

    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,从而增加 votes[candidate]
  2. 攻击者的举动: 攻击者监视接近限制的候选人(例如,votes[candidate] = 99)。 他们以低 gas 费反复调用该候选人的 vote,因为他们知道它会回滚。
  3. 结果:
  • 每次 vote 调用都会消耗 gas 但会回滚,从而浪费了攻击者的 gas(他们可以负担得起)。
  • 交易池中充满了失败的交易,从而延迟了合法的投票。
  • 用户会产生更高的 gas 成本来竞争,或者他们的交易会被延迟。

4. 影响: 合约减慢,用户面临更高的成本或失败的投票。

易受攻击的工作流程:浪费 Gas 的陷阱

  • 用户: 为候选人调用 vote,期望成功。
  • 攻击者: 在达到限制的候选人处发送垃圾邮件 vote,从而触发回滚。
  • 矿工: 处理失败的交易,阻塞交易池。
  • 结果: 合法的投票被延迟或花费更多的 gas。

补救:Gas 高效的投票设计

为了应对恶意破坏,我们限制用户操作,尽早验证输入,并使用熔断器。 以下是一个安全的投票合约。

// SPDX-License-License: MIT
pragma solidity ^0.8.10;

contract FixedVoting {
    mapping(address => uint256) public votes;
    mapping(address => bool) public hasVoted;
    address public admin;
    bool public paused;
    uint256 public constant MAX_VOTES = 100;

    modifier onlyAdmin() {
        require(msg.sender == admin, "Not admin");
        _;
    }

    modifier whenNotPaused() {
        require(!paused, "Contract paused");
        _;
    }

    constructor() {
        admin = msg.sender;
    }

    function vote(address candidate) external whenNotPaused {
        require(!hasVoted[msg.sender], "Already voted");
        require(votes[candidate] < MAX_VOTES, "Candidate reached vote limit");
        hasVoted[msg.sender] = true;
        votes[candidate]++;
        emit Voted(msg.sender, candidate);
    }

    function pause() external onlyAdmin {
        paused = true;
        emit Paused();
    }

    function unpause() external onlyAdmin {
        paused = false;
        emit Unpaused();
    }

    event Voted(address indexed voter, address indexed candidate);
    event Paused();
    event Unpaused();
}

它是如何工作的

  • 受限投票: hasVoted 映射确保每个地址一个投票,从而减少垃圾邮件。
  • 尽早验证: require(!hasVoted[msg.sender]) 尽早失败,从而最大程度地减少 gas 浪费。
  • 投票限制: MAX_VOTES 对每个候选人强制执行上限。
  • 熔断器: paused 标志和 whenNotPaused 修饰符在攻击期间暂停投票。
  • 访问控制: 只有管理员可以暂停/取消暂停。
  • 事件日志: Voted 事件有效地记录操作。

安全的工作流程:阻止恶意破坏者

用户:

  1. 调用 vote,传递 hasVotedMAX_VOTES 检查。
  2. 如果有效则成功,如果无效则尽早失败,从而最大程度地减少 gas 浪费。

攻击者:

  1. 尝试在投票后或在达到限制的候选人处发送垃圾邮件 vote
  2. 由于 hasVotedMAX_VOTES 立即回滚,从而浪费最少的 gas。
  3. 无法阻塞交易池,因为失败的成本很低。

结果: 合约保持高效,合法的投票可以顺利进行。

安全优势

  • 减少垃圾邮件: 每个地址一个投票限制了恶意破坏。
  • Gas 效率: 尽早验证可最大程度地减少 gas 浪费。
  • DoS 保护: 熔断器在攻击期间暂停操作。
  • 强大的设计: 访问控制和高效的事件确保可靠性。

用户和攻击者工作流程比较

易受攻击的交易所工作流程

  • 合法用户: 以 price = 1 ETH 提交 buy(100),期望支付 100 ETH。
  • 攻击者: 提交 updatePrice(2 ETH)buy(100),并支付更高的 gas 费。
  • 结果: 攻击者的交易首先执行,抢夺了这笔交易。 用户的交易失败或花费 200 ETH。 影响:用户失去价值,而攻击者获利。

安全的交易所工作流程

  • 合法用户: 提交带有哈希承诺的 commitBuy,然后提交 revealBuy。 承诺隐藏详细信息。
  • 攻击者: 无法解码承诺或有效地进行抢先交易。
  • 结果: 用户的购买以预期的价格执行。
  • 影响: 合约保持公平。

易受攻击的投票工作流程

  • 合法用户: 调用 vote,期望成功。
  • 攻击者: 在达到限制的候选人处发送垃圾邮件 vote,阻塞交易池。
  • 结果: 合法的投票被延迟或花费更多的 gas。
  • 影响: 投票过程减慢,使用户感到沮丧。

安全的投票工作流程

  • 合法用户: 调用 vote,通过验证检查。
  • 攻击者: 尽早在垃圾邮件尝试时回滚,从而浪费最少的 gas。
  • 结果: 合法的投票可以有效地进行。
  • 影响: 合约保持功能。

与前面部分的集成:整体防御

抢先交易和恶意破坏攻击放大了早期部分的漏洞:

  • 捐赠攻击 (第 3 部分): 攻击者可以将 ETH 强行置于合约中(通过 selfdestruct),并通过抢先竞标价格更新来操纵余额。
  • 状态膨胀 (第 2 部分): 恶意破坏可能会通过垃圾邮件发送膨胀映射的失败交易来加剧状态膨胀。
  • 无界循环 (第 1 部分): 抢先交易可能会针对 gas 密集型循环,从而延迟合法的交易。

为了解决这个问题,结合:

  • 拉取优于推送 (第 1 部分): 使用提款模式来隔离故障(例如,第 3 部分的 withdraw)。
  • 状态上限 (第 2 部分): 使用 MAX_VOTES 限制投票垃圾邮件,类似于市场中的 MAX_ITEMS
  • 显式 ETH 跟踪 (第 3 部分): 拒绝意外的 ETH,以防止操纵余额。
  • 提交-揭示和验证 (第 4 部分): 隐藏交易详细信息并尽早验证输入。

例如,FixedExchange 可以添加 MAX_BUYS_PER_BLOCK 上限来限制垃圾邮件,并且 FixedVoting 可以拒绝意外的 ETH 以避免捐赠攻击。

防止抢先交易和恶意破坏的最佳实践

使用以下实践,使其与 Solodit 清单和行业标准保持一致:

1. 使用提交-揭示方案:

  • 使用哈希承诺隐藏交易详细信息:
function commitBuy(bytes32 commitment) external payable {
    commitments[msg.sender] = commitment;
    commitTimestamps[msg.sender] = block.timestamp;
}

2. 实施时间锁:

  • 强制执行延迟以减少交易池暴露:
require(block.timestamp <= commitTimestamps[msg.sender] + COMMIT_WINDOW, "Commit window expired");

3. 限制用户操作:

  • 将投票等操作限制为每个地址一次:
require(!hasVoted[msg.sender], "Already voted");require(!hasVoted[msg.sender], "Already voted");

4. 尽早验证输入:

  • 在状态更新之前检查条件:
require(!hasVoted[msg.sender], "Already voted");require(votes[candidate] < MAX_VOTES, "Candidate reached vote limit");

5. 使用熔断器:

  • 在攻击期间暂停操作
function pause() external onlyAdmin { paused = true; }

6. 利用 EIP-1559:

  • 使用优先费用进行可预测的 gas 定价。

7. 使用事件记录:

  • 在没有大量状态更新的情况下确保透明度:
event Voted(address indexed voter, address indexed candidate);

8. 监控交易池:

  • 使用 Forta 等工具来检测可疑模式。

用于预防的先进工具(2025 年更新)

截至 2025 年 7 月,这些工具增强了预防:

  • Slither (0.10.x): 检测交易依赖逻辑中的抢先交易风险(slither --detect front-running)。
  • MythX: 分析交易池相关的漏洞和 gas 密集型路径。
  • Foundry: 通过差异模糊测试和主网分叉模拟交易池攻击。
  • Forta: 实时监控交易池活动,以检测抢先交易或恶意破坏模式。
  • OpenZeppelin Defender: 自动响应可疑交易。
  • MEV-Geth: 测试矿工可提取价值 (MEV) 场景。

测试弹性

确保你的合约可以通过以下方式抵御攻击:

1. 单元测试:

it("prevents front-running with commit-reveal", async () => {
    const commitment = ethers.utils.keccak256(
        ethers.utils.defaultAbiCoder.encode(
            ["address", "uint256", "uint256"],
            [user.address, 100, 12345]
        )
    );
    await exchange.commitBuy(commitment, { value: ethers.utils.parseEther("100") });
    await expect(exchange.updatePrice(2)).to.not.affect(
        exchange.revealBuy(100, 12345, { value: ethers.utils.parseEther("100") })
    );
});

it("rejects griefing votes", async () => {
    await voting.vote(candidate.address);
    await expect(voting.vote(candidate.address)).to.be.revertedWith("Already voted");
});

2. 模糊测试: 使用 Echidna 模拟随机交易池订单和失败的交易。

3. 主网分叉: 使用 Hardhat 分叉以太坊主网以测试 gas 动态。

4. 交易池监控: 使用 Forta 或自定义脚本来检测可疑模式。

真实案例研究:2020 年 Uniswap 和 Bancor

2020 年,Uniswap 面临恶意破坏攻击,机器人用失败的交易阻塞交易池,从而延迟了交易。 Bancor 受到抢先交易机器人的攻击,这些机器人从价格波动中获利。 这些事件导致了提交-揭示方案和后来版本中的 gas 优化,突出了积极的防御。

结论:驯服交易池掠夺者

抢先交易和恶意破坏攻击通过利用交易订单和浪费 gas 对以太坊 dApp 构成严重风险。 通过使用提交-揭示方案、输入验证和适当的访问控制(如 Solodit 清单中所述),开发人员可以大大降低这些威胁。 借助正确的工具和安全的设计模式,可以构建公平且有弹性的去中心化系统。

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

0 条评论

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