Core Contracts

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

Flash Voting Attack in BaseGauge Through Immediate Stake/Withdraw

Summary

The BaseGauge contract allows users to stake tokens and immediately vote with them before withdrawing, enabling flash loan attacks to manipulate governance votes without requiring any long-term token commitment.

Vulnerability Details

The vulnerability exists in the interaction between the stake, withdraw, and voteDirection functions in BaseGauge. While each function works correctly in isolation, their combination allows for vote manipulation:

function stake(uint256 amount) external nonReentrant updateReward(msg.sender) {
if (amount == 0) revert InvalidAmount();
_totalSupply += amount;
_balances[msg.sender] += amount;
stakingToken.safeTransferFrom(msg.sender, address(this), amount);
emit Staked(msg.sender, amount);
}
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 issue can be exploited as follows:

  1. Attacker takes a flash loan for a large amount of tokens

  2. Stakes the tokens in BaseGauge

  3. Calls voteDirection() to vote with their newly acquired voting power

  4. Immediately withdraws their tokens

  5. Repays the flash loan
    This has been demonstrated through testing:

it("allows staking, voting, and withdrawal in the same block", async () => {
const { baseGauge, rewardToken, veRAACToken, user1 } = await loadFixture(deployFixture);
const stakeAmount = ethers.parseEther("100");
await rewardToken.mint(user1.address, stakeAmount);
await rewardToken.connect(user1).approve(await baseGauge.getAddress(), stakeAmount);
const stakeTx = await baseGauge.connect(user1).stake(stakeAmount);
const voteTx = await baseGauge.connect(user1).voteDirection(5000);
const withdrawTx = await baseGauge.connect(user1).withdraw(stakeAmount);
// Block numbers showing operations can happen in quick succession:
// Stake block: 26335766
// Vote block: 26335767
// Withdraw block: 26335768
});

All of these actions can occur within consecutive blocks, costing only gas fees.

Impact

High. This vulnerability allows:

  1. Vote manipulation through flash loans

  2. No real token commitment required for voting

  3. Potential for governance attacks

  4. Undermining of the voting power system

Tools Used

  • Manual code review

  • Manual Review, Hardhat Testing Framework

Recommendations

Implement the following protections:

Add minimum stake duration before voting rights are granted:

mapping(address => uint256) public stakeTimestamp;
function stake(uint256 amount) external nonReentrant updateReward(msg.sender) {
// ... existing checks ...
stakeTimestamp[msg.sender] = block.timestamp;
}
function voteDirection(uint256 direction) public whenNotPaused updateReward(msg.sender) {
if (block.timestamp < stakeTimestamp[msg.sender] + MIN_STAKE_DURATION)
revert StakeDurationNotMet();
// ... rest of function ...
}

Lock tokens for a period after voting:

mapping(address => uint256) public tokensLockedUntil;
function voteDirection(uint256 direction) public whenNotPaused updateReward(msg.sender) {
// ... existing checks ...
tokensLockedUntil[msg.sender] = block.timestamp + VOTE_LOCK_DURATION;
}
function withdraw(uint256 amount) external nonReentrant updateReward(msg.sender) {
if (block.timestamp < tokensLockedUntil[msg.sender])
revert TokensLocked();
// ... rest of function ...
}

Consider implementing vote power snapshots at fixed intervals rather than using current balance.

Updates

Lead Judging Commences

inallhonesty Lead Judge
3 months ago
inallhonesty Lead Judge about 2 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.