Core Contracts

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

Dynamic Quorum Calculation Issue

Summary

The quorum calculation uses live total voting power instead of historical values from when the proposal was active. This violates the fundamental governance property that a proposal's success/failure should be immutable after voting ends. Proposal success/failure should be finalized at voting end, not retroactively changed.

Vulnerability Details

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;
}
/**
* @notice Gets the current quorum requirement
* @dev Calculates required quorum based on total voting power
* Uses quorumNumerator/QUORUM_DENOMINATOR ratio
* @return Current quorum threshold in voting power units
*/
function quorum() public view override returns (uint256) {
return (_veToken.getTotalVotingPower() * quorumNumerator) / QUORUM_DENOMINATOR;
}

Proposal success/failure should be finalized at voting end, not retroactively changed. Quorum is calculated using current total voting power (_veToken.getTotalVotingPower()), not the total at the proposal's end time. This allows Defeated proposals to later succeed if total voting power decreases. Successful proposals to later fail if total voting power increases.

The quorum calculation uses live total voting power instead of historical values from when the proposal was active. This violates the fundamental governance property that a proposal's success/failure should be immutable after voting ends.

Example:

  1. Proposal Creation (Total Voting Power = 100k)

    • Quorum = 4% of 100k = 4,000

    • Proposal receives 3,500 votes (for + against)

    • Result: Defeated (3,500 < 4,000)

  2. After Voting Ends

    • Many users unlock tokens → Total Voting Power drops to 50k

    • New quorum = 4% of 50k = 2,000

currentQuorum = 3,500 (original votes)
requiredQuorum = 2,000 (new calculation)

Proposal now succeeds retroactively (3,500 > 2,000)

Impact

Same proposal could alternate between Defeated/Succeeded states. Historical governance decisions become mutable

Tools Used

Foundry

Recommendations

Ensure the quorum is calculated using the total voting power at proposal creation time

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.