diff --git a/markets/perps-market/contracts/interfaces/IAsyncOrderModule.sol b/markets/perps-market/contracts/interfaces/IAsyncOrderModule.sol index 3e572bb14b..b4fbc494ad 100644 --- a/markets/perps-market/contracts/interfaces/IAsyncOrderModule.sol +++ b/markets/perps-market/contracts/interfaces/IAsyncOrderModule.sol @@ -55,9 +55,24 @@ interface IAsyncOrderModule { * @param marketId id of the market. * @param sizeDelta size of position. * @return orderFees incurred fees. + * @return fillPrice price at which the order would be filled. */ function computeOrderFees( uint128 marketId, int128 sizeDelta - ) external view returns (uint256 orderFees); + ) external view returns (uint256 orderFees, uint256 fillPrice); + + /** + * @notice For a given market, account id, and a position size, returns the required total account margin for this order to succeed + * @dev Useful for integrators to determine if an order will succeed or fail + * @param marketId id of the market. + * @param accountId id of the trader account. + * @param sizeDelta size of position. + * @return requiredMargin margin required for the order to succeed. + */ + function requiredMarginForOrder( + uint128 marketId, + uint128 accountId, + int128 sizeDelta + ) external view returns (uint256 requiredMargin); } diff --git a/markets/perps-market/contracts/modules/AsyncOrderModule.sol b/markets/perps-market/contracts/modules/AsyncOrderModule.sol index 84af91bf50..18b2296d4c 100644 --- a/markets/perps-market/contracts/modules/AsyncOrderModule.sol +++ b/markets/perps-market/contracts/modules/AsyncOrderModule.sol @@ -7,7 +7,9 @@ import {Account} from "@synthetixio/main/contracts/storage/Account.sol"; import {AccountRBAC} from "@synthetixio/main/contracts/storage/AccountRBAC.sol"; import {IAsyncOrderModule} from "../interfaces/IAsyncOrderModule.sol"; import {PerpsMarket} from "../storage/PerpsMarket.sol"; +import {PerpsAccount} from "../storage/PerpsAccount.sol"; import {AsyncOrder} from "../storage/AsyncOrder.sol"; +import {Position} from "../storage/Position.sol"; import {PerpsPrice} from "../storage/PerpsPrice.sol"; import {GlobalPerpsMarket} from "../storage/GlobalPerpsMarket.sol"; import {PerpsMarketConfiguration} from "../storage/PerpsMarketConfiguration.sol"; @@ -24,6 +26,7 @@ contract AsyncOrderModule is IAsyncOrderModule { using PerpsPrice for PerpsPrice.Data; using PerpsMarket for PerpsMarket.Data; using AsyncOrder for AsyncOrder.Data; + using PerpsAccount for PerpsAccount.Data; using SettlementStrategy for SettlementStrategy.Data; using GlobalPerpsMarket for GlobalPerpsMarket.Data; using PerpsMarketConfiguration for PerpsMarketConfiguration.Data; @@ -89,13 +92,49 @@ contract AsyncOrderModule is IAsyncOrderModule { function computeOrderFees( uint128 marketId, int128 sizeDelta - ) external view override returns (uint256 orderFees) { - PerpsMarket.Data storage perpsMarket = PerpsMarket.load(marketId); - int256 skew = perpsMarket.skew; + ) external view override returns (uint256 orderFees, uint256 fillPrice) { + (orderFees, fillPrice) = _computeOrderFees(marketId, sizeDelta); + } + + function requiredMarginForOrder( + uint128 accountId, + uint128 marketId, + int128 sizeDelta + ) external view override returns (uint256 requiredMargin) { + PerpsMarketConfiguration.Data storage marketConfig = PerpsMarketConfiguration.load( + marketId + ); + + Position.Data storage oldPosition = PerpsMarket.accountPosition(marketId, accountId); + ( + , + uint256 currentMaintenanceMargin, + uint256 currentTotalLiquidationRewards, + + ) = PerpsAccount.load(accountId).getAccountRequiredMargins(); + (uint256 orderFees, uint256 fillPrice) = _computeOrderFees(marketId, sizeDelta); + + return + AsyncOrder.getRequiredMarginWithNewPosition( + marketConfig, + marketId, + oldPosition.size, + oldPosition.size + sizeDelta, + fillPrice, + currentMaintenanceMargin, + currentTotalLiquidationRewards + ) + orderFees; + } + + function _computeOrderFees( + uint128 marketId, + int128 sizeDelta + ) private view returns (uint256 orderFees, uint256 fillPrice) { + int256 skew = PerpsMarket.load(marketId).skew; PerpsMarketConfiguration.Data storage marketConfig = PerpsMarketConfiguration.load( marketId ); - uint256 fillPrice = AsyncOrder.calculateFillPrice( + fillPrice = AsyncOrder.calculateFillPrice( skew, marketConfig.skewScale, sizeDelta, diff --git a/markets/perps-market/contracts/storage/AsyncOrder.sol b/markets/perps-market/contracts/storage/AsyncOrder.sol index 80fb2aaf53..463703fba4 100644 --- a/markets/perps-market/contracts/storage/AsyncOrder.sol +++ b/markets/perps-market/contracts/storage/AsyncOrder.sol @@ -328,7 +328,7 @@ library AsyncOrder { revert InsufficientMargin(runtime.currentAvailableMargin, runtime.orderFees); } - oldPosition = PerpsMarket.load(runtime.marketId).positions[runtime.accountId]; + oldPosition = PerpsMarket.accountPosition(runtime.marketId, runtime.accountId); PerpsMarket.validatePositionSize( perpsMarketData, @@ -339,7 +339,7 @@ library AsyncOrder { runtime.newPositionSize = oldPosition.size + runtime.sizeDelta; runtime.totalRequiredMargin = - _getRequiredMarginWithNewPosition( + getRequiredMarginWithNewPosition( marketConfig, runtime.marketId, oldPosition.size, @@ -466,7 +466,7 @@ library AsyncOrder { * @notice After the required margins are calculated with the old position, this function replaces the * old position data with the new position margin requirements and returns them. */ - function _getRequiredMarginWithNewPosition( + function getRequiredMarginWithNewPosition( PerpsMarketConfiguration.Data storage marketConfig, uint128 marketId, int128 oldPositionSize, diff --git a/markets/perps-market/contracts/storage/PerpsMarket.sol b/markets/perps-market/contracts/storage/PerpsMarket.sol index dc5934c15e..31e2a6ac23 100644 --- a/markets/perps-market/contracts/storage/PerpsMarket.sol +++ b/markets/perps-market/contracts/storage/PerpsMarket.sol @@ -310,4 +310,11 @@ library PerpsMarket { return traderUnrealizedPnl + unrealizedFunding - self.debtCorrectionAccumulator; } + + function accountPosition( + uint128 marketId, + uint128 accountId + ) internal view returns (Position.Data storage position) { + position = load(marketId).positions[accountId]; + } } diff --git a/markets/perps-market/test/integration/Orders/Order.marginValidation.test.ts b/markets/perps-market/test/integration/Orders/Order.marginValidation.test.ts index f615031dcb..ce22719c69 100644 --- a/markets/perps-market/test/integration/Orders/Order.marginValidation.test.ts +++ b/markets/perps-market/test/integration/Orders/Order.marginValidation.test.ts @@ -82,7 +82,7 @@ describe('Orders - margin validation', () => { describe('openPosition 1 failure', () => { let orderFees: ethers.BigNumber; before('get order fees', async () => { - orderFees = await systems().PerpsMarket.computeOrderFees(51, 3); + [orderFees] = await systems().PerpsMarket.computeOrderFees(51, 3); }); it('reverts if not enough margin', async () => { @@ -98,6 +98,15 @@ describe('Orders - margin validation', () => { wei(10_000) ); + const totalRequiredMargin = initialMargin + .add(getMaxLiquidationReward(liquidationMargin, wei(100), wei(500))) + .add(orderFees); + + assertBn.equal( + await systems().PerpsMarket.requiredMarginForOrder(2, 51, bn(3)), + totalRequiredMargin.toBN() + ); + await assertRevert( systems() .PerpsMarket.connect(trader1()) @@ -110,10 +119,7 @@ describe('Orders - margin validation', () => { referrer: ethers.constants.AddressZero, trackingCode: ethers.constants.HashZero, }), - `InsufficientMargin("${bn(100)}", "${initialMargin - .add(getMaxLiquidationReward(liquidationMargin, wei(100), wei(500))) - .add(orderFees) - .toString(18, true)}")` + `InsufficientMargin("${bn(100)}", "${totalRequiredMargin.toString(18, true)}")` ); }); }); @@ -149,7 +155,7 @@ describe('Orders - margin validation', () => { describe('openPosition 2 failure', () => { let orderFees: ethers.BigNumber; before('get order fees', async () => { - orderFees = await systems().PerpsMarket.computeOrderFees(50, 5); + [orderFees] = await systems().PerpsMarket.computeOrderFees(50, 5); }); it('reverts if not enough margin', async () => { @@ -186,6 +192,11 @@ describe('Orders - margin validation', () => { .add(liqReward) .add(orderFees); + assertBn.equal( + await systems().PerpsMarket.requiredMarginForOrder(2, 50, bn(5)), + totalRequiredMargin.toBN() + ); + await assertRevert( systems() .PerpsMarket.connect(trader1()) @@ -233,7 +244,7 @@ describe('Orders - margin validation', () => { describe('modify position', () => { let orderFees: ethers.BigNumber; before('get order fees', async () => { - orderFees = await systems().PerpsMarket.computeOrderFees(50, 5); + [orderFees] = await systems().PerpsMarket.computeOrderFees(50, 5); }); it('reverts if not enough margin', async () => { @@ -270,6 +281,11 @@ describe('Orders - margin validation', () => { .add(liqReward) .add(orderFees); + assertBn.equal( + await systems().PerpsMarket.requiredMarginForOrder(2, 50, bn(5)), + totalRequiredMargin.toBN() + ); + await assertRevert( systems() .PerpsMarket.connect(trader1()) diff --git a/markets/perps-market/test/integration/Orders/GetOrderFees.test.ts b/markets/perps-market/test/integration/helpers/maxSize.ts similarity index 100% rename from markets/perps-market/test/integration/Orders/GetOrderFees.test.ts rename to markets/perps-market/test/integration/helpers/maxSize.ts