Core Contracts

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

Governance Quorum Bypass Enables Minority Rule

Summary

The governance proposal execution can succeed even when it doesn't meet quorum requirements. This breaks a fundamental governance safety guarantee and could allow minority stakeholders to push through unauthorized changes.

The vulnerability centers around the proposal execution validation in Governance.sol. A proposal can be executed despite having insufficient votes to meet quorum, bypassing core governance checks.

We expects that successful execution requires both majority support (forVotes > againstVotes) and meeting quorum (forVotes + againstVotes >= quorumRequired). However, the execute() function in Governance.sol lacks proper validation of these requirements before proceeding with execution.

Vulnerability Details

The RAAC governance system faces a vulnerability in its proposal execution mechanism. At its core, the protocol aims to democratize real estate investment through on-chain governance, but the current implementation allows proposals to bypass essential quorum requirements.

Think of this like a homeowners' association making decisions without the minimum required attendance. Even if only 5 out of 100 homeowners show up, their votes could change community rules affecting everyone.

The Governance.sol contract's execute() function transitions proposals through states without properly validating total participation. The quorum check exists in the contract (defaulting to 4% of total voting power), but it's not enforced at the critical moment of execution.

Let's walk through a real scenario: A proposal gets created when the total veRAACToken voting power is 1,000,000. This means quorum requires 40,000 votes (4%). An attacker waits for a period of low participation, gets 25,000 votes in favor and 5,000 against. Despite having only 30,000 total votes (below quorum), the proposal can still execute because _isProposalSuccessful() only checks if forVotes > againstVotes.

execute _isProposalSuccessful()

function execute(uint256 proposalId) external override nonReentrant {
// πŸ“ Load proposal data
ProposalCore storage proposal = _proposals[proposalId];
if (proposal.executed) revert ProposalAlreadyExecuted(proposalId, block.timestamp);
// πŸ”„ Get current state
ProposalState currentState = state(proposalId);
// 🚦 State transition checks
if (currentState == ProposalState.Succeeded) {
// πŸ” Queue in timelock
_queueProposal(proposalId);
} else if (currentState == ProposalState.Queued) {
// ⚑ Execute queued proposal
_executeProposal(proposalId);
} else {
// ❌ Invalid state transition
revert InvalidProposalState(
proposalId,
currentState,
currentState == ProposalState.Active ? ProposalState.Succeeded : ProposalState.Queued,
"Invalid state for execution"
);
}
}
​
​
function _isProposalSuccessful(uint256 proposalId) internal view returns (bool) {
// πŸ“Š Load voting data
ProposalVote storage proposalVote = _proposalVotes[proposalId];
// πŸ”’ Calculate participation
uint256 currentQuorum = proposalVote.forVotes + proposalVote.againstVotes;
// 🎯 Get threshold
uint256 requiredQuorum = quorum();
// βš–οΈ Validate requirements
return currentQuorum >= requiredQuorum &&
proposalVote.forVotes > proposalVote.againstVotes;
}

Imagine changing property management decisions with just 3% voter participation. For a protocol managing real-world assets, governance integrity is paramount. The GaugeController uses these decisions to direct yield, while the BoostController applies voting power, both assuming legitimate governance outcomes.

Impact

This manifests when an attacker identifies periods of low voter engagement. During these windows, they can push through proposals affecting real estate management, yield distribution, and protocol parameters despite having far below the required 4% quorum (40,000 veRAAC votes when total voting power is 1,000,000).

The state machine in Governance.sol transitions proposals through various stages, but crucially misses quorum validation at the execution checkpoint. When a proposal receives 25,000 votes in favor and 5,000 against (total participation of just 3%), the contract sees a winning majority without enforcing minimum participation requirements.

Recommendations

The _isProposalSuccessful function already has the correct validation logic in place, but the issue lies in ensuring this validation is properly enforced during proposal state transitions.

function _isProposalSuccessful(uint256 proposalId) internal view returns (bool) {
// πŸ“Š Load voting data for this proposal
ProposalVote storage proposalVote = _proposalVotes[proposalId];
// πŸ”’ Calculate total participation
uint256 currentQuorum = proposalVote.forVotes + proposalVote.againstVotes;
// 🎯 Get required minimum participation threshold
uint256 requiredQuorum = quorum();
// βš–οΈ Dual validation gates
return currentQuorum >= requiredQuorum && // πŸ›‘οΈ Participation check
proposalVote.forVotes > proposalVote.againstVotes; // βœ… Majority check
}

The focus is on ensuring this function is called at all critical state transition points in the proposal lifecycle, particularly during the execute() flow.

function execute(uint256 proposalId) external override nonReentrant {
// πŸ”„ State transition point
if (state(proposalId) == ProposalState.Succeeded) {
// 🎯 Critical checkpoint - must validate success conditions
require(_isProposalSuccessful(proposalId), "Quorum not met");
_executeProposal(proposalId);
}
}
Updates

Lead Judging Commences

inallhonesty Lead Judge
5 months ago
inallhonesty Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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