Summary
The _processDistributions function in FeeCollector fails to distribute rewards to veRAACToken holders despite calculating their share.
function _processDistributions(uint256 totalFees, uint256[4] memory shares) internal {
uint256 contractBalance = raacToken.balanceOf(address(this));
if (contractBalance < totalFees) revert InsufficientBalance();
if (shares[0] > 0) {
uint256 totalVeRAACSupply = veRAACToken.getTotalVotingPower();
if (totalVeRAACSupply > 0) {
TimeWeightedAverage.createPeriod(
distributionPeriod,
block.timestamp + 1,
7 days,
shares[0],
totalVeRAACSupply
);
totalDistributed += shares[0];
} else {
shares[3] += shares[0];
}
}
if (shares[1] > 0) raacToken.burn(shares[1]);
if (shares[2] > 0) raacToken.safeTransfer(repairFund, shares[2]);
if (shares[3] > 0) raacToken.safeTransfer(treasury, shares[3]);
}
Vulnerability Details
In the condition of shares[0] > 0, a TimeWeightAverage lock period is created, but there is no additional calls to TimeWeightAverage to unlock RAACTokens for veRAACToken holder.
The timelock is there, but there is no logic for users to claims their rewards distributed over time.
Impact
veRAACToken holders never receive their share (shares[0])
Tokens remain stuck in FeeCollector
All reward accounting is incorrect
Tools Used
Manual
Recommendations
In _processDistributions transfer RAAC to the TimeWeightedAverage contract, and add withdraw logic there
function _processDistributions(uint256 totalFees, uint256[4] memory shares) internal {
uint256 contractBalance = raacToken.balanceOf(address(this));
if (contractBalance < totalFees) revert InsufficientBalance();
if (shares[0] > 0) {
uint256 totalVeRAACSupply = veRAACToken.getTotalVotingPower();
if (totalVeRAACSupply > 0) {
TimeWeightedAverage.createPeriod(
distributionPeriod,
block.timestamp + 1,
7 days,
shares[0],
totalVeRAACSupply
);
totalDistributed += shares[0]; //@audit this does not transfer raacToken, and there is no other calls in FeeCollector to TimeWeightedAverage
+ raacToken.safeTransfer(address(TimeWeightedAverage), shares[0]);
} else {
shares[3] += shares[0]; // Add to treasury if no veRAAC holders
}
}
if (shares[1] > 0) raacToken.burn(shares[1]);
if (shares[2] > 0) raacToken.safeTransfer(repairFund, shares[2]);
if (shares[3] > 0) raacToken.safeTransfer(treasury, shares[3]);
}
+ withdraw logic in TimeWeightedAverage for veRAACHolders
OR
Add withdraw logic in FeeCollector that uses TimeWeightedAverage information
Note that first proposition seems better as FeeCollector holds RAACToken for multiple parties (veRAACShare, burn, repair and treasury) so it can be complicated to do all of that in one place.