Skip to content

Commit 2cb8235

Browse files
authored
H-01 fix - Cancel order refactor (#2328)
1 parent 51ed5ac commit 2cb8235

File tree

5 files changed

+236
-38
lines changed

5 files changed

+236
-38
lines changed

markets/bfp-market/contracts/modules/OrderModule.sol

Lines changed: 98 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,15 @@ contract OrderModule is IOrderModule {
7676
Position.TradeParams tradeParams;
7777
}
7878

79+
struct Runtime_cancelOrder {
80+
uint128 accountId;
81+
uint128 marketId;
82+
bool isStale;
83+
bool isReady;
84+
bool isMarketSolvent;
85+
Order.Data order;
86+
}
87+
7988
// --- Helpers --- //
8089

8190
/// @dev Reverts when `fillPrice > limitPrice` when long or `fillPrice < limitPrice` when short.
@@ -472,56 +481,111 @@ contract OrderModule is IOrderModule {
472481
) external payable {
473482
FeatureFlag.ensureAccessToFeature(Flags.CANCEL_ORDER);
474483
PerpMarket.Data storage market = PerpMarket.exists(marketId);
475-
Account.Data storage account = Account.exists(accountId);
476-
Order.Data storage order = market.orders[accountId];
484+
Runtime_cancelOrder memory runtime;
477485

478-
// No order available to settle.
479-
if (order.sizeDelta == 0) {
486+
runtime.accountId = accountId;
487+
runtime.marketId = marketId;
488+
runtime.order = market.orders[accountId];
489+
490+
if (runtime.order.sizeDelta == 0) {
480491
revert ErrorUtil.OrderNotFound();
481492
}
482493

483494
PerpMarketConfiguration.GlobalData storage globalConfig = PerpMarketConfiguration.load();
484495

485-
uint64 commitmentTime = order.commitmentTime;
486-
(bool isStale, bool isReady) = isOrderStaleOrReady(commitmentTime, globalConfig);
496+
(runtime.isStale, runtime.isReady) = isOrderStaleOrReady(
497+
runtime.order.commitmentTime,
498+
globalConfig
499+
);
487500

488-
if (!isReady) {
501+
if (!runtime.isReady) {
489502
revert ErrorUtil.OrderNotReady();
490503
}
491504

492-
// Only do the price divergence check for non stale orders. All stale orders are allowed to be canceled.
493-
if (!isStale) {
494-
PerpMarketConfiguration.Data storage marketConfig = PerpMarketConfiguration.load(
495-
marketId
496-
);
505+
if (!runtime.isStale) {
506+
validateNonStaleOrderCancellation(runtime, priceUpdateData);
507+
}
497508

498-
// Order is within settlement window. Check if price tolerance has exceeded.
499-
uint256 pythPrice = PythUtil.parsePythPrice(
500-
globalConfig,
501-
marketConfig,
502-
commitmentTime,
503-
priceUpdateData
504-
);
505-
uint256 fillPrice = Order.getFillPrice(
506-
market.skew,
507-
marketConfig.skewScale,
508-
order.sizeDelta,
509-
pythPrice
510-
);
509+
uint256 keeperFee = chargeKeeperFee(accountId, marketId);
511510

512-
if (!isPriceToleranceExceeded(order.sizeDelta, fillPrice, order.limitPrice)) {
513-
revert ErrorUtil.PriceToleranceNotExceeded(
514-
order.sizeDelta,
515-
fillPrice,
516-
order.limitPrice
517-
);
518-
}
511+
emit OrderCanceled(accountId, marketId, keeperFee, runtime.order.commitmentTime);
512+
delete market.orders[accountId];
513+
}
514+
515+
function validateNonStaleOrderCancellation(
516+
Runtime_cancelOrder memory runtime,
517+
bytes calldata priceUpdateData
518+
) private {
519+
PerpMarketConfiguration.Data storage marketConfig = PerpMarketConfiguration.load(
520+
runtime.marketId
521+
);
522+
PerpMarket.Data storage market = PerpMarket.exists(runtime.marketId);
523+
524+
uint256 pythPrice = PythUtil.parsePythPrice(
525+
PerpMarketConfiguration.load(),
526+
marketConfig,
527+
runtime.order.commitmentTime,
528+
priceUpdateData
529+
);
530+
uint256 fillPrice = Order.getFillPrice(
531+
market.skew,
532+
marketConfig.skewScale,
533+
runtime.order.sizeDelta,
534+
pythPrice
535+
);
536+
AddressRegistry.Data memory addresses = AddressRegistry.Data({
537+
synthetix: ISynthetixSystem(SYNTHETIX_CORE),
538+
sUsd: SYNTHETIX_SUSD,
539+
oracleManager: ORACLE_MANAGER
540+
});
541+
542+
Position.Data storage oldPosition = market.positions[runtime.accountId];
543+
544+
int128 newPositionSize = oldPosition.size + runtime.order.sizeDelta;
545+
546+
// lockedCreditDelta is the change in credit that would be locked if the order was filled
547+
uint256 newMinCredit = PerpMarket.getMinimumCreditWithPositionSize(
548+
market,
549+
marketConfig,
550+
market.getOraclePrice(addresses),
551+
(MathUtil.abs(newPositionSize).toInt() - MathUtil.abs(oldPosition.size).toInt())
552+
.to128(),
553+
addresses
554+
);
555+
556+
// checks if the market would be solvent with this new credit delta
557+
runtime.isMarketSolvent = PerpMarket.isMarketSolventForCredit(
558+
market,
559+
newMinCredit,
560+
market.depositedCollateral[addresses.sUsd],
561+
addresses
562+
);
563+
564+
// Allow to cancel if the cancellation is due to market insolvency while not reducing the order
565+
// If not, check if fill price exceeded acceptable price
566+
if (
567+
(runtime.isMarketSolvent ||
568+
MathUtil.isSameSideReducing(oldPosition.size, newPositionSize)) &&
569+
!isPriceToleranceExceeded(runtime.order.sizeDelta, fillPrice, runtime.order.limitPrice)
570+
) {
571+
revert ErrorUtil.PriceToleranceNotExceeded(
572+
runtime.order.sizeDelta,
573+
fillPrice,
574+
runtime.order.limitPrice
575+
);
519576
}
577+
}
520578

521-
// If `isAccountOwner` then 0 else charge cancellation fee.
579+
function chargeKeeperFee(
580+
uint128 accountId,
581+
uint128 marketId
582+
) private returns (uint256 keeperFee) {
583+
Account.Data storage account = Account.exists(accountId);
584+
PerpMarket.Data storage market = PerpMarket.exists(marketId);
522585

523-
uint256 keeperFee;
524586
if (ERC2771Context._msgSender() != account.rbac.owner) {
587+
PerpMarketConfiguration.GlobalData storage globalConfig = PerpMarketConfiguration
588+
.load();
525589
uint256 ethPrice = INodeModule(ORACLE_MANAGER)
526590
.process(globalConfig.ethOracleNodeId)
527591
.price
@@ -538,9 +602,6 @@ contract OrderModule is IOrderModule {
538602
keeperFee
539603
);
540604
}
541-
542-
emit OrderCanceled(accountId, marketId, keeperFee, commitmentTime);
543-
delete market.orders[accountId];
544605
}
545606

546607
// --- Views --- //

markets/bfp-market/contracts/storage/PerpMarket.sol

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,4 +543,18 @@ library PerpMarket {
543543

544544
return totalValueUsd;
545545
}
546+
547+
function isMarketSolventForCredit(
548+
Data storage self,
549+
uint256 newMinCredit,
550+
uint256 delegatedSusdValue,
551+
AddressRegistry.Data memory addresses
552+
) internal view returns (bool isMarketSolvent) {
553+
// establish amount of collateral currently collateralizing outstanding perp markets
554+
int256 delegatedCollateralValue = getDelegatedCollateralValueUsd(self, addresses);
555+
delegatedCollateralValue += delegatedSusdValue.toInt();
556+
557+
// Market insolvent delegatedCollateralValue < credit
558+
isMarketSolvent = delegatedCollateralValue >= newMinCredit.toInt();
559+
}
546560
}

markets/bfp-market/contracts/utils/MathUtil.sol

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,8 @@ library MathUtil {
4242
function sameSide(int256 a, int256 b) internal pure returns (bool) {
4343
return (a == 0) || (b == 0) || (a > 0) == (b > 0);
4444
}
45+
46+
function isSameSideReducing(int128 a, int128 b) internal pure returns (bool) {
47+
return sameSide(a, b) && abs(b) < abs(a);
48+
}
4549
}

markets/bfp-market/storage.dump.json

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,75 @@
227227
"slot": "6",
228228
"offset": 0
229229
}
230+
],
231+
"Runtime_cancelOrder": [
232+
{
233+
"type": "uint128",
234+
"name": "accountId",
235+
"size": 16,
236+
"slot": "0",
237+
"offset": 0
238+
},
239+
{
240+
"type": "uint128",
241+
"name": "marketId",
242+
"size": 16,
243+
"slot": "0",
244+
"offset": 16
245+
},
246+
{
247+
"type": "bool",
248+
"name": "isStale",
249+
"size": 1,
250+
"slot": "1",
251+
"offset": 0
252+
},
253+
{
254+
"type": "bool",
255+
"name": "isReady",
256+
"size": 1,
257+
"slot": "1",
258+
"offset": 1
259+
},
260+
{
261+
"type": "bool",
262+
"name": "isMarketSolvent",
263+
"size": 1,
264+
"slot": "1",
265+
"offset": 2
266+
},
267+
{
268+
"type": "struct",
269+
"name": "order",
270+
"members": [
271+
{
272+
"type": "int128",
273+
"name": "sizeDelta"
274+
},
275+
{
276+
"type": "uint64",
277+
"name": "commitmentTime"
278+
},
279+
{
280+
"type": "uint256",
281+
"name": "limitPrice"
282+
},
283+
{
284+
"type": "uint128",
285+
"name": "keeperFeeBufferUsd"
286+
},
287+
{
288+
"type": "array",
289+
"name": "hooks",
290+
"value": {
291+
"type": "address"
292+
}
293+
}
294+
],
295+
"size": 128,
296+
"slot": "2",
297+
"offset": 0
298+
}
230299
]
231300
}
232301
},

markets/bfp-market/test/integration/modules/OrderModule.cancel.test.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,12 @@ import {
2727
isSusdCollateral,
2828
setBaseFeePerGas,
2929
setMarketConfiguration,
30+
setMarketConfigurationById,
3031
withExplicitEvmMine,
3132
} from '../../helpers';
3233
import { calcKeeperCancellationFee } from '../../calculations';
3334

34-
describe('OrderModule Cancelations', () => {
35+
describe('OrderModule Cancellations', () => {
3536
const bs = bootstrap(genBootstrap());
3637
const { systems, restore, provider, keeper, traders, collateralsWithoutSusd } = bs;
3738

@@ -524,4 +525,53 @@ describe('OrderModule Cancelations', () => {
524525
);
525526
});
526527
});
528+
529+
describe('cancel order due to market insolvency', () => {
530+
it('reverts if trying to cancel an order when market is solvent', async () => {
531+
const { BfpMarketProxy } = systems();
532+
533+
const { trader, marketId, market, collateral, collateralDepositAmount } = await depositMargin(
534+
bs,
535+
genTrader(bs)
536+
);
537+
538+
const order = await genOrder(bs, market, collateral, collateralDepositAmount);
539+
await commitOrder(bs, marketId, trader, order);
540+
541+
// fast forward to settlement
542+
const { publishTime, settlementTime } = await getFastForwardTimestamp(bs, marketId, trader);
543+
await fastForwardTo(settlementTime, provider());
544+
545+
const { updateData } = await getPythPriceDataByMarketId(bs, marketId, publishTime);
546+
547+
await assertRevert(
548+
BfpMarketProxy.connect(keeper()).cancelOrder(trader.accountId, marketId, updateData),
549+
`PriceToleranceNotExceeded("${order.sizeDelta}", "${order.fillPrice}", "${order.limitPrice}")`
550+
);
551+
});
552+
553+
it('allows to cancel an order if the market is insolvent', async () => {
554+
const { BfpMarketProxy } = systems();
555+
const { trader, marketId, market, collateral, collateralDepositAmount } = await depositMargin(
556+
bs,
557+
genTrader(bs)
558+
);
559+
560+
const order = await genOrder(bs, market, collateral, collateralDepositAmount, {
561+
desiredSize: bn(1),
562+
});
563+
await commitOrder(bs, marketId, trader, order);
564+
565+
// fast forward to settlement
566+
const { publishTime, settlementTime } = await getFastForwardTimestamp(bs, marketId, trader);
567+
await fastForwardTo(settlementTime, provider());
568+
569+
await setMarketConfigurationById(bs, marketId, {
570+
minCreditPercent: bn(1000),
571+
});
572+
573+
const { updateData } = await getPythPriceDataByMarketId(bs, marketId, publishTime);
574+
await BfpMarketProxy.connect(keeper()).cancelOrder(trader.accountId, marketId, updateData);
575+
});
576+
});
527577
});

0 commit comments

Comments
 (0)