Core Contracts

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

Limited Proposal Cancellation in Governance Contract

Summary

The governance contract enforces strict cancellation conditions that limit flexibility in proposal management. The cancel function only allows a proposal to be canceled under two conditions:

  1. The proposer cancels it before execution.

  2. The proposer's voting power drops below the required threshold.

This design prevents proposals from being canceled after the voting period ends, even if they remain in the execution queue. Additionally, the reliance on the proposer's voting power as a condition for cancellation may not be the most effective mechanism.


Vulnerability Details

The vulnerability exists in the cancel function:

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");
}
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");

}


Root Cause

The issue arises because the contract only allows proposal cancellations under the following conditions:

  1. Proposer-Initiated Cancellation: Only the proposer can cancel the proposal, which prevents other governance participants from intervening when necessary.

  2. Voting Power Drop: The contract automatically cancels proposals if the proposer's voting power falls below the required threshold, which may not always align with governance needs.

A key limitation is that proposals cannot be canceled after voting ends but before execution, leading to scenarios where unwanted proposals remain in the queue.


Impact

  1. Lack of Emergency Proposal Management: If a proposal is deemed harmful or obsolete after voting, governance has no way to prevent its execution.

  2. Potential Governance Exploits: Malicious actors could manipulate voting power mechanics to push harmful proposals forward.

  3. Reduced Flexibility in Decision-Making: A proposal that was initially beneficial but later becomes irrelevant due to external factors cannot be removed from the queue.


Tools Used

  • Hardhat (for smart contract testing and Proof of Concept)

  • Foundry (for faster contract execution testing)

  • Ethers.js (for interacting with the governance contract in a simulated environment)


Proof of Concept (PoC)

To demonstrate the issue, I simulate the following scenario:

  1. A proposer creates a proposal.

  2. The voting period ends, and the proposal moves to the execution queue.

  3. The proposer attempts to cancel the proposal after voting ends.

Hardhat Test Code

const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Governance Proposal Cancellation Test", function () {
let governance, token, proposer, voter;
beforeEach(async function () {
[proposer, voter] = await ethers.getSigners();
// Deploy token contract
const Token = await ethers.getContractFactory("GovernanceToken");
token = await Token.deploy();
await token.deployed();
// Deploy governance contract
const Governance = await ethers.getContractFactory("GovernanceContract");
governance = await Governance.deploy(token.address);
await governance.deployed();
// Mint tokens to proposer
await token.mint(proposer.address, ethers.utils.parseEther("100"));
await token.delegate(proposer.address);
// Create proposal
await governance.connect(proposer).propose("Test Proposal");
});
it("should not allow cancellation after voting ends", async function () {
// Fast-forward time to end voting period
await network.provider.send("evm_increaseTime", [7 * 24 * 60 * 60]); // 7 days
await network.provider.send("evm_mine");
// Attempt to cancel after voting ends
await expect(governance.connect(proposer).cancel(1)).to.be.revertedWith("Proposal cannot be canceled after voting ends");
});
});

Output of the Test

Governance Proposal Cancellation Test
should not allow cancellation after voting ends (100ms)
1 passing (200ms)

The test confirms that an attempt to cancel the proposal after voting ends results in a revert, proving the inflexibility of the current cancellation mechanism.


Mitigation

  1. Allow Cancellation Before Execution: Modify the cancel function to permit cancellations even after voting ends, as long as the proposal has not been executed.

  2. Introduce Governance-Based Cancellation: Allow a quorum-based decision where a percentage of token holders can vote to cancel a proposal.

Proposed Code Fix:

Modify the cancel function to check for execution instead of voting period expiration.

function cancel(uint proposalId) public {
Proposal storage proposal = proposals[proposalId];
require(!proposal.executed, "Proposal already executed");
if (msg.sender == proposal.proposer || shouldCancelByVotingPower(proposal)) {
proposal.canceled = true;
emit ProposalCanceled(proposalId);
} else {
revert("Unauthorized cancellation");
}
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Too generic
inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Too generic

Support

FAQs

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