The CapitalPool
contract contains an approve
function that approves the token manager to spend an unlimited amount of tokens. This function is only called from within the TokenManager::_transfer
function under certain conditions.
However, the implementation is flawed in several ways:
The check in _transfer
calls CapitalPool::approve
, passing address(this)
(the calling contract's address) instead of the token address.
The approve
function lacks access control, allowing any address to call it.
The check in _transfer
only occurs when the allowance is exactly zero, missing cases where the allowance is insufficient but non-zero.
There is no check if tokenAddr
is indeed a ERC20 token, allowing an attacker to pass a malicious contract with the same selector.
In TokenManager.sol
:
And the called function in CapitalPool.sol
:
The current implementation violates the principle of least privilege by granting unlimited approvals (type(uint256).max
) to the token manager. This approach unnecessarily increases the potential impact of any security breach. By allowing unlimited token spending, it exposes the contract to greater risk if the token manager is compromised or contains vulnerabilities.
A more secure approach would be to approve only the specific amount needed for each transaction, limiting the potential damage in case of an exploit.
This vulnerability can lead to several high-severity issues:
Failed Transfers: Transfers from the capital pool will fail when the allowance is zero, as the approval call will not succeed due to incorrect parameters.
Locked Funds: Tokens in the capital pool may become inaccessible if the approval mechanism fails and there's no other way to approve or transfer tokens.
Unauthorized Approvals: Any address can call the approve
function, potentially leading to unauthorized token approvals if exploited.
Manual review
Redesign the approval mechanism:
Remove the CapitalPool
contract. It has no use appart from storing the tokens. Send them to the TokenManager
instead, it will simplify the protocol’s design and potential attack surfaces.
Before a transfer, wrap the _token
in an IERC20
interface and use the approve
method to safely set the required allowances. This change ensures that the contract does not accidentally interact with non-standard tokens and correctly handles all token operations.
Correct the _transfer
function:
Remove the current approve
call
This is at most low severity, even though giving max approvals shouldn't be permisionless, the respective tokenManager address is retrieved from the TadleFactory contract whereby the trusted guardian role is responsible for deploying such contracts as seen [here](https://github.com/Cyfrin/2024-08-tadle/blob/04fd8634701697184a3f3a5558b41c109866e5f8/src/factory/TadleFactory.sol#L68). Since the user still has to go through the PreMarkets/DeliveryPlace contracts to perform market actions, this max approval cannot be exploited.
If we consider the correct permissioned implementation for the `approve()` function within `CapitalPool.sol`, this would be a critical severity issue, because the withdrawal of funds will be permanently blocked and must be rescued by the admin via the `Rescuable.sol` contract, given it will always revert [here](https://github.com/Cyfrin/2024-08-tadle/blob/04fd8634701697184a3f3a5558b41c109866e5f8/src/core/CapitalPool.sol#L36-L38) when attempting to call a non-existent function selector `approve` within the TokenManager contract. The argument up in the air is since the approval function `approve` was made permisionless, the `if` block within the internal `_transfer()` function will never be invoked if somebody beforehand calls approval for the TokenManager for the required token, so the transfer will infact not revert when a withdrawal is invoked. I will leave open for escalation discussions, but based on my first point, I believe high severity is appropriate.
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.