Tadle

Tadle
DeFiFoundry
27,750 USDC
View results
Submission Details
Severity: low
Valid

Lack of access control in `CapitalPool::approve` function which allows any address to approve token spending, leading to potential unauthorized token transfers and loss of funds

Summary:

The approve function in the CapitalPool.sol contract lacks proper access control, which allows any external address to approve the token manager to spend the pool's tokens, contrary to the intended design.

Vulnerability Details:

The approve function in the CapitalPool.sol contract is designed to be called only by the token manager, as indicated by the function comment. However, the function does not enforce this restriction. As a result, any external address can call this function and approve the token manager to spend the CapitalPool's tokens.

See the original approve function below:

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();
}
}

Impact:

This vulnerability poses a high risk to the protocol's funds. Any malicious actor can exploit this to approve the token manager to spend the maximum possible amount of any token held by the CapitalPool. This could lead to unauthorized token transfers and potential loss of funds.

Proof of Concept:

  1. Create 2 mock contracts, MockCapitalPool.sol and MockTadleFactory.sol within the mocks folder:

MockCapitalPool.sol contract;

// MockCapitalPool.sol
pragma solidity ^0.8.13;
import "../../src/interfaces/ICapitalPool.sol";
import "../../src/factory/ITadleFactory.sol";
import "../../src/libraries/RelatedContractLibraries.sol";
contract MockCapitalPool is ICapitalPool {
ITadleFactory public tadleFactory;
bytes4 private constant APPROVE_SELECTOR = bytes4(keccak256(bytes("approve(address,uint256)")));
constructor(address _tadleFactory) {
tadleFactory = ITadleFactory(_tadleFactory);
}
function approve(address tokenAddr) external override {
address tokenManager = tadleFactory.relatedContracts(uint8(RelatedContractLibraries.TOKEN_MANAGER));
(bool success,) = tokenAddr.call(abi.encodeWithSelector(APPROVE_SELECTOR, tokenManager, type(uint256).max));
if (!success) {
revert ApproveFailed();
}
}
// Implement other functions from ICapitalPool if necessary...
}

MockTadleFactory.sol contract;

// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.13;
import {ITadleFactory} from "../../src/factory/ITadleFactory.sol";
contract MockTadleFactory is ITadleFactory {
address public guardian;
mapping(uint8 => address) public relatedContracts;
// // Event declaration matching ITadleFactory
// event RelatedContractDeployed(uint256 _index, address _contractAddr);
constructor(address _guardian) {
guardian = _guardian;
}
function setRelatedContract(uint8 _relatedContractIndex, address _contract) external {
relatedContracts[_relatedContractIndex] = _contract;
emit RelatedContractDeployed(uint256(_relatedContractIndex), _contract);
}
function deployUpgradeableProxy(uint8 _relatedContractIndex, address _logic, bytes memory _data)
external
returns (address)
{
// In the mock, we'll just set the address directly instead of deploying a proxy
relatedContracts[_relatedContractIndex] = _logic;
emit RelatedContractDeployed(uint256(_relatedContractIndex), _logic);
return _logic;
}
// // Implement the errors from ITadleFactory
// error CallerIsNotGuardian(address guardian, address caller);
// error LogicAddrIsNotContract(address logicAddr);
}
  1. Create a test contract, AccessControl.t.sol within the test folder:

// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "./mocks/MockCapitalPool.sol";
import "./mocks/MockERC20Token.sol";
import "./mocks/MockTadleFactory.sol";
import "../src/libraries/RelatedContractLibraries.sol";
contract CapitalPoolTest is Test {
MockCapitalPool public capitalPool;
MockERC20Token public mockToken;
MockTadleFactory public mockTadleFactory;
address public tokenManager;
address public attacker;
function setUp() public {
mockTadleFactory = new MockTadleFactory(address(this));
mockToken = new MockERC20Token();
tokenManager = address(0x1234); // Simulated token manager address
attacker = address(0x5678); // Simulated attacker address
// Set up the mock TadleFactory to return our tokenManager address
mockTadleFactory.setRelatedContract(uint8(RelatedContractLibraries.TOKEN_MANAGER), tokenManager);
// Create MockCapitalPool with the mock TadleFactory
capitalPool = new MockCapitalPool(address(mockTadleFactory));
}
function testUnauthorizedApproval() public {
// Attacker calls the approve function
vm.prank(attacker);
capitalPool.approve(address(mockToken));
// Check that the approval was successful
uint256 allowance = mockToken.allowance(address(capitalPool), tokenManager);
assertEq(allowance, type(uint256).max, "Approval should have succeeded");
// This assertion passes, demonstrating the vulnerability
assertTrue(allowance > 0, "Attacker should not have been able to approve tokens");
}
}
  1. Run the test, forge test --mt testUnauthorizedApproval -vvvv. The result is as follows:

[⠊] Compiling...
[⠒] Compiling 2 files with Solc 0.8[⠢] Compiling 2 files with Solc 0.8[⠆] Compiling 2 files with Solc 0.8[⠰] Compiling 2 files with Solc 0.8[⠔] Compiling 2 files with Solc 0.8[⠒] Compiling 2 files with Solc 0.8[⠑] Compiling 2 files with Solc 0.8.26
[⠘] Solc 0.8.26 finished in 645.25m[⠃] Solc 0.8.26 finished in 645.25ms
Compiler run successful with warnings:
Warning (5667): Unused function parameter. Remove or comment out the variable name to silence this warning.
--> test/mocks/MockTadleFactory.sol:22:82:
|
22 | function deployUpgradeableProxy(uint8 _relatedContractIndex, address _logic, bytes memory _data)
| ^^^^^^^^^^^^^^^^^^
Ran 1 test for test/AccessControl.t.sol:CapitalPoolTest
[PASS] testUnauthorizedApproval() (gas: 53429)
Traces:
[53429] CapitalPoolTest::testUnauthorizedApproval()
├─ [0] VM::prank(0x0000000000000000000000000000000000005678)
│ └─ ← [Return]
├─ [36033] MockCapitalPool::approve(MockERC20Token: [0x2e234DAe75C793f67A35089C9d99245E1C58470b])
│ ├─ [2578] MockTadleFactory::relatedContracts(5) [staticcall]
│ │ └─ ← [Return] 0x0000000000000000000000000000000000001234
│ ├─ [24739] MockERC20Token::approve(0x0000000000000000000000000000000000001234, 115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77])
│ │ ├─ emit Approval(owner: MockCapitalPool: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a], spender: 0x0000000000000000000000000000000000001234, value: 115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77])
│ │ └─ ← [Return] true
│ └─ ← [Stop]
├─ [814] MockERC20Token::allowance(MockCapitalPool: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a], 0x0000000000000000000000000000000000001234) [staticcall]
│ └─ ← [Return] 115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77]
├─ [0] VM::assertEq(115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77], 115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77], "Approval should have succeeded") [staticcall]
│ └─ ← [Return]
├─ [0] VM::assertTrue(true, "Attacker should not have been able to approve tokens") [staticcall]
│ └─ ← [Return]
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 9.96ms (2.19ms CPU time)
Ran 1 test suite in 44.69ms (9.96ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
  • The test testUnauthorizedApproval() executed without any errors.

  • The approve function in MockCapitalPool was called by the attacker (address 0x5678).
    The MockTadleFactory correctly returned the token manager address (0x1234).
    The MockERC20Token successfully approved the maximum uint256 value for the token manager.

  • The allowance check passed, confirming that the approval was successful.

  • Both assertions in the test passed.

This test demonstrates that an unauthorized address (attacker) can successfully call the approve function and grant maximum allowance to the token manager.

Tools Used:

Manual review, Foundry, AI for troubleshooting

Recommended Mitigation:

Implement proper access control in the approve function using an onlyTokenManager modifier. Define the modifier in the CapitalPool.sol contract and apply this modifier to the approve function. See the updated CapitalPool.sol below:

// 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() {
+ address tokenManager = tadleFactory.relatedContracts(
+ RelatedContractLibraries.TOKEN_MANAGER
+ );
+ require(msg.sender == tokenManager, "Only token manager can call");
+ _;
+ }
/**
* @dev Approve token for token manager
* @notice only can be called by token manager
* @param tokenAddr address of token
*/
- function approve(address tokenAddr) external {
+ 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();
}
}
}

This modification ensures that only the token manager can call the approve function, preventing attackers exploiting this vulnerability.

Updates

Lead Judging Commences

0xnevi Lead Judge about 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

finding-CapitalPool-approve-missing-access-control

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.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.