Summary
Governance::castVote
function lacks validation checks to prevent voting on proposals that have been cancelled or executed. As a result, users can continue casting votes on proposals that are no longer active.
Vulnerability Details
In the castVote function, the contract only validates:
Failing to check if the proposal has been canceled or executed.
[](https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/governance/proposals/Governance.sol#L181-L194)
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);
}
...
Proof of Code:
Add this test to Governance.test.js
it.only("should not allow voting on canceled proposal", async () => {
await veToken.mock_setInitialVotingPower(
await user1.getAddress(),
ethers.parseEther("100000")
);
const targets = [await testTarget.getAddress()];
const values = [42];
const calldatas = [testTarget.interface.encodeFunctionData("setValue", [42])];
const description = "Test Proposal";
await governance.connect(user1).propose(
targets,
values,
calldatas,
description,
0
);
await time.increase(4 * 24 * 3600);
await governance.connect(user1).cancel(0);
await expect(
governance.connect(user1).castVote(0, true)
).to.be.reverted;
});
Impact
Vote tallies may continue to increase after a proposal is executed/cancelled
Creates misleading governance metrics and confused users
Users can waste gas attempting to vote on proposals that are no longer active
Tools Used
Manual Review
Foundry
Recommendations
Add validation checks in Governance::castVote
function to prevent voting on executed or cancelled proposals:
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.executed) revert ProposalAlreadyExecuted(proposalId);
+ if (proposal.canceled) revert ProposalCanceled(proposalId);
...
}