Home » Blog » Solidity Vulnerability: Denial of Service (DoS)

Solidity Vulnerability: Denial of Service (DoS)

Denial of service in a smart contract can happen when bad actors prevent your contract from being able to complete it’s coding logic.

Attackers can do this by taking advantage of external calls and having the calls do unexpected things.

In this scenario, never allow the vulnerable contract to send it ETH.

Code – https://github.com/derekarends/solidity-vulnerabilities/tree/main/denial-of-service

Demo – https://youtu.be/eqI4smy-SGk

Vulnerable Contract

pragma solidity ^0.8.0;
/// This contract is vulnerable to a denial of service (DoS) attack
contract VulnerableContract {
    address payable leader;
    uint256 public highestBid;
    /// Place a bid to the contract and if it is the highest bid
    /// send the previous leader the current bid and set the leader
    /// to the sender with the new highest bid
    function bid() external payable {
        require(msg.value > highestBid);
        // Refund the old leader, if it fails then revert
        require(leader.send(highestBid));
        leader = payable(msg.sender);
        highestBid = msg.value;
    }
    /// Helper function to check leader
    function getLeader() external view returns (address) {
        return leader;
    }
}

Here we are assuming the previous leader is good actor and will allow us to refund the ETH they had passed in. However, this assumption can be exploited by an attacker preventing ETH to be sent to the attacker’s contract.

By preventing the ETH from being sent, the vulnerable contract is no longer able to update new leaders even if they pass in more ETH.

Attacker Contract

pragma solidity ^0.8.0;
/// This contract will attack the vulnerable contract to demo DoS
contract AttackerContract {
    VulnerableContract vulnerableContract;
    constructor(VulnerableContract _vulnerableContract) {
        vulnerableContract = _vulnerableContract;
    }
    /// Prevent this contract from taking ETH
    fallback() external payable {
        revert();
    }
    /// Place a bid to the vulnerable contract
    function placeBid() external payable {
        require(msg.value > vulnerableContract.highestBid());
        vulnerableContract.bid{value: msg.value}();
    }
}

The attacker contract is pretty straight forward. It will allow the attacker to place a bid but not accept any ETH to be sent to it.

By adding the fallback function and having it revert the transaction any time ETH is sent to this contract it will revert the transaction, not accepting any ETH.

Preventing DoS

To prevent your contracts from being vulnerable to a denial of service attack you can implement the concept of pull payments.

Pull payments are used to allow users to withdraw their refunds instead of having the contract send them out. This will keep attackers from being able to exploit the contract and do a DoS on the contract.

Fixed Contract

pragma solidity ^0.8.0;
/// This contract is demostrates how to prevent a denial of service (DoS) attack
contract SecureContract {
    address payable leader;
    uint256 public highestBid;
    mapping(address => uint256) refunds;
    /// Place a bid to the contract and if it is the highest bid
    /// send the previous leader the current bid and set the leader
    /// to the sender with the new highest bid
    function bid() external payable {
        require(msg.value > highestBid);
        refunds[leader] += highestBid;
        leader = payable(msg.sender);
        highestBid = msg.value;
    }
    /// Helper function to check leader
    function getLeader() external view returns (address) {
        return leader;
    }
    /// Allow senders to retrieve their refunds
    function withdrawRefund() external payable {
        require(refunds[msg.sender] > 0, "No refunds available");
        (bool success, ) = msg.sender.call{value: refunds[msg.sender]}("");
        require(success, "Failed to transfer refund");
    }
}

This contract implements the pull payment system, where users will have to call the withdrawRefund function to get their refunds instead of having them sent when a new leader takes over.

One Last Note

Do not have the withdraw do any type of looping over addresses that have refunds.

// Store all addresses for refund
address[] refundAddresses;
/// Allow all senders to retrieve their refunds
function withdrawAllRefunds() external payable {
    for(uint256 i; i < refundAddresses.length; i++)
        (bool success, ) = refundAddresses[i].call{value: refunds[refundAddresses[i]}("");
        require(success, "Failed to transfer refund");
    }
}

By looping over all the addresses, if a single contract reverts the transaction, all transactions are reverted and the funds are now stuck in the contract.

Leave a Reply

Your email address will not be published.