Consider two users with equal voting power (250e18 each) when 800 RAAC tokens are distributed:
High - Directly causes loss of rewards that users are entitled to and creates systemic unfairness in reward distribution.
High - Comes into fruition after first round of rewards.
pragma solidity ^0.8.19;
import "contracts/core/tokens/RToken.sol";
import "contracts/core/tokens/RAACToken.sol";
import "contracts/core/tokens/veRAACToken.sol";
import "contracts/core/collectors/FeeCollector.sol";
import "contracts/core/governance/boost/BoostController.sol";
import "contracts/core/governance/gauges/BaseGauge.sol";
import "contracts/core/governance/gauges/GaugeController.sol";
import "contracts/core/governance/gauges/RAACGauge.sol";
import "contracts/core/governance/gauges/RWAGauge.sol";
import "contracts/core/governance/proposals/Governance.sol";
import "contracts/core/governance/proposals/TimelockController.sol";
import {Test, console} from "forge-std/Test.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract MockERC20 is ERC20 {
constructor() ERC20("Mock", "MCK") {}
function mint(address to, uint256 amount) public {
_mint(to, amount);
}
}
contract MasterGovernanceTest is Test {
RToken public rToken;
RAACToken public raacToken;
veRAACToken public veRaacToken;
FeeCollector public feeCollector;
address public treasury;
address public repairFund;
MockERC20 public mockCrvUSD;
address public protocolOwner = makeAddr("ProtocolOwner");
address public rTokenMinter = makeAddr("rTokenMinter");
address public rTokenBurner = makeAddr("rTokenBurner");
address public raacTokenMinter = makeAddr("RAACTokenMinter");
address public veRaacTokenMinter = makeAddr("veRaacTokenMinter");
address public Alice = makeAddr("Alice");
address public Bob = makeAddr("Bob");
uint256 constant MIN_LOCK_DURATION = 365 days;
uint256 constant MAX_LOCK_DURATION = 1460 days;
uint256 constant LOCK_AMOUNT = 1000 ether;
uint256 constant ADDITIONAL_LOCK_AMOUNT = 100 ether;
function setUp() public {
vm.startPrank(protocolOwner);
mockCrvUSD = new MockERC20();
rToken = new RToken(
"RToken",
"RTKN",
protocolOwner,
address(mockCrvUSD)
);
rToken.setMinter(rTokenMinter);
rToken.setBurner(rTokenBurner);
raacToken = new RAACToken(
protocolOwner,
100,
50
);
raacToken.setMinter(raacTokenMinter);
veRaacToken = new veRAACToken(address(raacToken));
veRaacToken.setMinter(veRaacTokenMinter);
raacToken.manageWhitelist(address(veRaacToken), true);
treasury = makeAddr("treasury");
repairFund = makeAddr("repairFund");
feeCollector = new FeeCollector(
address(raacToken),
address(veRaacToken),
treasury,
repairFund,
protocolOwner
);
vm.stopPrank();
vm.startPrank(raacTokenMinter);
raacToken.mint(Alice, LOCK_AMOUNT);
raacToken.mint(Bob, LOCK_AMOUNT);
vm.stopPrank();
}
function test_RewardsDistributionBug() public {
vm.startPrank(Alice);
raacToken.approve(address(veRaacToken), LOCK_AMOUNT);
veRaacToken.lock(LOCK_AMOUNT, MIN_LOCK_DURATION);
vm.stopPrank();
vm.startPrank(Bob);
raacToken.approve(address(veRaacToken), LOCK_AMOUNT);
veRaacToken.lock(LOCK_AMOUNT, MIN_LOCK_DURATION);
vm.stopPrank();
address feePayer = makeAddr("feePayer");
vm.startPrank(raacTokenMinter);
raacToken.mint(feePayer, 1000 ether);
vm.stopPrank();
vm.startPrank(feePayer);
raacToken.approve(address(feeCollector), 1000 ether);
feeCollector.collectFee(1000 ether, 0);
vm.stopPrank();
uint256 feeAmount = 1000 ether;
vm.startPrank(raacTokenMinter);
raacToken.mint(address(feeCollector), feeAmount);
vm.stopPrank();
vm.startPrank(protocolOwner);
feeCollector.distributeCollectedFees();
vm.stopPrank();
uint256 initialAliceRewards = feeCollector.getPendingRewards(Alice);
console.log("Alice rewards after first round (claims): ", initialAliceRewards);
vm.startPrank(Alice);
feeCollector.claimRewards(Alice);
vm.stopPrank();
uint256 initialBobRewards = feeCollector.getPendingRewards(Bob);
console.log("Bob rewards after first round (Does not claim): ", initialBobRewards);
vm.warp(block.timestamp + 7 days + 1);
address feePayer2 = makeAddr("feePayer2");
vm.startPrank(raacTokenMinter);
raacToken.mint(feePayer2, 1000 ether);
vm.stopPrank();
vm.startPrank(feePayer2);
raacToken.approve(address(feeCollector), 1000 ether);
feeCollector.collectFee(1000 ether, 0);
vm.stopPrank();
vm.startPrank(raacTokenMinter);
raacToken.mint(address(feeCollector), feeAmount);
vm.stopPrank();
vm.startPrank(protocolOwner);
feeCollector.distributeCollectedFees();
vm.stopPrank();
uint256 secondAliceRewards = feeCollector.getPendingRewards(Alice);
console.log("Alice rewards after second round: ", secondAliceRewards);
uint256 secondBobRewards = feeCollector.getPendingRewards(Bob);
console.log("Bob rewards after second round: ", secondBobRewards);
vm.startPrank(Bob);
feeCollector.claimRewards(Bob);
vm.stopPrank();
console.log("Bob RAAC balance after claiming (Some lost to burning): ", IERC20(address(raacToken)).balanceOf(Bob));
}
}