Beginner FriendlyFoundry
100 EXP
View results
Submission Details
Severity: high
Valid

`rewardPerVoter` is wrongly diminished by `Against` votes

Summary

According to the comments that point out how the rewards should be distributed, in case a proposal fails the ETH reward is sent back to the creator and if a proposal succeeds the ETH reward is divided between the For voters. The problem is the code is not working as intended because the rewardPerVoter is computed by dividing the totalRewards to the totalVotes.

Vulnerability Details

The protocol implements the reward distribution as follows:

uint256 totalRewards = address(this).balance;
// if the proposal was defeated refund reward back to the creator
// for the proposal to be successful it must have had more `For` votes
// than `Against` votes
if (totalVotesAgainst >= totalVotesFor) {
// proposal creator is trusted to create a proposal from an address
// that can receive ETH. See comment before declaration of `s_creator`
_sendEth(s_creator, totalRewards);
}
// otherwise the proposal passed so distribute rewards to the `For` voters
else {
uint256 rewardPerVoter = totalRewards / totalVotes;
for (uint256 i; i < totalVotesFor; ++i) {
...

The rewardPerVoter is wrongly divided by totalVotes, the consequences can be observed in the test attached below.
Copy the following code into a new test file:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;
import {Test, console} from "forge-std/Test.sol";
import {StdInvariant} from "forge-std/StdInvariant.sol";
import {VotingBooth} from "../src/VotingBooth.sol";
contract TestVotingBooth is StdInvariant, Test {
uint256 constant AMOUNT = 100e18;
uint256 constant VOTING_ADDRESSES = 7;
address[] voters;
VotingBooth booth;
function setUp() public {
// deal this contract the proposal reward
deal(address(this), AMOUNT);
address[] memory participants = new address[](VOTING_ADDRESSES);
address participant;
for (uint160 i = 1; i < VOTING_ADDRESSES + 1;) {
participant = address(i);
participants[i - 1] = participant;
unchecked {
++i;
}
}
for (uint256 j; j < participants.length;) {
targetSender(participants[j]);
unchecked {
++j;
}
}
// set target contract
booth = new VotingBooth{value: AMOUNT}(participants);
targetContract(address(booth));
}
function invariant_dacianTheGreatestFuzzer() public {
if (booth.isActive()) {
assertEq(address(booth).balance, AMOUNT);
} else {
assertEq(address(booth).balance, 0);
}
}
}

Run it with the following command:

forge test --mt invariant_dacianTheGreatestFuzzer -vvv

Yielding the following result (copied only the relevant part):

[22342] TestVotingBooth::invariant_dacianTheGreatestFuzzer()
├─ [2292] VotingBooth::isActive() [staticcall]
│ └─ ← false
├─ emit log(val: "Error: a == b not satisfied [uint]")
├─ emit log_named_uint(key: " Left", val: 25000000000000000000 [2.5e19])
├─ emit log_named_uint(key: " Right", val: 0)
├─ [0] VM::store(VM: [0x7109709ECfa91a80626fF3989D68f67F5b1DD12D], 0x6661696c65640000000000000000000000000000000000000000000000000000, 0x0000000000000000000000000000000000000000000000000000000000000001)
│ └─ ← ()
└─ ← ()
Test result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 11.59s
Ran 1 test suites: 0 tests passed, 1 failed, 0 skipped (1 total tests)
Failing tests:
Encountered 1 failing test in test/newTest.t.sol:TestVotingBooth
[FAIL. Reason: <no data>]
[Sequence]
sender=0x0000000000000000000000000000000000000007 addr=[src/VotingBooth.sol:VotingBooth]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f calldata=vote(bool) args=[false]
sender=0x0000000000000000000000000000000000000003 addr=[src/VotingBooth.sol:VotingBooth]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f calldata=vote(bool) args=[true]
sender=0x0000000000000000000000000000000000000007 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=0x0000000000000000000000000000000000000005 addr=[src/VotingBooth.sol:VotingBooth]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f calldata=vote(bool) args=[false]
sender=0x0000000000000000000000000000000000000003 addr=[src/VotingBooth.sol:VotingBooth]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f calldata=vote(bool) args=[true]
sender=0x0000000000000000000000000000000000000001 addr=[src/VotingBooth.sol:VotingBooth]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f calldata=vote(bool) args=[false]
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]
invariant_dacianTheGreatestFuzzer() (runs: 10000, calls: 149994, reverts: 115100)
Encountered a total of 1 failing tests, 0 tests succeeded
[22342] TestVotingBooth::invariant_dacianTheGreatestFuzzer()
├─ [2292] VotingBooth::isActive() [staticcall]
│ └─ ← false
├─ emit log(val: "Error: a == b not satisfied [uint]")
├─ emit log_named_uint(key: " Left", val: 25000000000000000000 [2.5e19])
├─ emit log_named_uint(key: " Right", val: 0)
├─ [0] VM::store(VM: [0x7109709ECfa91a80626fF3989D68f67F5b1DD12D], 0x6661696c65640000000000000000000000000000000000000000000000000000, 0x0000000000000000000000000000000000000000000000000000000000000001)
│ └─ ← ()
└─ ← ()
Test result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 11.59s
Ran 1 test suites: 0 tests passed, 1 failed, 0 skipped (1 total tests)
Failing tests:
Encountered 1 failing test in test/newTest.t.sol:TestVotingBooth
[FAIL. Reason: <no data>]
[Sequence]
sender=0x0000000000000000000000000000000000000007 addr=[src/VotingBooth.sol:VotingBooth]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f calldata=vote(bool) args=[false]
sender=0x0000000000000000000000000000000000000003 addr=[src/VotingBooth.sol:VotingBooth]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f calldata=vote(bool) args=[true]
sender=0x0000000000000000000000000000000000000007 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=0x0000000000000000000000000000000000000005 addr=[src/VotingBooth.sol:VotingBooth]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f calldata=vote(bool) args=[false]
sender=0x0000000000000000000000000000000000000003 addr=[src/VotingBooth.sol:VotingBooth]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f calldata=vote(bool) args=[true]
sender=0x0000000000000000000000000000000000000001 addr=[src/VotingBooth.sol:VotingBooth]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f calldata=vote(bool) args=[false]
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]
invariant_dacianTheGreatestFuzzer() (runs: 10000, calls: 149994, reverts: 115100)
Encountered a total of 1 failing tests, 0 tests succeeded

As we can see the contract has a balance of 25e18 ETH which will be permanently stuck.

Impact

Funds stuck in the contract.

For voters will receive less rewards compared to what they should.

Tools Used

Manual review + forge testing

Recommendations

Amend the code as follows:

if (totalVotesAgainst >= totalVotesFor) {
// proposal creator is trusted to create a proposal from an address
// that can receive ETH. See comment before declaration of `s_creator`
_sendEth(s_creator, totalRewards);
}
// otherwise the proposal passed so distribute rewards to the `For` voters
else {
-- uint256 rewardPerVoter = totalRewards / totalVotes;
++ uint256 rewardPerVoter = totalRewards / totalVotesFor;
for (uint256 i; i < totalVotesFor; ++i) {
Updates

Lead Judging Commences

0xnevi Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

VotingBooth._distributeRewards(): Incorrect computation of rewardPerVoter

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.