The FeeCollector::emergencyWithdraw function directly transfers tokens to the Treasury contract without updating its internal _balances mapping. As a result, the Treasury contract does not reflect the actual token balance, which leads to funds being permanently locked and unrecoverable.
The emergencyWithdraw function allows an authorized caller to withdraw tokens from the FeeCollector contract and send them to the Treasury. However, instead of using the deposit function in the Treasury contract, it transfers tokens directly using safeTransfer. Since Treasury tracks balances manually in _balances, this leads to a discrepancy where _balances[token] remains zero while the actual token balance is nonzero.
As a result, any subsequent calls to withdraw in the Treasury contract will fail due to the InsufficientBalance check, effectively locking the tokens inside the contract.
Issue:
_balances[token] remains zero after emergencyWithdraw, making every withdrawal attempt revert with InsufficientBalance().
The actual token balance is nonzero, but it cannot be accessed due to the incorrect _balances mapping.
PoC
Create a new file named FeeCollectorTreasury inside test/unit/core/collectors, then paste the test content into it. Finally, run the test using the following command:
This vulnerability results in permanent loss of funds, as tokens sent through emergencyWithdraw cannot be withdrawn later. Users and protocol managers will be unable to retrieve funds stored in the Treasury, leading to financial losses and disruptions in protocol operations.
Manual review
Avoid maintaining internal balance tracking; instead, rely on IERC20(token).balanceOf() to determine token holdings. Alternatively, ensure that funds transferred from the FeeCollector to the Treasury contract are processed through the deposit function to keep balances accurately recorded.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.