一、漏洞
// 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傳入以太,當(dāng)傳入的以太數(shù)值高于balance的時候,這個msg.sender就成為了king,而且在成為king之前,要把之前king傳入的以太返還回去:
(bool sent, ) = king.call{value: balance}("");
這個合約乍一看沒有什么問題,但是可能會導(dǎo)致拒絕服務(wù)攻擊,核心原因就在于,這個返回以太的代碼不一定成功執(zhí)行,一旦無法成功執(zhí)行,就阻塞在這里了。
我們的攻擊合約Attack,核心就是進行了一次KingOfEther的合約調(diào)用,試想一下這樣的過程:
- Bob通過claimThrone傳入了1 Ether,Bob成為King
- Alice通過claimThrone傳入了2 Ether,之前Bob傳入的1 Ether又通過call返還了Bob,Alice成為了King
- 此時Attack合約發(fā)布了,再由Attack通過claimThrone傳入了4 Ether,之前Alice傳入的2 Ether又通過call返還了Alice,Attack地址就成為了King
此時合約已經(jīng)被鎖定了- Tom通過claimThrone傳入了5 Ether,按照之前的流程,理應(yīng)由Tom成為新的King,但實際上,當(dāng)程序執(zhí)行到返還Attack傳入4 Ether時,由于我們的Attack合約并沒有
receive()或者fallback(),無法接收以太,于是失敗了 - 這樣任何新地址都無法成為King,整個合約就是
拒絕服務(wù)的狀態(tài)
二、預(yù)防手段
合約中,不要主動給地址發(fā)以太,發(fā)以太時也要仔細(xì)斟酌整個邏輯,思考會不會發(fā)生DoS攻擊。像我們這個例子中,就不能主動發(fā)送以太,可以自己設(shè)置一個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");
}
}