Summary
The Governance::castVote function does not check if a proposal has been canceled, allowing votes to be cast on invalid proposals.
Vulnerability Details
A proposal can be canceled using Governance::cancel, which sets its canceled state to true:
/contracts/core/governance/proposals/Governance.sol:268
268:
269: proposal.canceled = true;
270: emit ProposalCanceled(proposalId, msg.sender, "Proposal canceled by proposer");
271: }
272:
However, Governance::castVote does not verify if a proposal is canceled before allowing votes.
POC
Add this POC to Governance.test.js in describe("Voting", section:
/test/unit/core/governance/proposals/Governance.test.js:181
181: it.only("POC: vote can be cast on cancel proposals", async () => {
182: await veToken.mock_setVotingPower(await user1.getAddress(), ethers.parseEther("10000"));
183:
184: await governance.connect(owner).cancel(proposalId)
185: await expect(governance.connect(user1).castVote(proposalId, true))
186: .to.emit(governance, "VoteCast");
187:
188: });
and Run npx hardhat test
Impact
Votes on canceled proposals can lead to inappropriate vote casting.
Tools Used
Manual Review, Unit Testing
Recommendations
Add a check in castVote to prevent voting on canceled proposals:
@@ -182,6 +182,7 @@ contract Governance is IGovernance, Ownable, ReentrancyGuard {
// @audit : vote can be cast on cancel proposal
ProposalCore storage proposal = _proposals[proposalId];
if (proposal.startTime == 0) revert ProposalDoesNotExist(proposalId);
+ if(proposal.canceled) revert ProposalDoesNotExist(proposalId);
if (block.timestamp < proposal.startTime) {
revert VotingNotStarted(proposalId, proposal.startTime, block.timestamp);
}