Solidity While Loop (Code Examples)

whiteboard crypto logo
Published by:
Whiteboard Crypto
on

While loops are control structures that repeatedly run a block of code while a boolean condition remains true, or a break statement is encountered. Here’s an example of a while loop in Solidity:

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

contract WhileLoopSyntaxExample {

    function incrementingLoop(uint iterations) public pure {
        uint i = 0;
        while (i < iterations) {
            // Loop code goes here
            i++;
        }
    }

    function decrementingLoop(uint iterations) public pure {
        while (iterations > 0) {
            // Loop code goes here
            iterations--;
        }
    }
}

While loops create a “stripped down” looping structure that can use non-traditional loop conditions. This makes them useful for algorithms, such as those for sorting arrays or performing advanced mathematical and programming operations.

However, it is easy to accidentally create infinite loops if the developer forgets to update their loop condition.

Syntax Breakdown

Just like for loops, while loops typically consist of four components:

  • Counter Variable: A local variable used for counting loop iterations
  • Loop Condition: A boolean expression used to repeat the loop when true, or terminate it when false
  • Loop Body: The code block to repeat
  • Increment Expression: An assignment operation that alters the value of the counter variable

The counter variable is always declared outside the loop, and is altered inside the loop.

Whether the counter variable is altered at the end or the beginning of the body depends on whether its pre-increment or post-increment value is needed.

Avoiding Infinite Loops

A common pitfall that while loops suffer from is the ease with which an infinite loop can be created. While loops are infinitely repeating by default, and we have to impose finite constraints on them.

Forgetting Increment/Decrement Expressions

The most common culprit of this is forgetting to include a counter variable’s increment expression:

contract InfiniteLoops {

    // This loop will continue forever... forgot to decrement iterations!
    function infiniteLoop(uint iterations) public pure {
        while(iterations > 0){
            console.log("Iterations remaining: ", iterations);
        }
    }

    // This loop will terminate
    function finiteLoop(uint iterations) public pure {
        while(iterations > 0){
            console.log("Iterations remaining: ", iterations);
            iterations--;
        }
    }
}

The only difference between the above example that repeats forever and the one that doesn’t is iterations--. This is so easy to overlook that even experienced developers forget them from time to time.

Forgetting to Increment/Decrement Before Continue

Another common pitfall is forgetting to include an increment or decrement expression before a continue statement:

contract InfiniteLoops {

    // Uh oh, we forgot to decrement before calling `continue`!
    function infiniteLoop(uint iterations) public pure {
        while(iterations > 0){
            if(iterations == 5) continue;  // Forgot to decrement!

            console.log("Iterations remaining: ", iterations);
            iterations--;
        }
    }

    // Here we remembered to decrement before calling `continue`
    function finiteLoop(uint iterations) public pure {
        while(iterations > 0){
            if(iterations == 5){
                iterations--;
                continue
            }

            console.log("Iterations remaining: ", iterations);
            iterations--;
        }
    }

Unlike for loops, while loops don’t automatically run the increment expression after a continue statement, so we must include this expression ourselves. Where we place this expression is very important for ensuring our loop can terminate, but we also have complete freedom to place the expression wherever makes sense.

Use Cases

While loops have many use cases, and are much more flexible in their setup than for loops and do-while loops.

Incrementing and Decrementing Over Arrays

A major use case of loops in Solidity is to iterate over an array of values or structs. This is primarily done with for loops, but while loops can be built to serve the exact same function by including the same components.

Let’s add two new functions to our previous example that log the elements of the values array to the console, one in ascending order and the other in descending order:

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

import "hardhat/console.sol";

contract WhileLoopSyntax {

    uint[] public values;

    // Moves "down" the array and logs its values to the console
    function incrementingLoop() public pure {
        uint i = 0;
        while (i < values.length) {
            console.log(values[i]);
            i++;
        }
    }

    // Moves "up" the array and logs its values to the console
    function decrementingLoop() public pure {
        uint i = values.length
        while (i > 0) {
            i--;  // Notice we are decrementing first
            console.log(values[i]);
        }
    }
}

You may notice that the decrementingLoop() function decrements i before using its value. This is to avoid off-by-one bugs, out-of-bounds errors, and integer underflow errors that are common with decrementing loops.

Repeatedly Calling a Function

We can easily use a while loop to repeatedly call a function. For instance, let’s create an array and a pair of functions for generating and pushing pseudo-random numbers to it:

contract WhileLoopSyntax {
    uint[] public values;

    // Repeatedly pushes random values to the `values` array
    function addRandomValues(uint totalValues, uint maxValue) public {
        while(totalValues > 0){
            values.push(randomNumber(maxValue, totalValues));
            totalValues--;
        }
    }

    // Simple getter to display the `values` array
    function getValues() public view returns(uint[] memory){
        return values;
    }

    // !!! DO NOT USE IN PRODUCTION !!!
    // Generates a pseudo-random number for testing
    function randomNumber(uint max, uint salt) public view returns(uint){
        return uint(keccak256(abi.encode(salt, block.timestamp))) % max;
    }
}

In this example, we are calling the randomNumber() function to generate pseudo-random numbers, and the while loop repeatedly generates and pushes these numbers to the values array until the desired totalValue has been reached.

Algorithms and While Loops

Whereas for loops are specialized for iterating over arrays, while loops are specialized for implementing algorithms. An algorithm is a repeating sequence of steps that perform a complex task, often with an unknown number of repetitions.

While Solidity isn’t a great language for implementing algorithms due to gas concerns, they are great examples of situations where a while loop’s condition is not dependent upon a loop counter.

Let’s explore some common algorithms we can implement in Solidity.

Counting Digits in a Number

A simple example is an algorithm that counts the number of digits in an input number. This is a great example of a “non-traditional” loop, since the loop condition is not dependent upon a loop counter:

contract DigitCounter {
    function countDigits(uint number) public pure returns (uint digitCount) {
        if (number == 0) return 1;

        digitCount = 0;

        while (number != 0) {
            number /= 10; // Remove the last digit
            digitCount++;
        }

        return digitCount;
    }
}

This algorithm divides the input number by 10 each round, incrementing the digitCount by 1 each time, until the number reaches 0 (this happens due to integer division).

Converting Unsigned Integers to Strings

Sometimes we need to convert numbers to strings so we can concatenate them together, as Solidity does not provide any native functionality for concatenating strings and other data types together.

Converting from uint to string is a complex process that requires understanding how the EVM processes strings and integers, and how the ASCII table works.

Fortunately, we can convert integers into bytes, and bytes into strings:

contract UintToString {
    function uintToString(uint256 value) public pure returns (string memory) {
        // Handle zero case explicitly
        if (value == 0) return "0";

        // Count number of digits
        uint256 temp = value;
        uint256 digits;
        while (temp != 0) {
            digits++;
            temp /= 10;
        }

        // The buffer is used to store intermediate bytes characters
        bytes memory buffer = new bytes(digits);

        // Convert each digit to ASCII and store in buffer
        while (value != 0) {
            digits--;  // Start from the end and move backwards

            // Convert last digit to its ASCII value
            uint8 asciiDigit = (value % 10) + 48;

            // Convert to bytes1 and store in the buffer
            buffer[digits] = bytes1(asciiDigit);

            // Remove last digit from the value
            value /= 10;
        }

        // Convert the bytes buffer into a string
        return string(buffer);
    }
}

If you need to convert integers to strings in a real contract, then you should import OpenZeppelin’s Strings library, as their toString() function is highly gas-optimized.

Calculating Square Root Via Bablyonian Method

Let’s see an example of a function that calculates a square root using the Babylonian Method. The Babylonian Method is an ancient mathematical algorithm for approximating the square root of a number, and it is still in use today:

contract BabylonianSquareRoot {
    function sqrt(uint x, uint decimals) public pure returns (uint) {
        x *= 10**(decimals*2);

        if (x == 0) return 0;

        uint z = (x + 1) / 2;
        uint y = x;

        // Continue iterating until the approximation is stable
        while (z < y) {
            y = z;
            z = (x / z + z) / 2;
        }

        return y;
    }
}

A full explanation of how this algorithm works is far beyond the scope of this article, so you will just have to take it at face-value.

This function accepts two inputs: The x we wish to take the square root of, and how many decimals of accuracy we want the result to have. The output number will be an integer, so we must manually place the decimal point ourselves.

For example, sqrt(2, 8) will output 141421356, which is 1.41421356 after we add the decimal point. Then, sqrt(150, 8) outputs 1224744871, which is 12.24744871 with the decimal added. You can verify that these results are precise to their chosen decimal place with a calculator app.

Sorted Arrays and Insert/Remove Operations

While sorting arrays in Solidity is a very bad idea due to gas costs, implementing an array that uses insertion and removal algorithms to maintain a sorted state from its beginning is an acceptable compromise.

Let’s create a contract that maintains an array of sorted values, and includes insertion and removal functions:

contract SortedArray {
    uint[] public sortedValues;

    // Function to insert a value into the sorted array
    function insertValue(uint value) public {
        sortedValues.push(value); // Add the value to the end of the array
        uint i = sortedValues.length - 1;

        // Bubble the new value "up" the array to its correct position
        while (i > 0 && sortedValues[i] < sortedValues[i - 1]) {
            (sortedValues[i], sortedValues[i - 1]) = (sortedValues[i - 1], sortedValues[i]);
            i--;
        }
    }

    // Function to remove a value from the sorted array
    function removeValue(uint value) public {
        require(sortedValues.length > 0, "Array is empty");

        uint i = 0;
        bool found = false;

        // Find the value to remove
        while(!found && i < sortedValues.length) {
            if (sortedValues[i] == value) found = true;
            else i++;
        }

        require(found, "Value not found");

        // Bubble the value "down" the array until it reaches the end
        while (i < sortedValues.length - 1) {
            (sortedValues[i], sortedValues[i + 1]) = (sortedValues[i + 1], sortedValues[i]);
            i++;
        }

        sortedValues.pop(); // Pop the value from the array
    }
}

This example allows us to keep an array of sorted values where we never need to sort the entire array, as the only way to add or remove values is through sorted insertion and removal. Note that we used tuples to swap values, rather than relying on an intermediate variable.

There are ways to make this system far more gas-efficient, such as implementing Binary Search algorithms and caching the array in memory, but that’s a topic better left to a gas optimization techniques article.

Whereas while loops are less common in Solidity than for loops, they are useful when a custom looping control structure is needed. This is valuable when implementing algorithms, which often have an unknown number of repetitions involved.

Also, while loops do not provide any safeguards to prevent infinite loops, and the programmer is responsible for ensuring they function as-intended. However, in return they provide a great degree of customization of the loop’s behavior and form, allowing for more advanced designs.

whiteboard crypto logo

WhiteboardCrypto is the #1 online resource for crypto education that explains topics of the cryptocurrency world using analogies, stories, and examples so that anyone can easily understand them. Growing to over 870,000 Youtube subscribers, the content has been shared around the world, played in public conferences and universities, and even in Congress.