Tadle

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

Approval issue in `TokenManager::withdraw` function prevents ERC20 token transfers, locking users' funds permanently

Summary

The TokenManager contract has an issue in the withdraw function, which fails due to the CapitalPool contract not granting approval to the TokenManager contract for token transfers. As a result, the _safe_transfer_from function will revert, locking users' funds permanently within the contract.

Vulnerability Details

In the TokenManager contract's withdraw function, the following code attempts to transfer ERC20 tokens from the CapitalPool to the caller:

function withdraw(
address _tokenAddress,
TokenBalanceType _tokenBalanceType
) external whenNotPaused {
...
} else {
/**
* @dev token is ERC20 token
* @dev transfer from capital pool to msg sender
*/
_safe_transfer_from(
_tokenAddress,
capitalPoolAddr,
_msgSender(),
claimAbleAmount
);
}

However, this function relies on CapitalPool having given the necessary approval for TokenManager to perform the transfer. Since the CapitalPool contract's approve function isn't invoked before this transfer attempt, the approval is not granted, leading to the following _safe_transfer_from function call reverting:

_safe_transfer_from function:

contract Rescuable is Ownable, Pausable {
bytes4 private constant TRANSFER_SELECTOR =
bytes4(keccak256(bytes("transfer(address,uint256)")));
bytes4 private constant TRANSFER_FROM_SELECTOR =
bytes4(keccak256(bytes("transferFrom(address,address,uint256)")));
...
function _safe_transfer_from(address token, address from, address to, uint256 amount) internal {
(bool success,) = token.call(abi.encodeWithSelector(TRANSFER_FROM_SELECTOR, from, to, amount));
if (!success) {
revert TransferFailed();
}
}
}

PoC test

The following PoC demonstrates this issue. Note that I made changes to PreMarkets.sol to facilitate testing:

  • I modified the createOffer function to return the stock and offer addresses, which will be used for closing the offer in order to be able to withdraw the deposited amount. This change was made because working with events in Foundry is cumbersome. The updated function is as follows:

+ function createOffer(CreateOfferParams calldata params) external payable returns (address, address) {
...
emit CreateOffer(
offerAddr, makerAddr, stockAddr, params.marketPlace, _msgSender(), params.points, params.amount
);
+ return (stockAddr, offerAddr);
}

CMD to run the test forge test --mt test_PermanentDoS --via-ir -vv

function test_PermanentWithdrawalDoS() public {
vm.startPrank(user);
(address _stockAddr, address _offer) = preMarktes.createOffer(
CreateOfferParams(
marketPlace, address(mockUSDCToken), 1000, 0.01 * 1e18, 12000, 300, OfferType.Ask, OfferSettleType.Turbo
)
);
preMarktes.closeOffer(_stockAddr, _offer);
tokenManager.withdraw(address(mockUSDCToken), TokenBalanceType.MakerRefund);
vm.stopPrank();
}

Impact

Users will not be able to withdraw their funds as the _safe_transfer_from function will revert due to the lack of approval. This issue effectively permanently locks users' funds in the contract.

Tools Used

VSCode, Foundry

Recommendations

To resolve the issue, ensure that the CapitalPool contract grants approval to the TokenManager contract before attempting to perform the token transfer.

Here's the diff showing the needed change:

function withdraw(
address _tokenAddress,
TokenBalanceType _tokenBalanceType
) external whenNotPaused {
...
} else {
/**
* @dev token is ERC20 token
* @dev transfer from capital pool to msg sender
*/
+ ICapitalPool(_capitalPoolAddr).approve(_tokenAddress);
_safe_transfer_from(
_tokenAddress,
capitalPoolAddr,
_msgSender(),
claimAbleAmount
);
}
Updates

Lead Judging Commences

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

finding-TokenManager-safeTransferFrom-withdraw-missing-approve

This issue's severity has similar reasonings to #252, whereby 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. Similarly, the argument here is the approval function `approve` was made permisionless, so if somebody beforehand calls approval for the TokenManager for the required token, 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. It also has a slightly different root cause and fix whereby an explicit approval needs to be provided before a call to `_safe_transfer_from()`, if not, the alternative `_transfer()` function should be used to provide an approval, assuming a fix was implemented for issue #252

Support

FAQs

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