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

Incorrect Entry Price Adjustment on Position Size Reduction

Summary

The createMarketOrder function in the OrderBranch contract incorrectly updates the entry price when reducing the position size. This behavior is not expected as the entry price should remain unchanged when only the position size is reduced.

Vulnerability Details

When reducing a position size, the createMarketOrder function incorrectly changes the entry price. The function should maintain the original entry price as it is only the position size being reduced, not a new position being opened.

Proof of Concept

function testMarketPositionEntryPriceChangesAfterReducingSize() public {
changePrank({ msgSender: users.sasuke.account });
uint256 amount = 100000 * 10 ** 6;
deal({ token: address(usdc), to: users.sasuke.account, give: amount });
uint128 tradingAccountId = createAccountAndDeposit(amount, address(usdc));
int128 size = 1 * 10 ** 18;
perpsEngine.createMarketOrder(
OrderBranch.CreateMarketOrderParams({
tradingAccountId: tradingAccountId,
marketId: 1,
sizeDelta: size
})
);
perpsEngine.getActiveMarketOrder({ tradingAccountId: tradingAccountId });
bytes memory firstMockSignedReport =
getMockedSignedReport(0x00037da06d56d083fe599397a4769a042d63aa73dc4ef57709d31e9971a5b439, 100_000e18);
changePrank({ msgSender: marketOrderKeepers[1] });
perpsEngine.fillMarketOrder(tradingAccountId, 1, firstMockSignedReport);
Position.State memory position = perpsEngine.getPositionState(
tradingAccountId,
1,
100_000e18
);
//get entry price first time
UD60x18 initialEntryPrice = position.entryPriceX18;
// get position size
SD59x18 positionSize1 = position.sizeX18;
// reduce price to simulate losses..
uint256 updatedPrice = MOCK_BTC_USD_PRICE - 7000e18;
//uint256 updatedPrice = MOCK_BTC_USD_PRICE - 70000;
updateMockPriceFeed(BTC_USD_MARKET_ID, updatedPrice);
changePrank({ msgSender: users.sasuke.account });
// reduce trade size by 50% of trade
size = 0.5 * 10 ** 18;
perpsEngine.createMarketOrder(
OrderBranch.CreateMarketOrderParams({
tradingAccountId: tradingAccountId,
marketId: 1,
sizeDelta: -size
})
);
bytes memory firstMockSignedReport2 =
getMockedSignedReport(0x00037da06d56d083fe599397a4769a042d63aa73dc4ef57709d31e9971a5b439, updatedPrice);
// get pnl which should be 50% of trade to be closed
Position.State memory position2 = perpsEngine.getPositionState(
tradingAccountId,
1,
updatedPrice
);
// this pnl is 100% of unrealizeed pnl
SD59x18 PNL = position2.unrealizedPnlUsdX18;
// fill order
changePrank({ msgSender: marketOrderKeepers[1] });
perpsEngine.fillMarketOrder(tradingAccountId, 1, firstMockSignedReport2);
changePrank({ msgSender: users.sasuke.account });
// get position state
Position.State memory position3 = perpsEngine.getPositionState(
tradingAccountId,
1,
updatedPrice
);
// get entry price second time
UD60x18 newEntryPrice = position3.entryPriceX18;
// get position size should reduce
SD59x18 positionSize2 = position3.sizeX18;
// get pnl which should be the remaining 50% of initialsize left and 100% of new size
SD59x18 PNLAfterReduction = position3.unrealizedPnlUsdX18;
// entry price should remain thesame but changes
assertGt(initialEntryPrice.intoUint256(), newEntryPrice.intoUint256());
}

Root Cause:

When reducing a position, the oldPosition.update(...) method is called within the fillMarketOrder, which incorrectly resets the entry price to the current price.

function update(Data storage self, Data memory newPosition) internal {
self.size = newPosition.size;
self.lastInteractionPrice = newPosition.lastInteractionPrice; // @audit it resets it to current price
self.lastInteractionFundingFeePerUnit = newPosition.lastInteractionFundingFeePerUnit;
}

Impact

This vulnerability causes the entry price to be incorrectly adjusted when a trader reduces their position size, which can result in inaccurate calculations of unrealized profit and loss (PnL) and affect the trader's financial decisions and strategies.

Tools Used

  • Foundry

  • Zaros Website

Recommendations

  1. Preserve Entry Price on Position Reduction: Update the createMarketOrder function to ensure that the entry price remains unchanged when only the position size is reduced. This can be achieved by modifying the update function to avoid resetting the lastInteractionPrice when reducing the position size.

Updates

Lead Judging Commences

inallhonesty Lead Judge 11 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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