Core Contracts

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

Queued proposals can be blocked due to dynamic quorum calculation

Summary

The Governance contract's dynamic quorum calculation can prevent the execution of proposals that previously passed voting and are queued for execution.

Vulnerability Details

The execute function is called twice - once to queue a proposal in TimelockController and once to execute the queued proposal. Before each operation in execute, the state of the proposal is checked.

The issue arises from recalculating quorum every time state is called:

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;
if (proposal.executed) return ProposalState.Executed;
if (block.timestamp < proposal.startTime) return ProposalState.Pending;
if (block.timestamp < proposal.endTime) return ProposalState.Active;
// After voting period ends, check quorum and votes
ProposalVote storage proposalVote = _proposalVotes[proposalId];
uint256 currentQuorum = proposalVote.forVotes + proposalVote.againstVotes;
> uint256 requiredQuorum = quorum();
// Check if quorum is met and votes are in favor
> if (currentQuorum < requiredQuorum || proposalVote.forVotes <= proposalVote.againstVotes) {
return ProposalState.Defeated;
}
bytes32 id = _timelock.hashOperationBatch(
proposal.targets,
proposal.values,
proposal.calldatas,
bytes32(0),
proposal.descriptionHash
);
// If operation is pending in timelock, it's Queued
if (_timelock.isOperationPending(id)) {
return ProposalState.Queued;
}
// If not pending and voting passed, it's Succeeded
return ProposalState.Succeeded;
}

A proposal that already passed and is in the queued state (quorum met once and ready for execution), cannot be executed due to checking the state (recalculating quorum) again in execute before executing:

function execute(uint256 proposalId) external override nonReentrant {
ProposalCore storage proposal = _proposals[proposalId];
if (proposal.executed) revert ProposalAlreadyExecuted(proposalId, block.timestamp);
> ProposalState currentState = state(proposalId);
// Check if the proposal is in the correct state for execution
if (currentState == ProposalState.Succeeded) {
// Queue the proposal
_queueProposal(proposalId);
} else if (currentState == ProposalState.Queued) {
// Execute the queued proposal
_executeProposal(proposalId);
} else {
// If not in Succeeded or Queued state, revert
revert InvalidProposalState(
proposalId,
currentState,
currentState == ProposalState.Active ? ProposalState.Succeeded : ProposalState.Queued,
"Invalid state for execution"
);
}
}

The quorum function always returns 4% of the current total supply of veRAAC:

function quorum() public view override returns (uint256) {
return (_veToken.getTotalVotingPower() * quorumNumerator) / QUORUM_DENOMINATOR;
}

Example scenario:

  1. Total voting power = 1,000

  2. Quorum = 40 (4%)

  3. Proposal receives 50 votes (passes quorum) and is queued for execution

  4. Total voting power increases to 2,000

  5. New quorum = 80

  6. Before _executeProposal in execute is called, state sees 50 < 80

  7. Proposal becomes unexecutable despite previously passing

After voting ends, currentQuorum cannot change, but requiredQuorum is dynamic. Large locks in veRAACToken can significantly inflate requiredQuorum.

Impact

High: Proposals that successfully passed voting can become unexecutable due to changes in total voting power. This prevents critical governance actions, potentially leaving the protocol in a stuck state.

Recommendations

Consider calculating and storing the quorum for a proposal only when creating the proposal or when queuing it.

Updates

Lead Judging Commences

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

Governance::quorum uses current total voting power instead of proposal creation snapshot, allowing manipulation of threshold requirements to force proposals to pass or fail

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

Governance::quorum uses current total voting power instead of proposal creation snapshot, allowing manipulation of threshold requirements to force proposals to pass or fail

Support

FAQs

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