Closed sherlock-admin3 closed 4 months ago
This analysis seems wrong to me.
We have this check:
// skip non eligible proposals
if (proposals[i].forVotes < (proposals[i].totalSupply * proposalEligibilityQuorumBps_) / 10_000) {
delete proposals[i];
continue;
}
Which will result in all proposals with low vote counts to not be rewarded at all.
Also worth noting the issue is worded as if users are rewarded for votes, when in fact clients that facilitate votes (and other actions) are rewarded.
@eladmallel if the voter submits 0 votes in proposal but it has enough for votes then it will pas the check, correct? But even if it is so, there's basically no impact? The voters don't get rewards, but can clients get more rewards if there are lots of zero votes collected?
Voters can vote with zero votes. Clients with at least one vote in the period will be included in the list of clients the view function returns.
However, if you plug that list of clients into the reward function, proposals with insufficient votes are ignored.
So I don't understand what real issue arises here?
Yep, I thought something is here, but I now I see that it should be invalid.
Invalidation reason: the comments from the sponsor above.
DenTonylifer
medium
Voter can be rewarded for 0 votes in proposals
Summary
Voter can be rewarded if
proposals.length
> 1 and if he submitted at least 1 vote in 1 proposal.Vulnerability Detail
Attack scenario:
updateRewardsForProposalWritingAndVoting()
is called withuint32[] calldata votingClientIds
as a parameter.require(didClientIdHaveVotes[i], ' ');
because she had 1 vote in second proposal.Let's look at how this can happen step by step:
Voter can cast 0 votes in proposal, as there is no check if
votes == 0
. Then he will be added in arrayds._proposals[proposalId].voteClients[clientId]
, that will be used for rewards distribution:Before rewards distribution, function
getVotingClientIds()
is called to make sure that allclientId
are eligible for rewards. But there is a problem:sumVotes[i]
doesnt guarantees, that user has votes in ALL proposals. If there was 3 proposals,sumVotes[i]
must be at least = 3. Checkif (sumVotes[i] > 0)
is wrong, user with zero votes in all proposals except for one will be unfairy added innonZeroClientIds[]
array:sumVotes[AmelieCliendId]
=proposals[0].voteData[AmelieCliendId].votes = 0
+proposals[1].voteData[AmelieCliendId].votes = 1
+proposals[2].voteData[AmelieCliendId].votes = 0
= 1There is additional check in
Rewards.sol
to prevent rewarding users with zero votes - ifdidClientIdHaveVotes[AmelieCliendId]
is false, then function will revert. But the end of the loop,didClientIdHaveVotes[AmelieCliendId]
will be true. This is because in the second iteration the conditionvotes > 0
is true (becausevotes
is 1), and thus the expressiondidClientIdHaveVotes[AmelieCliendId] || votes > 0
becametrue || true
, which istrue
. After that,didClientIdHaveVotes[AmelieCliendId]
was overwritten totrue
. In the third iteration, the conditionvotes > 0
wasfalse
(becausevotes
is 0), but becausedidClientIdHaveVotes[AmelieCliendId]
was alreadytrue
, the expressiontrue || false
still remainstrue
. Thus,didClientIdHaveVotes[AmelieCliendId]
remainstrue
at the end of the loop:Malicious Amelie passed all checks and received rewards for 3 proposals, however she actually participate only in 1.
Impact
Malicious voter can steal funds due to incorrect rewards distibution. This will happen with a high probability because
updateRewardsForProposalWritingAndVoting()
is very gas expensive and not going to be called frequently, after every proposal, thusproposals.length
can be = 3, 5 or more.Code Snippet
[https://github.com/sherlock-audit/2024-03-nouns-dao-2/blob/main/nouns-monorepo/packages/nouns-contracts/contracts/governance/NounsDAOVotes.sol#L193]() [https://github.com/sherlock-audit/2024-03-nouns-dao-2/blob/main/nouns-monorepo/packages/nouns-contracts/contracts/client-incentives/Rewards.sol#L518]() [https://github.com/sherlock-audit/2024-03-nouns-dao-2/blob/main/nouns-monorepo/packages/nouns-contracts/contracts/client-incentives/Rewards.sol#L425-L427]()
Tool used
Manual Review
Recommendation
Do not allow voters to submit zero votes in
NounsDAOVotes.castVoteInternal()
:And always check that voter has >= 1 votes in all proposals that going to be rewarded in
Rewards.getVotingClientIds()
: