In the VotingBooth::_distributeRewards function the rewardPerVoter is not calculated correctly and the for voters can receive less rewards than they should receive.
After the proposal is passed the rewards should be distributed among the for voters and after that the contract balance should be zero. The VotingBooth::vote function calls VotingBooth::_distributeRewards function that checks if the proposal pass or not and calculates the rewardPerVoter, this is the reward that the for voter should receive. But in this calculation the totalRewards are divided by totalVotes instead of totalVotesFor. In the calculation for the last voter is used also totalVotes. The totalVotes are all voters that vote with for and against. Therefore, the for voter will receive less rewards than it is intended if there are against voters and the proposal is passed. The against voters do not receive rewards, therefore there are rewards in the contract balance after the distribution. If all voters are for voters the rewards are distributed correctly. The following code shows the places with wrong calculation:
If we have 9 allowed voters, the quorum is reached at the 5th voter. So, let's consider we have 5 votes: 2 false and 3 true. The proposal passes and the for voters should receive rewards and the balance of the contract should be zero after that. But the calculation of the rewardPerVoter in the VotingBooth::_distributeRewards function includes also the against voters and their rewards remains in the contract after the distribution. And the for voters receive less rewards than they should receive.
In the test file VotingBoothTest.t.sol the test case that demonstrates the rewards distribution among voters uses only true votes. But the described problem arises when one or more of the votes are false and the proposal passes. The following test function testVotePassesAllRewardsDistributed demonstrates this problem. The function can be added to the test file and executed by Foundry command: forge test --match-test "testVotePassesAllRewardsDistributed" -vvv.
This test shows that the rewards in the contract after the distribution among the for voters are: rewards in contract after distribution 4000000000000000000. But the intended behavior of the protocol is that if the proposol passes and the rewards are distributed among the for voters, the rewards in the contract after that should be zero. But the assertion address(booth).balance == 0 in the showed test fails. That is because the wrong logic in the calculation of rewardPerVoter. In this calculation the rewards are divided by the number of all voters instead of the number of the for voters. Only the for voters receive rewards. Therefore the part of the rewards that is calculated for the rest voters (against voters) remains in the contract. In that way the for voters receive less reward than they actually should receive.
I found this issue by manual review and unit test, but the task was to write fuzz test. I'm not sure if my fuzz test is written in the correct way (it is my first attempt to write fuzz test). It found the issue. And I write this explanation, because this is 'learning' audit and I want to increase my knowledges in testing. I believe that the unit test is enough for the proof of this issue.
Here is my fuzz test:
This test can be added to the test file of the project and executed through the command: forge test --match-test "testFuzzVote" -vvvv.
The test function checks several states of the protocol:
If the voting ends after the quorum is reached.
If rewards match starting balance if the against votes are more or equal to the for voters
If the contract's balance is zero after rewards distribution.
If the balance of the contract is greather than 0 when the proposal is active.
And the test fails at the assertion that the contract's balance is zero after rewards distribution.
Manual Review, Foundry
Replace the totalVotes with totalVotesFor in calculation rewardPerVoter in the VotingBooth::_distributeRewards function.
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.