In the FeeCollector contract, the mechanism used to update user reward tracking during claims causes a denial-of-service (DOS) condition. After a user makes their first claim, the contract updates their reward record to the entire totalDistributed value, making subsequent reward calculations return zero even when additional fees are collected. This design flaw causes users to be permanently unable to claim further rewards, locking funds that they are entitled to.
This would be a large part of the user base as even if the difference between the previous totalDistributed and the current totalDistributed is very large (causing a very small percentage requirement for example 0.5%), all users who claimed at a particular period cannot each have the necessary percentage voting power required because a percentage of anything must add up to 100%, 500 users cannot each have 0.5% of the total.
pragma solidity ^0.8.19;
import {FeeCollector} from "../../../../contracts/core/collectors/FeeCollector.sol";
import {RAACToken} from "../../../../contracts/core/tokens/RAACToken.sol";
import {veRAACToken} from "../../../../contracts/core/tokens/veRAACToken.sol";
import {Test, console} from "forge-std/Test.sol";
contract TestSuite is Test {
FeeCollector feeCollector;
RAACToken raacToken;
veRAACToken veRAACTok;
address treasury;
address repairFund;
address admin;
uint256 initialSwapTaxRate = 100;
uint256 initialBurnTaxRate = 50;
function setUp() public {
treasury = makeAddr("treasury");
repairFund = makeAddr("repairFund");
admin = makeAddr("admin");
raacToken = new RAACToken(admin, initialSwapTaxRate, initialBurnTaxRate);
veRAACTok = new veRAACToken(address(raacToken));
feeCollector = new FeeCollector(address(raacToken), address(veRAACTok), treasury, repairFund, admin);
vm.startPrank(admin);
raacToken.setFeeCollector(address(feeCollector));
raacToken.setMinter(admin);
vm.stopPrank();
}
function testDOSAfterFirstClaim() public {
address user1 = makeAddr("user1");
address user2 = makeAddr("user2");
address user3 = makeAddr("user3");
uint256 maxDuration = veRAACTok.MAX_LOCK_DURATION();
uint256 minDuration = veRAACTok.MIN_LOCK_DURATION();
uint256 mintAmount = 5e18;
uint256 feeAmount = 1e18;
vm.startPrank(admin);
raacToken.mint(admin, feeAmount * 2);
raacToken.mint(user1, mintAmount);
raacToken.mint(user2, mintAmount);
raacToken.mint(user3, mintAmount);
raacToken.approve(address(feeCollector), feeAmount * 2);
feeCollector.collectFee(feeAmount, 0);
vm.stopPrank();
vm.startPrank(user1);
raacToken.approve(address(veRAACTok), mintAmount);
veRAACTok.lock(mintAmount, maxDuration);
console.log("User1 veRaac balance: ", veRAACTok.balanceOf(user1));
vm.stopPrank();
vm.startPrank(user2);
raacToken.approve(address(veRAACTok), mintAmount);
veRAACTok.lock(mintAmount, minDuration);
console.log("User2 veRaac balance: ", veRAACTok.balanceOf(user2));
vm.stopPrank();
vm.startPrank(user3);
raacToken.approve(address(veRAACTok), mintAmount);
veRAACTok.lock(mintAmount, maxDuration);
console.log("User3 veRaac balance: ", veRAACTok.balanceOf(user3));
vm.stopPrank();
vm.prank(admin);
feeCollector.distributeCollectedFees();
uint256 previousTotalDistributedAmount = feeCollector.totalDistributed();
console.log("totalDistributed value after first distribution: ", previousTotalDistributedAmount);
vm.prank(user1);
feeCollector.claimRewards(user1);
vm.prank(user2);
feeCollector.claimRewards(user2);
vm.prank(user3);
feeCollector.claimRewards(user3);
vm.warp(7 days + 1);
vm.startPrank(admin);
feeCollector.collectFee(feeAmount, 0);
feeCollector.distributeCollectedFees();
vm.stopPrank();
uint256 currentTotalDistributedAmount = feeCollector.totalDistributed();
console.log("totalDistributed value after second distribution: ", currentTotalDistributedAmount);
vm.prank(user1);
vm.expectRevert();
feeCollector.claimRewards(user1);
vm.prank(user2);
vm.expectRevert();
feeCollector.claimRewards(user2);
vm.prank(user3);
vm.expectRevert();
feeCollector.claimRewards(user3);
assertEq(0, feeCollector.getPendingRewards(user1));
assertEq(0, feeCollector.getPendingRewards(user2));
assertEq(0, feeCollector.getPendingRewards(user3));
console.log("Balance of feeCollector: ", raacToken.balanceOf(address(feeCollector)));
}
}
Most users are unable to claim rewards they are entitled to, causing accumulation of unclaimable rewards.
Consider redesigning the claim function to track actual user rewards claimed, and using that in determining the user's future claim amount.