Summary
The voting mechanism in RAACGauge can be manipulated using flash loans since there is no lock-up period or pre-vote token holding requirement for casting votes. An attacker can borrow RAAC tokens, convert to veRAACToken, vote, and repay the loan in a single transaction.
Vulnerability Details
function voteEmissionDirection(uint256 direction) external whenNotPaused {
voteDirection(direction);
}
function voteDirection(uint256 direction) public whenNotPaused updateReward(msg.sender) {
if (direction > 10000) revert InvalidWeight();
uint256 votingPower = IERC20(IGaugeController(controller).veRAACToken()).balanceOf(msg.sender);
if (votingPower == 0) revert NoVotingPower();
totalVotes = processVote(
userVotes[msg.sender],
direction,
votingPower,
totalVotes
);
emit DirectionVoted(msg.sender, direction, votingPower);
}
The vulnerability exists because:
No minimum token holding period verification
Vote power checks only current balance
Allows immediate voting after obtaining tokens
No snapshot mechanism for vote counting
PoC
it("should allow flash loan voting manipulation", async function() {
const FlashLoanAttacker = await ethers.getContractFactory("MockFlashLoanAttacker");
const flashLoanAttacker = await FlashLoanAttacker.deploy(
raacToken.address,
veRAACToken.address,
raacGauge.address
);
await raacToken.transfer(flashLoanAttacker.address, ethers.parseEther("1000000"));
await flashLoanAttacker.executeFlashLoanAttack(
ethers.parseEther("1000000"),
5000
);
const voteDirection = await raacGauge.userVotes(flashLoanAttacker.address);
expect(voteDirection.direction).to.equal(5000);
expect(voteDirection.weight).to.equal(0);
});
Impact
Vote manipulation through flash loans
Governance decisions can be controlled by attackers
Protocol parameters can be maliciously altered
Undermines democratic governance process
Tools Used
Recommendations
Implement a minimum token holding period before voting
Add vote power snapshots at fixed intervals
Calculate voting power based on historical balances
Add time-weighted voting mechanisms
function voteEmissionDirection(uint256 direction) external whenNotPaused {
require(getTokenHoldingPeriod(msg.sender) >= MIN_HOLDING_PERIOD, "Insufficient holding period");
uint256 votingPower = getPriorVotes(msg.sender, block.number - 1);
_processVoteWithSnapshot(direction, votingPower);
}