The veRAACToken contract's implementation of voting power decay creates a discrepancy between the total voting power reported by getTotalVotingPower() and the actual sum of all users' voting power. This discrepancy grows over time and results in a portion of users' rewards in `FeeCollector` to become permanently unclaimable.
The issue is due to how the individual voting power and total voting power are calculated. The discrepancy occurs because individual voting power is calculated after accounting for the voting power decay of each token over time, while the total voting power doesn't account for this decay and instead just returns the total tokens in circulation. The difference grows larger as more time passes causing a loss for users' when claiming their tokens in `FeeCollector::claimRewards`.
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 testUserVotingPowerDoesntAddUpToTotalVotingPowerCausingLossOfUserFees() public {
address user1 = makeAddr("user1");
address user2 = makeAddr("user2");
uint256 mintAmount = 1e18;
uint256 maxDuration = veRAACTok.MAX_LOCK_DURATION();
vm.startPrank(admin);
raacToken.mint(user1, mintAmount);
raacToken.mint(user2, mintAmount);
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, maxDuration);
console.log("User2 veRaac balance: ", veRAACTok.balanceOf(user2));
vm.stopPrank();
vm.warp(7 days);
uint256 expectedTotalVotingPower = veRAACTok.getTotalVotingPower();
uint256 actualTotalVotingPower = veRAACTok.getVotingPower(user1) + veRAACTok.getVotingPower(user2);
assertGt(expectedTotalVotingPower, actualTotalVotingPower);
}
}
A portion of fees allocated to veRAACToken holders becomes unclaimable and this percentage of unclaimable rewards grows over time.