code-423n4 / 2023-05-ajna-findings

2 stars 0 forks source link

Not allowing for Standard funding with < 0 votes to be executed will lead to almost no funds to be ever executed. #405

Closed code423n4 closed 1 year ago

code423n4 commented 1 year ago

Lines of code

https://github.com/code-423n4/2023-05-ajna/blob/main/ajna-grants/src/grants/base/StandardFunding.sol#L441

Vulnerability details

Impact

Due to the need of proposals.fundingVotesReceived > 0 proposals will be almost impossible to execute if voters use their voting power optimally

Proof of Concept

This is the line which creates a lot of trouble:

if (proposal.fundingVotesReceived < 0) revert InvalidProposalSlate();

In order to understand the core of the problem, we must first note a few key ideas.

Key Idea 1

If there are 3 standard funding proposals (A, B and C), which compete each against each other and no 2 of them can be executed in the same slate due to GBC constraints, giving +10 votes to A and -0 to each of B and C is the same as giving 0 to A and -10 votes to B and -10 votes to C and is also the same as giving +5 votes to A and -5 votes to each of B and C.

Key Idea 2

If a user has 100 voting power and he has to split it between 2 proposals, the highest net-votes impact he can have is if he splits the votes equally between the proposals. With other words, if a^2 + b^2 = 100 the highest value of |a| + |b| (the net-votes impact of a user) is when |a| = |b| This will be explained below in math terms: since a^2 + b^2 = 100 => b^2 = 100 - a^2 => $|b| = \sqrt{100 - a^2}$ Therefore, we can express |a| + |b| as $|a| + \sqrt{100 - a^2}$ in order to find the highest value of $|a| + \sqrt{100 - a^2}$ we need to find |a|'s value in the derivative of the function when it equals to 0 $1 - |a|/\sqrt{100 - a^2} = 0$ or in other words: $|a| = \sqrt{100 - a^2}$ => |a| = |b|

The case is the same for any amount of proposals a user has to split his votes between => he will have the highest net-votes impact if he splits it between all of the proposals. If a user has voting power of 100 and there are proposals A, B, C and D and the user is supporting proposal A, his most optimal way of voting is voting +5 for A and -5 for each of B, C and D. Now, if every proposal is competing for themselves, and no two can be executed simultaneously, the impact will be simply the same as if the user had given his max of 10 votes to project A. However, since more than 1 proposal can be executed together, as long as they don't exceed the GBC constraint, this way of voting is more optimal since if we compare A to B + C + D, A is now 20 votes ahead, rather than the 10 it would've been if the user had put all of his votes for it. The key note we should take from this is that overall this is net-negative voting, as if combine all of the A, B, C and D's votes after the voting they are less than what they were before the user's voting.

In the case where there are 10 proposals, and no 2 can be executed simultaneously due to GBC constraints, and a user with 100 votes/ 10 000 voting power who supports proposal 1 believes all other proposals are relatively-equal competitors, his most optimal way of voting would be +92 votes for his supported proposal and -13 to each of the other ones. Since 92 + 9*(-13) = -25 this is overall net-negative voting too.

This sums up why these net-negative voting strategies are optimal. As long as there isn't a very heavy outlier, and all users vote in their most optimal way, all proposals will end with negative net-votes and no proposal will be executed. Even if there is a outlier, which has just above 0 net-votes, the supporters of the other proposals will know their supported proposals have no chance of making it and can therefore focus on just griefing the outlier making sure it doesn't make it too.

In the end, as long as users are aware of their most optimal voting strategy, an incredibly heavy outlier will need to exist in order for a proposal to have positive net-votes.

Tools Used

Manual review

Recommended Mitigation Steps

Add logic to be able to execute proposals with total votes < 0. One way would be to add the absolute value of the votes of the lowest voted proposal (call it X) to every proposal. When comparing slates, adjusting would have to be made as a slate consisting of 3 proposals will have advantage over a proposal containing only 2 slates, since due to the recommendation one of the slates will receive an extra of 3X while the other will receive only 2X extra.

Assessed type

Math

c4-sponsor commented 1 year ago

MikeHathaway marked the issue as sponsor disputed

Picodes commented 1 year ago

What this report shows is basically that due to the constraint if (proposal.fundingVotesReceived < 0) revert InvalidProposalSlate(); it is relatively easy to block proposals.

However, I think:

c4-judge commented 1 year ago

Picodes changed the severity to QA (Quality Assurance)