Defending Against DoS: Strategies to Prevent Denial of Service Attacks in Smart Contracts

1 November, 2024
article image
Education

Contents

A Denial of Service (DoS) attack is an attempt to make a service, like a website, stop working. For smart contracts written in Solidity, this could mean that a specific contract or the whole blockchain network might crash.

This attack renders a smart contract inaccessible to users, potentially resulting in financial losses and disruptions to critical services. The loss of trust in the platform caused by such an attack can lead to user attrition and damage its reputation.

Such an attack has several subtypes:

  • Unexpected Revert
  • Block Gas Limit
  • Block Stuffing

Unexpected Revert

To demonstrate how an unexpected revert can lead to a DoS attack in a smart contract, consider the following auction example.

contract AuctionHouse {
    address currentBidder;
    uint256 highestBidAmount;

    function placeBid() public payable {
        require(msg.value > highestBidAmount, "Bid must be higher than the current highest bid");
        require(payable(currentBidder).send(highestBidAmount), "Failed to send Ether to previous bidder");

        currentBidder = msg.sender;
        highestBidAmount = msg.value;
    }
}

This contract has a placeBid() function that handles new bids. The key point is the refund of the previous highest bid. If this refund fails for any reason (e.g., the previous bidder’s address points to a contract that always rejects funds), the entire transaction will be reverted. This means no subsequent bids can be accepted until the refund issue is resolved, making the contract vulnerable to a DoS attack.

To demonstrate this vulnerability, consider the following malicious contract:

contract MaliciousBidder {
    AuctionHouse auctionHouse;

    constructor(AuctionHouse _auctionHouseAddress) {
        auctionHouse = AuctionHouse(_auctionHouseAddress);
    }

    function placeMaliciousBid() public payable {
        auctionHouse.placeBid{value: msg.value}();
    }
}

The MaliciousBidder contract initializes the address of a deployed AuctionHouse contract in its constructor. This allows the attacker to access the AuctionHouse contract’s functions. In the placeMaliciousBid() function, the placeBid() function of the AuctionHouse the contract is called to place a bid.

How does the attack happen?

Suppose some users start placing bids.

  • User 1 places a bid of 3 ETH and becomes the auction leader.
  • User 2 offers 5 ETH, outbidding User 1. User 1 gets their 3 ETH back, and User 2 becomes the new leader.
  • User 3 offers 6 ETH, outbidding User 2. User 2 gets their 5 ETH back, and User 3 becomes the new leader.
  • The attacker uses their `MaliciousBidder` contract and offers, for example, 7 ETH. Their contract becomes the new auction leader, and User 3 gets their 6 ETH back.

Any subsequent user trying to place a bid will encounter a problem. When the auction contract tries to return funds to the attacker’s contract (in this case, 7 ETH), an error will occur.

The attacker’s contract is specifically designed to not be able to receive Ether. When the auction contract tries to send Ether to the attacker’s contract address, an error occurs. This is due to the lack of special functions (receive() or fallback) in the attacker’s contract, which are typically used to receive Ether.

As a result of such an attack, the auction contract becomes unavailable to other users, as no new bids can be accepted. This is a classic example of an unexpected revert attack.

Block Gas Limit

A transaction will fail if a block reaches its gas limit and the transaction requires more gas than is available.

If a transaction, particularly one returning funds, cannot be completed due to insufficient gas, the entire refund process will halt, and the funds will be stuck.

Here’s an example of a for loop that might run out of gas:

contract RefundDistribution {
	address[] private refundRecipients;
	mapping (address => uint) public refundAmounts;

function distributeRefunds() external onlyOwner {
// Unknown length iteration based on how many addresses participated
    for(uint i; i < refundRecipients.length; i++) {
    // Doubly bad, now a single failure on send will hold up all funds
       require(refundRecipients[i].send(refundAmounts[refundRecipients[i]]))
    }
}

Block Stuffing

An attacker can perform a block-stuffing attack by submitting a large number of high-gas transactions. This allows them to monopolize the block space for several blocks, effectively preventing other transactions from being included.

Let’s look at this scenario:

A malicious actor, Bob, targets a popular decentralized exchange (DEX) on the Ethereum blockchain. His goal is to disrupt the DEX’s operations and prevent users from trading.

  1. Bob creates a massive number of Ethereum transactions, each designed to be computationally expensive. These transactions could involve complex smart contract interactions or large token transfers.
  2. Bob floods the Ethereum network with these high-gas transactions. They are submitted to multiple nodes, ensuring widespread dissemination.
  3. As the DEX’s smart contracts process these computationally expensive transactions, the network becomes congested. The DEX’s servers may struggle to handle the increased load, leading to performance degradation and potential downtime.
  4. DEX users attempting to trade or interact with other features may experience delays, errors, or even complete inaccessibility. This can cause significant inconvenience and financial losses for users.
  5. The disruption caused by the block stuffing attack can damage the DEX’s reputation and erode user confidence. It may also lead to financial losses for the DEX’s operators.

Security Methods

  • Implement mechanisms to handle external calls asynchronously, allowing the contract to continue functioning even if external calls fail. This prevents the contract from being locked in a state of failure.
  • Accurately estimate gas costs for transactions to prevent unexpected failures due to insufficient gas. Use tools and libraries that can help with gas estimation.
  • Implement comprehensive error handling to gracefully manage unexpected situations and prevent the contract from entering an inconsistent state.
  • Implement Role-Based Access Control (RBAC) to assign specific permissions to different roles within the contract. This prevents a single role from having excessive control and reduces the risk of unauthorized actions.

Identified Vulnerability

Let’s take a look at some of the victims who have been affected by this attack.

  1. Zaros
  1. Ironblocks
  1. Nabla
  1. Zap
  1. Arrakis Valantis

Conclusion

DoS attacks pose a serious threat to the functionality and reliability of smart contracts, leading to operational disruptions and financial losses. Understanding these vulnerabilities and their consequences empowers you to create more secure contracts by optimizing gas consumption, setting operational limits, and controlling access.

But securing your smart contracts doesn’t end there. Regular code audits are crucial to maintaining robust security in the ever-evolving blockchain landscape.

That’s where Oxor.io comes in. We specialize in identifying and mitigating vulnerabilities like DoS attacks. Our expert team will conduct comprehensive audits to ensure your smart contracts are both optimized and secure.

Don’t let vulnerabilities compromise your blockchain project. Secure your smart contracts and protect your reputation by partnering with Oxor.io. Together, we’ll fortify your blockchain initiatives and contribute to a safer, more reliable Web3 ecosystem.

Telegram
Education

Contents

Telegram

Have a question?

Have a question?

Stay Connected with OXORIO

We're here to help and guide you through any inquiries you might have about blockchain security and audits.