Tadle

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

Partial Settlement Refund Discrepancy in Ask Offers

Summary

When an Ask offer is partially settled, the maker (seller) does not receive the expected proportional refund of their collateral. This discrepancy could lead to significant financial losses for users and undermine the fairness of the protocol.

Vulnerability Details

[Relevant code] (https://github.com/Cyfrin/2024-08-tadle/blob/main/src/core/DeliveryPlace.sol#L222-L325)

The vulnerability lies in the settleAskMaker function of the DeliveryPlace contract. When a maker partially settles an Ask offer, the function fails to correctly calculate and distribute the proportional refund of the collateral. Instead of receiving a partial refund corresponding to the settled portion, the maker receives no refund at all.

This issue arises from an incorrect implementation of the settlement logic, where the collateral refund calculation does not account for partial settlements. The function appears to be treating all settlements as full settlements, resulting in no refund for partial settlements.

POC

In existing test suite, add the following test

function testPartialSettlementDiscrepancy() public {
// Setup: Create an Ask offer
vm.startPrank(user);
deal(address(mockUSDCToken), user, 100000000 * 10 ** 18);
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
preMarktes.createOffer(
CreateOfferParams(
marketPlace,
address(mockUSDCToken),
1000, // 1000 points
1000 ether,
10000, // 100% collateral
300,
OfferType.Ask,
OfferSettleType.Protected
)
);
vm.stopPrank();
address offerAddr = GenerateAddress.generateOfferAddress(0);
// User2 takes the offer
vm.startPrank(user2);
deal(address(mockUSDCToken), user2, 100000000 * 10 ** 18);
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
preMarktes.createTaker(offerAddr, 1000);
vm.stopPrank();
// Partial settlement
vm.prank(user1);
systemConfig.updateMarket(
"Backpack",
address(mockPointToken),
1 ether,
block.timestamp - 1,
3600
);
uint256 initialMakerBalance = tokenManager.userTokenBalanceMap(user, address(mockUSDCToken), TokenBalanceType.MakerRefund);
console2.log("Initial Maker Balance:", initialMakerBalance);
vm.startPrank(user);
deal(address(mockPointToken), user, 100000000 * 10 ** 18);
mockPointToken.approve(address(tokenManager), type(uint256).max);
// Log offer info before settlement
OfferInfo memory offerInfoBefore = preMarktes.getOfferInfo(offerAddr);
console2.log("Offer Status Before:", uint(offerInfoBefore.offerStatus));
console2.log("Offer Used Points Before:", offerInfoBefore.usedPoints);
deliveryPlace.settleAskMaker(offerAddr, 500); // Settle 50%
vm.stopPrank();
uint256 finalMakerBalance = tokenManager.userTokenBalanceMap(user, address(mockUSDCToken), TokenBalanceType.MakerRefund);
console2.log("Final Maker Balance:", finalMakerBalance);
uint256 refundedAmount = finalMakerBalance - initialMakerBalance;
console2.log("Refunded Amount:", refundedAmount);
// Log offer info after settlement
OfferInfo memory offerInfoAfter = preMarktes.getOfferInfo(offerAddr);
console2.log("Offer Status After:", uint(offerInfoAfter.offerStatus));
console2.log("Offer Used Points After:", offerInfoAfter.usedPoints);
console2.log("Offer Settled Points:", offerInfoAfter.settledPoints);
// The maker should receive 50% of the collateral back
assertEq(refundedAmount, 500 ether, "Maker should receive half of the collateral for 50% settlement");
}

Impact

Loss of funds for makers in case of partial settlements

Tools Used

Manual Review, foundry

Recommendations

Add logic to fix the partial settlement correctly.

function settleAskMaker(address _offer, uint256 _settledPoints) external {
// ... (existing validation code)
uint256 settledPointTokenAmount = marketPlaceInfo.tokenPerPoint * _settledPoints;
ITokenManager tokenManager = tadleFactory.getTokenManager();
if (settledPointTokenAmount > 0) {
tokenManager.tillIn{value: msg.value}(
_msgSender(),
marketPlaceInfo.tokenAddress,
settledPointTokenAmount,
true
);
}
- uint256 collateralFee;
- if (offerInfo.usedPoints > offerInfo.settledPoints) {
- if (offerInfo.offerStatus == OfferStatus.Virgin) {
- collateralFee = 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
- );
-
- collateralFee = OfferLibraries.getDepositAmount(
- offerInfo.offerType,
- offerInfo.collateralRate,
- usedAmount,
- true,
- Math.Rounding.Floor
- );
- }
- }
+ // Calculate collateral fee for the settled portion
+ uint256 settledAmount = offerInfo.amount.mulDiv(
+ _settledPoints,
+ offerInfo.points,
+ Math.Rounding.Floor
+ );
+ uint256 collateralFee = OfferLibraries.getDepositAmount(
+ offerInfo.offerType,
+ offerInfo.collateralRate,
+ settledAmount,
+ true,
+ Math.Rounding.Floor
+ );
+ // Calculate and distribute refund
+ uint256 refundAmount = collateralFee.mulDiv(
+ _settledPoints,
+ offerInfo.points,
+ Math.Rounding.Floor
+ );
+ tokenManager.addTokenBalance(
+ TokenBalanceType.MakerRefund,
+ offerInfo.authority,
+ makerInfo.tokenAddress,
+ refundAmount
+ );
+ // Calculate and distribute revenue
+ uint256 revenueAmount = settledAmount;
+ tokenManager.addTokenBalance(
+ TokenBalanceType.SalesRevenue,
+ offerInfo.authority,
+ makerInfo.tokenAddress,
+ revenueAmount
+ );
+ // Calculate and distribute tax
+ uint256 taxAmount = revenueAmount.mulDiv(
+ makerInfo.eachTradeTax,
+ 10000,
+ Math.Rounding.Floor
+ );
+ tokenManager.addTokenBalance(
+ TokenBalanceType.TaxIncome,
+ offerInfo.authority,
+ makerInfo.tokenAddress,
+ taxAmount
+ );
IPerMarkets perMarkets = tadleFactory.getPerMarkets();
perMarkets.settledAskOffer(
_offer,
_settledPoints,
settledPointTokenAmount
);
+ emit SettleAskMaker(
+ makerInfo.marketPlace,
+ offerInfo.maker,
+ _offer,
+ _msgSender(),
+ _settledPoints,
+ settledPointTokenAmount,
+ refundAmount,
+ revenueAmount,
+ taxAmount
+ );
}
Updates

Lead Judging Commences

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

finding-DeliveryPlace-settleAskTaker-settleAskMaker-partial-settlements

Valid high, in settleAskTaker/settleAskMaker, if the original offer maker performs a partial final settlement, the existing checks [here](https://github.com/Cyfrin/2024-08-tadle/blob/main/src/core/DeliveryPlace.sol#L356-L358) and [here](https://github.com/Cyfrin/2024-08-tadle/blob/04fd8634701697184a3f3a5558b41c109866e5f8/src/core/DeliveryPlace.sol#L230-L232) will cause an revert when attempting to complete a full settlement, resulting in their collateral being locked and requiring a rescue from the admin. To note, although examples in the documentation implies settlement in a single click, it is not stated that partial settlements are not allowed, so I believe it is a valid user flow.

Support

FAQs

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