In the previous version of Zaros, users were only allowed to create on-chain orders using the OrderBranch::createMarketOrder() function.
This would create a pending order that keepers would be able to fill later on.
In case the account is liquidated before the order is filled, the order is cancelled to prevent the account from having a position they did not intend to have after their liquidation.
In this new version Zaros has introduced off-chain orders that users sign on the frontend and the logic for filling them has been added to SettlementBranch::fillOffchainOrders()
This can negatively impact users and result in the users unintentionally opening a new position (and subtracting fees from their collateral) while they would not want after being liquidated.
The following coded PoC demonstrates a scenario in which a user has a long position and wants to reduce it by supplying a negative delta using off-chain orders. However the price of the longed asset crashes and keepers liquidate his position. After the liquidation, keepers fulfill his pending off-chain order, basically creating a new short position which the user did not intend to.
import { UD60x18, ud60x18 } from "@prb-math/UD60x18.sol";
*/
function testOffchainOrdersNotCancelled() external {
uint256 ethBalance = 1e18;
int128 tradeSize = 10e18;
uint128 marketId = ETH_USD_MARKET_ID;
uint256 price = MOCK_ETH_USD_PRICE;
deal({ token: address(wEth), to: users.naruto.account, give: ethBalance });
vm.startPrank(users.naruto.account);
uint128 tradingAccountId = createAccountAndDeposit(ethBalance, address(wEth));
changePrank({ msgSender: users.naruto.account });
openManualPosition(marketId, ETH_USD_STREAM_ID, price, tradingAccountId, tradeSize);
price = MOCK_ETH_USD_PRICE - 100e18;
updateMockPriceFeed(marketId, price);
uint128[] memory liquidatableAccountsIds = perpsEngine.checkLiquidatableAccounts(0, 1);
assertEq(1, liquidatableAccountsIds.length);
assertEq(liquidatableAccountsIds[0], 0);
changePrank({ msgSender: users.naruto.account });
tradeSize = -5e18;
uint128 markPrice = perpsEngine.getMarkPrice(
marketId, price, tradeSize
).intoUint128();
bytes32 salt = bytes32(block.prevrandao);
bytes32 structHash = keccak256(
abi.encode(
Constants.CREATE_OFFCHAIN_ORDER_TYPEHASH,
tradingAccountId,
marketId,
tradeSize,
markPrice,
false,
uint120(0),
salt
)
);
bytes32 digest = keccak256(abi.encodePacked("\x19\x01", perpsEngine.DOMAIN_SEPARATOR(), structHash));
(uint8 v, bytes32 r, bytes32 s) = vm.sign({ privateKey: users.naruto.privateKey, digest: digest });
price = MOCK_ETH_USD_PRICE / 2;
updateMockPriceFeed(ETH_USD_MARKET_ID, price);
liquidatableAccountsIds = perpsEngine.checkLiquidatableAccounts(0, 1);
assertEq(1, liquidatableAccountsIds.length);
assertEq(liquidatableAccountsIds[0], 1);
changePrank({ msgSender: liquidationKeeper });
perpsEngine.liquidateAccounts(liquidatableAccountsIds);
OffchainOrder.Data[] memory offchainOrders = new OffchainOrder.Data[](1);
offchainOrders[0] = OffchainOrder.Data({
tradingAccountId: tradingAccountId,
marketId: marketId,
sizeDelta: tradeSize,
targetPrice: markPrice,
shouldIncreaseNonce: false,
nonce: 0,
salt: salt,
v: v,
r: r,
s: s
});
bytes memory mockSignedReport = getMockedSignedReport(ETH_USD_STREAM_ID, price);
changePrank({ msgSender: OFFCHAIN_ORDERS_KEEPER_ADDRESS });
perpsEngine.fillOffchainOrders(marketId, offchainOrders, mockSignedReport);
bool hasOffchainOrderBeenFilled = TradingAccountHarness(address(perpsEngine))
.workaround_hasOffchainOrderBeenFilled(tradingAccountId, structHash);
assertTrue(hasOffchainOrderBeenFilled, "hasOffchainOrderBeenFilled");
liquidatableAccountsIds = perpsEngine.checkLiquidatableAccounts(0, 1);
assertEq(1, liquidatableAccountsIds.length);
assertEq(liquidatableAccountsIds[0], 0);
(,,UD60x18 maintenanceMarginUsdX18,) = perpsEngine.getAccountMarginBreakdown(tradingAccountId);
assertNotEq(maintenanceMarginUsdX18.intoUint256(), 0);
}
Just like classic on-chain market orders are cancelled after liquidation, off-chain orders should also be cancelled.