Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: low
Valid

Ability to Cast Votes on Canceled Proposals in `Governance.sol` contract

Summary

An issue exists in the Governance.sol contract where users can still cast votes on proposals that have already been canceled. Although these votes do not impact the governance process, they introduce unnecessary gas costs, create confusion, and can mislead users regarding the status of proposals.

Vulnerability Details

This is the propose function used to create a proposal: https://github.com/Cyfrin/2025-02-raac/blob/main/contracts/core/governance/proposals/Governance.sol#L127-L168

function propose(
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
string memory description,
ProposalType proposalType
) external override returns (uint256) {
//logic
}

This is the cancel function which cancels a proposal. It can be called by the proposer anytime and by anyone when proposer's voting power drops below threshold.: https://github.com/Cyfrin/2025-02-raac/blob/main/contracts/core/governance/proposals/Governance.sol#L252-L270

function cancel(uint256 proposalId) external override {
ProposalCore storage proposal = _proposals[proposalId];
if (proposal.startTime == 0) revert ProposalDoesNotExist(proposalId);
ProposalState currentState = state(proposalId);
if (currentState == ProposalState.Executed) {
revert InvalidProposalState(proposalId, currentState, ProposalState.Active, "Cannot cancel executed proposal");
}
// Only proposer or if proposer's voting power dropped below threshold
if (msg.sender != proposal.proposer &&
_veToken.getVotingPower(proposal.proposer) >= proposalThreshold) {
revert InsufficientProposerVotes(proposal.proposer,
_veToken.getVotingPower(proposal.proposer), proposalThreshold, "Proposer lost required voting power");
}
proposal.canceled = true;
emit ProposalCanceled(proposalId, msg.sender, "Proposal canceled by proposer");
}

It is important to note that it sets: proposal.canceled = true;

And this is the castVote function:

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 castVote() function does not check whether a proposal has been canceled before allowing users to vote. As a result, even after a proposal is successfully canceled through the cancel() function, users can still submit votes on it. This issue arises because:

  • The cancel() function marks a proposal as canceled but does not prevent further voting.

  • The castVote() function lacks a check to verify if the proposal has been canceled.

  • Votes cast on canceled proposals have no actual effect but still consume gas

Proof of Concept (PoC)

Steps to Reproduce:

1, A user creates a proposal using the propose() function.

2, The proposer or another eligible user cancels the proposal via the cancel() function.

3, Another user attempts to vote on the canceled proposal using castVote(proposalId, true/false).

4, The vote is successfully registered, even though the proposal has been canceled.

Expected Behavior:

Once a proposal is canceled, users should not be able to vote on it.

Actual Behavior:

Users can still call castVote() function and register votes even after the proposal is canceled, leading to gas wastage and potential confusion.

Impact

  • Users spend gas on votes that do not impact governance decisions.

  • Participants may believe their votes still count.

  • Potential misleading of users about proposal status.

Tools Used

Manual Review

Recommendations

Modify the castVote() function to check if the proposal has been canceled before allowing a vote to be cast.
Add this check:

if (proposal.canceled) {
revert InvalidProposalState(proposalId, ProposalState.Canceled, ProposalState.Active, "Cannot vote on canceled proposal");
}

Updated castVote function:

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.canceled) { //@audit add this check
revert InvalidProposalState(proposalId, ProposalState.Canceled, ProposalState.Active, "Cannot vote on canceled proposal");
}
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;
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Governance::castVote lacks canceled/executed proposal check, allowing users to waste gas voting on proposals that can never be executed

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Governance::castVote lacks canceled/executed proposal check, allowing users to waste gas voting on proposals that can never be executed

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.