When a proposal successfully achieves quorum in the VotingBooth contract, a certain portion of the reward is inappropriately locked within the contract.
This issue occurs when the quorum is met for a successful proposal in the VotingBooth contract and totalVotesAgainst is greater than zero causing an amount of Ether (ETH) proportional to totalVotesAgainst to not be distributed to the participants that voted in favor of the proposal. The root cause is traced to the calculation method of rewardPerVoter within the VotingBooth::_distributeRewards() function, where rewardPerVoter is calculated using totalVotes as the divisor instead of totalVotesFor, causing the rewardPerVoter to be too low.
In contrast, for a failed proposal reaching quorum, the entire rewardAmount is appropriately returned to VotingBooth::s_creator.
The following stateful fuzzing test demonstrates that there exists a sequence of votes where, upon reaching quorum, the contract still possesses ETH, which it should not.
Output:
These issues prompted the creation of the following unit tests, which demonstrate that the remaining balance is caused by votes against the proposal.
This test examines the behavior of the booth contract when a proposal passes but has at least one vote against it. In such a scenario, if the bug is present, the contract will not correctly send the ETH to the "For" voters, and the contract will still have ETH in it.
This passing test examines the behavior of the booth contract when all votes are cast in favor of the proposal. In this case, the contract should not have any ETH remaining because the number of "For" votes equals the total number of voters.
These unit tests help identify issues related to the contract's handling of ETH and voting outcomes. Specifically showing how votes Against cause ETH to remain permanently locked in the contract.
Looking at a snippet from VotingBooth::_distributeRewards():
We can see that in first line in the else clause that when rewardPerVoter is calculated totalVotes is used to split the reward instead of totalVotesFor which is the root cause that causes Eth to remain stranded in the contract.
This flaw affects all participants eligible for rewards. As long as there are votes against a proposal, a portion of the reward ETH, proportional to totalVotesAgainst, will invariably remain locked in the contract. Significantly, the absence of a public or external function to retrieve this ETH means that the locked funds become permanently inaccessible.
Invariant Testing with Forge
Comprehensive Unit Testing
In-depth Manual Code Review
To ensure proper distribution of rewards, it is advised to modify the calculation of rewardPerVoter. This can be achieved by dividing the totalRewards by totalVotesFor, rather than totalVotes. The suggested code alteration is as follows:
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.