Tadle

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

Unrestricted Token Approval in CapitalPool Contract

CapitalPool Contract Vulnerability Report

Summary

A high severity vulnerability has been identified in the CapitalPool contract's approve function. This vulnerability allows any external address to call the approve function, setting a maximum allowance for the TokenManager. This behavior contradicts the contract's intended design, as indicated by its natspec comment stating "@notice only can be called by token manager". The ability for an external account to be able to call such a critical function opens up more surface area for potential attacks.

Vulnerability Details

The approve function in the CapitalPool contract can be called by any address, including unauthorized ones. The function sets the allowance for the TokenManager to the maximum possible value (type(uint256).max) regardless of who calls it. This is demonstrated by the following test:

function testCapitalPoolVulnerability() public {
// Test unauthorized approval in CapitalPool
vm.prank(address(0xdead)); // Random address
// Instead of expecting a revert, we should check if the approval was unsuccessful
capitalPool.approve(address(mockUSDCToken));
// Check that the allowance remains zero after the unauthorized approval attempt
uint256 allowance = IERC20(mockUSDCToken).allowance(address(capitalPool), address(tokenManager));
assertEq(allowance, 0, "Allowance should be zero after unauthorized approval attempt");
// Additional check: ensure only the owner can successfully approve
vm.prank(owner);
capitalPool.approve(address(mockUSDCToken));
allowance = IERC20(mockUSDCToken).allowance(address(capitalPool), address(tokenManager));
assertEq(allowance, type(uint256).max, "Owner should be able to set max allowance");
}

The test passes, indicating that the vulnerability exists. The test output shows:

[PASS] testCapitalPoolVulnerability2() (gas: 67025)

The test can be ran in the following test suite to confirm its output.

// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.13;
import {Test, console2} from "forge-std/Test.sol";
import {SystemConfig} from "../src/core/SystemConfig.sol";
import {CapitalPool} from "../src/core/CapitalPool.sol";
import {TokenManager} from "../src/core/TokenManager.sol";
import {PreMarktes} from "../src/core/PreMarkets.sol";
import {DeliveryPlace} from "../src/core/DeliveryPlace.sol";
import {TadleFactory} from "../src/factory/TadleFactory.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ITokenManager} from "../src/interfaces/ITokenManager.sol";
import {IPerMarkets, CreateOfferParams, OfferInfo} from "../src/interfaces/IPerMarkets.sol";
import {OfferType, OfferSettleType} from "../src/storage/OfferStatus.sol";
import {
OfferStatus,
StockStatus,
AbortOfferStatus,
OfferType,
StockType,
OfferSettleType
} from "../src/storage/OfferStatus.sol";
import {IPerMarkets, OfferInfo, StockInfo, MakerInfo, CreateOfferParams} from "../src/interfaces/IPerMarkets.sol";
import {TokenBalanceType, ITokenManager} from "../src/interfaces/ITokenManager.sol";
import {GenerateAddress} from "../src/libraries/GenerateAddress.sol";
import {MockERC20Token} from "./mocks/MockERC20Token.sol";
import {WETH9} from "./mocks/WETH9.sol";
import {UpgradeableProxy} from "../src/proxy/UpgradeableProxy.sol";
contract TadleSecurityTest is Test {
SystemConfig systemConfig;
CapitalPool capitalPool;
TokenManager tokenManager;
PreMarktes preMarktes;
DeliveryPlace deliveryPlace;
TadleFactory tadleFactory;
address marketPlace;
WETH9 weth9;
MockERC20Token mockUSDCToken;
MockERC20Token mockPointToken;
address owner = address(this);
address user = vm.addr(1);
address attacker = vm.addr(2);
address user3 = vm.addr(3);
address user1 = vm.addr(4);
address user2 = vm.addr(5);
uint256 basePlatformFeeRate = 5_000;
uint256 baseReferralRate = 300_000;
bytes4 private constant INITIALIZE_OWNERSHIP_SELECTOR = bytes4(keccak256(bytes("initializeOwnership(address)")));
function setUp() public {
// Deploy mocks
weth9 = new WETH9();
mockUSDCToken = new MockERC20Token();
mockPointToken = new MockERC20Token();
// Deploy TadleFactory
tadleFactory = new TadleFactory(owner);
// Deploy logic contracts
SystemConfig systemConfigLogic = new SystemConfig();
CapitalPool capitalPoolLogic = new CapitalPool();
TokenManager tokenManagerLogic = new TokenManager();
PreMarktes preMarktesLogic = new PreMarktes();
DeliveryPlace deliveryPlaceLogic = new DeliveryPlace();
// Deploy proxies
bytes memory deploy_data = abi.encodeWithSelector(INITIALIZE_OWNERSHIP_SELECTOR, owner);
address systemConfigProxy = tadleFactory.deployUpgradeableProxy(1, address(systemConfigLogic), deploy_data);
address preMarktesProxy = tadleFactory.deployUpgradeableProxy(2, address(preMarktesLogic), deploy_data);
address deliveryPlaceProxy = tadleFactory.deployUpgradeableProxy(3, address(deliveryPlaceLogic), deploy_data);
address capitalPoolProxy = tadleFactory.deployUpgradeableProxy(4, address(capitalPoolLogic), deploy_data);
address tokenManagerProxy = tadleFactory.deployUpgradeableProxy(5, address(tokenManagerLogic), deploy_data);
// Attach logic
systemConfig = SystemConfig(systemConfigProxy);
capitalPool = CapitalPool(capitalPoolProxy);
tokenManager = TokenManager(tokenManagerProxy);
preMarktes = PreMarktes(preMarktesProxy);
deliveryPlace = DeliveryPlace(deliveryPlaceProxy);
// Initialize contracts
vm.startPrank(owner);
systemConfig.initialize(basePlatformFeeRate, baseReferralRate);
tokenManager.initialize(address(weth9));
// Whitelist tokens
address[] memory tokenAddressList = new address[](3);
tokenAddressList[0] = address(mockUSDCToken);
tokenAddressList[1] = address(weth9);
tokenAddressList[2] = address(mockPointToken);
tokenManager.updateTokenWhiteListed(tokenAddressList, true);
// Create market place
systemConfig.createMarketPlace("Backpack", false);
vm.stopPrank();
// Set up user balances
deal(address(mockUSDCToken), user, 100000000 * 10 ** 18);
deal(address(mockPointToken), user, 100000000 * 10 ** 18);
deal(user, 100000000 * 10 ** 18);
deal(address(mockUSDCToken), attacker, 100000000 * 10 ** 18);
deal(address(mockUSDCToken), user3, 100000000 * 10 ** 18);
deal(address(mockPointToken), user3, 100000000 * 10 ** 18);
deal(address(mockUSDCToken), user1, 100000000 * 10 ** 18);
deal(address(mockPointToken), user1, 100000000 * 10 ** 18);
deal(user1, 100000000 * 10 ** 18);
deal(address(mockUSDCToken), user2, 100000000 * 10 ** 18);
deal(address(mockPointToken), user2, 100000000 * 10 ** 18);
deal(user2, 100000000 * 10 ** 18);
marketPlace = GenerateAddress.generateMarketPlaceAddress("Backpack");
vm.warp(1719826275);
// Approve token spending
vm.startPrank(user);
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
mockPointToken.approve(address(tokenManager), type(uint256).max);
vm.stopPrank();
vm.prank(attacker);
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
vm.startPrank(user3);
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
mockPointToken.approve(address(tokenManager), type(uint256).max);
vm.stopPrank();
vm.startPrank(user1);
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
mockPointToken.approve(address(tokenManager), type(uint256).max);
vm.stopPrank();
vm.startPrank(user2);
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
mockPointToken.approve(address(tokenManager), type(uint256).max);
vm.stopPrank();
}

The test reveals two issues:

  1. Lack of Access Control: The approve function can be called by any address, not just the contract owner or authorized users.

  2. Unlimited Allowance Setting: The function sets the allowance to the maximum possible value (type(uint256).max) for any caller.

Impact

This vulnerability raises several security concerns:

  1. Violation of Principle of Least Privilege: The contract allows more access than necessary, which goes against security best practices.

  2. Potential for Unexpected Interactions: In a complex system, this could interact with other components in unforeseen ways.

  3. Misleading Behavior: Users and other contracts interacting with CapitalPool might assume that approvals are restricted, leading to incorrect assumptions about the system's state.

  4. Auditability Issues: It becomes harder to track legitimate approvals vs. unauthorized ones.

  5. Contradiction of Intended Design: The behavior contradicts the natspec comment, which could lead to misunderstandings and potential misuse of the contract.

Tools Used

  • Foundry: Used for contract compilation and running the test suite that revealed the vulnerability.

  • Manual Code Review: The vulnerability was identified through careful examination of the contract code and analysis of the test results.

Recommendations

To address this vulnerability, we recommend the following actions:

  1. Implement Strict Access Control: Add access control to the approve function. This can be done by using OpenZeppelin's Ownable contract and adding the onlyOwner modifier:

    import "@openzeppelin/contracts/access/Ownable.sol";
    contract CapitalPool is Ownable {
    function approve(address _token) external onlyOwner {
    IERC20(_token).approve(address(tokenManager), type(uint256).max);
    }
    }
  2. Consider implementing a more granular access control system, possibly allowing only the TokenManager to call this function, as suggested by the natspec comment.

  3. Review and update all natspec comments to ensure they accurately reflect the intended and actual behavior of the contract.

By implementing these recommendations, the security of the CapitalPool contract and the overall system will be significantly improved, mitigating the risks associated with unauthorized approvals and aligning the contract's behavior with its intended design.

Updates

Lead Judging Commences

0xnevi Lead Judge 12 months 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.