Core Contracts

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

Voting Power Snapshot Missing

Summary

Voting power is based on current balance, not a snapshot at proposal creation\

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;

Vulnerability Details

The castVote() function in Governance.sol has a fundamental trust assumption, it relies on current voting power rather than historical snapshots. The key line:

Initially, let's go through this State

  • Alice has 100,000 veRAACTokens locked

  • Bob creates a governance proposal to adjust protocol parameters

  • The proposal enters voting period after votingDelay

The Issue Unfolds

Alice sees the proposal and plans to vote, but notices she can manipulate her voting power: Governance.sol#L196

uint256 weight = _veToken.getVotingPower(msg.sender);
// This line gets current voting power rather than using a snapshot from proposal creation.

This creates a temporal disconnect between proposal creation and vote casting. The code assumes voting power at voting time represents legitimate governance weight, which breaks the core principle of time-weighted governance.

  1. Alice executes her attack:

    • She increases her lock amount to 500,000 veRAACTokens

    • Calls castVote(proposalId, true) with her inflated voting power

    • Immediately after voting, she reduces her lock back to 100,000

  2. The manipulation succeeds because:

    • No snapshot of voting power is taken at proposal creation

    • proposalVote.forVotes gets inflated by temporary voting power

    • The contract only checks hasVoted[msg.sender] to prevent multiple votes

Resulting Impact

  • Proposal voting becomes manipulated

  • A single user can temporarily boost their voting power

  • The quorum() calculation becomes unreliable

  • Governance decisions no longer reflect true long-term token holder intent

If the proposal was to adjust MAX_WEEKLY_EMISSION in RAACGauge from 500,000 to 1,000,000, Alice's manipulated vote could swing the outcome despite only having legitimate voting power of 100,000 tokens.

Impact

Given the protocol's governance controls critical parameters like:

  • MAX_WEEKLY_EMISSION (500,000 RAAC) in RAACGauge

  • Fee distributions in FeeCollector

  • Treasury management

A malicious actor could:

  1. Flash loan veRAACTokens

  2. Cast votes with inflated power

  3. Return borrowed tokens

  4. Impact: Up to 100% of protocol parameters could be maliciously modified

This mirrors the Beanstalk governance exploit (April 2022) where an attacker flash loaned $1B in assets to gain emergency voting power. The attacker proposed and executed a malicious governance proposal within a single transaction by exploiting the lack of voting power snapshots.

Tools Used

vs

Recommendations

Store voting power at proposal creation. n the current Governance.sol, we could enhance the propose() function to capture voting power snapshots when proposals are created.

function propose(...) external override returns (uint256) {
uint256 proposalId = _proposalCount++;
// Create voting power snapshot at proposal creation
mapping(address => uint256) storage votingPowerSnapshot;
votingPowerSnapshot[msg.sender] = _veToken.getVotingPower(msg.sender);
_proposals[proposalId] = ProposalCore({
// Existing fields...
votingPowerSnapshot: votingPowerSnapshot
});
}

Then modify castVote() to use this snapshot

function castVote(uint256 proposalId, bool support) external returns (uint256) {
ProposalCore storage proposal = _proposals[proposalId];
uint256 weight = proposal.votingPowerSnapshot[msg.sender];
// The function...
}

This aligns with how the protocol's other time-sensitive components work.

  • TimeWeightedAverage.sol for tracking historical values

  • veRAACToken.sol checkpoint system

  • GaugeController.sol period-based weight tracking

Updates

Lead Judging Commences

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

Governance.castVote uses current voting power instead of proposal creation snapshot, enabling vote manipulation through token transfers and potential double-voting

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

Governance.castVote uses current voting power instead of proposal creation snapshot, enabling vote manipulation through token transfers and potential double-voting

Support

FAQs

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