Tadle

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

TokenManager's Inability to Withdraw ERC20 Tokens Due to a Misuse of _safe_transfer_from

Summary

A critical vulnerability in the TokenManager contract prevents users from withdrawing ERC20 tokens (other than WETH) from the CapitalPool due to the absence of an approval call. The use of _safe_transfer_from instead of the internal _transfer function results in a lack of necessary approval, causing a denial of service (DOS) for users attempting to withdraw funds.

Vulnerability Details

  • Found in src/core/TokenManager.sol at Line 175

@>: _safe_transfer_from is used in case _tokenAddress != wrappedNativeToken instead of the internal TokenManager:_transfer, which has the necessary approve call to the CapitalPool to grant transfering permission to the TokenManager. Without the approval via _transfer, it is impossible to draw ERC20 tokens from the CapitalPool and return those funds to the user.

137: function withdraw(
138: address _tokenAddress,
139: TokenBalanceType _tokenBalanceType
140: ) external whenNotPaused {
...
153: if (_tokenAddress == wrappedNativeToken) {
...
170: } else {
171: /**
172: * @dev token is ERC20 token
173: * @dev transfer from capital pool to msg sender
174: */
175:@> _safe_transfer_from(
176: _tokenAddress,
177: capitalPoolAddr,
178: _msgSender(),
179: claimAbleAmount
180: );
181: }
...
189: }

POC

Add following POC and run forge test -vvv --mt test_withdraw_ERC20_reverts

// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.13;
import {PreMarketsTest} from "./PreMarkets.t.sol";
import {TokenBalanceType} from "../src/interfaces/ITokenManager.sol";
contract POC is PreMarketsTest {
function test_withdraw_ERC20_reverts() public {
// Mock userTokenBalanceMap to be able to simulate withdrawal
vm.prank(address(preMarktes));
tokenManager.addTokenBalance(TokenBalanceType.SalesRevenue, user1, address(mockUSDCToken), 1 ether);
// Confirm user1 actually has eligible claimable SalesRevenue
assertEq(tokenManager.userTokenBalanceMap(
address(user1),
address(mockUSDCToken),
TokenBalanceType.SalesRevenue
), 1 ether, "User not have enough claimables");
// Send some USDC to CapitalPool for withdrawal
deal(address(mockUSDCToken), address(capitalPool), 100 ether);
// Try to withdraw eligible incomes
vm.startPrank(user1);
tokenManager.withdraw(address(mockUSDCToken), TokenBalanceType.SalesRevenue);
}
}

Result shows that the transaction failed with ERC20InsufficientAllowance

├─ [12862] UpgradeableProxy::withdraw(MockERC20Token: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a], 2)
│ ├─ [12343] TokenManager::withdraw(MockERC20Token: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a], 2) [delegatecall]
│ │ ├─ [2534] TadleFactory::relatedContracts(4) [staticcall]
│ │ │ └─ ← [Return] UpgradeableProxy: [0x76006C4471fb6aDd17728e9c9c8B67d5AF06cDA0]
│ │ ├─ [2963] MockERC20Token::transferFrom(UpgradeableProxy: [0x76006C4471fb6aDd17728e9c9c8B67d5AF06cDA0], 0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF, 1000000000000000000 [1e18])
│ │ │ └─ ← [Revert] ERC20InsufficientAllowance(0x6891e60906DEBeA401F670D74d01D117a3bEAD39, 0, 1000000000000000000 [1e18])
│ │ └─ ← [Revert] TransferFailed()
│ └─ ← [Revert] TransferFailed()
└─ ← [Revert] TransferFailed()

Impact

In the case of non-WETH withdrawals, users requests will be DOS-ed because the TokenManager is unable to transfer funds from the CapitalPool due to the lack of approval. This causes users to lose their funds.

Tools Used

Foundry Test

Recommendations

Use TokenManager:_transfer() at Line 233 instead of _safe_transfer_from() to ensure that the necessary approval is granted for transferring ERC20 tokens.

- _safe_transfer_from(
- _tokenAddress,
- capitalPoolAddr,
- _msgSender(),
- claimAbleAmount
- );
+ _transfer(
+ _tokenAddress,
+ capitalPoolAddr,
+ _msgSender(),
+ claimAbleAmount,
+ capitalPoolAddr
+ );
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.