Tadle

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

TokenManager::withdraw() method won't be reverted in case CapitalPool hasn't enough funds

Summary

It is intended that withdraw function reverts the transaction if the CapitalPool contract does not have enough balance for the token transfer however it won't be failed even if CapitalPool does not have any token. Therefore the withdraw function will be executed succesfuly althought it is supposed to fail.

Vulnerability Details

As It is shown below withdraw function uses the _safe_transfer_from() method in order to send ERC20 tokens from CapitalPool to TokenManager contract.

function withdraw(
address _tokenAddress,
TokenBalanceType _tokenBalanceType
) external whenNotPaused {
uint256 claimAbleAmount = userTokenBalanceMap[_msgSender()][
_tokenAddress
][_tokenBalanceType];
if (claimAbleAmount == 0) {
return;
}
address capitalPoolAddr = tadleFactory.relatedContracts(
RelatedContractLibraries.CAPITAL_POOL
);
if (_tokenAddress == wrappedNativeToken) {
/**
* @dev token is native token
* @dev transfer from capital pool to msg sender
* @dev withdraw native token to token manager contract
* @dev transfer native token to msg sender
*/
_transfer(
wrappedNativeToken,
capitalPoolAddr,
address(this),
claimAbleAmount,
capitalPoolAddr
);
IWrappedNativeToken(wrappedNativeToken).withdraw(claimAbleAmount);
payable(msg.sender).transfer(claimAbleAmount);
} else {
/**
* @dev token is ERC20 token
* @dev transfer from capital pool to msg sender
*/
_safe_transfer_from(
_tokenAddress,
capitalPoolAddr,
_msgSender(),
claimAbleAmount
);
}
emit Withdraw(
_msgSender(),
_tokenAddress,
_tokenBalanceType,
claimAbleAmount
);
}

The _safe_transfer_from method of Rescuable.sol() will do following low level call (bool success, ) = token.call( abi.encodeWithSelector(TRANSFER_FROM_SELECTOR, from, to, amount) ) where TRANSFER_FROM_SELECTOR is bytes4 private constant TRANSFER_FROM_SELECTOR = bytes4(keccak256(bytes("transferFrom(address,address,uint256)"))) and check if it's return value false however the low level call function won't be reverted if CapitalPool has not enough funds to send. This issue can be reproduced by following test method :

function test_transferfrom_does_not_work_correctly() public{
vm.startPrank(address(tokenManager));//imporsonate tokenManager
capitalPool.approve(address(mockUSDCToken));
tokenManager.withdraw(address(mockUSDCToken),TokenBalanceType.SalesRevenue);
/*since there is no any SalesRevenue in the TokenManager the following calls should revert
however they will be passed.
*/
tokenManager.withdraw(address(mockUSDCToken),TokenBalanceType.SalesRevenue);
tokenManager.withdraw(address(mockUSDCToken),TokenBalanceType.SalesRevenue);
tokenManager.withdraw(address(mockUSDCToken),TokenBalanceType.SalesRevenue);
vm.stopPrank();
}

Impact

Since low level transfer calls used in TokenManager::tillIn() as well ,TokenManager contract can work unintendedly and this may affect overall contract logic.

Tools Used

Manual analysis and Foundry

Recommendations

The problem can be fixed as implementing following dependencies to TokenManager.sol : SafeERC20 library of OpenZeppelin and ERC20 interface then changing this line to IERC20(_tokenAddress).safeTransferFrom(capitalPoolAddr, _msgSender(), claimAbleAmount). Additionaly changing all the low level transfer and transferFrom function calls with safeTransfer and safeTransferFrom would be better because the low level calls don't revert on failure if call isn't failed because of unsufficient gas or called function's revert.In case the called function does not exist or it simply returns a value,low level call returns true.Therefore in such situations following code snippets from Rescuable.sol won't revert even if transferFrom or transfer function does not work correctly.

(bool success, ) = token.call(
abi.encodeWithSelector(TRANSFER_FROM_SELECTOR, from, to, amount)
);
if (!success) {
revert TransferFailed();
}
(bool success, ) = token.call(
abi.encodeWithSelector(TRANSFER_SELECTOR, to, amount)
);
if (!success) {
revert TransferFailed();
}
Updates

Lead Judging Commences

0xnevi Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

[invalid] finding-weird-erc-20-return-boolean-Rescuable

I believe the issues and duplicates do not warrant low severity severity as even if the call to transfers returns false instead of reverting, there is no impact as it is arguably correct given there will be insufficient funds to perform a rescue/withdrawal. This will not affect `tillIn()` as there are explicit balance [checks that revert accordingly](https://github.com/Cyfrin/2024-08-tadle/blob/04fd8634701697184a3f3a5558b41c109866e5f8/src/core/TokenManager.sol#L255-L260) to prevent allowing creation of offers without posting the necessary collateral

Support

FAQs

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