Core Contracts

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

Missing Vote Delay Protection in GaugeController

Summary

The GaugeController contract has a defined VOTE_DELAY constant but fails to implement the voting delay protection mechanism. This allows users to vote multiple times in rapid succession, which deviates from the intended voting mechanics and could lead to vote manipulation.

Vulnerability Details

The contract defines the delay constant but doesn't enforce it:

/// @notice Required delay between votes
uint256 public constant VOTE_DELAY = 10 days;
/// @notice Minimum allowed vote delay
uint256 public constant MIN_VOTE_DELAY = 1 days;
/// @notice Maximum allowed vote delay
uint256 public constant MAX_VOTE_DELAY = 10 days;
// Vote function missing delay check
function vote(address gauge, uint256 weight) external override whenNotPaused {
if (!isGauge(gauge)) revert GaugeNotFound();
if (weight > WEIGHT_PRECISION) revert InvalidWeight();
uint256 votingPower = veRAACToken.balanceOf(msg.sender);
if (votingPower == 0) revert NoVotingPower();
uint256 oldWeight = userGaugeVotes[msg.sender][gauge];
userGaugeVotes[msg.sender][gauge] = weight;
_updateGaugeWeight(gauge, oldWeight, weight, votingPower);
emit WeightUpdated(gauge, oldWeight, weight);
}

PoC

In order to run the test you need to:

  1. Run foundryup to get the latest version of Foundry

  2. Install hardhat-foundry: npm install --save-dev @nomicfoundation/hardhat-foundry

  3. Import it in your Hardhat config: require("@nomicfoundation/hardhat-foundry");

  4. Make sure you've set the BASE_RPC_URL in the .env file or comment out the forking option in the hardhat config.

  5. Run npx hardhat init-foundry

  6. There is one file in the test folder that will throw an error during compilation so rename the file in test/unit/libraries/ReserveLibraryMock.sol to => ReserveLibraryMock.sol_broken so it doesn't get compiled anymore (we don't need it anyways).

  7. Create a new folder test/foundry

  8. Paste the below code into a new test file i.e.: FoundryTest.t.sol

  9. Run the test: forge test --mc FoundryTest -vvvv

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;
import {Test} from "forge-std/Test.sol";
import {console2} from "forge-std/console2.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ERC20Mock} from "../../contracts/mocks/core/tokens/ERC20Mock.sol";
import {GaugeController} from "../../contracts/core/governance/gauges/GaugeController.sol";
import {RAACGauge} from "../../contracts/core/governance/gauges/RAACGauge.sol";
import {RWAGauge} from "../../contracts/core/governance/gauges/RWAGauge.sol";
import {veRAACToken} from "../../contracts/core/tokens/veRAACToken.sol";
import {IGaugeController} from "../../contracts/interfaces/core/governance/gauges/IGaugeController.sol";
contract FoundryTest is Test {
// Contracts
ERC20Mock public raacToken;
ERC20Mock public stakingToken;
ERC20Mock public rewardToken;
veRAACToken public veToken;
GaugeController public controller;
RAACGauge public raacGauge;
RWAGauge public rwaGauge;
// Test addresses
address public admin = address(this);
address public alice = address(0x1);
address public bob = address(0x2);
address public carol = address(0x3);
// Constants
uint256 public constant INITIAL_SUPPLY = 1_000_000e18;
uint256 public constant LOCK_AMOUNT = 100_000e18;
uint256 public constant YEAR = 365 days;
function setUp() public {
// Deploy mock tokens
raacToken = new ERC20Mock("RAAC Token", "RAAC");
rewardToken = new ERC20Mock("Reward Token", "RWD");
// Deploy veToken
veToken = new veRAACToken(address(raacToken));
// Deploy controller
controller = new GaugeController(address(veToken));
// Deploy gauges
raacGauge = new RAACGauge(address(rewardToken), address(veToken), address(controller));
rwaGauge = new RWAGauge(address(rewardToken), address(veToken), address(controller));
// Setup initial token balances
raacToken.mint(alice, INITIAL_SUPPLY);
raacToken.mint(bob, INITIAL_SUPPLY);
rewardToken.mint(address(controller), INITIAL_SUPPLY * 10);
// Add gauges to controller
vm.startPrank(admin);
controller.addGauge(address(raacGauge), IGaugeController.GaugeType.RAAC, 0);
controller.addGauge(address(rwaGauge), IGaugeController.GaugeType.RWA, 0);
vm.stopPrank();
// Setup approvals
vm.startPrank(alice);
raacToken.approve(address(veToken), type(uint256).max);
veToken.approve(address(raacGauge), type(uint256).max);
vm.stopPrank();
vm.startPrank(bob);
raacToken.approve(address(veToken), type(uint256).max);
veToken.approve(address(raacGauge), type(uint256).max);
vm.stopPrank();
}
function test_NoVotingDelay() public {
vm.startPrank(alice);
veToken.lock(LOCK_AMOUNT, YEAR);
// vote multiple times without delay
controller.vote(address(raacGauge), 8000);
controller.vote(address(raacGauge), 2000);
controller.vote(address(raacGauge), 10000);
vm.stopPrank();
}
}

Impact

  • Users can rapidly change their votes

  • Potential for vote timing attacks

  • Ability to react to other voters' actions immediately

  • Gauge weights can be manipulated more easily

Tools Used

  • Manual Review

  • Foundry

Recommendations

Implement vote delay check:

mapping(address => mapping(address => uint256)) public lastUserVoteTime;
function vote(address gauge, uint256 weight) external override whenNotPaused {
if (!isGauge(gauge)) revert GaugeNotFound();
if (weight > WEIGHT_PRECISION) revert InvalidWeight();
// Add vote delay check
uint256 lastVoteTime = lastUserVoteTime[msg.sender][gauge];
if (block.timestamp < lastVoteTime + VOTE_DELAY) {
revert VoteDelayNotElapsed();
}
uint256 votingPower = veRAACToken.balanceOf(msg.sender);
if (votingPower == 0) revert NoVotingPower();
uint256 oldWeight = userGaugeVotes[msg.sender][gauge];
userGaugeVotes[msg.sender][gauge] = weight;
_updateGaugeWeight(gauge, oldWeight, weight, votingPower);
// Update last vote time
lastUserVoteTime[msg.sender][gauge] = block.timestamp;
emit WeightUpdated(gauge, oldWeight, weight);
}

}

Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Validated
Assigned finding tags:

GaugeController::vote never enforces VOTE_DELAY or updates lastVoteTime, allowing users to spam votes and manipulate gauge weights without waiting

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Validated
Assigned finding tags:

GaugeController::vote never enforces VOTE_DELAY or updates lastVoteTime, allowing users to spam votes and manipulate gauge weights without waiting

Support

FAQs

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