Summary
Line link: https://github.com/Cyfrin/2024-08-tadle/blob/04fd8634701697184a3f3a5558b41c109866e5f8/src/core/CapitalPool.sol#L24
In CapitalPool.sol , function approve must only can be called by token manager but there is no limitation for that. Everyone can call this function.
pragma solidity ^0.8.13;
import {CapitalPoolStorage} from "../storage/CapitalPoolStorage.sol";
import {ICapitalPool} from "../interfaces/ICapitalPool.sol";
import {RelatedContractLibraries} from "../libraries/RelatedContractLibraries.sol";
import {Rescuable} from "../utils/Rescuable.sol";
* @title CapitalPool
* @notice Implement the capital pool
*/
contract CapitalPool is CapitalPoolStorage, Rescuable, ICapitalPool {
bytes4 private constant APPROVE_SELECTOR =
bytes4(keccak256(bytes("approve(address,uint256)")));
constructor() Rescuable() {}
* @dev Approve token for token manager
* @notice only can be called by token manager
* @param tokenAddr address of token
*/
function approve(address tokenAddr) external {
address tokenManager = tadleFactory.relatedContracts(
RelatedContractLibraries.TOKEN_MANAGER
);
(bool success, ) = tokenAddr.call(
abi.encodeWithSelector(
APPROVE_SELECTOR,
tokenManager,
type(uint256).max
)
);
if (!success) {
revert ApproveFailed();
}
}
}
Vulnerability Details
Line link: https://github.com/Cyfrin/2024-08-tadle/blob/04fd8634701697184a3f3a5558b41c109866e5f8/src/core/TokenManager.sol#L233
In TokenManager.sol there is a function which is function _transfer and uses this approve to send tokens.
function _transfer(
address _token,
address _from,
address _to,
uint256 _amount,
address _capitalPoolAddr
) internal {
uint256 fromBalanceBef = IERC20(_token).balanceOf(_from);
uint256 toBalanceBef = IERC20(_token).balanceOf(_to);
if (
_from == _capitalPoolAddr &&
IERC20(_token).allowance(_from, address(this)) == 0x0
) {
ICapitalPool(_capitalPoolAddr).approve(address(this));
}
_safe_transfer_from(_token, _from, _to, _amount);
uint256 fromBalanceAft = IERC20(_token).balanceOf(_from);
uint256 toBalanceAft = IERC20(_token).balanceOf(_to);
if (fromBalanceAft != fromBalanceBef - _amount) {
revert TransferFailed();
}
if (toBalanceAft != toBalanceBef + _amount) {
revert TransferFailed();
}
}
Impact
The fact that the approve function can be called by anyone, especially when used together with the _transfer function, can lead to serious security vulnerabilities. These vulnerabilities could allow malicious individuals to exploit the system, resulting in unjust gains or disrupting the proper functioning of the system.
Potential System Vulnerabilities
Unauthorized Transfer Authorization:
The unrestricted access to the approve function allows unauthorized users to perform approval actions on the CapitalPool contract.
This could lead to unlimited allowances being granted to the tokenManager address, allowing it to withdraw any desired tokens from CapitalPool.
In such a case, a malicious actor could use the approve function to transfer tokens within the system to another address.
Manipulation and Double-Spending Risk:
A malicious user could continuously call the approve function, frequently granting unlimited allowances to the tokenManager address.
With these allowances, it could be possible to repeatedly transfer the same tokens, enabling double-spending or manipulation.
Denial of Service (DoS) Attacks:
A malicious actor could repeatedly call the approve function to overload the system, causing it to become busy.
This could consume the system's resources, leading to slowdowns or even a complete halt in processing."
Tools Used
Manual review
Recommendations
An access control like the following can be added at the beginning of the function:
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.13;
import {CapitalPoolStorage} from "../storage/CapitalPoolStorage.sol";
import {ICapitalPool} from "../interfaces/ICapitalPool.sol";
import {RelatedContractLibraries} from "../libraries/RelatedContractLibraries.sol";
import {Rescuable} from "../utils/Rescuable.sol";
/**
* @title CapitalPool
* @notice Implement the capital pool
*/
contract CapitalPool is CapitalPoolStorage, Rescuable, ICapitalPool {
bytes4 private constant APPROVE_SELECTOR =
bytes4(keccak256(bytes("approve(address,uint256)")));
constructor() Rescuable() {}
+ modifier onlyTokenManager() {
+ require(msg.sender == tokenManager, "Caller is not the token manager");
+ _;
+ }
/**
* @dev Approve token for token manager
* @notice only can be called by token manager
* @param tokenAddr address of token
*/
+ function approve(address tokenAddr) external onlyTokenManager {
address tokenManager = tadleFactory.relatedContracts(
RelatedContractLibraries.TOKEN_MANAGER
);
(bool success, ) = tokenAddr.call(
abi.encodeWithSelector(
APPROVE_SELECTOR,
tokenManager,
type(uint256).max
)
);
if (!success) {
revert ApproveFailed();
}
}
}