本文深入探讨了智能合约中高级拒绝服务(DoS)攻击,重点介绍了状态膨胀和Gas消耗攻击(Gas Griefing)的原理、攻击方式以及实际案例,并通过代码示例展示了如何通过限制状态增长、批量处理数据、实施熔断机制等手段来构建具有DoS防御能力的智能合约。
“你的合约没有被黑客攻击——但没有人可以使用它了。”
想象一下:你启动了一个 NFT 市场。它很热闹。用户正在上架商品、交易 NFT,并且你的 dApp 正在获得关注。然后有一天,一切都慢了下来。交易失败,gas费飙升,用户开始抱怨。发生了什么事?
你没有被黑客攻击。但你被 griefed 了。
欢迎来到智能合约上的 高级拒绝服务 (DoS) 攻击:隐秘的那种。这些包括 状态膨胀 (state bloating) 和 gas 消耗 (gas griefing) ——这些攻击不会利用你逻辑中的错误,而是通过使其使用成本过高或不可能使用来扼杀你的合约。
在我们 智能合约安全:Solodit 清单系列 的第二篇文章中,我们将深入研究 SOL-AM-DoS-2:状态增长和 gas 利用如何悄悄地杀死你的 dApp。我们将探索真实世界的例子,剖析易受攻击的代码,并演练最佳实践来保护你的合约。
智能合约存在于以太坊虚拟机 (EVM) 中,其中每个操作都会消耗 gas。攻击者可以:
这不是理论。以前已经有人做过了。2018 年的 Fomo3D 攻击著名地展示了膨胀的状态如何停滞 dApp 的奖金支付。
高级 DoS 攻击针对两个关键漏洞:
智能合约运行在以太坊虚拟机(EVM)上,其中每个操作——从存储读取、附加到数组或调用外部合约——都会消耗 gas。高级 DoS 攻击利用这一点来:
Solodit 清单的 SOL-AM-DoS-2 强调主动状态管理和 gas 优化,以防止这些攻击。让我们探讨它们是如何工作的以及如何应对它们。
在病毒式以太坊游戏 Fomo3D 中,攻击者通过小额交易对合约进行垃圾邮件攻击,使其状态膨胀。这使得像奖金分配这样的关键功能过于耗 gas,从而延迟支付并使用户感到沮丧。该事件强调了在具有面向公众功能的合约中对强大的状态管理的需求。
考虑一个简单的交易市场合约,用户可以在其中添加和处理商品(例如,NFT 或库存)。此合约容易受到状态膨胀和 gas 消耗的攻击:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;
contract VulnerableMarketplace {
mapping(address => uint256[]) public userItems;
// Add an item to the user's inventory
// 向用户的库存中添加物品
function addItem(uint256 itemId) external {
userItems[msg.sender].push(itemId);
}
// Process all items for a user (e.g., listing or updating)
// 处理用户的全部物品(例如,上架或更新)
function processItems(address user) external {
uint256[] storage items = userItems[user];
for (uint256 i = 0; i < items.length; i++) {
// Expensive operation: e.g., emit event or update state
// 昂贵的操作:例如,发出事件或更新状态
emit ItemProcessed(user, items[i]);
}
}
event ItemProcessed(address indexed user, uint256 itemId);
}
此合约容易受到两次毁灭性攻击:
addItem()
数千次(例如,通过自动化脚本),从而使 userItems[attacker]
膨胀 5,000 多个条目。processItems(attacker)
时,循环的 gas 成本超过 3000 万的 gas 限制,从而导致交易失败。function processItems(address user) external {
uint256[] storage items = userItems[user];
for (uint256 i = 0; i < items.length; i++) {
(bool success, ) = user.call{value: 0, gas: 50000}("");
require(success, "Call failed");
}
}
2. 恶意接收者:
contract MaliciousReceiver {
receive() external payable {
uint256 x = 0;
while (gasleft() > 1000) { x++; } // Burn gas
// 燃烧gas
revert("No calls allowed"); // Or revert
// 或恢复
}
}
这会导致 processItems
失败,从而阻止所有用户的功能。
userItems
映射允许无限制的数组增长,并且 processItems
在单个交易中迭代整个数组。攻击者可以利用这一点来使合约无法使用,从而阻止项目处理并将用户锁定在关键功能之外。
为了解决这些漏洞,Solodit 清单建议限制状态增长,批量处理数据,并最大程度地减少外部调用。这是交易市场合约的安全版本:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;
contract FixedMarketplace {
mapping(address => uint256[]) public userItems;
uint256 public constant MAX_ITEMS = 100; // Cap on items per user
// 每个用户的物品上限
bool public paused; // Circuit breaker
// 断路器
address public admin; // For pause control
// 用于暂停控制
modifier onlyAdmin() {
require(msg.sender == admin, "Not admin");
_;
}
modifier whenNotPaused() {
require(!paused, "Contract paused");
_;
}
constructor() {
admin = msg.sender;
}
// Add an item with a cap
// 添加带有上限的物品
function addItem(uint256 itemId) external whenNotPaused {
require(userItems[msg.sender].length < MAX_ITEMS, "Item limit reached");
userItems[msg.sender].push(itemId);
emit ItemAdded(msg.sender, itemId);
}
// Process items in batches
// 批量处理物品
function processItems(address user, uint256 start, uint256 limit) external whenNotPaused {
uint256[] storage items = userItems[user];
require(start < items.length, "Invalid start index");
require(start < items.length, "无效的起始索引");
uint256 end = start + limit < items.length ? start + limit : items.length;
for (uint256 i = start; i < end; i++) {
// Process item: e.g., emit event or update state
// 处理物品:例如,发出事件或更新状态
emit ItemProcessed(user, items[i]);
}
emit BatchProcessed(user, start, end);
}
// Clear items to reduce state
// 清除物品以减少状态
function clearItems(address user, uint256 start, uint256 limit) external whenNotPaused onlyAdmin {
uint256[] storage items = userItems[user];
require(start < items.length, "Invalid start index");
require(start < items.length, "无效的起始索引");
uint256 end = start + limit < items.length ? start + limit : items.length;
for (uint256 i = start; i < end; i++) {
items[i] = 0; // Mark as processed
// 标记为已处理
}
if (end == items.length) {
delete userItems[user]; // Clear entire array
// 清除整个数组
}
emit ItemsCleared(user);
}
// 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 ItemAdded(address indexed user, uint256 itemId);
event ItemProcessed(address indexed user, uint256 itemId);
event BatchProcessed(address indexed user, uint256 start, uint256 end);
event ItemsCleared(address indexed user);
event Paused();
event Unpaused();
}
MAX_ITEMS
常量(设置为 100)限制了每个用户的物品数量,从而防止了无限制的数组。processItems
函数处理固定数量的物品(例如,每次调用 50 个),从而确保 gas 成本保持在区块限制内。clearItems
函数允许管理员批量清除已处理的物品,从而降低存储成本并防止膨胀。paused
标志和 whenNotPaused
修饰符可以在疑似攻击期间停止操作,而 onlyAdmin
将控制权限制为授权用户。ItemAdded
、ItemProcessed
、BatchProcessed
、ItemsCleared
、Paused
、Unpaused
)提供 gas 高效的日志记录,其中 BatchProcessed
将多个物品处理聚合到一个事件中。安全合约遵循强大的工作流程:
addItem
,该函数在附加到 userItems
之前检查 MAX_ITEMS
。如果达到限制,则交易将恢复,从而防止状态膨胀。start
索引和 limit
调用 processItems
(例如,处理物品 0-50,然后处理物品 50-100)。即使对于大型数组,这也能使 gas 成本保持可预测并在区块限制内。clearItems
以将已处理的物品标记为零或删除整个数组,从而降低存储成本。这仅限于管理员,以防止滥用。addItem
和 processItems
,直到问题得到解决。MAX_ITEMS
上限可防止失控的存储增长。攻击者部署在外部调用期间消耗 gas 或恢复的合约,从而扰乱调用者。
缓解措施:
function processItems(address user, uint256 start, uint256 limit) external whenNotPaused {
uint256[] storage items = userItems[user];
require(start < items.length, "Invalid start index");
require(start < items.length, "无效的起始索引");
uint256 end = start + limit < items.length ? start + limit : items.length;
for (uint256 i = start; i < end; i++) {
// Process without external calls
// 在没有外部调用的情况下进行处理
emit ItemProcessed(user, items[i]);
}
emit BatchProcessed(user, start, end);
}
攻击者用垃圾条目淹没映射以使 gas 成本飙升。
缓解措施:
function clearItemsBatch(address user, uint256 start, uint256 limit) external whenNotPaused onlyAdmin {
uint256[] storage items = userItems[user];
require(start < items.length, "Invalid start index");
require(start < items.length, "无效的起始索引");
uint256 end = start + limit < items.length ? start + limit : items.length;
for (uint256 i = start; i < end; i++) {
items[i] = 0;
}
if (end == items.length) {
delete userItems[user];
}
emit ItemsCleared(user);
}Mitigation:
//缓解措施:
在循环中发射过多的事件会增加 gas 成本。
缓解措施:
event BatchProcessed(address indexed user, uint256 start, uint256 end);
function processItems(address user, uint256 start, uint256 limit) external whenNotPaused {
uint256[] storage items = userItems[user];
require(start < items.length, "Invalid start index");
require(start < items.length, "无效的起始索引");
uint256 end = start + limit < items.length ? start + limit : items.length;
for (uint256 i = start; i < end; i++) {
// Process item
// 处理项目
}
emit BatchProcessed(user, start, end);
}
为了与 Solodit 清单和行业标准保持一致,请采用以下实践:
MAX_ITEMS
等常量来限制数组和映射,从而防止无限制的增长。BatchProcessed
)而不是每个物品的事件,以节省 gas。onlyAdmin
)。calldata
以降低 gas 成本。forge test --gas-report
)等工具来识别效率低下之处。7. 状态监控:
8. 全面的测试:
截至 2025 年 7 月,这些工具增强了 DoS 防护:
要确保你的合约能够抵抗状态膨胀和 gas 消耗:
it("handles large item lists safely", async () => {
// 安全地处理大型物品列表
const user = accounts[0];
for (let i = 0; i < 100; i++) {
await marketplace.addItem(i, { from: user });
}
await expect(marketplace.processItems(user, 0, 50)).to.not.be.reverted;
await expect(marketplace.clearItems(user, 0, 50, { from: admin })).to.not.be.reverted;
});
forge test --gas-report
识别高 gas 函数并对其进行优化。在 2022 年,一个 DeFi 质押池允许用户在动态数组中对奖励进行排队。攻击者使用多个钱包来垃圾邮件发送微小的奖励条目,从而使状态膨胀。claimRewards
函数变得 gas 密集型,从而将合法用户锁定在资金之外。该项目通过实施批量处理和状态清除来缓解这种情况,但该事件导致了大量的用户流失,并强调了在面向公众的合约中主动状态管理的关键需求。
状态膨胀和 gas 消耗利用以太坊的 gas 机制来破坏智能合约。通过实施限制状态增长、批量处理和强大的状态管理——正如 Solodit 的 SOL-AM-DoS-2 所建议的那样——开发者可以构建 DoS 抵抗合约。Slither、MythX 和 Foundry 等工具,结合断路器和高效事件等最佳实践,可确保在压力下的功能。
这篇文章是 智能合约安全:Solodit 清单系列 的一部分。接下来,我们将解决重入攻击,剖析 DAO 黑客攻击并实施安全模式,如 OpenZeppelin 的 ReentrancyGuard。无论你是构建交易市场、DeFi 协议还是 NFT 平台,掌握这些防御对于安全、可扩展和无需信任的系统至关重要。
请继续关注,并保持安全地构建!
- 原文链接: medium.com/@ankitacode11...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!