Articles Home

Common Vulnerabilities in Solidity: Denial of Service (DOS)

Mar 30, 2023

Solidity is a blockchain-based programming language for smart contracts, and is one of the preferred languages for writing smart contracts on Ethereum. While Solidity-written smart contracts offer many benefits in automating and decentralizing transactions, they may also have security issues when handling transactions.

Among them, DoS attacks are a common Solidity security vulnerability, whose main purpose is to prevent other users from accessing the contract by occupying contract resources, causing it to crash or not function properly.

In this article, we will explore the types of DoS attacks that may exist in Solidity contracts, and provide some best practices for addressing these vulnerabilities. We hope that through this article, we can help Solidity developers and users better understand and prevent the threat of denial of service attacks, and further enhance the security and reliability of smart contracts.

What Is Denial Of Service (DOS) ?

Traditional network security denial of service (DoS): DoS is an abbreviation for denial of service. A denial of service occurs when an interference to a service reduces or eliminates its availability. The following are examples of common denial-of-service attacks against network protocols: SYN Flood, IP Spoofing, UDP Flood, Ping Flood, Teardrop Attack, LAND attack, Smurf attack, Fraggle attack, and so on.

Smart contract denial-of-service attack: A security issue that can result in code logic errors, compatibility issues, or excessive call depth (a feature of blockchain virtual machines), causing smart contracts to fail to function properly. Smart contract denial of service attack methods are relatively simple, including but not limited to the following three:

Vulnerability Example

After the introduction above, we have gained some understanding of denial of service attacks. Now, let's take a closer look at this type of attack through the following typical code example:

        
            // SPDX-License-Identifier: MIT
            pragma solidity ^0.8.13;

            contract KingOfEther {
                address public king;
                uint public king;

                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;
                }
            }
        
    

Vulnerability Analysis

Through the above code, we can see that the KingOfEther contract allows players to compete for the "King" title by calling the claimThrone() function and depositing any amount of ether greater than the previous player. When a player deposits more ether than the previous player, the deposited ether will remain in the contract and the player will receive the "King" title, while the ether deposited by the previous player will be refunded.

We found that the logic of crowning a new King and refunding the previous King's ether is handled in the same claimThrone() function, which also checks the return value of the refund operation, "sent". Let's combine these features to complete the attack.

Here is the attack contract we deployed:

        
            // SPDX-License-Identifier: MIT
            pragma solidity ^0.8.13;

            contract Attack {
                KingOfEther KingOfEther;

                constructor(KingOfEther_kinOfEther){
                    kingOfEther = KingOfEther(_kingOfEther);
                }

                functon attack() public payable {
                    kingOfEther.claimThrone{value:msg.value}();
                }
            }
        
    

We first deploy the attack contract Attack using KingOfEthe's address and then call Attack.attack() to punch a certain amount of Ether into the KingOfEther contract. When a new player deposits more ether than us, the refund logic in KingOfEther.claimThrone() will be triggered, and the ether we deposited through Attack.attack() will be refunded to the Attack contract. However, since the attack contract does not implement the fallback() method of payable, it cannot receive ether.

At this point, the refund logic in KingOfEther.claimThrone() will fail, and the returned value of "sent" will be constantly reverted due to being false and failing the require(sent, "Failed to send Ether") check. Since every refund operation will be reverted, no new "King" can be produced in the KingOfEther contract after the Attack contract. This is how we completed the denial of service attack.

How To Prevent DOS Attacks In Smart Contracts ?

To prevent denial of service attacks in smart contracts, here are some common defensive measures:

These defensive measures are just some common suggestions, and contract developers should also perform reasonable security design and implementation according to their own actual situations and needs, in order to minimize the risk of smart contracts being targeted by DoS attacks.