Tadle

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

Possibility to drain tokens from CapitalPool

Summary

Any user with non-zero balance have an ability to withdraw unlitimed amount of tokens.

Vulnerability Details

As can be seen in TokenManager, L113-129, once a related contract calls addTokenBalance, the corresponding balance will be credited to the user via internal balance tracking.

More precisely, it adds the balance to userTokenBalanceMap, which represents the amount of tokens a user has, determined by the user’s address, the specific token’s address, and the type of token balance (used in the system to differentiate between the multiple ways the user could have obtained the tokens).

function addTokenBalance(
TokenBalanceType _tokenBalanceType,
address _accountAddress,
address _tokenAddress,
uint256 _amount
) external onlyRelatedContracts(tadleFactory, _msgSender()) {
userTokenBalanceMap[_accountAddress][_tokenAddress][
_tokenBalanceType
] += _amount;
emit AddTokenBalance(
_accountAddress,
_tokenAddress,
_tokenBalanceType,
_amount
);
}

Once a user obtains some tokens, they instantly become able to withdraw them by calling withdraw on TokenManager, specifying the token’s address and the type of token balance.

function withdraw(
address _tokenAddress,
TokenBalanceType _tokenBalanceType
)

As can be seen in the withdraw function, the contract assumes that the claimable amount of a token is equal to userTokenBalanceMap[_msgSender()][_tokenAddress][_tokenBalanceType].

While this is correct, the contract doesn’t update this value anywhere in the code, making it possible for the user to withdraw the token associated with _tokenAddress indefinitely, until the contract is fully drained of _tokenAddress token.

One more way to drain the CapitalPool currently is to do so via reentrancy, since there is no reentrancy guard, and the balance doesn't get updated at all.
But reentrancy is not needed. Since the balance doesn't get updated, it's possible to drain CapitalPool as was described above.

Impact

Any user is able to steal all tokens where they have a non-zero balance from CapitalPool, making the system completely unsafe.

Also, when fixing this issue it's crucial to follow CEI pattern, since otherwise it will be possible to drain the CapitalPool via reentrancy since there is no reentrancy guard.

Tools Used

Manual Review

Recommendations

Set userTokenBalanceMap[_msgSender()][_tokenAddress][_tokenBalanceType] to 0, right after calculating claimAbleAmount.

function withdraw(
address _tokenAddress,
TokenBalanceType _tokenBalanceType
) external whenNotPaused {
uint256 claimAbleAmount = userTokenBalanceMap[_msgSender()][
_tokenAddress
][_tokenBalanceType];
+ userTokenBalanceMap[_msgSender()][
+ _tokenAddress
+ ][_tokenBalanceType] = 0;
...

This way we ensure the user can't withdraw balance given to him multiple times, also following CEI pattern.

Updates

Lead Judging Commences

0xnevi Lead Judge 12 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.