Critical access control vulnerability in treasury/repair fund update mechanisms allows any address to hijack protocol funds after timelock expiration. Missing role validation enables complete bypass of admin privileges.
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../contracts/core/collectors/FeeCollector.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MockRAACToken is ERC20, Ownable {
constructor() ERC20("MockRAAC", "MRAAC") Ownable(msg.sender) {
_mint(msg.sender, 1_000_000_000e18);
}
function burn(uint256 amount) external {
_burn(msg.sender, amount);
}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
}
contract MockveRAACToken {
mapping(address => uint256) public votingPower;
uint256 public totalVotingPower;
function setVotingPower(address user, uint256 power) external {
votingPower[user] = power;
totalVotingPower += power;
}
function getVotingPower(address user) external view returns (uint256) {
return votingPower[user];
}
function getTotalVotingPower() external view returns (uint256) {
return totalVotingPower;
}
}
* @title FeeCollectorTest
* @notice This Foundry test verifies two things:
* 1. That the fee distribution calculation (which does multiplication and division by BASIS_POINTS)
* results in distributed shares whose sum equals the total fee amount. This helps expose any rounding errors.
* 2. That the distribution period is created with a fixed 7-day window.
*/
contract FeeCollectorTest is Test {
FeeCollector feeCollector;
MockRAACToken mockRAACToken;
MockveRAACToken mockveRAACToken;
address admin;
address user;
address distributor;
address treasury;
address repairFund;
function setUp() public {
admin = vm.addr(1);
user = vm.addr(2);
distributor = vm.addr(3);
treasury = vm.addr(4);
repairFund = vm.addr(5);
mockRAACToken = new MockRAACToken();
mockveRAACToken = new MockveRAACToken();
vm.prank(admin);
feeCollector = new FeeCollector(
address(mockRAACToken),
address(mockveRAACToken),
treasury,
repairFund,
admin
);
mockveRAACToken.setVotingPower(admin, 1000 ether);
mockRAACToken.mint(admin, 1_000_000_000e18);
vm.startPrank(admin);
mockRAACToken.transfer(user, 100_000e18);
mockRAACToken.transfer(distributor, 100_000e18);
vm.stopPrank();
vm.startPrank(user);
mockRAACToken.approve(address(feeCollector), type(uint256).max);
vm.stopPrank();
vm.startPrank(distributor);
mockRAACToken.approve(address(feeCollector), type(uint256).max);
vm.stopPrank();
}
function testOnlyAdminCanApplyTreasuryUpdate() public {
address newTreasuryAddr = vm.addr(10);
vm.prank(admin);
feeCollector.setTreasury(newTreasuryAddr);
vm.warp(block.timestamp + 2 days);
vm.prank(user);
vm.expectRevert("UnauthorizedCaller()");
feeCollector.applyTreasuryUpdate();
vm.prank(admin);
feeCollector.applyTreasuryUpdate();
assertEq(feeCollector.treasury(), newTreasuryAddr);
}
function testOnlyAdminCanApplyRepairFundUpdate() public {
address newRepairFundAddr = vm.addr(11);
vm.prank(admin);
feeCollector.setRepairFund(newRepairFundAddr);
vm.warp(block.timestamp + 2 days);
vm.prank(user);
vm.expectRevert("UnauthorizedCaller()");
feeCollector.applyRepairFundUpdate();
vm.prank(admin);
feeCollector.applyRepairFundUpdate();
assertEq(feeCollector.repairFund(), newRepairFundAddr);
}
}