• YouTube Channel
  • System Status
  • VS Code Extension
  • Increasing Length Array as Loop Variable

    Overview

    What is the Increasing Length Array as Loop Variable vulnerability?

    Solidity, like many programming languages, offers both looping constructs and array data types. It is a relatively common pattern in many languages to use the length of an array as the control variable for a loop, iterating over each entry of the array within the loop body. In the context of Ethereum, unlike many other programmable systems, there is a possibility that a transaction may run out of gas and fail to complete its computation, which may particularly be an issue when processing loops, if the bound of the loop grows too large. Thus, loops that use an array length that can grow but never decrease may be at risk of gas-related denial of service.

    Further reading: Solidity Documentation: Array Members

    Technical example of vulnerable code

      // SPDX-License-Identifier: Unlicense
      pragma solidity ^0.8.0;
    
      contract VotingSystem {
          struct Candidate {
              string name;
              uint256 voteCount;
          }
    
          Candidate[] public candidates;
    
          mapping(address => bool) public hasVoted;
    
          // Add a new candidate to the system
          function addCandidate(string calldata _name) external {
              candidates.push(Candidate({name: _name, voteCount: 0}));
          }
    
          // Vote for a candidate
          function vote(uint256 _candidateIndex) external {
              require(!hasVoted[msg.sender], "You have already voted.");
              require(_candidateIndex < candidates.length, "Invalid candidate index.");
    
              candidates[_candidateIndex].voteCount += 1;
              hasVoted[msg.sender] = true;
          }
    
          // Function to tally votes for each candidate (can become stuck due to gas limits)
          function tallyVotes() external view returns (string memory winnerName) {
              uint256 highestVoteCount = 0;
              for (uint256 i = 0; i < candidates.length; i++) {
                  if (candidates[i].voteCount > highestVoteCount) {
                      highestVoteCount = candidates[i].voteCount;
                      winnerName = candidates[i].name;
                  }
              }
          }
      }
    
    

    In the example above, the VotingSystem contract implements a way of tallying votes for candidates on-chain; in practice, this could be as part of a Decentralized Autonomous Organization (DAO). The function tallyVotes() loops over all of the candidates recorded in the contract, using the .length property of the candidates array to control the loop; however, this array can only grow in size and cannot decrease, meaning if too many candidates are added to the array, the tally will never succeed due to gas limitations.

    Technical example of how to fix the vulnerability

      // SPDX-License-Identifier: Unlicense
      pragma solidity ^0.8.0;
    
      contract VotingSystemUpdated {
          struct Candidate {
              string name;
              uint256 voteCount;
          }
    
          Candidate[] public candidates;
          // Track the current leading candidate's index and vote count for efficient access
          uint256 private leadingCandidateIndex;
          uint256 private leadingVoteCount;
    
          mapping(address => bool) public hasVoted;
    
          constructor() {
              // Initialize leading candidate index and vote count to an impossible condition
              leadingCandidateIndex = type(uint256).max;
              leadingVoteCount = 0;
          }
    
          // Add a new candidate to the system
          function addCandidate(string calldata _name) external {
              candidates.push(Candidate({name: _name, voteCount: 0}));
              // Initialize leading candidate if this is the first candidate being added
              if (leadingCandidateIndex == type(uint256).max) {
                  leadingCandidateIndex = 0;
                  leadingVoteCount = 0;
              }
          }
    
          // Vote for a candidate
          function vote(uint256 _candidateIndex) external {
              require(!hasVoted[msg.sender], "You have already voted.");
              require(_candidateIndex < candidates.length, "Invalid candidate index.");
    
              candidates[_candidateIndex].voteCount += 1;
              hasVoted[msg.sender] = true;
    
              // Update the current leader if this candidate now has more votes
              if (candidates[_candidateIndex].voteCount > leadingVoteCount) {
                  leadingCandidateIndex = _candidateIndex;
                  leadingVoteCount = candidates[_candidateIndex].voteCount;
              }
          }
    
          // Function to get the current leading candidate, avoiding the loop
          function getCurrentLeader() external view returns (string memory winnerName, uint256 voteCount) {
              // Ensure there is at least one candidate
              require(leadingCandidateIndex != type(uint256).max, "No candidates available.");
              return (candidates[leadingCandidateIndex].name, leadingVoteCount);
          }
      }
    
    

    In the revised example, contract VotingSystemUpdated has reworked logic to track the leading candidate when the vote() function is called, rather than using a loop over the candidates array in the previous example, avoiding the possibility of the loop running out of gas.