Summary
PreMarkets::listOffer uses the preOffers collateralRate instead of the provided one(_collateralRate) to calculate the amount of collateral required for a protected offer ,due to this , an attacker can provide an absurdly large collateral rate that is then used to calculate the refund amount when they cancel/settle/abort their offer. Effectively, they can drain the contract by providing just the right value as their collateral rate
Vulnerability Details
From the code snippet here where the transferAmount is calculated
if (makerInfo.offerSettleType == OfferSettleType.Protected) {
uint256 transferAmount = OfferLibraries.getDepositAmount(
offerInfo.offerType,
-> offerInfo.collateralRate,
_amount,
true,
Math.Rounding.Ceil
);
ITokenManager tokenManager = tadleFactory.getTokenManager();
tokenManager.tillIn{value: msg.value}(
_msgSender(),
makerInfo.tokenAddress,
transferAmount,
false
);
}
The transferAmount deposited by the sender is calculated using the collateralRate of the stockInfo.preOffer not the collateralRate of the offer itself about to be created.
However ,when the offer is closed/settled/aborted , the transferAmount is calculated using the collateralRate of the offer itself. One such case is in PreMarkets::closeOffer
https://github.com/Cyfrin/2024-08-tadle/blob/04fd8634701697184a3f3a5558b41c109866e5f8/src/core/PreMarkets.sol#L433-L456
* @dev update refund token from capital pool to balance
* @dev offer settle type is protected or original offer
*/
if (
makerInfo.offerSettleType == OfferSettleType.Protected ||
stockInfo.preOffer == address(0x0)
) {
-> uint256 refundAmount = OfferLibraries.getRefundAmount(
offerInfo.offerType,
offerInfo.amount,
offerInfo.points,
offerInfo.usedPoints,
offerInfo.collateralRate
);
ITokenManager tokenManager = tadleFactory.getTokenManager();
tokenManager.addTokenBalance(
TokenBalanceType.MakerRefund,
_msgSender(),
makerInfo.tokenAddress,
refundAmount
);
}
When offer type is Ask , getRefundAmount calculates the refund amount as (amount - usedAmount)*collateralRate,
Impact
HIGH/CRITICAL - Severe loss of funds
Tools Used
Manual Review
Recommendations
function listOffer(
address _stock,
uint256 _amount,
uint256 _collateralRate
) external payable {
//...ommited for brevity
/// @dev change abort offer status when offer settle type is turbo
if (makerInfo.offerSettleType == OfferSettleType.Turbo) {
address originOffer = makerInfo.originOffer;
OfferInfo memory originOfferInfo = offerInfoMap[originOffer];
if (_collateralRate != originOfferInfo.collateralRate) {
revert InvalidCollateralRate();
}
originOfferInfo.abortOfferStatus = AbortOfferStatus.SubOfferListed;
}
/// @dev transfer collateral when offer settle type is protected
if (makerInfo.offerSettleType == OfferSettleType.Protected) {
uint256 transferAmount = OfferLibraries.getDepositAmount(
offerInfo.offerType,
- offerInfo.collateralRate,
+ _collateralRate,
_amount,
true,
Math.Rounding.Ceil
);
ITokenManager tokenManager = tadleFactory.getTokenManager();
tokenManager.tillIn{value: msg.value}(
_msgSender(),
makerInfo.tokenAddress,
transferAmount,
false
);
}
address offerAddr = GenerateAddress.generateOfferAddress(stockInfo.id);
if (offerInfoMap[offerAddr].authority != address(0x0)) {
revert OfferAlreadyExist();
}
//...ommited function body for brevity as it is irrelevant
}