Summary
The castVote
function allows the proposer of a governance proposal to vote on their own proposal. This creates a potential exploit where proposers with large voting power can arbitrarily create and pass proposals without fair community participation. Additionally, this opens the system to governance attacks where a single entity can dominate decision-making.
Vulnerability Details
The vulnerability stems from the fact that any veToken holder, including the proposer, is allowed to vote on their own proposals. Since vote weight is determined by veToken holdings, a proposer with significant voting power can submit a proposal and single-handedly pass it without external checks.
Code Reference
In the castVote
function, there is no check to prevent the proposer from voting:
function castVote(uint256 proposalId, bool support) external override returns (uint256) {
ProposalCore storage proposal = _proposals[proposalId];
if (proposal.startTime == 0) revert ProposalDoesNotExist(proposalId);
if (block.timestamp < proposal.startTime) {
revert VotingNotStarted(proposalId, proposal.startTime, block.timestamp);
}
if (block.timestamp > proposal.endTime) {
revert VotingEnded(proposalId, proposal.endTime, block.timestamp);
}
ProposalVote storage proposalVote = _proposalVotes[proposalId];
if (proposalVote.hasVoted[msg.sender]) {
revert AlreadyVoted(proposalId, msg.sender, block.timestamp);
}
uint256 weight = _veToken.getVotingPower(msg.sender);
if (weight == 0) {
revert NoVotingPower(msg.sender, block.number);
}
proposalVote.hasVoted[msg.sender] = true;
if (support) {
proposalVote.forVotes += weight;
} else {
proposalVote.againstVotes += weight;
}
emit VoteCast(msg.sender, proposalId, support, weight, "");
return weight;
}
The issue arises because msg.sender is not checked against the proposer of the proposal:
ProposalCore storage proposal = _proposals[proposalId];
Also seeing as the state of a proposal (if suceeded) is entirely dependent on meeting quorum
which is based on total voting weights, it means a central authority can take over.
ProposalVote storage proposalVote = _proposalVotes[proposalId];
uint256 currentQuorum = proposalVote.forVotes + proposalVote.againstVotes;
uint256 requiredQuorum = quorum();
if (currentQuorum < requiredQuorum || proposalVote.forVotes <= proposalVote.againstVotes) {
return ProposalState.Defeated;
}
Impact
Governance Centralization: A single entity with a large veToken balance can propose and approve changes without resistance.
Protocol Manipulation: Malicious actors can create and pass self-beneficial proposals, such as withdrawing funds, changing governance rules, or bypassing security constraints.
Loss of Decentralization: Community members lose meaningful participation in governance as large stakeholders control outcomes.
Tools Used
Recommendations
Fixed Code:
function castVote(uint256 proposalId, bool support) external override returns (uint256) {
ProposalCore storage proposal = _proposals[proposalId];
if (proposal.startTime == 0) revert ProposalDoesNotExist(proposalId);
if (block.timestamp < proposal.startTime) {
revert VotingNotStarted(proposalId, proposal.startTime, block.timestamp);
}
if (block.timestamp > proposal.endTime) {
revert VotingEnded(proposalId, proposal.endTime, block.timestamp);
}
if (proposal.proposer == msg.sender) {
revert ProposerCannotVote(proposalId, msg.sender);
}
ProposalVote storage proposalVote = _proposalVotes[proposalId];
if (proposalVote.hasVoted[msg.sender]) {
revert AlreadyVoted(proposalId, msg.sender, block.timestamp);
}
uint256 weight = _veToken.getVotingPower(msg.sender);
if (weight == 0) {
revert NoVotingPower(msg.sender, block.number);
}
proposalVote.hasVoted[msg.sender] = true;
if (support) {
proposalVote.forVotes += weight;
} else {
proposalVote.againstVotes += weight;
}
emit VoteCast(msg.sender, proposalId, support, weight, "");
return weight;
}