Core Contracts

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

Dynamic Quorum Calculation After Voting Period Enables Vote Manipulation Through Total Voting Power Changes

Relevant Context

The Governance contract implements voting functionality where proposals require a quorum to pass. The quorum is calculated as a percentage of the total voting power from the veRAACToken contract. The state() function determines if a proposal has succeeded by checking if the total votes meet the quorum requirement.

Finding Description

The Governance#state function calculates the required quorum dynamically by calling quorum() even after the voting period has ended. This means the quorum requirement can be manipulated in the period between voting end and proposal execution.

The root cause is that the quorum is checked against the current total voting power when state() is called, rather than using a fixed quorum from a critical point in the proposal lifecycle (creation or voting end). This makes the quorum requirement a moving target that can be manipulated through veRAACToken contract's withdraw and lock functions, which alter the total voting power and consequently the quorum calculation.

Impact Explanation

High. An attacker can manipulate proposal outcomes by changing the total voting power after voting has concluded but before execution. This allows proposals to pass or fail based on post-voting changes to the quorum requirement, undermining the integrity of the governance system.

Likelihood Explanation

High. The window between voting end and execution provides ample opportunity for manipulation. The veRAACToken contract's withdrawal, and lock functionalities provide a straightforward mechanism to reduce, and increase total voting power, and there are no restrictions on withdrawals, and lockings during this period.

Proof of Concept

  1. Initial state: Total voting power is 1000, quorum is 4% (40 voting power)

  2. Proposal voting period ends with 38.5 voting power in favor

  3. Attacker withdraws expired locks via veRAACToken#withdraw

  4. Total voting power decreases to 961.5

  5. New quorum requirement becomes 961.5 * 4% = 38.46 voting power

  6. When execute() calls state(), the proposal passes since 38.5 > 38.46

  7. The proposal executes despite not meeting the quorum requirement that existed when voting ended

Recommendation

Store the required quorum at proposal creation time in the ProposalCore struct:

struct ProposalCore {
uint256 id;
address proposer;
ProposalType proposalType;
uint256 startTime;
uint256 endTime;
bool executed;
bool canceled;
bytes32 descriptionHash;
address[] targets;
uint256[] values;
bytes[] calldatas;
+ uint256 requiredQuorum;
}
function propose(...) external override returns (uint256) {
// ... existing code ...
uint256 proposalId = _proposalCount++;
uint256 startTime = block.timestamp + votingDelay;
uint256 endTime = startTime + votingPeriod;
+ uint256 requiredQuorum = quorum();
_proposals[proposalId] = ProposalCore({
// ... existing fields ...
+ requiredQuorum: requiredQuorum
});
}

Then use this stored value in the state function instead of calling quorum().

Updates

Lead Judging Commences

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

Governance allows execution of previously-defeated proposals if quorum requirements are later lowered, enabling unexpected resurrection of old proposals

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

Governance allows execution of previously-defeated proposals if quorum requirements are later lowered, enabling unexpected resurrection of old proposals

Support

FAQs

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

Give us feedback!