Core Contracts

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

Lack of Time-Weighted Voting and Weight Decay in GaugeController

Summary

The GaugeController contract implements a voting mechanism that lacks proper time-weighted voting and weight decay functionality. This is a significant deviation from the established Curve Finance model and what is documented. This leads to manipulation of gauge weights and improper reward distribution.

Vulnerability Details

The current implementation uses a simple arithmetic calculation for updating gauge weights:

function _updateGaugeWeight(address gauge, uint256 oldWeight, uint256 newWeight, uint256 votingPower) internal {
Gauge storage g = gauges[gauge];
uint256 oldGaugeWeight = g.weight;
uint256 newGaugeWeight = oldGaugeWeight -
((oldWeight * votingPower) / WEIGHT_PRECISION) +
((newWeight * votingPower) / WEIGHT_PRECISION);
g.weight = newGaugeWeight;
g.lastUpdateTime = block.timestamp;
}

1.No Linear Decay:

  • Votes maintain their full weight until explicitly changed

  • No automatic reduction in voting power over time

  • Weights don't reflect the diminishing influence of older votes

2. Lack of Time-Weighting:

  • Votes are counted at face value without considering time

  • No bias/slope mechanism for gradual power reduction

  • No consideration of veToken lock duration

3.Immediate Weight Updates:

  • Changes take effect immediately without proper scheduling

  • No checkpoint system for tracking historical weights

  • No mechanism for calculating weights at specific timestamps

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";
import {IGauge} from "../../contracts/interfaces/core/governance/gauges/IGauge.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");
stakingToken = new ERC20Mock("Staking Token", "STK");
// Deploy veToken
veToken = new veRAACToken(address(raacToken));
// Deploy controller
controller = new GaugeController(address(veToken));
// Deploy gauges
raacGauge = new RAACGauge(address(rewardToken), address(stakingToken), address(controller));
rwaGauge = new RWAGauge(address(rewardToken), address(stakingToken), address(controller));
// Setup initial token balances
raacToken.mint(alice, INITIAL_SUPPLY);
stakingToken.mint(alice, INITIAL_SUPPLY);
raacToken.mint(bob, INITIAL_SUPPLY);
stakingToken.mint(bob, INITIAL_SUPPLY);
rewardToken.mint(address(raacGauge), INITIAL_SUPPLY * 10);
rewardToken.mint(address(rwaGauge), 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);
stakingToken.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);
stakingToken.approve(address(raacGauge), type(uint256).max);
vm.stopPrank();
}
function test_WeightUpdate() public {
// Alice locks 100k tokens for 1 year
vm.startPrank(alice);
veToken.lock(LOCK_AMOUNT, YEAR);
controller.vote(address(raacGauge), 5000); // 50% weight
// Check gauge weights
uint256 gaugeWeightAfterFirstVote = controller.getGaugeWeight(address(raacGauge));
console2.log("RAAC Gauge Weight:", gaugeWeightAfterFirstVote);
// 6 months later...
vm.warp(block.timestamp + 183 days);
// In Curve's implementation: Vote weight would have decayed by ~12.5%
// In current implementation: Vote weight remains at full
// Alice votes again with same power
controller.vote(address(raacGauge), 5000); // 50% weight
// Check gauge weights
uint256 gaugeWeightAfterSecondVote = controller.getGaugeWeight(address(raacGauge));
console2.log("RAAC Gauge Weight:", gaugeWeightAfterSecondVote);
vm.stopPrank();
assertEq(gaugeWeightAfterFirstVote, gaugeWeightAfterSecondVote);
}
}

Impact

  • Users can maintain influence without active participation

  • No incentive to maintain long-term veToken locks

  • Potential for last-minute vote changes before reward distribution

  • Rewards may be distributed based on stale or manipulated weights

  • No correlation between lock duration and voting influence

  • Potential for gaming reward distributions

  • Misalignment with veToken time-locking mechanism

Tools Used

  • Foundry

  • Manual Review

Recommendations

  • Uses slope and bias to create time-decaying votes (slope/bias is already implemented in veRAACToken but make sure this works as expected)

  • Votes should naturally decrease over time based on lock duration

  • Schedule weight changes for future week

  • Implement a checkpoint system to maintain historical points for accurate weight calculation

Updates

Lead Judging Commences

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

GaugeController doesn't decay gauge vote weights when veRAAC voting power expires, allowing users to influence reward distribution with expired voting power

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

GaugeController doesn't decay gauge vote weights when veRAAC voting power expires, allowing users to influence reward distribution with expired voting power

Support

FAQs

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