Core Contracts

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

Lack of voting power snapshot in governance enables proposal manipulation

Description

The Governance::castVote function uses current voting power instead of snapshotting power at proposal creation time. This allows users to manipulate their voting power after a proposal starts by locking additional tokens, enabling unfair influence over active proposals.

Proof of Concept

  1. User creates proposal when voting power is 100k veRAAC (meets threshold)

  2. Proposal enters voting period after 1 day delay

  3. User locks additional RAAC tokens during voting period, increasing voting power to 200k veRAAC

  4. User casts vote with doubled voting power

  5. Proposal outcome is skewed by artificially inflated voting power

Relevant code snippet:

// Governance.sol
function castVote(uint256 proposalId, bool support) external override returns (uint256) {
// ...
uint256 weight = _veToken.getVotingPower(msg.sender); // Uses current power
// ...
}

Add test in Governance.test.js:

it("allows voting power manipulation after proposal creation", async () => {
// Setup initial voting power
await veToken.mock_setInitialVotingPower(
await owner.getAddress(),
PROPOSAL_THRESHOLD
);
// Create proposal
const tx = await governance
.connect(owner)
.propose(
[await testTarget.getAddress()],
[0],
[testTarget.interface.encodeFunctionData("setValue", [42])],
"Manipulation Test",
0
);
const receipt = await tx.wait();
const event = receipt.logs.find(
(log) => governance.interface.parseLog(log)?.name === "ProposalCreated"
);
const proposalId = event.args.proposalId;
// Advance to voting period
await time.increase(VOTING_DELAY);
// Increase voting power after proposal creation and cast vote
await veToken.mock_setVotingPower(
await owner.getAddress(),
PROPOSAL_THRESHOLD * 2n
);
await governance.connect(owner).castVote(proposalId, true);
// Verify inflated voting power counted
const [forVotes] = await governance.getVotes(proposalId);
expect(forVotes).to.equal(PROPOSAL_THRESHOLD * 2n);
});

Impact

High severity. Attackers can:

  • Swing close governance votes by temporarily boosting power

  • Manipulate protocol parameters against community intent

Recommendation

  • Snapshot voting power at proposal vote cast:

// Governance.sol
+ mapping(uint256 => uint256) private _proposalSnapshots;
function propose(...) {
// ...
+ _proposalSnapshots[proposalId] = block.number;
}
function castVote(...) {
// ...
- uint256 weight = _veToken.getVotingPower(msg.sender);
+ uint256 weight = _veToken.getPastVotes(msg.sender, _proposalSnapshots[proposalId]);
}
  • Enforce snapshot in voting validation:

function castVote(...) {
require(
_proposalSnapshots[proposalId] != 0,
"Voting snapshot not initialized"
);
// ... rest of validation ...
}
Updates

Lead Judging Commences

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

Give us feedback!