Summary
The castVote() function lacks validation of proposal state, allowing users to vote on canceled proposals which pollutes vote tracking data.
Vulnerability Details
In Governance::castVote(), there is no check to verify if the proposal has been canceled before accepting votes. The function only validates:
Proposal exists
Vote timing (after start, before end)
User hasn't voted before
User has voting power
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);
}
...
}
The root cause is the missing validation of proposal.canceled status before processing the vote.
Impact
While this issue has LOW severity since canceled proposals cannot be executed regardless of votes, it allows:
Misleading vote statistics for canceled proposals
Unnecessary state changes and gas costs for users voting on canceled proposals
Recommendations
Approach 1: Add State Check
function castVote(uint256 proposalId, bool support) external override returns (uint256) {
ProposalCore storage proposal = _proposals[proposalId];
if (proposal.startTime == 0) revert ProposalDoesNotExist(proposalId);
+ if (proposal.canceled) revert ProposalCanceled(proposalId);
...
}
Approach 2: Use State Function
function castVote(uint256 proposalId, bool support) external override returns (uint256) {
ProposalCore storage proposal = _proposals[proposalId];
+ ProposalState currentState = state(proposalId);
+ if (currentState != ProposalState.Active) {
+ revert InvalidProposalState(proposalId, currentState, ProposalState.Active);
+ }
...
}