# WTF Solidity 合约安全: S09. 拒绝服务 **Published by:** [0xAA](https://paragraph.com/@wtfacademy/) **Published on:** 2022-11-13 **URL:** https://paragraph.com/@wtfacademy/wtf-solidity-s09 ## Content 我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 推特:@0xAA_Science|@WTFAcademy_ 社区:Discord|微信群|官网 wtf.academy 所有代码和教程开源在github: github.com/AmazingAng/WTFSolidity这一讲,我们将介绍智能合约的拒绝服务(Denial of Service, DoS)漏洞,并介绍预防的方法。NFT项目 Akutar 曾因为 DoS 漏洞损失 11,539 ETH,当时价值 3400 万美元。DoS​在 Web2 中,拒绝服务攻击(DoS)是指通过向服务器发送大量垃圾信息或干扰信息的方式,导致服务器无法向正常用户提供服务的现象。而在 Web3,它指的是利用漏洞使得智能合约无法正常提供服务。 在2022年4月,一个很火的 NFT 项目名为 Akutar,他们使用荷兰拍卖进行公开发行,筹集了 11,539.5 ETH,非常成功。之前持有他们社区Pass的参与者会得到 0.5 ETH的退款,但是他们处理退款的时候,发现智能合约不能正常运行,全部资金被永远锁在了合约里。他们的智能合约有拒绝服务漏洞。漏洞例子​下面我们学习一个简化了的 Akutar 合约,名字叫 DoSGame。这个合约逻辑很简单,游戏开始时,玩家们调用 deposit() 函数往合约里存款,合约会记录下所有玩家地址和相应的存款;当游戏结束时,refund()函数被调用,将 ETH 依次退款给所有玩家。// SPDX-License-Identifier: MIT pragma solidity ^0.8.4; // 有DoS漏洞的游戏,玩家们先存钱,游戏结束后,调用deposit退钱。 contract DoSGame { bool public refundFinished; mapping(address => uint256) public balanceOf; address[] public players; // 所有玩家存ETH到合约里 function deposit() external payable { require(!refundFinished, "Game Over"); require(msg.value > 0, "Please donate ETH"); // 记录存款 balanceOf[msg.sender] = msg.value; // 记录玩家地址 players.push(msg.sender); } // 游戏结束,退款开始,所有玩家将依次收到退款 function refund() external { require(!refundFinished, "Game Over"); uint256 pLength = players.length; // 通过循环给所有玩家退款 for(uint256 i; i < pLength; i++){ address player = players[i]; uint256 refundETH = balanceOf[player]; (bool success, ) = player.call{value: refundETH}(""); require(success, "Refund Fail!"); balanceOf[player] = 0; } refundFinished = true; } function balance() external view returns(uint256){ return address(this).balance; } } 这里的漏洞在于,refund() 函数中利用循环退款的时候,是使用的 call 函数,将激活目标地址的回调函数,如果目标地址为一个恶意合约,在回调函数中加入了恶意逻辑,退款将不能正常进行。(bool success, ) = player.call{value: refundETH}(""); 下面我们写个攻击合约, attack() 函数中将调用 DoSGame 合约的 deposit() 存款并参与游戏;fallback() 回调函数将回退所有向该合约发送ETH的交易,对DoSGame 合约中的DoS漏洞进行了攻击,所有退款将不能正常进行,资金被锁在合约中,就像 Akutar 合约中的一万多枚ETH一样。contract Attack { fallback() external payable{ revert("DoS Attack!"); } function attack(address gameAddr) external payable { DoSGame dos = DoSGame(gameAddr); dos.deposit{value: msg.value}(); } } Remix 复现​部署 DoSGame 合约。调用 DoSGame 合约的 deposit(),进行存款并参与游戏。部署 Attack 合约。调用 Attack 合约的 attack(),进行存款并参与游戏。调用 DoSGame 合约refund(),进行退款,发现不能正常运行,攻击成功。预防方法​很多逻辑错误都可能导致智能合约拒绝服务,所以开发者在写智能合约时要万分谨慎。以下是一些需要特别注意的地方:外部合约的函数调用(例如 call)失败时不会使得重要功能卡死,比如将上面漏洞合约中的 require(success, "Refund Fail!"); 去掉,退款在单个地址失败时仍能继续运行。合约不会出乎意料的自毁。合约不会进入无限循环。require 和 assert 的参数设定正确。退款时,让用户从合约自行领取(push),而非批量发送给用户(pull)。确保回调函数不会影响正常合约运行。确保当合约的参与者(例如 owner)永远缺席时,合约的主要业务仍能顺利运行。总结​这一讲,我们介绍了智能合约的拒绝服务,并举了 Akutar 项目因为该漏洞损失了一万多枚ETH。很多逻辑错误都能导致DoS,开发者写智能合约时要万分谨慎,比如退款要让用户自行领取,而非合约批量发送给用户。 ## Publication Information - [0xAA](https://paragraph.com/@wtfacademy/): Publication homepage - [All Posts](https://paragraph.com/@wtfacademy/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@wtfacademy): Subscribe to updates - [Twitter](https://twitter.com/0xAA_Science): Follow on Twitter