Core Contracts

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

Proposer Can Manipulate Proposal Execution Timing

Summary

The governance contract allows a proposer to delay proposal execution indefinitely by continuously updating the proposal threshold. Additionally, if the proposer loses voting power after submitting a proposal, the contract does not cancel the proposal, allowing inactive or malicious proposers to block governance.

Vulnerability Details

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

Problems:

  1. Proposer can submit a proposal and immediately transfer out their voting power. The contract does not check proposer’s current voting power before allowing the proposal to stay active.

  2. No enforcement of proposer eligibility after proposal submission. Even if the proposer drops below the proposalThreshold, their proposal remains valid until someone manually cancels it.

PoC

  1. Attacker submits a proposal with sufficient voting power.

  2. Attacker immediately transfers or unstakes their tokens, dropping below the threshold.

  3. Proposal remains active and cannot be canceled by anyone except the attacker.

  4. Governance system gets blocked because the proposal is stuck indefinitely.

Impact

Governance can be blocked indefinitely by inactive or malicious proposers.

Tools Used

Manual Review

Recommendations

Modify cancel() to check whether the proposer still meets the proposal threshold before allowing the proposal to continue.

function cancel(uint256 proposalId) external override {
ProposalCore storage proposal = _proposals[proposalId];
// Check if the proposal exists; if not, revert
if (proposal.startTime == 0) revert ProposalDoesNotExist(proposalId);
// Get the current state of the proposal
ProposalState currentState = state(proposalId);
// Prevent cancellation if the proposal has already been executed
if (currentState == ProposalState.Executed) {
revert InvalidProposalState(proposalId, currentState, ProposalState.Active, "Cannot cancel executed proposal");
}
// Fetch the current voting power of the proposer
uint256 proposerVotes = _veToken.getVotingPower(proposal.proposer);
// New Check: If proposer lost voting power and is below the threshold, cancel automatically
if (proposerVotes < proposalThreshold) {
proposal.canceled = true;
emit ProposalCanceled(proposalId, msg.sender, "Proposer no longer meets threshold");
return; // Exit early after cancellation
}
// Ensure only the proposer can manually cancel their proposal
if (msg.sender != proposal.proposer) {
revert UnauthorizedCancel(msg.sender);
}
// Cancel the proposal
proposal.canceled = true;
emit ProposalCanceled(proposalId, msg.sender, "Proposal canceled by proposer");
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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