Summary
If the 51% quorum is reached and the for
voters are more than the against
voters, the contract balance should be sent to the for
voters.
Vulnerability Details
The vulnerability in the contract is because of the following calculation in the _distributedRewards()
function:
uint256 rewardPerVoter = totalRewards / totalVotes;
....
_sendEth(s_votersFor[i], rewardPerVoter);
This formula calculates the rewardPerVoter
based on the totalVotes
. But in case the for
voters win, the rewardPerVoter
is sent only to the for
voters. Remaining amount is left in the VotingBooth
contract. This can happen if majority (>50%) of the voters vote for
and 1 or more voters vote against
.
Impact
In the above scenario, some of the balance remains in the contract and for
voters receive less than the expected amount.
Below is the statefulFuzz test to prove the vulnerability:
pragma solidity ^0.8.23;
import {VotingBooth} from "../src/VotingBooth.sol";
import {Test, console, console2} from "forge-std/Test.sol";
import {StdInvariant} from "forge-std/StdInvariant.sol";
contract Invariant is StdInvariant, Test {
uint256 constant ETH_REWARD = 10e18;
address[] public voters = [ address(0x1), address(0x2), address(0x3), address(0x4), address(0x5), address(0x6), address(0x7), address(0x8), address(0x9)];
VotingBooth votingBooth;
function setUp() public {
votingBooth = new VotingBooth{value: ETH_REWARD}(voters);
for (uint256 i = 0; i < voters.length; i++) {
targetSender(voters[i]);
}
targetContract(address(votingBooth));
}
function statefulFuzz_CheckIfVotingBalanceAlwaysZero() public view {
if (!votingBooth.isActive()) {
console.log("Voting is completed!");
console.log(address(votingBooth).balance);
assert((address(votingBooth).balance) == 0);
}
}
}
Foundry.toml:
[fuzz]
runs = 100
seed = "0x1"
[invariant]
runs = 20
depth = 10
fail_on_revert = false
As it can be seen in the output of the test, when there is one or more against
votes there is some amount remaining in the contract balance:
$forge test --mt statefulFuzz_CheckIfVotingBalanceAlwaysZero -vv
[⠰] Compiling...
No files changed, compilation skipped
Running 1 test for test/Invariant.t.sol:Invariant
[FAIL. Reason: panic: assertion failed (0x01)]
[Sequence]
sender=0x0000000000000000000000000000000000000006 addr=[src/VotingBooth.sol:VotingBooth]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f calldata=vote(bool) args=[false]
sender=0x0000000000000000000000000000000000000002 addr=[src/VotingBooth.sol:VotingBooth]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f calldata=vote(bool) args=[true]
sender=0x0000000000000000000000000000000000000003 addr=[src/VotingBooth.sol:VotingBooth]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f calldata=vote(bool) args=[true]
sender=0x0000000000000000000000000000000000000004 addr=[src/VotingBooth.sol:VotingBooth]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f calldata=vote(bool) args=[true]
sender=0x0000000000000000000000000000000000000005 addr=[src/VotingBooth.sol:VotingBooth]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f calldata=vote(bool) args=[false]
statefulFuzz_CheckIfVotingBalanceAlwaysZero() (runs: 20, calls: 199, reverts: 100)
Logs:
Voting is completed!
4000000000000000000
Test result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 29.84ms
Ran 1 test suites: 0 tests passed, 1 failed, 0 skipped (1 total tests)
Failing tests:
Encountered 1 failing test in test/Invariant.t.sol:Invariant
[FAIL. Reason: panic: assertion failed (0x01)]
[Sequence]
sender=0x0000000000000000000000000000000000000006 addr=[src/VotingBooth.sol:VotingBooth]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f calldata=vote(bool) args=[false]
sender=0x0000000000000000000000000000000000000002 addr=[src/VotingBooth.sol:VotingBooth]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f calldata=vote(bool) args=[true]
sender=0x0000000000000000000000000000000000000003 addr=[src/VotingBooth.sol:VotingBooth]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f calldata=vote(bool) args=[true]
sender=0x0000000000000000000000000000000000000004 addr=[src/VotingBooth.sol:VotingBooth]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f calldata=vote(bool) args=[true]
sender=0x0000000000000000000000000000000000000005 addr=[src/VotingBooth.sol:VotingBooth]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f calldata=vote(bool) args=[false]
statefulFuzz_CheckIfVotingBalanceAlwaysZero() (runs: 20, calls: 199, reverts: 100)
Encountered a total of 1 failing tests, 0 tests succeeded
Tools Used
Recommendations
uint256 rewardPerVoter = totalRewards / totalVotes;
totalVotes
can be replaced with totalVotesFor