Summary
The withdraw function in the TokenManager.sol contract lacks an approve function call for ERC20 tokens when transferring funds from the CapitalPool contract. This omission prevents users from withdrawing their ERC20 tokens, as the required allowance is not set.
Vulnerability Details
When users attempt to withdraw ERC20 tokens through the withdraw function, the code does not include an approve function call to grant the necessary allowance to the CapitalPool contract. As a result, the subsequent transfer fails due to insufficient allowance, leaving users unable to retrieve their ERC20 tokens.
https://github.com/Cyfrin/2024-08-tadle/blob/04fd8634701697184a3f3a5558b41c109866e5f8/src/core/TokenManager.sol#L170-L181
Impact
Users are unable to withdraw their ERC20 tokens from the CapitalPool, leading to a significant disruption in token liquidity and functionality. This could result in a loss of user trust and hinder the protocol's operations.
Proof of Concept
This is the test code.
function test_ask_turbo_chain() public {
vm.startPrank(user);
preMarktes.createOffer(
CreateOfferParams(
marketPlace,
address(mockUSDCToken),
1000,
0.01 * 1e18,
12000,
300,
OfferType.Ask,
OfferSettleType.Turbo
)
);
address offerAddr = GenerateAddress.generateOfferAddress(0);
vm.stopPrank();
vm.startPrank(user2);
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
preMarktes.createTaker(offerAddr, 700);
vm.startPrank(user);
vm.expectRevert();
tokenManager.withdraw(address(mockUSDCToken), TokenBalanceType.SalesRevenue);
capitalPool.approve(address(mockUSDCToken));
tokenManager.withdraw(address(mockUSDCToken), TokenBalanceType.TaxIncome);
tokenManager.withdraw(address(mockUSDCToken), TokenBalanceType.SalesRevenue);
vm.stopPrank();
}
The result is like this.
├─ [8858] UpgradeableProxy::withdraw(MockERC20Token: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a], 2)
│ ├─ [8339] TokenManager::withdraw(MockERC20Token: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a], 2) [delegatecall]
│ │ ├─ [534] TadleFactory::relatedContracts(4) [staticcall]
│ │ │ └─ ← [Return] UpgradeableProxy: [0x76006C4471fb6aDd17728e9c9c8B67d5AF06cDA0]
│ │ ├─ [2959] MockERC20Token::transferFrom(UpgradeableProxy: [0x76006C4471fb6aDd17728e9c9c8B67d5AF06cDA0], 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf, 7000000000000000 [7e15])
│ │ │ └─ ← [Revert] ERC20InsufficientAllowance(0x6891e60906DEBeA401F670D74d01D117a3bEAD39, 0, 7000000000000000 [7e15])
│ │ └─ ← [Revert] TransferFailed()
│ └─ ← [Revert] TransferFailed()
├─ [34067] UpgradeableProxy::approve(MockERC20Token: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a])
│ ├─ [29055] CapitalPool::approve(MockERC20Token: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a]) [delegatecall]
│ │ ├─ [534] TadleFactory::relatedContracts(5) [staticcall]
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
│ │ │ └─ ← [Return] UpgradeableProxy: [0x76006C4471fb6aDd17728e9c9c8B67d5AF06cDA0]
│ │ ├─ [3723] MockERC20Token::transferFrom(UpgradeableProxy: [0x76006C4471fb6aDd17728e9c9c8B67d5AF06cDA0], 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf, 7000000000000000 [7e15])
│ │ │ ├─ emit Transfer(from: UpgradeableProxy: [0x76006C4471fb6aDd17728e9c9c8B67d5AF06cDA0], to: 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf, value: 7000000000000000 [7e15])
│ │ │ └─ ← [Return] true
│ │ ├─ emit Withdraw(authority: 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf, tokenAddress: MockERC20Token: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a], tokenBalanceType: 2, amount: 7000000000000000 [7e15])
│ │ └─ ← [Stop]
│ └─ ← [Return]
As you can see, withdraw() function call is only working properly after direct call of capitalPool.approve.
Tools Used
Manual code review
Recommendations
Add an approve function call before transferring ERC20 tokens from the CapitalPool contract to ensure that the required allowance is set.
} else {
ICapitalPool(capitalPoolAddr).approve(_tokenAddress);
* @dev token is ERC20 token
* @dev transfer from capital pool to msg sender
*/
_safe_transfer_from(
_tokenAddress,
capitalPoolAddr,
_msgSender(),
claimAbleAmount
);
}