Description
The Governance::state function calculates quorum requirements using the current total voting power instead of a snapshot taken at proposal creation. This allows manipulation of proposal outcomes by altering the total voting power after proposal creation but before state determination.
Proof of Concept
Create proposal when total voting power is high (20M supply → 800k quorum)
Cast votes below initial quorum (700k < 800k)
Before voting ends, total voting power decreases through token withdrawals
The reduced total voting power lowers the absolute quorum requirement
Original votes now exceed the new quorum threshold
Add test in Governance.test.js:
it("manipulates proposal state through total voting power changes", async () => {
await veToken.mock_setTotalSupply(ethers.parseEther("20000000"));
await veToken.mock_setVotingPower(owner.address, PROPOSAL_THRESHOLD);
const tx = await governance
.connect(owner)
.propose(
[testTarget.target],
[0],
[testTarget.interface.encodeFunctionData("setValue", [42])],
"Quorum Test",
0
);
const receipt = await tx.wait();
const event = receipt.logs.find((l) => l.fragment.name === "ProposalCreated");
const proposalId = event.args.proposalId;
await time.increase(VOTING_DELAY);
await veToken.mock_setVotingPower(user1.address, ethers.parseEther("700000"));
await governance.connect(user1).castVote(proposalId, true);
await veToken.mock_setTotalSupply(ethers.parseEther("10000000"));
await time.increase(VOTING_PERIOD);
expect(await governance.state(proposalId)).to.equal(ProposalState.Succeeded);
});
Impact
High Severity - Allows attackers to:
Force through proposals that should have failed by manipulating total supply
Invalidate voter intentions through post-creation quorum changes
Destabilize governance outcomes with timing-based attacks
Recommendation
contracts/core/governance/proposals/Governance.sol
struct ProposalCore {
// ... existing fields ...
+ uint256 quorumAtCreation;
}
function propose(...) external {
// ... existing code ...
_proposals[proposalId] = ProposalCore({
// ... existing fields ...
+ quorumAtCreation: quorum()
});
}
function state(uint256 proposalId) public view {
// ... existing code ...
- uint256 requiredQuorum = quorum();
+ uint256 requiredQuorum = proposal.quorumAtCreation;
}
function quorum(uint256 proposalId) public view {
return _veToken.getPastTotalSupply(proposal.startTime);
}