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 whenfalse
- 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 string
s:
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.