DeFiFoundry
60,000 USDC
View results
Submission Details
Severity: medium
Invalid

Market orders lack price checks. If prices fluctuate drastically, it will cause unnecessary losses to users.

Summary

Market orders lack price checks. If prices fluctuate drastically, it will cause unnecessary losses to users.

Vulnerability Details

Users create orders through OrderBranch::createMarketOrder(), but the order data will only save marketId, sizeDelta, timestamp.

// OrderBranch::createMarketOrder()
// store pending order details
marketOrder.update({ marketId: params.marketId, sizeDelta: params.sizeDelta });
// MarketOrder::update()
function update(Data storage self, uint128 marketId, int128 sizeDelta) internal {
self.marketId = marketId;
self.sizeDelta = sizeDelta;
self.timestamp = uint128(block.timestamp);
}
// MarketOrder::Data()
struct Data {
uint128 marketId;
int128 sizeDelta;
uint128 timestamp;
}

When keeper calls SettlementBranch::fillMarketOrder(), it directly gets the current fill price and calls SettlementBranch::_fillOrder() without any checks.

//
/// @notice Fills a pending market order created by the given trading account id at a given market id.
/// @param tradingAccountId The trading account id.
/// @param marketId The perp market id.
/// @param priceData The price data of market order.
function fillMarketOrder(
uint128 tradingAccountId,
uint128 marketId,
bytes calldata priceData
)
external
onlyMarketOrderKeeper(marketId)
{
// working data
FillMarketOrder_Context memory ctx;
// fetch storage slot for perp market's market order config
SettlementConfiguration.Data storage settlementConfiguration =
SettlementConfiguration.load(marketId, SettlementConfiguration.MARKET_ORDER_CONFIGURATION_ID);
// load trader's pending order; reverts if no pending order
MarketOrder.Data storage marketOrder = MarketOrder.loadExisting(tradingAccountId);
// enforce that keeper is filling the order for the correct marketId
if (marketId != marketOrder.marketId) {
revert Errors.OrderMarketIdMismatch(marketId, marketOrder.marketId);
}
// fetch storage slot for perp market
PerpMarket.Data storage perpMarket = PerpMarket.load(marketId);
// fetch storage slot for global config
GlobalConfiguration.Data storage globalConfiguration = GlobalConfiguration.load();
// verifies provided price data following the configured settlement strategy
// returning the bid and ask prices
(ctx.bidX18, ctx.askX18) =
settlementConfiguration.verifyOffchainPrice(priceData, globalConfiguration.maxVerificationDelay);
// cache the order's size delta
ctx.sizeDeltaX18 = sd59x18(marketOrder.sizeDelta);
// cache the order side
ctx.isBuyOrder = ctx.sizeDeltaX18.gt(SD59x18_ZERO);
// buy order -> match against the ask price
// sell order -> match against the bid price
ctx.indexPriceX18 = ctx.isBuyOrder ? ctx.askX18 : ctx.bidX18;
// verify the provided price data against the verifier and ensure it's valid, then get the mark price
// based on the returned index price.
@> ctx.fillPriceX18 = perpMarket.getMarkPrice(ctx.sizeDeltaX18, ctx.indexPriceX18);
// perform the fill
_fillOrder(
tradingAccountId,
marketId,
SettlementConfiguration.MARKET_ORDER_CONFIGURATION_ID,
ctx.sizeDeltaX18,
@> ctx.fillPriceX18
);
// reset pending order details
marketOrder.clear();
}

We can imagine a scenario:

  1. The user creates a market order with a target price of 1e18.

  2. When the keeper executes the order, the price fluctuates greatly, even far exceeding the user's target price. However, the user's margin balance is sufficient to support the transaction, and the transaction will still be completed, but the price is not the price the user expected. This transaction is bound to cause unnecessary losses to the user.

Poc

Please add the test code to test/integration/perpetuals/settlement-branch/fillMarketOrder/fillMarketOrder.t.sol and execute

function test_createMarketOrder_and_fillMarketOrder() public {
Test_GivenTheMarginBalanceUsdIsOverTheMaintenanceMarginUsdRequired_Context memory ctx;
ctx.marketId = BTC_USD_MARKET_ID;
ctx.marginValueUsd = 100_000e18;
deal({ token: address(usdz), to: users.naruto.account, give: ctx.marginValueUsd });
// Config first fill order
ctx.fuzzMarketConfig = getFuzzMarketConfig(ctx.marketId);
ctx.marketOrderKeeper = marketOrderKeepers[ctx.fuzzMarketConfig.marketId];
ctx.tradingAccountId = createAccountAndDeposit(ctx.marginValueUsd, address(usdz));
ctx.firstOrderSizeDelta = 10e18;
// The price when the user creates an order
assertEq(ctx.fuzzMarketConfig.mockUsdPrice,100000000000000000000000);
// create market order
perpsEngine.createMarketOrder(
OrderBranch.CreateMarketOrderParams({
tradingAccountId: ctx.tradingAccountId,
marketId: ctx.fuzzMarketConfig.marketId,
sizeDelta: ctx.firstOrderSizeDelta
})
);
// Simulate instantaneous price fluctuations. When the keeper executes the order, the price rises by 20%
ctx.firstMockSignedReport =
getMockedSignedReport(ctx.fuzzMarketConfig.streamId, ctx.fuzzMarketConfig.mockUsdPrice * 12000 / 10000 );
changePrank({ msgSender: ctx.marketOrderKeeper });
// The user has sufficient margin balance, the order is filled successfully
perpsEngine.fillMarketOrder(ctx.tradingAccountId, ctx.fuzzMarketConfig.marketId, ctx.firstMockSignedReport);
// Now get the user's position status, you can see that the final execution price (entryPriceX18) when the keeper fills in the order has far exceeded the user's expectations
Position.State memory positionState = perpsEngine.getPositionState(ctx.tradingAccountId,ctx.fuzzMarketConfig.marketId,ctx.fuzzMarketConfig.mockUsdPrice);
assertGt(positionState.entryPriceX18.intoUint256(),120000000000000000000000);
}
// Ran 1 test for test/integration/perpetuals/settlement-branch/fillMarketOrder/fillMarketOrder.t.sol:FillMarketOrder_Integration_Test
// [PASS] test_createMarketOrder_and_fillMarketOrder() (gas: 1093915)

Code Snippet

https://github.com/Cyfrin/2024-07-zaros/blob/d687fe96bb7ace8652778797052a38763fbcbb1b/src/perpetuals/branches/OrderBranch.sol#L351-L352
https://github.com/Cyfrin/2024-07-zaros/blob/d687fe96bb7ace8652778797052a38763fbcbb1b/src/perpetuals/leaves/MarketOrder.sol#L43-L47
https://github.com/Cyfrin/2024-07-zaros/blob/d687fe96bb7ace8652778797052a38763fbcbb1b/src/perpetuals/leaves/MarketOrder.sol#L19-L23
https://github.com/Cyfrin/2024-07-zaros/blob/d687fe96bb7ace8652778797052a38763fbcbb1b/src/perpetuals/branches/SettlementBranch.sol#L103-L166

Impact

Market orders lack price checks. If prices fluctuate drastically, it will cause unnecessary losses to users.

Tools Used

Manual Review

Recommendations

  1. Add a target price item similar to SettlementBranch::fillOffchainOrders()::ctx.offchainOrder.targetPrice in the order data

// MarketOrder::
struct Data {
uint128 marketId;
int128 sizeDelta;
uint128 timestamp;
+ uint128 targetPrice;
}
  1. Add targetPrice parameter update logic in OrderBranch::createMarketOrder() and MarketOrder::update()

  2. Add the check logic similar to that in SettlementBranch::fillOffchainOrders() in SettlementBranch::fillMarketOrder()

ctx.fillPriceX18 = perpMarket.getMarkPrice(ctx.sizeDeltaX18, ctx.indexPriceX18);
// if the order increases the trading account's position (buy order), the fill price must be less than or
// equal to the target price, if it decreases the trading account's position (sell order), the fill price
// must be greater than or equal to the target price.
+ ctx.isFillPriceValid = (ctx.isBuyOrder && marketOrder.targetPrice >= ctx.fillPriceX18.intoUint256())
+ || (!ctx.isBuyOrder && marketOrder.targetPrice <= ctx.fillPriceX18.intoUint256());
+ if (!ctx.isFillPriceValid) {
+ revert("FillPrice error");
+ }
// perform the fill
_fillOrder(
tradingAccountId,
marketId,
SettlementConfiguration.MARKET_ORDER_CONFIGURATION_ID,
ctx.sizeDeltaX18,
ctx.fillPriceX18
);
// reset pending order details
marketOrder.clear();
Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Known issue
Assigned finding tags:

fillMarketOrder lacks slippage protection

Support

FAQs

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