Summary
distributeCollectedFees in FeeCollector directly sends token to treasury, which leads to loss of funds because Treasury can't handle direct transfers. Instead the deposit should be used.
Vulnerability Details
We can see that distributeCollectedFees calls _processDistributions which uses safeTransfer to transfer tokens to the Treasury contract.
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]);
}
If we look at the deposit function in the Treasury contract we can see that it increases _balances state variable.
function deposit(address token, uint256 amount) external override nonReentrant {
if (token == address(0)) revert InvalidAddress();
if (amount == 0) revert InvalidAmount();
IERC20(token).transferFrom(msg.sender, address(this), amount);
_balances[token] += amount;
_totalValue += amount;
emit Deposited(token, amount);
}
When manager tries to withdraw tokens using withdraw function, the call will revert since the tokens were sent directly and _balances state variables was not increased.
function withdraw(
address token,
uint256 amount,
address recipient
) external override nonReentrant onlyRole(MANAGER_ROLE) {
if (token == address(0)) revert InvalidAddress();
if (recipient == address(0)) revert InvalidRecipient();
if (_balances[token] < amount) revert InsufficientBalance();
_balances[token] -= amount;
_totalValue -= amount;
IERC20(token).transfer(recipient, amount);
emit Withdrawn(token, amount, recipient);
}
Impact
Tokens that are sent directly to the Treasury are lost. FeeCollector does that using distributeCollectedFees function.
Tools Used
Manual Review, Hardhat
Recommendations
In distributeCollectedFees use deposit function to send tokens to Treasury.