第二部分:如何保护你的智能合约免受状态膨胀和Gas消耗攻击
“你的合约没有被黑客攻击——但没有人可以使用它了。”
简介:高级 DoS 攻击的隐藏风险
想象一下:你启动了一个 NFT 市场。它很热闹。用户正在上架商品、交易 NFT,并且你的 dApp 正在获得关注。然后有一天,一切都慢了下来。交易失败,gas费飙升,用户开始抱怨。发生了什么事?
你没有被黑客攻击。但你被 griefed 了。
欢迎来到智能合约上的 高级拒绝服务 (DoS) 攻击:隐秘的那种。这些包括 状态膨胀 (state bloating) 和 gas 消耗 (gas griefing) ——这些攻击不会利用你逻辑中的错误,而是通过使其使用成本过高或不可能使用来扼杀你的合约。
在我们 智能合约安全:Solodit 清单系列 的第二篇文章中,我们将深入研究 SOL-AM-DoS-2:状态增长和 gas 利用如何悄悄地杀死你的 dApp。我们将探索真实世界的例子,剖析易受攻击的代码,并演练最佳实践来保护你的合约。
为什么状态膨胀和 Gas 消耗很重要
智能合约存在于以太坊虚拟机 (EVM) 中,其中每个操作都会消耗 gas。攻击者可以:
- 使 gas 使用量猛增,使正常的交易变得无法承受。
- 破坏核心功能,例如商品列表或提款。
- 损害信任,因为用户因缓慢或破碎的体验而离开。
这不是理论。以前已经有人做过了。2018 年的 Fomo3D 攻击著名地展示了膨胀的状态如何停滞 dApp 的奖金支付。
这些攻击如何运作
高级 DoS 攻击针对两个关键漏洞:
- 状态膨胀: 攻击者用过多的条目淹没合约存储(例如,数组或映射),从而使处理此数据的操作的 gas 成本飙升。例如,用成千上万件商品垃圾邮件攻击市场可能会使函数超过区块 gas 限制,从而使其无法使用。
- Gas 消耗: 攻击者部署恶意合约,这些合约消耗过多的 gas 或在外部调用期间恢复,从而扰乱调用合约的逻辑并阻止关键函数。
为什么高级 DoS 攻击很重要
智能合约运行在以太坊虚拟机(EVM)上,其中每个操作——从存储读取、附加到数组或调用外部合约——都会消耗 gas。高级 DoS 攻击利用这一点来:
- 使成本猛增:膨胀合约状态以使合法操作成本高昂,从而阻止用户。
- 阻止功能:导致交易失败或回滚,从而停止诸如项目处理或提款之类的关键功能。
- 损害信任:让用户感到沮丧并损害 dApp 的声誉,正如 2018 年的 Fomo3D 攻击中所见,状态膨胀延迟了奖金分配。
Solodit 清单的 SOL-AM-DoS-2 强调主动状态管理和 gas 优化,以防止这些攻击。让我们探讨它们是如何工作的以及如何应对它们。
真实案例:Fomo3D (2018)
在病毒式以太坊游戏 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 限制,从而导致交易失败。 - 合法用户或管理员无法处理物品,从而有效地停滞了交易市场。
- Gas 消耗利用:
- 如果 processItems 包括一个外部调用(例如,调用用户的合约),则攻击者会部署一个恶意合约,该合约会消耗 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
,直到问题得到解决。 - 攻击缓解:受限的数组大小可防止状态膨胀,并且批量处理可避免 gas 限制问题。恶意接收者无法中断处理,因为循环中未进行外部调用。
安全优势
- 受控的 Gas 使用量:批量处理确保即使有大型数据集,交易仍然可行。
- 有界状态:
MAX_ITEMS
上限可防止失控的存储增长。 - 隔离的故障:恶意用户无法阻止其他用户的处理,因为操作是分批进行的并且是独立的。
- 紧急控制:暂停机制可在攻击期间保护合约。
- 高效的存储:清除已处理的物品可降低长期 gas 成本。
其他攻击向量和缓解措施
1. 通过外部调用消耗 Gas
攻击者部署在外部调用期间消耗 gas 或恢复的合约,从而扰乱调用者。
缓解措施:
- 避免在循环中进行外部调用。
- 对调用使用有限的 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);
}
2. 通过映射滥用造成状态膨胀
攻击者用垃圾条目淹没映射以使 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:
//缓解措施:
3. 过多的事件发射
在循环中发射过多的事件会增加 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);
}
DoS 防护智能合约的最佳实践
为了与 Solodit 清单和行业标准保持一致,请采用以下实践:
- 限制状态增长:使用
MAX_ITEMS
等常量来限制数组和映射,从而防止无限制的增长。 - 批量处理:以固定大小的批次(例如,每次调用 50 个物品)处理数据,以使 gas 成本保持可管理。
- 高效的事件使用:发射聚合事件(例如,
BatchProcessed
)而不是每个物品的事件,以节省 gas。 - 断路器:实施暂停/取消暂停机制以在攻击期间停止操作,仅限于管理员。
- 访问控制:对敏感功能(如状态清除)使用基于角色的访问(例如,
onlyAdmin
)。 - Gas 优化:
- 对只读输入使用
calldata
以降低 gas 成本。 - 尽量减少循环中的存储操作。
- 使用 Foundry 的 gas 报告(
forge test --gas-report
)等工具来识别效率低下之处。
7. 状态监控:
- 实时跟踪数组长度和 gas 使用情况。
- 使用 Forta 等监控工具来检测异常的状态增长或 gas 消耗。
8. 全面的测试:
- 使用 Hardhat 或 Foundry 模拟大型数据集(例如,10,000 个物品)。
- 使用 Echidna 等模糊测试工具来测试边缘情况。
- 分叉主网以在实际条件下测试 gas 限制。
DoS 防护工具(2025 年更新)
截至 2025 年 7 月,这些工具增强了 DoS 防护:
- Slither (0.10.x):检测高 gas 操作 (slither — detect high-gas)。
- MythX:结合静态和动态分析来处理 gas 密集型代码。
- Foundry:支持模糊测试和主网分叉以进行 gas 测试。
- Forta:实时监控状态膨胀和 gas 消耗。
- Remix Gas Profiler:可视化每个函数的 gas 使用情况。
- Gas Station Network (GSN):将 gas 成本分包给用于元交易的中继器。
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;
});
- 模糊测试:使用 Echidna 模拟随机输入和大型数据集,确保合约能够处理边缘情况。
- 主网分叉:使用 Hardhat 分叉以太坊主网,以在实际条件下测试 gas 限制。
- Gas 分析:使用 Foundry 的
forge test --gas-report
识别高 gas 函数并对其进行优化。
真实案例:2022 年 DeFi 质押池
在 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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
版权声明
本文仅代表作者观点,不代表区块链技术网立场。
本文系作者授权本站发表,未经许可,不得转载。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。