Relevant GitHub Links
https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/collectors/Treasury.sol#L64-L77
https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/collectors/Treasury.sol#L87-L95
Summary
The Treasury contract tracks allocation limits for managers but never enforces them during withdrawals, allowing managers to withdraw unlimited amounts regardless of their allocation.
Vulnerability Details
The Treasury contract implements an allocation system through allocateFunds()
that sets spending limits for managers. However, these allocations are never checked during withdrawals:
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);
}
A manager with MANAGER_ROLE can withdraw any amount up to the total balance, bypassing their allocation limit entirely.
Impact
High severity as:
Managers can withdraw unlimited funds beyond their intended allocation limits
Complete bypass of the allocation system intended to limit withdrawals
Direct risk to protocol funds as single manager can drain treasury
Tools Used
Manual review
Recommendations
Add allocation enforcement in the withdraw function:
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();
if (_allocations[msg.sender][recipient] < amount) revert ExceedsAllocation();
_allocations[msg.sender][recipient] -= amount;
_balances[token] -= amount;
_totalValue -= amount;
IERC20(token).transfer(recipient, amount);
emit Withdrawn(token, amount, recipient);
}