Solidity Smart Contract Unbounded Loops DOS Attack Vulnerability Explained with REAL Example
Unbounded loops in solidity can lead to denial-of-service (DoS) attacks and run out of gas issues in Solidity Smart Contracts.
In this article, we’ll delve into the details of this vulnerability and discuss effective mitigation strategies.
You can also watch the following video where we expose an unbounded loop vulnerability in a REAL codebase which is part of an auditing contest:
What is Unbounded Loop?
Unbounded loops are essentially loops without a defined endpoint, meaning they can theoretically run indefinitely. In Solidity smart contracts, these loops can be particularly problematic, as they consume an unpredictable amount of gas. This unpredictability can be exploited by malicious actors to drain the contract of resources or render it unusable.
Here are some examples:
For example:
for(i; i< ???; i++) { // Danger!!! }
while(true) { // Danger!!! }
for every user, do something …
when there is no limit on the number of users.
for every item, do something …
when there is no limit on the number of items.
This situation may seem all too familiar, but it carries a significant risk. In Solidity and smart contract, we must steer clear of such code patterns because they spell disaster for scalability.
Some Basic Concepts to Understand
Let’s unpack these concepts:
Ethereum Gas
Think of it as the currency for computational work on the Ethereum network. Every operation exacts a cost in gas, and that cost is passed on to the user.
block.gasLimit
This property is determined by validators on the network, not developers, and not the users! It’s akin to the size of a fuel tank. When a transaction depletes its allocated gas, the transaction reverts. We cannot execute transactions that exceed the block’s gas limit.
The Problem
When a loop consumes more gas than the block’s limit allows, the corresponding transaction fails and doesn’t get added to the blockchain. It’s essential to prioritize bounded loops, especially when updating contract state variables within each iteration. Ideally, it’s best to avoid loops that modify contract state variables altogether.
In essence, scalability challenges arise due to the finite nature of gas and the gasLimit, both of which are integral to the Ethereum ecosystem. Understanding these concepts is crucial when it comes to designing smart contracts, because within one small design mistake we might break our smart contract and expose it to a potential DOS attack (Denial of Service).
Example Scenario
Consider a scenario where a smart contract is designed to distribute rewards to winners of a contest.
function _distribute(address token, address[] memory winners, uint256[] memory percentages, bytes memory data)
internal
{
// token address input check
if (token == address(0)) revert Distributor__NoZeroAddress();
if (!_isWhiteListed(token)) {
revert Distributor__InvalidTokenAddress();
}
// winners and percentages input check
if (winners.length == 0 || winners.length != percentages.length) revert Distributor__MismatchedArrays();
uint256 percentagesLength = percentages.length;
uint256 totalPercentage;
for (uint256 i; i < percentagesLength;) {
totalPercentage += percentages[i];
unchecked {
++i;
}
}
// check if totalPercentage is correct
if (totalPercentage != (10000 - COMMISSION_FEE)) {
revert Distributor__MismatchedPercentages();
}
IERC20 erc20 = IERC20(token);
uint256 totalAmount = erc20.balanceOf(address(this));
// if there is no token to distribute, then revert
if (totalAmount == 0) revert Distributor__NoTokenToDistribute();
uint256 winnersLength = winners.length; // cache length
for (uint256 i; i < winnersLength;) {
uint256 amount = totalAmount * percentages[i] / BASIS_POINTS;
erc20.safeTransfer(winners[i], amount);
unchecked {
++i;
}
}
// send commission fee as well as all the remaining tokens to STADIUM_ADDRESS to avoid dust remaining
_commissionTransfer(erc20);
emit Distributed(token, winners, percentages, data);
}
The contract contains two arrays with a matching length:
- For the winners
- For their respective percentages of the prize pool.
The contract iterates through the percentage array to calculate the total percentages and then iterates through the winners to distribute tokens. This approach is not optimized and can result in high gas consumption.
In more extreme cases, if there are “too many winners” the gas cost of running the distribution function and iterating through 2 huge arrays twice might be bigger than the block gas limit, in that case, rewards can’t be distributed and the distribution function will be broken because it will always be reverted due to out of gas error.
So how do we solve this issue?
One effective approach is to consolidate these multiple loops into a single loop. By doing so, gas usage can be significantly optimized, making the contract more secure and efficient.
An alternative approach could entail the adoption of a pull-over-push methodology. Rather than the contest manager initiating the “reward distribution” by pushing rewards to all the winners, the smart contract maintains a record of the winners, allowing them to individually claim their portions while covering the transaction gas costs themselves. This shift in approach enhances user autonomy and efficiency within the system.
The Tip of The Iceberg
If you’re passionate about understanding the intricacies of Solidity smart contract vulnerabilities, like the one we’ve discussed regarding unbounded loops, and you’re eager to explore more in-depth topics in the realm of blockchain security, there’s an exciting opportunity waiting for you.
Consider enrolling in my Smart Contract Hacking Course. This comprehensive course not only dives into the vulnerabilities we’ve explored today but also covers many other critical aspects of blockchain security, smart contract auditing, and hacking techniques.
By joining this course, you’ll gain valuable knowledge and practical skills that can set you on the path to becoming a certified smart contract hacker. Moreover, as a student, you’ll become part of our exclusive, closed community. Here, you’ll connect with fellow learners, exchange insights, and embark on this exciting journey together.