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.