智能合约里的 “拒绝服务“ 攻击:让你的合约变成 “死机的手机“

发布于:2025-08-18 ⋅ 阅读:(17) ⋅ 点赞:(0)

你有没有遇到过手机突然卡死,点什么都没反应的情况?在区块链世界里,智能合约也可能遭遇类似的 "罢工"—— 这就是 "拒绝服务攻击"(Denial of Service,简称 DoS)。今天用大白话讲讲合约里的 DoS 攻击是怎么回事,以及如何防范。

先理解:什么是拒绝服务攻击?

简单说,拒绝服务攻击就是通过各种手段,让合约的核心功能彻底失效,谁都用不了

就像:

  • 你经营一家自助餐厅,有人故意把所有座位占满却不消费,真正的顾客进不来
  • 你开了个快递柜,有人把所有格子都塞一些废纸条,导致正常包裹放不下

在合约里,DoS 攻击会让转账、提款、投票等关键功能彻底卡住,严重的甚至会让合约里的资产永远取不出来。

合约里最常见的 3 种 DoS 攻击手法

1. 利用 "必须成功的批量操作"—— 最容易踩的坑

很多合约会设计批量操作功能(比如批量发工资、批量退款),如果代码里用了for循环一次性处理所有用户,就可能被攻击。

漏洞合约示例(批量退款):
contract BatchRefund {
    address[] public refundees; // 退款名单
    mapping(address => uint) public amounts; // 每个人该退的钱

    // 管理员添加退款名单
    function addRefundee(address who, uint amount) public {
        refundees.push(who);
        amounts[who] = amount;
    }

    // 批量退款(有漏洞!)
    function refundAll() public {
        // 循环给每个人退款
        for (uint i = 0; i < refundees.length; i++) {
            address payable who = payable(refundees[i]);
            // 只要有一次转账失败,整个函数就会卡住
            (bool success, ) = who.call{value: amounts[who]}("");
            require(success, "给某个人退款失败了");
        }
    }
}
攻击方式:

黑客只需要用一个 "有问题的地址"(比如一个没有receive函数的合约地址)加入退款名单。当refundAll()执行到这个地址时,转账会失败,require会触发revert,整个批量退款就会卡住。

结果就是:所有人都拿不到退款,功能彻底报废

2. 用 "gas 炸弹" 耗尽资源 —— 让交易永远失败

以太坊的每笔交易都有 gas 限制(相当于 "燃料上限"),如果合约里有需要大量计算的功能,黑客可以故意触发这些功能,让 gas 消耗超过上限,导致交易失败。

漏洞合约示例(投票系统):
contract BadVoting {
    mapping(address => uint) public votes; // 每个人的票数
    address[] public voters; // 投票人名单

    // 投票
    function vote() public {
        votes[msg.sender]++;
        voters.push(msg.sender); // 每次投票都记录地址
    }

    // 计算总票数(有漏洞!)
    function countTotalVotes() public view returns (uint) {
        uint total = 0;
        // 遍历所有投票人计算总数
        for (uint i = 0; i < voters.length; i++) {
            total += votes[voters[i]];
        }
        return total;
    }
}
攻击方式:

黑客可以用大量不同的地址反复调用vote(),让voters数组变得非常长(比如 10 万个地址)。当有人想调用countTotalVotes()时,遍历 10 万个地址需要的 gas 会远远超过区块上限,导致这个函数永远无法执行。

结果就是:投票系统的计票功能彻底瘫痪

3. 控制关键权限 —— 让合约变成 "死账户"

如果合约的核心功能(比如提款、升级)依赖某个单一地址(比如管理员),而这个地址因为私钥丢失、被黑等原因失控,就会导致合约 "永久停机"。这也算一种特殊的 DoS。

漏洞场景:
contract AdminControl {
    address public admin; // 管理员地址
    mapping(address => uint) public balances;

    constructor() {
        admin = msg.sender; // 部署者成为管理员
    }

    // 提款必须由管理员触发
    function withdraw(address to, uint amount) public {
        require(msg.sender == admin, "不是管理员");
        (bool success, ) = payable(to).call{value: amount}("");
        require(success);
    }
}
攻击 / 风险方式:

如果管理员的私钥丢了,或者管理员地址是个合约且该合约功能已失效,那么withdraw()函数就永远没人能调用,合约里的资产就成了 "死钱"。

这种情况在实际中很常见,每年都有大量加密资产因为 "管理员私钥丢失" 而永久冻结。

如何防范拒绝服务攻击?

针对不同的 DoS 攻击,有不同的防御方案,但核心原则是:避免单点依赖,控制操作复杂度

1. 解决批量操作问题:化整为零

把一次性的批量操作,拆分成多次小批量操作,即使某一次失败,也不影响整体。

修复批量退款合约:

contract SafeBatchRefund {
    address[] public refundees;
    mapping(address => uint) public amounts;
    uint public nextIndex; // 记录下次开始退款的位置

    function refundBatch(uint batchSize) public {
        uint end = nextIndex + batchSize;
        // 防止数组越界
        if (end > refundees.length) end = refundees.length;

        // 本次只退batchSize个人
        for (uint i = nextIndex; i < end; i++) {
            address payable who = payable(refundees[i]);
            (bool success, ) = who.call{value: amounts[who]}("");
            if (success) {
                nextIndex++; // 只有成功了才更新索引
            } else {
                // 失败了就跳过,下次再试
                break;
            }
        }
    }
}

这样即使某个人退款失败,也能继续给其他人退款,不会全军覆没。

2. 解决 gas 炸弹:限制计算复杂度

  • 避免在合约里写需要遍历超长数组的函数
  • 提前计算并限制单次操作的复杂度
  • 重要功能尽量设计成 "常量级" 或 "线性级" 复杂度

修复投票系统:

contract GoodVoting {
    mapping(address => uint) public votes;
    uint public totalVotes; // 直接记录总票数,不用每次计算

    function vote() public {
        votes[msg.sender]++;
        totalVotes++; // 投票时直接更新总数
    }

    // 直接返回已记录的总数,无需遍历
    function countTotalVotes() public view returns (uint) {
        return totalVotes;
    }
}

3. 解决权限问题:去中心化治理

  • 避免单一管理员,改用多签钱包(需要多个管理员同意才能操作)
  • 关键功能设计成 "时间锁"(操作需要等待一段时间才能执行,给社区反应时间)
  • 重要合约可以引入 DAO 治理,让社区共同决定关键操作

示例(多签简化版):

contract MultiSig {
    address[] public admins; // 多个管理员
    uint public required; // 需要多少个管理员同意

    constructor(address[] memory _admins, uint _required) {
        admins = _admins;
        required = _required;
    }

    // 提款需要足够多管理员同意
    function withdraw(address to, uint amount) public {
        // 检查是否有足够多管理员授权(实际实现更复杂)
        require(isApprovedByEnoughAdmins(), "同意人数不足");
        // ... 执行提款 ...
    }
}

总结:拒绝服务攻击的本质是 "卡住关键流程"

DoS 攻击不像重入攻击那样直接偷钱,但它能让你的合约彻底失效,造成的损失可能更大(比如无法提款的资金池)。

防范的核心思路是:

  • 别把所有鸡蛋放在一个篮子里(避免单点依赖)
  • 复杂操作要拆分(避免一次性处理太多任务)
  • 给系统留 "后路"(失败了能重试,权限丢了有备选)

写合约时多问自己:"如果这个功能卡住了,有没有备用方案?" 能帮你避开大多数 DoS 陷阱。


网站公告

今日签到

点亮在社区的每一天
去签到