The gas war
January 11, 2024•870 words
Introduction
I thought I had a high.
I participated in the Voting Booth bug bounty and reported a high-rated vulnerability.
But it was rejected.
Because I did not read a key piece of information that rendered all my thought process completely useless.
Let's see what it was. Let us reopen the imaginary wound.
The source code
You can find the source code on my other blog post, the triumphant one about the bug that was, indeed, approved and validated. It is here, if you are interested.
What follows is my actual report to the platform, the very submission I sent to the project.
Summary
The voting mechanism implemented determines the output of a vote as soon as the quorum is reached. With this approach, only the first votes up to the quorum threshold have meaning for the output of the poll. If everyone of the allowed voters wishes to vote, they will have to force their transactions among the first votes, or whatever preference they expressed will not matter to the overall result. In order to be selected among the first voters, they will have to pay higher gas fees, meaning the result of the vote is strongly correlated to the wealth and amount of money each voter has, instead of the real reflection of the will of the voting pool.
Vulnerability Details
The comments in the VotingBooth.sol::vote
function state:
// check if quorum has been reached. Quorum is reached
// when at least 51% of the total allowed voters have cast
// their vote. For example if there are 5 allowed voters:
//
// first votes For
// second votes For
// third votes Against
//
// Quorum has now been reached (3/5) and the vote will pass as
// votesFor (2) > votesAgainst (1).
// @auditor what if the remaining two votes (only one necessary, actually) were against but were not included in the block? The output of the poll would be opposite
// This system of voting doesn't require a strict majority to
// pass the proposal (it didn't require 3 For votes), it just
// requires the quorum to be reached (enough people to vote)
As clearly stated by my comment marked with the keyword @auditor
, the example scenario shows the main flaw of this voting mechanism: if voters do not outbid each other on gas fees, the result of the vote can be completely different from the actual reflection of the will of the voters.
To represent the issue more schematically, let's consider the example discussed in the provided comments:
- five voters are allowed to vote;
- with the quorum set at 51%, this means that after the third vote is cast, the remaining two votes are irrelevant, as the result of the poll is computed and the opportunity to vote is de-activated;
- all users wishing to vote have to make sure they are picked in the next block, and before - at least - two of the other voters, or their preference will not matter;
- to make sure their transaction is inserted before other voters, they must be willing to pay higher gas fees;
- users capable of spending more can determine the output of the poll, even if their preference does not match the actual majority.
Impact
A smaller group of voters with larger financial possibilities can determine the result of the entire poll, if they represent the majority of the preferences when the vote counter reaches the quorum.
Tools Used
Manual review, VSCode
Recommendations
Implementing a minimum time threshold to allow everyone to vote represents a potential fix to the issue.
The mechanism of determining the result after the quorum is reached can be maintained, but only if a reasonable amount of time to vote is guaranteed: this way, all users willing to vote can manage to express their preference and determine the result of the poll.
What went wrong
The bug was rejected.
When reading the documentation, at the start of the review, I didn't pay enough attention to the fact that the project would be deployed on Arbitrum.
This plays all the difference in the world, for this very kind of bugs.
As a matter of fact, Arbitrum does not have the exact same structure as Ethereum, being a Layer 2 solution: the key point, in this case, is the fact that it does not have a mempool. Arbitrum, like other Layer 2 solutions, uses a sequencer that puts transactions in a batch and later submits them to be recorded on the Layer 1 chain.
What changes is that influencing the sequencer is not as straightforward as paying higher gas fees: the inner workings of the sequencer are more "opaque", and there is no guarantee that a higher fee will mean that the transaction is included before others. The sequencer might follow different ordering algorithms, prioritizing other transactions due to internal policies.
This makes the attack described above more difficult to implement, as the attacker cannot be certain that they will be able to front-run other users of the protocol.
Which also means, no High-vulnerability to brag about online.
As Winnie the Pooh would say, Oh Bother ;)