_distributeRewards() attempts to get rid of any dust amount in the contract by awarding 1 wei (rounding up) to the last for voter. This however, is not a sufficient check and can still cause dust to remain, because it could be more than 1 wei.
This dust is then stuck & not redeemable in any way since the contract lacks any direct withdrawal or transfer or selfDestruct function.
The protocol incorrectly assumes that rounding up the reward amount for the last for voter is sufficient to get rid of a dust amount which would always be 1 wei. This is not the case for various combinations of totalRewards
and totalVotes
. Consider the following examples:
testVotePassesAndMoneyIsSent()
but with ETH_REWARD
as 20e18
:We'll follow the exact steps in the pre-existing test testVotePassesAndMoneyIsSent(), with just one difference: Change the reward amount or ETH_REWARD
from 10e18
to 20e18
-
Suppose s_totalAllowedVoters
= 5
Votes needed to reach quorum = 3 votes (Since )
Let us have totalRewards
= 20 ether ( = 20e18 )
3 voters vote, all of them for, and then rewards are distributed, i.e. totalVotes = 3
and also totalVotesFor = 3
. Let's call this the divisor
.
First 2 voters get , where i.e. as per L192
voter gets (6666666666666666666 + 1) wei = 6666666666666666667 wei as per L207
1 wei of dust now stuck in the contract ( wei).
Suppose s_totalAllowedVoters
= 9
Votes needed to reach quorum = 5 votes (Since )
Let us have totalRewards
= 1 ether + 4 wei
5 voters vote, all of them for, and then rewards are distributed, i.e. totalVotes = 5
and also totalVotesFor = 5
. Let's call this the divisor
.
First 4 voters get , where i.e. as per L192
voter gets (0.2 ether + 1 wei) as per L207
3 wei of dust now stuck in the contract.
The above problem will occur for all situations where leaves a remainder . The maximum amount of dust which can be stuck is 3 wei, for which divisor
needs to be 5 and totalRewards
should be , where is some integer such that i.e. .
Make the following change in test/VotingBoothTest.t.sol
and run forge test --mt testVotePassesAndMoneyIsSent -vv
. The assertion inside the pre-existing test testVotePassesAndMoneyIsSent() will now fail because 1 wei of dust still remains after distributing rewards. This is because wei, not 1 wei.
We can use fuzz testing to let foundry figure out values which will break the invariant of "no dust remains in the contract". We'll design a fuzz test which tests different combinations of the following params:
totalRewardEth
numberOfVoters (total voters
elgible to vote)
Save the following code inside a new file test/VotingBoothTestFuzzer.t.sol
and run via forge test --mt testFuzz_DustRemains -vv
.
We'll run the fuzz test a couple of times so that we can see multiple counterexamples :
Output of run:
Output of run:
Undistributed dust remains in the contract even after rewards have been distributed to the for voters.
Submitting this as Medium impact because it can be seen that the protocol has taken clear steps in L206-208 to ensure that no dust remains in the contract, but the logic fails to achieve the same.
Foundry, manual inspection.
Make the following change. This will result in the last for voter receiving their rewards + all the dust amount:
NOTE: The above fix is a partial fix due to another bug which exists in the contract (raised separately) titled: Incorrect calculation for rewardPerVoter
, where we see that the rewardPerVoter
in L192 should be calculated by dividing with totalVotesFor
instead of totalVotes
. Once that is fixed, the above recommendation works well. If the above recommendation is applied without that fix, then the last for voter could be benefitted unfairly and would get rewards much higher than the others (This will happen in cases where totalVotesFor
totalVotes
). Hence, this recommendation needs to be seen in conjunction with that fix.
It is also to be noted that this particular issue exists independently even after that fix (to bug Incorrect calculation for rewardPerVoter
) has been applied, so can't be ignored.
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.