The BaseGauge contract's `rewardPerTokenStored` only increases and never decreases, even when there are less rewards available to distribute. This leads to inflated reward calculations and failed claim attempts.
1. Always adds to the previous `rewardPerTokenStored` value
2. Never considers actual available rewards
3. Doesn't decrease when reward rate or available rewards decrease
pragma solidity ^0.8.19;
import {ERC20Mock} from "../../../../contracts/mocks/core/tokens/ERC20Mock.sol";
import {FeeCollector} from "../../../../contracts/core/collectors/FeeCollector.sol";
import {Treasury} from "../../../../contracts/core/collectors/Treasury.sol";
import {RAACToken} from "../../../../contracts/core/tokens/RAACToken.sol";
import {veRAACToken} from "../../../../contracts/core/tokens/veRAACToken.sol";
import {GaugeController, IGaugeController} 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 {Test, console} from "forge-std/Test.sol";
contract TestSuite is Test {
FeeCollector feeCollector;
Treasury treasury;
RAACToken raacToken;
veRAACToken veRAACTok;
GaugeController gaugeController;
RAACGauge raacGauge;
RAACGauge raacGauge2;
RWAGauge rwaGauge;
ERC20Mock rewardToken;
address repairFund;
address admin;
uint256 initialSwapTaxRate = 100;
uint256 initialBurnTaxRate = 50;
uint256 initialWeight = 5000;
function setUp() public {
repairFund = makeAddr("repairFund");
admin = makeAddr("admin");
rewardToken = new ERC20Mock("Reward Token", "RWT");
treasury = new Treasury(admin);
raacToken = new RAACToken(admin, initialSwapTaxRate, initialBurnTaxRate);
veRAACTok = new veRAACToken(address(raacToken));
feeCollector = new FeeCollector(address(raacToken), address(veRAACTok), address(treasury), repairFund, admin);
vm.startPrank(admin);
raacToken.setFeeCollector(address(feeCollector));
raacToken.setMinter(admin);
gaugeController = new GaugeController(address(veRAACTok));
raacGauge = new RAACGauge(address(rewardToken), address(raacToken), address(gaugeController));
raacGauge2 = new RAACGauge(address(rewardToken), address(raacToken), address(gaugeController));
rwaGauge = new RWAGauge(address(rewardToken), address(raacToken), address(gaugeController));
gaugeController.addGauge(address(raacGauge), IGaugeController.GaugeType.RAAC, initialWeight);
gaugeController.addGauge(address(raacGauge2), IGaugeController.GaugeType.RAAC, initialWeight);
gaugeController.addGauge(address(rwaGauge), IGaugeController.GaugeType.RWA, initialWeight);
vm.stopPrank();
}
function testRewardPerTokenStoredNeverDecreasesEvenWhenTheresLessRewardsAvailableToDistribute() public {
address Alice = makeAddr("Alice");
uint256 stakeAmount = 100e18;
uint256 largeRewardAmount = 1000e18;
uint256 smallRewardAmount = 10e18;
vm.startPrank(admin);
raacToken.mint(Alice, stakeAmount);
rewardToken.mint(address(raacGauge), largeRewardAmount + smallRewardAmount);
vm.stopPrank();
vm.startPrank(Alice);
raacToken.approve(address(raacGauge), stakeAmount);
raacGauge.stake(stakeAmount);
vm.stopPrank();
vm.startPrank(address(gaugeController));
raacGauge.notifyRewardAmount(largeRewardAmount);
vm.stopPrank();
uint256 initialRewardPerToken = raacGauge.rewardPerTokenStored();
uint256 initialRewardRate = raacGauge.rewardRate();
vm.warp(block.timestamp + 1 days);
vm.startPrank(address(gaugeController));
raacGauge.notifyRewardAmount(smallRewardAmount);
vm.stopPrank();
uint256 newRewardPerToken = raacGauge.rewardPerTokenStored();
uint256 newRewardRate = raacGauge.rewardRate();
console.log("Initial reward per token:", initialRewardPerToken);
console.log("New reward per token:", newRewardPerToken);
console.log("Initial reward rate:", initialRewardRate);
console.log("New reward rate:", newRewardRate);
assertGt(newRewardPerToken, initialRewardPerToken);
vm.warp(block.timestamp + 1 days);
vm.startPrank(Alice);
uint256 earnedRewards = raacGauge.earned(Alice);
console.log("Earned rewards shown:", earnedRewards);
console.log("Actual reward token balance:", rewardToken.balanceOf(address(raacGauge)));
vm.expectRevert();
raacGauge.getReward();
vm.stopPrank();
}
}
1. Users to see inflated reward amounts they can't actually claim
2. Failed transactions when trying to claim rewards due to insufficient balance
3. DOS as users waste gas on failed claim attempts
4. Loss of trust in the reward system
Track actual available rewards and use it to cap `rewardPerTokenStored` or add a mechanism to decrease `rewardPerTokenStored` when rewards decrease