Beginner FriendlyFoundry
100 EXP
View results
Submission Details
Severity: high
Invalid

Distributing rewards immediately after reaching `MIN_QUORUM` gives an advantage to those who voted first, leading to those who have less than 50% voting power to win

Description: The VotingBooth::vote function closes the voting immediately after reaching 51% of voters, which gives an unfair advantage to those who managed to vote first.

@> if (totalCurrentVotes * 100 / s_totalAllowedVoters >= MIN_QUORUM) {
// mark voting as having been completed
s_votingComplete = true;
// distribute the voting rewards
_distributeRewards();
}

It violates the voters' equal rights principle.
Bearing in mind that everyone wants to vote, it is possible to win with ~34% of voting power if there are 3 allowed voters, with 40% of voting power with 5 allowed voters, ~29% of voting power for 7 allowed voters, and finally ~33% of voting power for 9 allowed voters.

For example, imagine there are 5 voters. 2 of them want to vote "against", 3 of the them want to vote "for". If everyone has a possibility to vote, the "for" voters would win, because the have 60% of voting power. But in the current code implementation, if the 2 "against" voters vote first, they create 40% of quorum. If the next "for" voter votes, the quorum reaches 60%, which leads to closing the voting with 2 "against" votes, and 1 "for" vote. As a result, the outcome is not appropriate to what all voters want, which destroys democracy and justice.

This can have two outcomes:

  1. The "for" voter can understand that his/her vote will reach the MIN_QUORUM, and they will lose anyway, so the voter will not vote at all. It will stop the voting process.

  2. The "for" voter will vote, but his/her vote will have no power.

Note This attack can be also implemented by frontrunning.

Impact: Voting can be closed in favor of those, who has lower voting power.

Proof of Concept:

  1. All voters who has less voting power, but at least 33%, vote first.

  2. Any other voters vote until reaching MIN_QUORUM.

  3. After reaching the MIN_QUORUM, the first voters win.

Proof of Code:

Code

Place the following into VotingBoothTest.t.sol

function testPeopleWith34PercentPowerWinTheVoting() public {
uint256 ETH_REWARD = 3e18;
deal(address(this), ETH_REWARD);
address[] memory voters = new address[](9);
voters[0] = address(0x1); // against voter
voters[1] = address(0x2); // against voter
voters[2] = address(0x3); // against voter
voters[3] = address(0x4); // for voter
voters[4] = address(0x5); // for voter
voters[5] = address(0x6); // for voter
voters[6] = address(0x7); // for voter
voters[7] = address(0x8); // for voter
voters[8] = address(0x9); // for voter
console.log("Voting power against: ~", 3*100/voters.length, "%");
console.log("Voting power for: ~", 6*100/voters.length, "%");
// Voting power against: ~ 33 %
// Voting power for: ~ 66 %
booth = new VotingBooth{value: ETH_REWARD}(voters);
vm.prank(address(0x1));
booth.vote(false);
vm.prank(address(0x2));
booth.vote(false);
vm.prank(address(0x3));
booth.vote(false);
vm.prank(address(0x4));
booth.vote(true);
vm.prank(address(0x5));
booth.vote(true);
// All money went to the creator even though "against" voters have ~33% of voting power
assert(!booth.isActive());
assert(address(this).balance == ETH_REWARD);
}

Recommended Mitigation: Remove the automatic voting closing after the MIN_QUORUM has been reached. Add manual voting closure by anyone or a trusted creator after the specific publicly known deadline. Thus every voter's vote will matter.

Updates

Lead Judging Commences

0xnevi Lead Judge
over 1 year ago
0xnevi Lead Judge over 1 year ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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