Core Contracts

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

Proposer Voting Exploit

Summary

The castVote function allows the proposer of a governance proposal to vote on their own proposal. This creates a potential exploit where proposers with large voting power can arbitrarily create and pass proposals without fair community participation. Additionally, this opens the system to governance attacks where a single entity can dominate decision-making.

Vulnerability Details

The vulnerability stems from the fact that any veToken holder, including the proposer, is allowed to vote on their own proposals. Since vote weight is determined by veToken holdings, a proposer with significant voting power can submit a proposal and single-handedly pass it without external checks.

Code Reference

In the castVote function, there is no check to prevent the proposer from voting:

function castVote(uint256 proposalId, bool support) external override returns (uint256) {
ProposalCore storage proposal = _proposals[proposalId];
if (proposal.startTime == 0) revert ProposalDoesNotExist(proposalId);
if (block.timestamp < proposal.startTime) {
revert VotingNotStarted(proposalId, proposal.startTime, block.timestamp);
}
if (block.timestamp > proposal.endTime) {
revert VotingEnded(proposalId, proposal.endTime, block.timestamp);
}
ProposalVote storage proposalVote = _proposalVotes[proposalId];
if (proposalVote.hasVoted[msg.sender]) {
revert AlreadyVoted(proposalId, msg.sender, block.timestamp);
}
uint256 weight = _veToken.getVotingPower(msg.sender);
if (weight == 0) {
revert NoVotingPower(msg.sender, block.number);
}
proposalVote.hasVoted[msg.sender] = true;
if (support) {
proposalVote.forVotes += weight;
} else {
proposalVote.againstVotes += weight;
}
emit VoteCast(msg.sender, proposalId, support, weight, "");
return weight;
}

The issue arises because msg.sender is not checked against the proposer of the proposal:

ProposalCore storage proposal = _proposals[proposalId];

Also seeing as the state of a proposal (if suceeded) is entirely dependent on meeting quorumwhich is based on total voting weights, it means a central authority can take over.

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

Impact

  • Governance Centralization: A single entity with a large veToken balance can propose and approve changes without resistance.

  • Protocol Manipulation: Malicious actors can create and pass self-beneficial proposals, such as withdrawing funds, changing governance rules, or bypassing security constraints.

  • Loss of Decentralization: Community members lose meaningful participation in governance as large stakeholders control outcomes.

Tools Used

  • Manual Code Review

Recommendations

  • To mitigate this issue, update the castVote function to prevent proposers from voting on their own proposals. This can be done by adding a require statement that checks if msg.sender is the proposer:

Fixed Code:

function castVote(uint256 proposalId, bool support) external override returns (uint256) {
ProposalCore storage proposal = _proposals[proposalId];
if (proposal.startTime == 0) revert ProposalDoesNotExist(proposalId);
if (block.timestamp < proposal.startTime) {
revert VotingNotStarted(proposalId, proposal.startTime, block.timestamp);
}
if (block.timestamp > proposal.endTime) {
revert VotingEnded(proposalId, proposal.endTime, block.timestamp);
}
// Prevent proposer from voting on their own proposal
if (proposal.proposer == msg.sender) {
revert ProposerCannotVote(proposalId, msg.sender);
}
ProposalVote storage proposalVote = _proposalVotes[proposalId];
if (proposalVote.hasVoted[msg.sender]) {
revert AlreadyVoted(proposalId, msg.sender, block.timestamp);
}
uint256 weight = _veToken.getVotingPower(msg.sender);
if (weight == 0) {
revert NoVotingPower(msg.sender, block.number);
}
proposalVote.hasVoted[msg.sender] = true;
if (support) {
proposalVote.forVotes += weight;
} else {
proposalVote.againstVotes += weight;
}
emit VoteCast(msg.sender, proposalId, support, weight, "");
return weight;
}
  • Alternatively a minimum number of REQUIRED voters should be implemented

Updates

Lead Judging Commences

inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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