Summary
The condition for ctx.shouldUseMaintenanceMargin
in OrderBranch::createMarketOrder()
is incomplete and does not work as expected. The check is performed with the wrong value.
Vulnerability Details
function createMarketOrder(CreateMarketOrderParams calldata params) external {
CreateMarketOrderContext memory ctx;
if (params.sizeDelta == 0) {
revert Errors.ZeroInput("sizeDelta");
}
ctx.sizeDeltaX18 = sd59x18(params.sizeDelta);
GlobalConfiguration.Data storage globalConfiguration = GlobalConfiguration.load();
SettlementConfiguration.Data storage settlementConfiguration =
SettlementConfiguration.load(params.marketId, SettlementConfiguration.MARKET_ORDER_CONFIGURATION_ID);
ctx.isIncreasing = Position.isIncreasing(params.tradingAccountId, params.marketId, params.sizeDelta);
if (ctx.isIncreasing) {
globalConfiguration.checkMarketIsEnabled(params.marketId);
settlementConfiguration.checkIsSettlementEnabled();
}
TradingAccount.Data storage tradingAccount =
TradingAccount.loadExistingAccountAndVerifySender(params.tradingAccountId);
@> ctx.isMarketWithActivePosition = tradingAccount.isMarketWithActivePosition(params.marketId);
if (!ctx.isMarketWithActivePosition) {
tradingAccount.validatePositionsLimit();
}
PerpMarket.Data storage perpMarket = PerpMarket.load(params.marketId);
perpMarket.checkTradeSize(ctx.sizeDeltaX18);
Position.Data storage position = Position.load(params.tradingAccountId, params.marketId);
ctx.positionSizeX18 = sd59x18(position.size);
perpMarket.checkOpenInterestLimits(
ctx.sizeDeltaX18, ctx.positionSizeX18, ctx.positionSizeX18.add(ctx.sizeDeltaX18)
);
MarketOrder.Data storage marketOrder = MarketOrder.load(params.tradingAccountId);
(
ctx.marginBalanceUsdX18,
ctx.requiredInitialMarginUsdX18,
ctx.requiredMaintenanceMarginUsdX18,
ctx.orderFeeUsdX18,
ctx.settlementFeeUsdX18,
) = simulateTrade({
tradingAccountId: params.tradingAccountId,
marketId: params.marketId,
settlementConfigurationId: SettlementConfiguration.MARKET_ORDER_CONFIGURATION_ID,
sizeDelta: params.sizeDelta
});
@>
@>
@> ctx.shouldUseMaintenanceMargin = !Position.isIncreasing(params.tradingAccountId, params.marketId, params.sizeDelta)
&& ctx.isMarketWithActivePosition;
@> ctx.requiredMarginUsdX18 =
ctx.shouldUseMaintenanceMargin ? ctx.requiredMaintenanceMarginUsdX18 : ctx.requiredInitialMarginUsdX18;
tradingAccount.validateMarginRequirement(
ctx.requiredMarginUsdX18, ctx.marginBalanceUsdX18, ctx.orderFeeUsdX18.add(ctx.settlementFeeUsdX18)
);
marketOrder.checkPendingOrder();
marketOrder.update({ marketId: params.marketId, sizeDelta: params.sizeDelta });
emit LogCreateMarketOrder(msg.sender, params.tradingAccountId, params.marketId, marketOrder);
}
Refer to the comments in the code to determine the conditions:
The code logic is as follows:
ctx.shouldUseMaintenanceMargin = !Position.isIncreasing(params.tradingAccountId, params.marketId, params.sizeDelta)
&& ctx.isMarketWithActivePosition;
We can assume the following scenario
The original position is long: oldPosition.size = 1e18
.
The size is increased: params.sizeDelta = -3e18
.
Then the new position is actually newPosition.size = 1e18 + -3e18 = -2e18
. It can be seen that the actual position size increases after the position direction changes, but we get ctx.shouldUseMaintenanceMargin = true
. And ctx.requiredMaintenanceMarginUsdX18
is used to initialize ctx.requiredMarginUsdX18
for the following related checks. This is contrary to expectations
Poc
To view the value of the variable, we add the following test code to OrderBranch::createMarketOrder()
.
+ import {console} from "forge-std/console.sol";
ctx.requiredMarginUsdX18 =
ctx.shouldUseMaintenanceMargin ? ctx.requiredMaintenanceMarginUsdX18 : ctx.requiredInitialMarginUsdX18;
+ console.log("ctx.requiredMarginUsdX18:",ctx.requiredMarginUsdX18.intoUint256());
+ console.log("ctx.requiredMaintenanceMarginUsdX18:",ctx.requiredMaintenanceMarginUsdX18.intoUint256());
+ console.log("ctx.requiredInitialMarginUsdX18:",ctx.requiredInitialMarginUsdX18.intoUint256());
Please add the test code to test/integration/perpetuals/settlement-branch/fillMarketOrder/fillMarketOrder.t.sol
and execute
function testOrderBranch_createMarketOrder_shouldUseMaintenanceMargin() 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 });
ctx.fuzzMarketConfig = getFuzzMarketConfig(ctx.marketId);
ctx.marketOrderKeeper = marketOrderKeepers[ctx.fuzzMarketConfig.marketId];
ctx.tradingAccountId = createAccountAndDeposit(ctx.marginValueUsd, address(usdz));
ctx.firstOrderSizeDelta = 1e18;
console.log("------------ The first time a user calls createMarketOrder() ------------");
perpsEngine.createMarketOrder(
OrderBranch.CreateMarketOrderParams({
tradingAccountId: ctx.tradingAccountId,
marketId: ctx.fuzzMarketConfig.marketId,
sizeDelta: ctx.firstOrderSizeDelta
})
);
ctx.firstMockSignedReport =
getMockedSignedReport(ctx.fuzzMarketConfig.streamId, ctx.fuzzMarketConfig.mockUsdPrice);
changePrank({ msgSender: ctx.marketOrderKeeper });
perpsEngine.fillMarketOrder(ctx.tradingAccountId, ctx.fuzzMarketConfig.marketId, ctx.firstMockSignedReport);
console.log("------------ The second time a user calls createMarketOrder() ------------");
changePrank({ msgSender: users.naruto.account });
ctx.secondOrderSizeDelta = -3e18;
perpsEngine.createMarketOrder(
OrderBranch.CreateMarketOrderParams({
tradingAccountId: ctx.tradingAccountId,
marketId: ctx.fuzzMarketConfig.marketId,
sizeDelta: ctx.secondOrderSizeDelta
})
);
}
From the output, we can find that:
When the user creates an order for the first time, ctx.requiredInitialMarginUsdX18
is used to initialize ctx.requiredMarginUsdX18
, which is correct.
When the user creates an order for the second time, although the direction is to reduce the position, the actual position is increased compared to the previous state, but here ctx.requiredMaintenanceMarginUsdX18
is used incorrectly to initialize ctx.requiredMarginUsdX18
.
Code Snippet
https://github.com/Cyfrin/2024-07-zaros/blob/d687fe96bb7ace8652778797052a38763fbcbb1b/src/perpetuals/branches/OrderBranch.sol#L242-L355
Impact
The condition for ctx.shouldUseMaintenanceMargin
in OrderBranch::createMarketOrder()
is incomplete and does not work as expected. The check is performed with the wrong value.
Tools Used
Manual Review
Recommendations
To sum up, we should consider adding a supplementary condition: ctx.sizeDeltaX18.abs() <= ctx.positionSizeX18.abs()
For example:
- ctx.shouldUseMaintenanceMargin = !Position.isIncreasing(params.tradingAccountId, params.marketId, params.sizeDelta)
- && ctx.isMarketWithActivePosition;
+ ctx.shouldUseMaintenanceMargin = !Position.isIncreasing(params.tradingAccountId, params.marketId, params.sizeDelta)
+ && ctx.isMarketWithActivePosition
+ && (ctx.sizeDeltaX18.abs() <= ctx.positionSizeX18.abs());
test again,it's work.
Ran 1 test for test/integration/perpetuals/settlement-branch/fillMarketOrder/fillMarketOrder.t.sol:FillMarketOrder_Integration_Test
[PASS] testOrderBranch_createMarketOrder_shouldUseMaintenanceMargin() (gas: 1212794)
------------ The first time a user calls createMarketOrder() ------------
@> ctx.requiredMarginUsdX18: 1000000050000000000000
ctx.requiredMaintenanceMarginUsdX18: 500000025000000000000
@> ctx.requiredInitialMarginUsdX18: 1000000050000000000000
changePrank is deprecated. Please use vm.startPrank instead.
------------ The second time a user calls createMarketOrder() ------------
changePrank is deprecated. Please use vm.startPrank instead.
@> ctx.requiredMarginUsdX18: 1999999900000000000000
ctx.requiredMaintenanceMarginUsdX18: 999999950000000000000
@> ctx.requiredInitialMarginUsdX18: 1999999900000000000000