本文深入探讨了以太坊智能合约中抢跑交易和恶意干扰攻击的原理、风险及应对措施。文章通过分析实际案例和提供代码示例,展示了如何利用承诺-揭示方案、时间锁、访问控制和熔断机制等技术手段,来保护智能合约免受攻击,确保交易的公平性和合约的稳定性。
智能合约安全:Solodit 清单系列 的第 4 部分
想象一下你正在一个以太坊驱动的拍卖会上,准备抢购一个稀有的 NFT。 你提交了你的竞标,确信它会被接受。 但在它执行之前,攻击者监视公共交易池,提交一个更高 gas 费的交易,并超过了你的出价。 或者更糟的是,有人向你的投票 dApp 发送垃圾交易,浪费 gas 并冻结功能。 欢迎来到 抢先交易 (front-running) 和 恶意破坏 (griefing) 攻击领域,以太坊的透明性变成了一把双刃剑。
本期 智能合约安全:Solodit 清单系列 的第四部分侧重于:
这些攻击威胁金融安全、合约完整性和用户信任。 随着以太坊的区块 gas 限制约为 3000 万(截至 2025 年 6 月),即使只有少数不良行为者也可以阻塞 dApp 或操纵订单流。 本指南探讨了这些攻击是如何运作的,以及如何使用代码示例、工作流程和最佳实践来防御它们。
以太坊的交易池是一个公共队列,交易等待被挖矿。 矿工优先考虑更高的 gas 费用,这为攻击者创造了机会:
Solodit 清单的 SOL-AM-FrontRunning 和 SOL-AM-Griefing 强调交易隐私、gas 高效设计和强大的验证,以减轻这些风险。 让我们探讨每种攻击以及如何应对它们。
抢先交易利用以太坊透明的交易池,其中待处理的交易对所有人(包括矿工和机器人)可见。 攻击者监视交易池中价值高的交易(例如,DEX 中低价购买),并提交 gas 费更高的竞争交易以首先执行。 常见的策略包括:
尽管以太坊在 2021 年的伦敦升级 (EIP-1559) 使 gas 定价更可预测,但由于矿工有能力优先处理高优先级的交易,抢先交易仍然存在。
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);
}
以下是攻击者如何利用此合约:
price = 1 ETH
提交 buy(100)
(总计:100 ETH),并支付标准 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 费。为了应对抢先交易,我们使用 提交-揭示方案 来隐藏交易详细信息,并添加时间锁和熔断器等保护措施。 以下是交易所的安全版本。
// 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();
}
commitBuy
),将其从交易池中隐藏。 他们稍后揭示详细信息( revealBuy
),经验证后执行。COMMIT_WINDOW
(1 小时)确保及时揭示,防止无限期的承诺。paused
标志和 whenNotPaused
修饰符在攻击期间暂停操作。用户:
keccak256(abi.encodePacked(msg.sender, amount, nonce))
)调用 commitBuy
,并发送 ETH。revealBuy
,如果有效,则执行购买。攻击者:
commitTimestamps
的限制。恶意破坏攻击涉及提交旨在失败的交易,浪费 gas 并破坏合约逻辑。 攻击者针对具有外部调用或用户输入的合约,利用 require
条件来触发回滚。 常见的策略包括:
恶意破坏不会窃取资金,但会通过增加成本和阻止功能来导致 DoS。
在 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);
}
以下是攻击者如何利用此合约:
vote
,从而增加 votes[candidate]
。votes[candidate] = 99
)。 他们以低 gas 费反复调用该候选人的 vote
,因为他们知道它会回滚。vote
调用都会消耗 gas 但会回滚,从而浪费了攻击者的 gas(他们可以负担得起)。4. 影响: 合约减慢,用户面临更高的成本或失败的投票。
vote
,期望成功。vote
,从而触发回滚。为了应对恶意破坏,我们限制用户操作,尽早验证输入,并使用熔断器。 以下是一个安全的投票合约。
// 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
事件有效地记录操作。用户:
vote
,传递 hasVoted
和 MAX_VOTES
检查。攻击者:
vote
。hasVoted
或 MAX_VOTES
立即回滚,从而浪费最少的 gas。结果: 合约保持高效,合法的投票可以顺利进行。
price = 1 ETH
提交 buy(100)
,期望支付 100 ETH。updatePrice(2 ETH)
和 buy(100)
,并支付更高的 gas 费。commitBuy
,然后提交 revealBuy
。 承诺隐藏详细信息。vote
,期望成功。vote
,阻塞交易池。vote
,通过验证检查。抢先交易和恶意破坏攻击放大了早期部分的漏洞:
selfdestruct
),并通过抢先竞标价格更新来操纵余额。为了解决这个问题,结合:
withdraw
)。MAX_VOTES
限制投票垃圾邮件,类似于市场中的 MAX_ITEMS
。例如,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:
7. 使用事件记录:
event Voted(address indexed voter, address indexed candidate);
8. 监控交易池:
截至 2025 年 7 月,这些工具增强了预防:
slither --detect front-running
)。确保你的合约可以通过以下方式抵御攻击:
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 受到抢先交易机器人的攻击,这些机器人从价格波动中获利。 这些事件导致了提交-揭示方案和后来版本中的 gas 优化,突出了积极的防御。
抢先交易和恶意破坏攻击通过利用交易订单和浪费 gas 对以太坊 dApp 构成严重风险。 通过使用提交-揭示方案、输入验证和适当的访问控制(如 Solodit 清单中所述),开发人员可以大大降低这些威胁。 借助正确的工具和安全的设计模式,可以构建公平且有弹性的去中心化系统。
- 原文链接: medium.com/@ankitacode11...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!