Core Contracts

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

Proposal can be queued even if they doesn't reach the required quorum or a successfull proposal can be defeated.

Summary

Proposal can still be queued even if they doesn't reach the required quorum. Plus a successfull proposal can also be defeated.

Vulnerability Details

Following is the execute function which handles queuing of proposals

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"
);
}
}

State function is as follows

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;
}

Quorum function is as follows

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

Now suppose proposal end time has been passed or it is just about to end. Lets say quorum required = 4%

Suppose end time has passed and still this proposal has not been executed. Note this can also happen just before the end time.

Suppose total voting power is 4000 tokens, quorum numerator 4, quorum denominator = 100 so quorum required = 160 tokens.

Suppose till now For votes = 155 and against votes are 0

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;
}

Due to the above if condition if the current quorum remains same even after end time , this proposal will be defeated. Seeing this suppose the ve token holders which voted in favour of this proposal decide to unlock their raac token thus burning the ve racc token equal to 155 tokens. This will decrease the total supply of ve raac token. Due to this _veToken.getTotalVotingPower() will get reduced to 3845 tokens and now the quorum required will be 153 tokens and as for votes are still 155 tokens this proposal will be succeded. So by just burning a lot ve tokens after voting for the proposal users can successfully execute their proposals

Now suppose a proposal has been successfully queued. Now again execute is called in order to execute the proposal in timelock contract.

Now supose that the total voting power increased i.e more ve tokens we minted and now current qourum becomes less than the required quorum then the following check will return defeated proposal.

And as end time has passed current quorum can never increase .

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;
}

Instead of returning queued proposal it now returns defeated proposal this will cause failure in executing the proposal.

This defeated proposal can later be cancelled when the proposer voting power reduces to less than the threshold required

function cancel(uint256 proposalId) external override {
ProposalCore storage proposal = _proposals[proposalId];
if (proposal.startTime == 0) revert ProposalDoesNotExist(proposalId);
ProposalState currentState = state(proposalId);
if (currentState == ProposalState.Executed) {
revert InvalidProposalState(proposalId, currentState, ProposalState.Active, "Cannot cancel executed proposal");
}
// Only proposer or if proposer's voting power dropped below threshold
if (msg.sender != proposal.proposer &&
_veToken.getVotingPower(proposal.proposer) >= proposalThreshold) {
revert InsufficientProposerVotes(proposal.proposer,
_veToken.getVotingPower(proposal.proposer), proposalThreshold, "Proposer lost required voting power");
}
proposal.canceled = true;
emit ProposalCanceled(proposalId, msg.sender, "Proposal canceled by proposer");
}

Thus a succesfull proposal can still be defeated.

Impact

Proposal which will eventually be defeated can be made successful even by not having voting power and vice versa too.

Tools Used

Manual review

Recommendations

Use a different mechanismto calculate the quorum required.

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.