Core Contracts

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

Governance quorum() can be manipulated to impact proposal result

Description

The current implementation of governance quorum calculation in the RAAC protocol uses the present total voting supply instead of taking a snapshot at proposal creation time. This creates potential attack vectors that could manipulate voting outcomes.

The vulnerable code is in the quorum() function:

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

This implementation allows the quorum requirement to fluctuate based on changes in total voting power during a proposal's lifecycle.

Impact

Consider these 3 problematic scenarios which arise due to the current implementation style:

Scenario 1: Decreasing Total Supply to Pass Proposals

Initial State:
- Total Supply: 1,000,000 veRAAC
- Quorum needed: 40,000 veRAAC (4%)
- YES votes: 35,000 veRAAC
- Proposal status: FAILING (votes < quorum)
Sequence of events:
- Some of the locked tokens which were used as voting power to vote YES expire DURING the course of the proposal i.e. before proposal end time
- Total supply decreases to 800,000 veRAAC
- New quorum requirement reduces to 32,000 veRAAC
- Existing 35,000 YES votes now exceed quorum
- Proposal passes despite potentially not having broad enough support
Note that another variation to this is:
- The expired tokens can be transferred to another wallet, then locked once again and used for voting YES on the same proposal, leading to double-voting.

Scenario 2: Increasing Total Supply to Invalidate Passing Proposals

Initial State:
- Total Supply: 1,000,000 veRAAC
- Quorum needed: 40,000 veRAAC (4%)
- YES votes: 42,000 veRAAC
- Proposal status: PASSING (votes > quorum)
Sequence of events:
- Attacker observes a proposal they oppose is about to pass with 42,000 YES votes
- Attacker (or an honest user using the system normally) locks significant RAAC (e.g., 100,000) to increase total veRAAC supply to 1,100,000
- New quorum becomes 44,000 veRAAC (4% of 1,100,000)
- Existing 42,000 YES votes no longer meet quorum requirement
- Proposal fails despite having significant support relative to initial conditions

Scenario 3: Proposal can be DEFEATED after**** _queueProposal and before**** _executeProposal

This one can happen inadvertently, in the normal course of operations.
Initial State:
- Total Supply: 1,000,000 veRAAC
- Quorum needed: 40,000 veRAAC (4%)
- YES votes: 42,000 veRAAC
- Proposal status: PASSING (votes > quorum)
Sequence of events:
- The protcol mandates that execute() be called twice. The first call results in '_queueProposal()' call and after the queueing time is up, the second call results in '_executeProposal()'
- Before the internal call to either of these 2 aforementioned functions, a call to 'state()' happens to check the currentState
- state() makes this check each time:
// 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;
}
- So if quorum() has increased due to more users locking in their tokens during the queuing period, the proposal which has succeeded previously will now be marked as 'ProposalState.Defeated'
- The inverse may happen too.
- In fact generalizing this, if a DEFEATED proposal is not cancelled then execute() can be called any time in the future over and over again until a condition is met when quorum() is above the required one. At this point of time, it can succeed and execute to completion.

Mitigation

Implement snapshot-based quorum calculation:

function propose(...) external override returns (uint256) {
// ... existing validation code ...
// Take snapshot of total voting power
uint256 snapshotBlock = block.number;
_checkpointState.setProposalSnapshot(proposalId, snapshotBlock);
// ... rest of propose function ...
}
function quorum(uint256 proposalId) public view override returns (uint256) {
uint256 snapshotBlock = proposalPowerSnapshots[proposalId];
if (snapshotBlock == 0) revert InvalidProposal();
uint256 totalSupplyAtSnapshot = _veToken.getPastTotalSupply(snapshotBlock);
return (totalSupplyAtSnapshot * quorumNumerator) / QUORUM_DENOMINATOR;
}

Note that getPastTotalSupply() is available inside PowerCheckpoint.sol library which is already imported in veRAACToken.sol.

These changes will ensure that:

  • Quorum requirements remain consistent throughout a proposal's lifecycle

  • Voting power manipulations after proposal creation cannot affect the proposal's quorum requirement

Updates

Lead Judging Commences

inallhonesty Lead Judge 3 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 3 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.