Back to blogs
Written by
Hans
Published on
April 8, 2025

Solodit Checklist Explained (2): Denial-of-Service Attacks Part 2

Learn how to prevent denial-of-service (DoS) attacks in smart contracts by securing queues, handling low-decimal tokens, and managing external calls safely.

Table of Contents

Welcome back to the "Solodit Checklist Explained" series. We're continuing our exploration of the Solodit smart contract audit checklist, focusing on practical strategies for writing secure and resilient smart contracts.

This checklist helps you identify potential vulnerabilities and implement proactive security measures. In our previous installment, "Solodit Checklist Explained (1)", we studied three checklist items (withdrawal pattern, minimum transaction amount, and blacklisting) related to Denial-of-Service (DoS) attacks and provided solutions for developers. If you haven't already, I recommend reviewing that article before proceeding.

In this post, we'll address three additional items from the checklist, again focusing on DoS attacks. We'll delve into queue processing vulnerabilities, the challenges presented by low-decimal tokens, and the importance of handling external contract calls safely

For the best experience, open a tab with the Solodit checklist to refer to it.

A brief overview

Here's what we'll cover today:

  • SOL-AM-DOSA-4: Blocking Queue Processing: This problem occurs when an attacker manipulates a queue to halt or disrupt its operation, leading to a DoS. We'll examine how attackers can compromise processing queues.

  • SOL-AM-DOSA-5: Low Decimal Tokens: The issue arises when dealing with tokens with a low number of decimals. Calculations, particularly divisions, can be truncated down to zero, leading to unexpected and detrimental behavior. Such low-precision tokens can lead to integer division issues that disrupt key functions.

  • SOL-AM-DOSA-6: Unsafe External Calls: We'll investigate how reliance on external contracts without proper error handling can create vulnerabilities. The problem occurs when the failure of an external call isn't managed correctly, potentially causing the entire contract to revert.

Real-world exploits have demonstrated the potential damage these vulnerabilities can inflict. Projects have suffered substantial losses due to seemingly minor oversights.

The Solodit checklist is based on audit findings, bug bounty reports, and real-world incidents. By studying these checklist items, you can learn from past mistakes and improve the security of your code.

Now, let's examine each checklist item using illustrative code examples. These examples are simplified to highlight the core vulnerabilities.

SOL-AM-DOSA-4: Can an attacker block or prevent queue processing to cause service disruption?

The problem: If your smart contract relies on queues for task processing, an attacker might manipulate a specific status within the queue to prevent correct processing.

The checklist question: "Can an attacker block or prevent queue processing to cause service disruption?"

The fix: Your queue processing mechanism requires robust error handling and fallback mechanisms to ensure process continuation even when issues arise.

Example:

Consider a withdrawal queue example:

  1. Users request withdrawals, and some flags indicate the request is active.
  2. Attackers can exploit resetUserStatus after their withdrawal is enqueued and DoS other users from doing the same.
   // VULNERABLE FUNCTION: Can be exploited
    function resetUserStatus() external {
        // Anyone can reset their status while remaining in the queue
        withdrawalRequested[msg.sender] = false;
        // Note: User is not removed from the queue!
    }

    // Process the next withdrawal in the queue
    function processNextWithdrawal() external {
        require(withdrawalQueue.length > currentIndex, "No withdrawals to process");

        // Get the next withdrawal
        Withdrawal memory withdrawal = withdrawalQueue[currentIndex];

        // VULNERABLE: This check can be exploited by an attacker by resetting the status
        require(withdrawalRequested[withdrawal.user], "Withdrawal no longer requested");

        // Process the withdrawal
        uint256 amount = withdrawal.amount;
        require(balances[withdrawal.user] >= amount, "Insufficient balance");

        // Update balance
        balances[withdrawal.user] -= amount;

        // Reset withdrawal request
        withdrawalRequested[withdrawal.user] = false;

        // Send funds
        (bool success, ) = payable(withdrawal.user).call{value:amount}("");
        require(success, "Failed to send funds");

        // Move to next in queue
        currentIndex++;
    }


To exploit this, an attacker can do the following:

  1. Initiate a withdrawal request.
  2. Call the resetUserStatus function.
  3. The processNextWithdrawal function will revert, causing an ongoing DoS attack.

Remediation:

  • Restrict modifying withdrawalRequested status to the admin.
  • Ensure validation checks to avoid zero-value transactions.
  • Implement a fallback function to handle unexpected errors.

SOL-AM-DOSA-5: Can low decimal tokens cause DoS?

The problem: Tokens with a low number of decimals can lead to issues with integer division, resulting in rounding down to zero.

Imagine a token streaming contract that distributes tokens over a period. If tokensPerSecond rounds to zero due to integer division with low-decimal tokens, the distribution function will be blocked.

The checklist question
: "Can low decimal tokens cause DoS?"

The fix
: Implement logic to handle low decimals that prevents breaking the transaction process due to rounding errors.

Example
:

Consider a TokenStream contract that streams a certain amount of tokens to users, where:

  1. total_tokens need to be transferred to the contract
  2. token_per_second can be rounded to zero, since we are using a token with 1 decimal
  3. distributeTokens function will revert

contract TokenStream {
    IERC20 public token;
    uint256 public streamDuration;
    uint256 public tokensPerSecond;

    constructor(IERC20 _token, uint256 _streamDuration, uint256 _tokensPerSecond) {
        token = _token;
        streamDuration = _streamDuration;
        tokensPerSecond = _tokensPerSecond;
    }

    function distributeTokens(address recipient) external {
        uint256 balance = token.balanceOf(address(this));
        uint256 amount = tokensPerSecond * streamDuration;

        uint256 tokensToSend = amount > balance ? balance : amount;

        require(tokensToSend > 0, "Insufficient tokens to stream");
        token.transfer(recipient, tokensToSend);
    }
}

contract LowDecimalToken is ERC20 {
    constructor() ERC20("LowDecimalToken", "LDT") {
        _mint(msg.sender, 100000 * (10 ** decimals()));
    }

    function decimals() public view virtual override returns (uint8) {
        return 1; // Simulate a low decimal token
    }
}


The testDOSWithLowDecimalTokens test in TokenStreamTest will revert in this case.

Remediation: Ensure the contract handles low decimal tokens correctly by scaling math formulas to mitigate integer rounding during calculations.

SOL-AM-DOSA-6: Does the protocol handle external contract interactions safely?

The problem: Many smart contracts rely on external contracts for interactions. Unexpected behaviors from external contracts can cause the whole system to revert. Failing to handle these external errors leads to a DoS vulnerability.

The checklist question: "Does the protocol handle external contract interactions safely?"

The fix: Ensure robust error handling for external contract interactions to protect protocol integrity regardless of external contract performance.

Example: Consider a contract that interacts with an external Chainlink price feed. Without proper error handling using try/catch, any revert from the external feed will cascade upward, and the contract will revert.

  1. The getPrice function retrieves the price feed data.
  2. When the external Chainlink Oracle fails, the entire code reverts.
  3. The calculateSomethingImportant function depends on getPrice and will also revert.
contract PriceDependentContract {
    AggregatorV3Interface public priceFeed;

    constructor(address _priceFeed) {
        priceFeed = AggregatorV3Interface(_priceFeed);
    }

    // Vulnerable function that retrieves the price without handling potential Chainlink reverts
    function getPrice() public view returns (uint256) {
        (, int256 price, , , ) = priceFeed.latestRoundData(); // Vulnerable line: No error handling
        require(price > 0, "Price must be positive");
        return uint256(price);
    }

    function calculateSomethingImportant() public view returns (uint256) {
        uint256 price = getPrice();
        // ... some important calculation using the price
        return price * 2;
    }

Remediation: Wrap external contract calls in try/catch blocks to handle reverted errors and implement a fallback or cached value. 

NOTE: There is an edge case where the external contract spends gas intentionally, which can make the catch block fail, too! We will discuss this later.

Conclusion

We've explored three critical checklist items designed to strengthen your smart contracts against DoS attacks: queue processing vulnerabilities, the challenges associated with low decimal tokens, and the importance of handling external contract interactions safely.

Remember, the provided examples aim to illustrate core vulnerabilities. Understanding the underlying principles and adapting these concepts to your specific use cases is essential.

By implementing these recommendations, you can significantly improve the security and resilience of your smart contracts.

Stay tuned for the next installment of the "Solodit Checklist Explained" series.

Secure your protocol today

Join some of the biggest protocols and companies in creating a better internet. Our security researchers will help you throughout the whole process.
Stay on the bleeding edge of security
Carefully crafted, short smart contract security tips and news freshly delivered every week.