Tadle

Tadle
DeFi
30,000 USDC
View results
Submission Details
Severity: high
Valid

Reentrancy Vulnerability in `withdraw` Function Allows Unlimited Token Withdrawal

Summary

The withdraw function in TokenManager.sol contains a vulnerability that allows a single user to repeatedly withdraw claimable tokens until the pool is drained. This occurs because the function does not reset the user's balance to zero after a withdrawal, enabling reentrancy attacks.

Vulnerability Details

Location: TokenManager.sol::withdraw

Description:

The withdraw function is designed to allow users to withdraw claimable tokens. However, it does not reset the userTokenBalanceMap for the user after the tokens are withdrawn. As a result, a malicious user can call the withdraw function multiple times in quick succession, draining the token pool.

Proof of Concept

function withdraw(
address _tokenAddress,
TokenBalanceType _tokenBalanceType
) external whenNotPaused {
uint256 claimAbleAmount = userTokenBalanceMap[_msgSender()][_tokenAddress][_tokenBalanceType]; // @audit: Missing code to reset the user balance
if (claimAbleAmount == 0) {
return;
}
address capitalPoolAddr = tadleFactory.relatedContracts(RelatedContractLibraries.CAPITAL_POOL);
if (_tokenAddress == wrappedNativeToken) {
// Transfer native token from capital pool to msg sender
_transfer(wrappedNativeToken, capitalPoolAddr, address(this), claimAbleAmount, capitalPoolAddr);
IWrappedNativeToken(wrappedNativeToken).withdraw(claimAbleAmount);
payable(msg.sender).transfer(claimAbleAmount);
} else {
// Transfer ERC20 token from capital pool to msg sender
_safe_transfer_from(_tokenAddress, capitalPoolAddr, _msgSender(), claimAbleAmount);
}
emit Withdraw(_msgSender(), _tokenAddress, _tokenBalanceType, claimAbleAmount);
}

Root Cause:

The user's claimable amount is fetched and used for the withdrawal, but it is not reset to zero afterwards, allowing the same amount to be withdrawn multiple times.

Impact

This vulnerability allows a single user to drain the entire pool of tokens by repeatedly withdrawing the same claimable amount. This can lead to a complete depletion of the token reserves, causing significant financial loss to the contract and its users.

Tools Used

  • Manual review

Recommendations

Reset User Balance After Withdrawal:
Update the withdraw function to reset the user's balance to zero after a successful withdrawal.

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);
// Reset user balance to zero
userTokenBalanceMap[_msgSender()][_tokenAddress][_tokenBalanceType] = 0;
if (_tokenAddress == wrappedNativeToken) {
_transfer(wrappedNativeToken, capitalPoolAddr, address(this), claimAbleAmount, capitalPoolAddr);
IWrappedNativeToken(wrappedNativeToken).withdraw(claimAbleAmount);
payable(msg.sender).transfer(claimAbleAmount);
} else {
_safe_transfer_from(_tokenAddress, capitalPoolAddr, _msgSender(), claimAbleAmount);
}
emit Withdraw(_msgSender(), _tokenAddress, _tokenBalanceType, claimAbleAmount);
}
Updates

Lead Judging Commences

0xnevi Lead Judge 10 months ago
Submission Judgement Published
Validated
Assigned finding tags:

finding-TokenManager-withdraw-userTokenBalanceMap-not-reset

Valid critical severity finding, the lack of clearance of the `userTokenBalanceMap` mapping allows complete draining of the CapitalPool contract. Note: This would require the approval issues highlighted in other issues to be fixed first (i.e. wrong approval address within `_transfer` and lack of approvals within `_safe_transfer_from` during ERC20 withdrawals)

Support

FAQs

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