Core Contracts

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

Users can cancel queued proposals

Summary

Users can cancel queued proposals

Vulnerability Details

cancel requires either the msg.sender to cancel the proposal or anyone can do it if msg.sender has less tokens than proposalThreshold.

https://github.com/Cyfrin/2025-02-raac/blob/main/contracts/core/governance/proposals/Governance.sol#L252

function cancel(uint256 proposalId) external override {
// ...
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");
}

However an issue appears that any proposal, no matter at what stage can get canceled, and if so state returns Canceled

https://github.com/Cyfrin/2025-02-raac/blob/main/contracts/core/governance/proposals/Governance.sol#L288

function state(uint256 proposalId) public view override returns (ProposalState) {
ProposalCore storage proposal = _proposals[proposalId];
if (proposal.startTime == 0) revert ProposalDoesNotExist(proposalId);
if (proposal.canceled) return ProposalState.Canceled;

Even proposals which have passed the quorum, have more FOR than AGAINST votes and are queued to be executed can be canceled.

https://github.com/Cyfrin/2025-02-raac/blob/main/contracts/core/governance/proposals/Governance.sol#L220

function execute(uint256 proposalId) external override nonReentrant {
ProposalCore storage proposal = _proposals[proposalId];
if (proposal.executed) revert ProposalAlreadyExecuted(proposalId, block.timestamp);
ProposalState currentState = state(proposalId);
if (currentState == ProposalState.Succeeded) {
_queueProposal(proposalId);
} else if (currentState == ProposalState.Queued) {
_executeProposal(proposalId);
} else {
revert InvalidProposalState(
proposalId,
currentState,
currentState == ProposalState.Active ? ProposalState.Succeeded : ProposalState.Queued,
"Invalid state for execution"
);
}
}

Example:

  1. There are a few active proposals

  • first is to change the threshold to 150k

  • second is to move funds around

  • third is to deploy and configure a new gouge

  1. The second and third have passed and are already queued in the time lock

  2. The first proposal gets executed

  3. Malicious user calls cancel and prevents the already accepted and queued proposals from being executed

Impact

Change in proposalThreshold can instantly remove even queued proposals, that have passed all the necessary checks.

Tools Used

Manual review

Recommendations

If a proposal is queued don't cancel it even if the one who proposed happens to have less than proposalThreshold. Consider adding this:

ProposalState currentState = state(proposalId);
- if (currentState == ProposalState.Executed) {
+ if (currentState == ProposalState.Executed && currentState == ProposalState.Queued) {
revert InvalidProposalState(proposalId, currentState, ProposalState.Active, "Cannot cancel executed proposal");
}
Updates

Lead Judging Commences

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

Governance::cancel allows canceling proposals in Succeeded and Queued states, enabling single actors to override community decisions

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

Governance::cancel allows canceling proposals in Succeeded and Queued states, enabling single actors to override community decisions

Support

FAQs

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

Give us feedback!