Core Contracts

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

Missing Vote Frequency Control in GaugeController

Vulnerability Details

In the GaugeController contract, the vote function lacks implementation of vote frequency controls despite having state variables designed for this purpose. The contract defines:

/// @notice Last vote timestamp for each user
mapping(address => uint256) public lastVoteTime;
/// @notice Required delay between votes
uint256 public constant VOTE_DELAY = 10 days;

However, the vote function does not utilize these variables to enforce voting delays:

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);
}

Missing Checks

  1. The function does not check lastVoteTime for the user

  • The function does not enforce the VOTE_DELAY between votes

  • The function does not update lastVoteTime after a successful vote

Impact

Users can vote repeatedly for gauges without any time restrictions or cooldown period, potentially allowing manipulation of gauge weights through rapid successive votes.

Proof of concept

Adding Foundry to a Hardhat project

  • create foundryTests folder in the test folder, and make test GaugesTests.t.sol file, and run the following test:

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.28;
    import {Test, console} from "../../lib/forge-std/src/Test.sol";
    import {GaugeController, IGaugeController} from "../../contracts/core/governance/gauges/GaugeController.sol";
    import "../../contracts/interfaces/core/governance/gauges/IGaugeController.sol";
    import {RAACGauge} from "../../contracts/core/governance/gauges/RAACGauge.sol";
    import {RWAGauge} from "../../contracts/core/governance/gauges/RWAGauge.sol";
    import {ERC20Mock} from "../../contracts/mocks/core/tokens/ERC20Mock.sol";
    import {MockVeToken} from "../../contracts/mocks/core/tokens/MockVeToken.sol";
    contract GaugesTest is Test {
    GaugeController public controller;
    MockVeToken public veRAACToken;
    ERC20Mock public rewardToken;
    ERC20Mock public stakingToken;
    RAACGauge public raacGauge;
    RWAGauge public rwaGauge;
    address public admin = address(0x1);
    address public alice = address(0x2);
    address public bob = address(0x3);
    address public carol = address(0x4);
    // Constants from GaugeController
    uint256 public constant WEIGHT_PRECISION = 10000;
    uint256 public constant VOTE_DELAY = 10 days;
    event WeightUpdated(address indexed gauge, uint256 oldWeight, uint256 newWeight);
    event GaugeAdded(address indexed gauge, uint8 indexed gaugeType);
    function setUp() public {
    // Deploy mock tokens
    veRAACToken = new MockVeToken();
    rewardToken = new ERC20Mock("RewardToken", "RWD");
    stakingToken = new ERC20Mock("StakingToken", "STK");
    vm.startPrank(admin);
    // Deploy controller
    controller = new GaugeController(address(veRAACToken));
    vm.stopPrank();
    // Deploy gauges
    raacGauge = new RAACGauge(address(rewardToken), address(stakingToken), address(controller));
    rwaGauge = new RWAGauge(address(rewardToken), address(stakingToken), address(controller));
    // Setup initial states
    veRAACToken.mint(alice, 1000e18);
    veRAACToken.mint(bob, 500e18);
    veRAACToken.mint(carol, 250e18);
    }
    function test_AddGauge() public {
    vm.startPrank(admin);
    // Add the gauge
    controller.addGauge(address(raacGauge), IGaugeController.GaugeType.RAAC, 100);
    vm.stopPrank();
    // Verify the gauge was added
    assertTrue(controller.isGauge(address(raacGauge)));
    assertEq(controller.getGaugeWeight(address(raacGauge)), 100);
    }
    function test_VoteWithoutDelay() public {
    test_AddGauge();
    vm.startPrank(alice);
    // First vote
    controller.vote(address(raacGauge), 5000);
    // Second vote immediately after
    controller.vote(address(raacGauge), 3000);
    // Third vote
    controller.vote(address(raacGauge), 7000);
    vm.stopPrank();
    // The last vote should be recorded
    assertEq(controller.userGaugeVotes(alice, address(raacGauge)), 7000);
    }
    }

Recommendations

Add vote frequency controls to the vote function:

function vote(address gauge, uint256 weight) external override whenNotPaused {
if (!isGauge(gauge)) revert GaugeNotFound();
if (weight > WEIGHT_PRECISION) revert InvalidWeight();
// Check if enough time has passed since last vote
+ uint256 timeSinceLastVote = block.timestamp - lastVoteTime[msg.sender];
+ if (timeSinceLastVote < VOTE_DELAY) {
revert VoteDelayNotMet();
}
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
+ lastVoteTime[msg.sender] = block.timestamp;
emit WeightUpdated(gauge, oldWeight, weight);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 7 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 7 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.

Give us feedback!