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:
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
A large number of voters (e.g., 1000 or more) participate in the voting process.
The contract calls selectPresident
after the voting period has ended.
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]);
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);
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);
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]);
}
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; ) {
unchecked {
i++;
}
}