President Elector

First Flight #24
Beginner FriendlyFoundry
100 EXP
View results
Submission Details
Severity: medium
Invalid

High Gas Consumption in selectPresident and _selectPresidentRecursive May Cause Transaction Failure DoS

Summary

The RankedChoice.sol contract contains an inefficiency in the selectPresident and _selectPresidentRecursive functions. The loops that iterate over voters and candidates result in high gas consumption, particularly in scenarios with a large number of voters or candidates, which could lead to transaction failures or inefficiencies.

Vulnerability Details

Affected code:

  • selectPresident function

  • _selectPresidentRecursive function

In the selectPresident function, the following loop iterates over all voters and their ordered candidate selections:

for (uint256 i = 0; i < VOTERS.length; i++) {
address[] memory orderedCandidates = s_rankings[VOTERS[i]][s_voteNumber];
for (uint256 j = 0; j < orderedCandidates.length; j++) {
if (!_isInArray(s_candidateList, orderedCandidates[j])) {
s_candidateList.push(orderedCandidates[j]);
}
}
}

Similarly, in the _selectPresidentRecursive function, the contract loops over the candidate list and compares their vote counts in multiple rounds:

for (uint256 i = 1; i < candidateList.length; i++) {
uint256 votes = s_candidateVotesByRound[candidateList[i]][s_voteNumber][roundNumber];
if (votes < fewestVotes) {
fewestVotes = votes;
fewestVotesCandidate = candidateList[i];
}
}

These loops are dependent on the length of VOTERS and s_candidateList, potentially leading to gas inefficiencies. In scenarios where there are a large number of voters or candidates, the contract may consume excessive gas, which can cause the transaction to fail or make it unfeasible to elect a new president.

Exploit Scenario

  1. A large number of voters (e.g., 1000 or more) participate in the voting process.

  2. The contract calls selectPresident after the voting period has ended.

  3. Due to the high gas consumption from iterating over all voters and candidates, the transaction fails or consumes a significant amount of gas.

Impact

The primary impact of this inefficiency is potential Denial of Service (DoS) in the selectPresident function due to excessive gas consumption, which could make it impossible to select a new president in the election. Additionally, this inefficiency can result in significant gas costs even for successful transactions, making the election process costly in large-scale deployments.

Proof of Concept

The following gas test was designed to measure the gas consumption during the voting and president selection process with 300 voters using different candidates.

function testGasConsumption() public {
assert(rankedChoice.getCurrentPresident() != candidates[0]);
// Simulación con 300 votantes
console.log("Gas for first 300 voters");
orderedCandidates = [
candidates[0], candidates[1], candidates[2], candidates[3], candidates[4],
candidates[5], candidates[6], candidates[7], candidates[8], candidates[9]
];
uint256 startingIndex = 0;
uint256 endingIndex = 300;
uint256 gasBeforeFirstWave = gasleft();
for (uint256 i = startingIndex; i < endingIndex; i++) {
vm.prank(voters[i]);
rankedChoice.rankCandidates(orderedCandidates);
}
uint256 gasAfterFirstWave = gasleft();
uint256 gasUsedFirst = gasBeforeFirstWave - gasAfterFirstWave;
console.log("Gas for 300 voters:", gasUsedFirst);
// Un último votante para verificar el comportamiento
orderedCandidates = [
candidates[0], candidates[11], candidates[12], candidates[13], candidates[14],
candidates[15], candidates[16], candidates[17], candidates[18], candidates[19]
];
vm.prank(voters[301]);
uint256 gasBeforeLastVote = gasleft();
rankedChoice.rankCandidates(orderedCandidates);
uint256 gasAfterLastVote = gasleft();
uint256 gasUsedLast = gasBeforeLastVote - gasAfterLastVote;
console.log("Gas for last voter after 300 votes:", gasUsedLast);
// Simulación de selección de presidente
vm.warp(block.timestamp + rankedChoice.getDuration());
uint256 gasBeforeSelectPresident = gasleft();
rankedChoice.selectPresident();
uint256 gasAfterSelectPresident = gasleft();
uint256 gasUsedSelectPresident = gasBeforeSelectPresident - gasAfterSelectPresident;
console.log("Gas used for electing president with 300 voters:", gasUsedSelectPresident);
assertEq(rankedChoice.getCurrentPresident(), candidates[0]);
}
/// TEST OUTPUT:
Gas for 300 voters:137685980
Gas for last voter after 300 votes: 138187473
Gas used for electing president with 300 voters: 40977599

Tools Used

Manual review - Foundry

Recommendations

Loop Optimization

One key area for improvement is reducing gas consumption in loops. Instead of repeatedly accessing a storage variable in each iteration, it’s more efficient to store it in a local variable:

- for (uint256 i = 0; i < VOTERS.length; i++) {
+ uint256 votersLength = VOTERS.length;
+ for (uint256 i = 0; i < votersLength; i++) {
- for (uint256 j = 0; j < s_rankings[VOTERS[i]][s_voteNumber].length; j++) {
+ uint256 voteRankingLength = s_rankings[VOTERS[i]][s_voteNumber].length;
+ for (uint256 j = 0; j < voteRankingLength; j++) {

Using unchecked

To further save gas during increment operations, it is recommended to use unchecked where there is no risk of overflow, such as in loops that are controlled by external logic:

for (uint256 i = 0; i < length; ) {
// loop logic
unchecked {
i++;
}
}

Updates

Lead Judging Commences

inallhonesty Lead Judge 9 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

[INVALID] A high number of voters can lead to OOG in selecting the president

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.