diff --git a/markets/perps-market/contracts/mocks/FeeCollectorMock.sol b/markets/perps-market/contracts/mocks/FeeCollectorMock.sol index dc92265a23..654b1556e0 100644 --- a/markets/perps-market/contracts/mocks/FeeCollectorMock.sol +++ b/markets/perps-market/contracts/mocks/FeeCollectorMock.sol @@ -17,7 +17,7 @@ contract FeeCollectorMock is IFeeCollector { uint128 marketId, uint256 feeAmount, address sender - ) external override returns (uint256) { + ) external view override returns (uint256) { // mention the variables in the block to prevent unused local variable warning marketId; sender; diff --git a/markets/perps-market/contracts/modules/AsyncOrderModule.sol b/markets/perps-market/contracts/modules/AsyncOrderModule.sol index a7b5ad970c..845c0004d8 100644 --- a/markets/perps-market/contracts/modules/AsyncOrderModule.sol +++ b/markets/perps-market/contracts/modules/AsyncOrderModule.sol @@ -14,6 +14,7 @@ import {PerpsPrice} from "../storage/PerpsPrice.sol"; import {GlobalPerpsMarket} from "../storage/GlobalPerpsMarket.sol"; import {PerpsMarketConfiguration} from "../storage/PerpsMarketConfiguration.sol"; import {SettlementStrategy} from "../storage/SettlementStrategy.sol"; +import {MathUtil} from "../utils/MathUtil.sol"; import {Flags} from "../utils/Flags.sol"; /** @@ -104,11 +105,12 @@ contract AsyncOrderModule is IAsyncOrderModule { uint128 marketId, int128 sizeDelta ) external view override returns (uint256 orderFees, uint256 fillPrice) { - (orderFees, fillPrice) = _computeOrderFees( - marketId, - sizeDelta, - PerpsPrice.getCurrentPrice(marketId, PerpsPrice.Tolerance.DEFAULT) - ); + return + _computeOrderFeesWithPrice( + marketId, + sizeDelta, + PerpsPrice.getCurrentPrice(marketId, PerpsPrice.Tolerance.DEFAULT) + ); } /** @@ -119,7 +121,29 @@ contract AsyncOrderModule is IAsyncOrderModule { int128 sizeDelta, uint256 price ) external view override returns (uint256 orderFees, uint256 fillPrice) { - (orderFees, fillPrice) = _computeOrderFees(marketId, sizeDelta, price); + return _computeOrderFeesWithPrice(marketId, sizeDelta, price); + } + + function _computeOrderFeesWithPrice( + uint128 marketId, + int128 sizeDelta, + uint256 price + ) internal view returns (uint256 orderFees, uint256 fillPrice) { + // create a fake order commitment request + AsyncOrder.Data memory order = AsyncOrder.Data( + 0, + AsyncOrder.OrderCommitmentRequest(marketId, 0, sizeDelta, 0, 0, bytes32(0), address(0)) + ); + + PerpsAccount.Data storage account = PerpsAccount.load(order.request.accountId); + + // probably should be doing this but cant because the interface (view) doesn't allow it + //perpsMarketData.recomputeFunding(orderPrice); + + PerpsAccount.MemoryContext memory ctx = account.getOpenPositionsAndCurrentPrices( + PerpsPrice.Tolerance.DEFAULT + ); + (, , , fillPrice, orderFees) = order.createUpdatedPosition(price, ctx); } /** @@ -135,13 +159,27 @@ contract AsyncOrderModule is IAsyncOrderModule { ); } + function requiredMarginImmut( + uint128 accountId, + uint128 marketId, + int128 sizeDelta + ) external returns (uint256 requiredMargin) { + return + _requiredMarginForOrderWithPrice( + accountId, + marketId, + sizeDelta, + PerpsPrice.getCurrentPrice(marketId, PerpsPrice.Tolerance.DEFAULT) + ); + } + function requiredMarginForOrder( uint128 accountId, uint128 marketId, int128 sizeDelta ) external view override returns (uint256 requiredMargin) { return - _requiredMarginForOrder( + _requiredMarginForOrderWithPrice( accountId, marketId, sizeDelta, @@ -155,59 +193,58 @@ contract AsyncOrderModule is IAsyncOrderModule { int128 sizeDelta, uint256 price ) external view override returns (uint256 requiredMargin) { - return _requiredMarginForOrder(accountId, marketId, sizeDelta, price); + return _requiredMarginForOrderWithPrice(accountId, marketId, sizeDelta, price); } - function _requiredMarginForOrder( + function _requiredMarginForOrderWithPrice( uint128 accountId, uint128 marketId, int128 sizeDelta, - uint256 orderPrice + uint256 price ) internal view returns (uint256 requiredMargin) { - PerpsMarketConfiguration.Data storage marketConfig = PerpsMarketConfiguration.load( - marketId + // create a fake order commitment request + AsyncOrder.Data memory order = AsyncOrder.Data( + 0, + AsyncOrder.OrderCommitmentRequest( + marketId, + accountId, + sizeDelta, + 0, + 0, + bytes32(0), + address(0) + ) ); - Position.Data storage oldPosition = PerpsMarket.accountPosition(marketId, accountId); - PerpsAccount.Data storage account = PerpsAccount.load(accountId); - (uint256 currentInitialMargin, , ) = account.getAccountRequiredMargins( + PerpsAccount.Data storage account = PerpsAccount.load(order.request.accountId); + + // probably should be doing this but cant because the interface (view) doesn't allow it + //perpsMarketData.recomputeFunding(orderPrice); + + PerpsAccount.MemoryContext memory ctx = account.getOpenPositionsAndCurrentPrices( PerpsPrice.Tolerance.DEFAULT ); - (uint256 orderFees, uint256 fillPrice) = _computeOrderFees(marketId, sizeDelta, orderPrice); - return - AsyncOrder.getRequiredMarginWithNewPosition( - account, - marketConfig, - marketId, - oldPosition.size, - oldPosition.size + sizeDelta, - fillPrice, - currentInitialMargin - ) + orderFees; - } + uint256 orderFees; + Position.Data memory oldPosition; + Position.Data memory newPosition; + (ctx, oldPosition, newPosition, , orderFees) = order.createUpdatedPosition(price, ctx); - function _computeOrderFees( - uint128 marketId, - int128 sizeDelta, - uint256 orderPrice - ) private view returns (uint256 orderFees, uint256 fillPrice) { - int256 skew = PerpsMarket.load(marketId).skew; - PerpsMarketConfiguration.Data storage marketConfig = PerpsMarketConfiguration.load( - marketId - ); - fillPrice = AsyncOrder.calculateFillPrice( - skew, - marketConfig.skewScale, - sizeDelta, - orderPrice + // say no margin is required for shrinking position size + if (MathUtil.isSameSideReducing(oldPosition.size, newPosition.size)) { + return 0; + } + + (, uint256 totalCollateralValueWithoutDiscount) = account.getTotalCollateralValue( + PerpsPrice.Tolerance.DEFAULT ); - orderFees = AsyncOrder.calculateOrderFee( - sizeDelta, - fillPrice, - skew, - marketConfig.orderFees + uint256 possibleLiquidationReward; + (requiredMargin, , possibleLiquidationReward) = PerpsAccount.getAccountRequiredMargins( + ctx, + totalCollateralValueWithoutDiscount ); + + return requiredMargin + possibleLiquidationReward + orderFees; } } diff --git a/markets/perps-market/contracts/modules/AsyncOrderSettlementPythModule.sol b/markets/perps-market/contracts/modules/AsyncOrderSettlementPythModule.sol index 6b33ef8043..506a72e914 100644 --- a/markets/perps-market/contracts/modules/AsyncOrderSettlementPythModule.sol +++ b/markets/perps-market/contracts/modules/AsyncOrderSettlementPythModule.sol @@ -81,7 +81,7 @@ contract AsyncOrderSettlementPythModule is GlobalPerpsMarket.load().checkLiquidation(runtime.accountId); - Position.Data storage oldPosition; + Position.Data memory oldPosition; // Load the market before settlement to capture the original market size PerpsMarket.Data storage market = PerpsMarket.loadValid(runtime.marketId); diff --git a/markets/perps-market/contracts/modules/LiquidationModule.sol b/markets/perps-market/contracts/modules/LiquidationModule.sol index 1fdfdf52c8..d7aa6c935b 100644 --- a/markets/perps-market/contracts/modules/LiquidationModule.sol +++ b/markets/perps-market/contracts/modules/LiquidationModule.sol @@ -20,6 +20,7 @@ import {MarketUpdate} from "../storage/MarketUpdate.sol"; import {IMarketEvents} from "../interfaces/IMarketEvents.sol"; import {KeeperCosts} from "../storage/KeeperCosts.sol"; import {AsyncOrder} from "../storage/AsyncOrder.sol"; +import {Position} from "../storage/Position.sol"; /** * @title Module for liquidating accounts. @@ -47,14 +48,26 @@ contract LiquidationModule is ILiquidationModule, IMarketEvents { .load() .liquidatableAccounts; PerpsAccount.Data storage account = PerpsAccount.load(accountId); + PerpsAccount.MemoryContext memory ctx = account.getOpenPositionsAndCurrentPrices( + PerpsPrice.Tolerance.STRICT + ); if (!liquidatableAccounts.contains(accountId)) { + ( + uint256 totalCollateralValueWithDiscount, + uint256 totalCollateralValueWithoutDiscount + ) = account.getTotalCollateralValue(PerpsPrice.Tolerance.STRICT); + ( bool isEligible, int256 availableMargin, , uint256 requiredMaintenaceMargin, uint256 expectedLiquidationReward - ) = account.isEligibleForLiquidation(PerpsPrice.Tolerance.STRICT); + ) = PerpsAccount.isEligibleForLiquidation( + ctx, + totalCollateralValueWithDiscount, + totalCollateralValueWithoutDiscount + ); if (isEligible) { (uint256 flagCost, uint256 seizedMarginValue) = account.flagForLiquidation(); @@ -67,12 +80,12 @@ contract LiquidationModule is ILiquidationModule, IMarketEvents { flagCost ); - liquidationReward = _liquidateAccount(account, flagCost, seizedMarginValue, true); + liquidationReward = _liquidateAccount(ctx, flagCost, seizedMarginValue, true); } else { revert NotEligibleForLiquidation(accountId); } } else { - liquidationReward = _liquidateAccount(account, 0, 0, false); + liquidationReward = _liquidateAccount(ctx, 0, 0, false); } } @@ -87,15 +100,28 @@ contract LiquidationModule is ILiquidationModule, IMarketEvents { revert AccountHasOpenPositions(accountId); } - (bool isEligible, ) = account.isEligibleForMarginLiquidation(PerpsPrice.Tolerance.STRICT); + PerpsAccount.MemoryContext memory ctx = account.getOpenPositionsAndCurrentPrices( + PerpsPrice.Tolerance.STRICT + ); + ( + uint256 totalCollateralValueWithDiscount, + uint256 totalCollateralValueWithoutDiscount + ) = account.getTotalCollateralValue(PerpsPrice.Tolerance.STRICT); + (bool isEligible, ) = PerpsAccount.isEligibleForMarginLiquidation( + ctx, + totalCollateralValueWithDiscount, + totalCollateralValueWithoutDiscount + ); if (isEligible) { // margin is sent to liquidation rewards distributor in getMarginLiquidationCostAndSeizeMargin - uint256 marginLiquidateCost = KeeperCosts.load().getFlagKeeperCosts(account.id); + uint256 marginLiquidateCost = KeeperCosts.load().getFlagKeeperCosts( + account.getNumberOfUpdatedFeedsRequired() + ); uint256 seizedMarginValue = account.seizeCollateral(); // keeper is rewarded in _liquidateAccount liquidationReward = _liquidateAccount( - account, + ctx, marginLiquidateCost, seizedMarginValue, true @@ -132,7 +158,14 @@ contract LiquidationModule is ILiquidationModule, IMarketEvents { for (uint256 i = 0; i < numberOfAccountsToLiquidate; i++) { uint128 accountId = liquidatableAccounts[i].to128(); - liquidationReward += _liquidateAccount(PerpsAccount.load(accountId), 0, 0, false); + liquidationReward += _liquidateAccount( + PerpsAccount.load(accountId).getOpenPositionsAndCurrentPrices( + PerpsPrice.Tolerance.STRICT + ), + 0, + 0, + false + ); } } @@ -154,7 +187,14 @@ contract LiquidationModule is ILiquidationModule, IMarketEvents { continue; } - liquidationReward += _liquidateAccount(PerpsAccount.load(accountId), 0, 0, false); + liquidationReward += _liquidateAccount( + PerpsAccount.load(accountId).getOpenPositionsAndCurrentPrices( + PerpsPrice.Tolerance.STRICT + ), + 0, + 0, + false + ); } } @@ -174,9 +214,19 @@ contract LiquidationModule is ILiquidationModule, IMarketEvents { return true; } - (isEligible, , , , ) = PerpsAccount.load(accountId).isEligibleForLiquidation( + PerpsAccount.Data storage account = PerpsAccount.load(accountId); + PerpsAccount.MemoryContext memory ctx = account.getOpenPositionsAndCurrentPrices( PerpsPrice.Tolerance.DEFAULT ); + ( + uint256 totalCollateralValueWithDiscount, + uint256 totalCollateralValueWithoutDiscount + ) = account.getTotalCollateralValue(PerpsPrice.Tolerance.DEFAULT); + (isEligible, , , , ) = PerpsAccount.isEligibleForLiquidation( + ctx, + totalCollateralValueWithDiscount, + totalCollateralValueWithoutDiscount + ); } function canLiquidateMarginOnly( @@ -186,7 +236,18 @@ contract LiquidationModule is ILiquidationModule, IMarketEvents { if (account.hasOpenPositions()) { return false; } else { - (isEligible, ) = account.isEligibleForMarginLiquidation(PerpsPrice.Tolerance.DEFAULT); + PerpsAccount.MemoryContext memory ctx = account.getOpenPositionsAndCurrentPrices( + PerpsPrice.Tolerance.DEFAULT + ); + ( + uint256 totalCollateralValueWithDiscount, + uint256 totalCollateralValueWithoutDiscount + ) = account.getTotalCollateralValue(PerpsPrice.Tolerance.DEFAULT); + (isEligible, ) = PerpsAccount.isEligibleForMarginLiquidation( + ctx, + totalCollateralValueWithDiscount, + totalCollateralValueWithoutDiscount + ); } } @@ -211,123 +272,122 @@ contract LiquidationModule is ILiquidationModule, IMarketEvents { ); } - struct LiquidateAccountRuntime { - uint128 accountId; - uint256 totalFlaggingRewards; - uint256 totalLiquidated; - bool accountFullyLiquidated; - uint256 totalLiquidationCost; - uint256 price; - uint128 positionMarketId; - uint256 loopIterator; // stack too deep to the extreme - } - - /** - * @dev liquidates an account - */ - function _liquidateAccount( - PerpsAccount.Data storage account, - uint256 costOfFlagExecution, - uint256 totalCollateralValue, - bool positionFlagged - ) internal returns (uint256 keeperLiquidationReward) { - LiquidateAccountRuntime memory runtime; - runtime.accountId = account.id; - uint256[] memory openPositionMarketIds = account.openPositionMarketIds.values(); - uint256[] memory prices = PerpsPrice.getCurrentPrices( - openPositionMarketIds, - PerpsPrice.Tolerance.STRICT - ); - - for ( - runtime.loopIterator = 0; - runtime.loopIterator < openPositionMarketIds.length; - runtime.loopIterator++ - ) { - runtime.positionMarketId = openPositionMarketIds[runtime.loopIterator].to128(); - runtime.price = prices[runtime.loopIterator]; - + function _liquidateAccountPositions( + PerpsAccount.MemoryContext memory ctx, + uint256 totalCollateralValue + ) internal returns (uint256 totalLiquidated, uint256 totalFlaggingRewards) { + uint256 i; + for (i = 0; i < ctx.positions.length; i++) { ( uint256 amountLiquidated, int128 newPositionSize, - int128 sizeDelta, - uint256 oldPositionAbsSize, MarketUpdate.Data memory marketUpdateData - ) = account.liquidatePosition(runtime.positionMarketId, runtime.price); - - // endorsed liquidators do not get flag rewards - if ( - ERC2771Context._msgSender() != - PerpsMarketConfiguration.load(runtime.positionMarketId).endorsedLiquidator - ) { - // using oldPositionAbsSize to calculate flag reward - runtime.totalFlaggingRewards += PerpsMarketConfiguration - .load(runtime.positionMarketId) - .calculateFlagReward(oldPositionAbsSize.mulDecimal(runtime.price)); - } + ) = PerpsAccount.load(ctx.accountId).liquidatePosition(ctx.positions[i], ctx.prices[i]); if (amountLiquidated == 0) { continue; } - runtime.totalLiquidated += amountLiquidated; + totalLiquidated += amountLiquidated; emit MarketUpdated( - runtime.positionMarketId, - runtime.price, + ctx.positions[i].marketId, + ctx.prices[i], marketUpdateData.skew, marketUpdateData.size, - sizeDelta, + newPositionSize - ctx.positions[i].size, marketUpdateData.currentFundingRate, marketUpdateData.currentFundingVelocity, marketUpdateData.interestRate ); emit PositionLiquidated( - runtime.accountId, - runtime.positionMarketId, + ctx.accountId, + ctx.positions[i].marketId, amountLiquidated, newPositionSize ); } + for (uint256 j = 0; j <= MathUtil.min(i, ctx.positions.length - 1); j++) { + // using oldPositionAbsSize to calculate flag reward + if ( + ERC2771Context._msgSender() != + PerpsMarketConfiguration.load(ctx.positions[j].marketId).endorsedLiquidator + ) { + totalFlaggingRewards += PerpsMarketConfiguration + .load(ctx.positions[j].marketId) + .calculateFlagReward( + MathUtil.abs(ctx.positions[j].size).mulDecimal(ctx.prices[j]) + ); + } + } + if ( ERC2771Context._msgSender() != - PerpsMarketConfiguration.load(runtime.positionMarketId).endorsedLiquidator + PerpsMarketConfiguration + .load(ctx.positions[MathUtil.min(i, ctx.positions.length - 1)].marketId) + .endorsedLiquidator ) { // Use max of collateral or positions flag rewards uint256 totalCollateralLiquidateRewards = GlobalPerpsMarketConfiguration .load() .calculateCollateralLiquidateReward(totalCollateralValue); - runtime.totalFlaggingRewards = MathUtil.max( + totalFlaggingRewards = MathUtil.max( totalCollateralLiquidateRewards, - runtime.totalFlaggingRewards + totalFlaggingRewards + ); + } + + return (totalLiquidated, totalFlaggingRewards); + } + + /** + * @dev liquidates an account + */ + function _liquidateAccount( + PerpsAccount.MemoryContext memory ctx, + uint256 costOfFlagExecution, + uint256 totalCollateralValue, + bool positionFlagged + ) internal returns (uint256 keeperLiquidationReward) { + uint256 totalLiquidated; + uint256 totalFlaggingRewards; + if (ctx.positions.length > 0) { + (totalLiquidated, totalFlaggingRewards) = _liquidateAccountPositions( + ctx, + totalCollateralValue ); + } else { + totalFlaggingRewards = GlobalPerpsMarketConfiguration + .load() + .calculateCollateralLiquidateReward(totalCollateralValue); } + bool accountFullyLiquidated; - runtime.totalLiquidationCost = - KeeperCosts.load().getLiquidateKeeperCosts() + + uint256 totalLiquidationCost = KeeperCosts.load().getLiquidateKeeperCosts() + costOfFlagExecution; - if (positionFlagged || runtime.totalLiquidated > 0) { + if (positionFlagged || totalLiquidated > 0) { keeperLiquidationReward = _processLiquidationRewards( - positionFlagged ? runtime.totalFlaggingRewards : 0, - runtime.totalLiquidationCost, + positionFlagged ? totalFlaggingRewards : 0, + totalLiquidationCost, totalCollateralValue ); - runtime.accountFullyLiquidated = account.openPositionMarketIds.length() == 0; + accountFullyLiquidated = + PerpsAccount.load(ctx.accountId).openPositionMarketIds.length() == 0; if ( - runtime.accountFullyLiquidated && - GlobalPerpsMarket.load().liquidatableAccounts.contains(runtime.accountId) + accountFullyLiquidated && + GlobalPerpsMarket.load().liquidatableAccounts.contains(ctx.accountId) ) { - GlobalPerpsMarket.load().liquidatableAccounts.remove(runtime.accountId); + GlobalPerpsMarket.load().liquidatableAccounts.remove(ctx.accountId); } } emit AccountLiquidationAttempt( - runtime.accountId, + ctx.accountId, keeperLiquidationReward, - runtime.accountFullyLiquidated + accountFullyLiquidated ); } diff --git a/markets/perps-market/contracts/modules/PerpsAccountModule.sol b/markets/perps-market/contracts/modules/PerpsAccountModule.sol index 2d906b1a7c..75f932054b 100644 --- a/markets/perps-market/contracts/modules/PerpsAccountModule.sol +++ b/markets/perps-market/contracts/modules/PerpsAccountModule.sol @@ -121,19 +121,23 @@ contract PerpsAccountModule is IPerpsAccountModule { /** * @inheritdoc IPerpsAccountModule */ - function totalCollateralValue(uint128 accountId) external view override returns (uint256) { - return - PerpsAccount.load(accountId).getTotalCollateralValue( - PerpsPrice.Tolerance.DEFAULT, - false - ); + function totalCollateralValue( + uint128 accountId + ) external view override returns (uint256 totalValue) { + (, totalValue) = PerpsAccount.load(accountId).getTotalCollateralValue( + PerpsPrice.Tolerance.DEFAULT + ); } /** * @inheritdoc IPerpsAccountModule */ function totalAccountOpenInterest(uint128 accountId) external view override returns (uint256) { - return PerpsAccount.load(accountId).getTotalNotionalOpenInterest(); + PerpsAccount.Data storage account = PerpsAccount.load(accountId); + PerpsAccount.MemoryContext memory ctx = account.getOpenPositionsAndCurrentPrices( + PerpsPrice.Tolerance.DEFAULT + ); + return PerpsAccount.getTotalNotionalOpenInterest(ctx); } /** @@ -176,9 +180,14 @@ contract PerpsAccountModule is IPerpsAccountModule { function getAvailableMargin( uint128 accountId ) external view override returns (int256 availableMargin) { - availableMargin = PerpsAccount.load(accountId).getAvailableMargin( + PerpsAccount.Data storage account = PerpsAccount.load(accountId); + PerpsAccount.MemoryContext memory ctx = account.getOpenPositionsAndCurrentPrices( PerpsPrice.Tolerance.DEFAULT ); + (uint256 totalCollateralValueWithDiscount, ) = account.getTotalCollateralValue( + PerpsPrice.Tolerance.DEFAULT + ); + availableMargin = PerpsAccount.getAvailableMargin(ctx, totalCollateralValueWithDiscount); } /** @@ -188,7 +197,18 @@ contract PerpsAccountModule is IPerpsAccountModule { uint128 accountId ) external view override returns (int256 withdrawableMargin) { PerpsAccount.Data storage account = PerpsAccount.load(accountId); - withdrawableMargin = account.getWithdrawableMargin(PerpsPrice.Tolerance.DEFAULT); + PerpsAccount.MemoryContext memory ctx = account.getOpenPositionsAndCurrentPrices( + PerpsPrice.Tolerance.DEFAULT + ); + ( + uint256 totalCollateralValueWithDiscount, + uint256 totalCollateralValueWithoutDiscount + ) = account.getTotalCollateralValue(PerpsPrice.Tolerance.DEFAULT); + withdrawableMargin = PerpsAccount.getWithdrawableMargin( + ctx, + totalCollateralValueWithoutDiscount, + totalCollateralValueWithDiscount + ); } /** @@ -211,8 +231,14 @@ contract PerpsAccountModule is IPerpsAccountModule { return (0, 0, 0); } - (requiredInitialMargin, requiredMaintenanceMargin, maxLiquidationReward) = account - .getAccountRequiredMargins(PerpsPrice.Tolerance.DEFAULT); + PerpsAccount.MemoryContext memory ctx = account.getOpenPositionsAndCurrentPrices( + PerpsPrice.Tolerance.DEFAULT + ); + (, uint256 totalCollateralValueWithoutDiscount) = account.getTotalCollateralValue( + PerpsPrice.Tolerance.DEFAULT + ); + (requiredInitialMargin, requiredMaintenanceMargin, maxLiquidationReward) = PerpsAccount + .getAccountRequiredMargins(ctx, totalCollateralValueWithoutDiscount); // Include liquidation rewards to required initial margin and required maintenance margin requiredInitialMargin += maxLiquidationReward; diff --git a/markets/perps-market/contracts/modules/PerpsMarketModule.sol b/markets/perps-market/contracts/modules/PerpsMarketModule.sol index e503290eda..302c2c7b75 100644 --- a/markets/perps-market/contracts/modules/PerpsMarketModule.sol +++ b/markets/perps-market/contracts/modules/PerpsMarketModule.sol @@ -74,13 +74,7 @@ contract PerpsMarketModule is IPerpsMarketModule { int128 orderSize, uint256 price ) external view override returns (uint256) { - return - AsyncOrder.calculateFillPrice( - PerpsMarket.load(marketId).skew, - PerpsMarketConfiguration.load(marketId).skewScale, - orderSize, - price - ); + return PerpsMarket.load(marketId).calculateFillPrice(orderSize, price); } /** diff --git a/markets/perps-market/contracts/storage/AsyncOrder.sol b/markets/perps-market/contracts/storage/AsyncOrder.sol index 7e84c2f1cd..7e66c45aa9 100644 --- a/markets/perps-market/contracts/storage/AsyncOrder.sol +++ b/markets/perps-market/contracts/storage/AsyncOrder.sol @@ -225,30 +225,47 @@ library AsyncOrder { } /** - * @dev Struct used internally in validateOrder() to prevent stack too deep error. + * @notice Builds state variables of the resulting state if a user were to complete the given order. + * Useful for validation or various getters on the modules. */ - struct SimulateDataRuntime { - bool isEligible; - int128 sizeDelta; - uint128 accountId; - uint128 marketId; - uint256 fillPrice; - uint256 orderFees; - uint256 availableMargin; - uint256 currentLiquidationMargin; - uint256 accumulatedLiquidationRewards; - int128 newPositionSize; - uint256 newNotionalValue; - int256 currentAvailableMargin; - uint256 requiredInitialMargin; - uint256 initialRequiredMargin; - uint256 totalRequiredMargin; - Position.Data newPosition; - bytes32 trackingCode; + function createUpdatedPosition( + Data memory order, + uint256 orderPrice, + PerpsAccount.MemoryContext memory ctx + ) + internal + view + returns ( + PerpsAccount.MemoryContext memory newCtx, + Position.Data memory oldPosition, + Position.Data memory newPosition, + uint256 fillPrice, + uint256 orderFees + ) + { + PerpsMarket.Data storage perpsMarketData = PerpsMarket.load(order.request.marketId); + + fillPrice = perpsMarketData.calculateFillPrice(order.request.sizeDelta, orderPrice).to128(); + oldPosition = PerpsMarket.load(order.request.marketId).positions[order.request.accountId]; + newPosition = Position.Data({ + marketId: order.request.marketId, + latestInteractionPrice: fillPrice.to128(), + latestInteractionFunding: perpsMarketData.lastFundingValue.to128(), + latestInterestAccrued: 0, + size: oldPosition.size + order.request.sizeDelta + }); + + // update the account positions list, so we can now conveniently recompute required margin + newCtx = PerpsAccount.upsertPosition(ctx, newPosition); + + orderFees = perpsMarketData.calculateOrderFee( + newPosition.size - oldPosition.size, + fillPrice + ); } /** - * @notice Checks if the order request can be settled. + * @notice Checks if the order request can be settled. This function effectively simulates the future state and verifies it is good post settlement. * @dev it recomputes market funding rate, calculates fill price and fees for the order * @dev and with that data it checks that: * @dev - the account is eligible for liquidation @@ -262,124 +279,113 @@ library AsyncOrder { Data storage order, SettlementStrategy.Data storage strategy, uint256 orderPrice - ) internal returns (Position.Data memory, uint256, uint256, Position.Data storage oldPosition) { - /// @dev runtime stores order settlement data and prevents stack too deep - SimulateDataRuntime memory runtime; - - runtime.accountId = order.request.accountId; - runtime.marketId = order.request.marketId; - runtime.sizeDelta = order.request.sizeDelta; - - if (runtime.sizeDelta == 0) { + ) + internal + returns ( + Position.Data memory newPosition, + uint256 orderFees, + uint256 fillPrice, + Position.Data memory oldPosition + ) + { + if (order.request.sizeDelta == 0) { revert ZeroSizeOrder(); } - PerpsAccount.Data storage account = PerpsAccount.load(runtime.accountId); - + PerpsAccount.MemoryContext memory ctx = PerpsAccount + .load(order.request.accountId) + .getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); ( - runtime.isEligible, - runtime.currentAvailableMargin, - runtime.requiredInitialMargin, - , - - ) = account.isEligibleForLiquidation(PerpsPrice.Tolerance.DEFAULT); - - if (runtime.isEligible) { - revert PerpsAccount.AccountLiquidatable(runtime.accountId); - } + uint256 totalCollateralValueWithDiscount, + uint256 totalCollateralValueWithoutDiscount + ) = PerpsAccount.load(order.request.accountId).getTotalCollateralValue( + PerpsPrice.Tolerance.DEFAULT + ); - PerpsMarket.Data storage perpsMarketData = PerpsMarket.load(runtime.marketId); - perpsMarketData.recomputeFunding(orderPrice); + // verify if the account is *currently* liquidatable + // we are only checking this here because once an account enters liquidation they are not allowed to dig themselves out by repaying + { + int256 currentAvailableMargin; + { + bool isEligibleForLiquidation; + (isEligibleForLiquidation, currentAvailableMargin, , , ) = PerpsAccount + .isEligibleForLiquidation( + ctx, + totalCollateralValueWithDiscount, + totalCollateralValueWithoutDiscount + ); + + if (isEligibleForLiquidation) { + revert PerpsAccount.AccountLiquidatable(order.request.accountId); + } + } - PerpsMarketConfiguration.Data storage marketConfig = PerpsMarketConfiguration.load( - runtime.marketId - ); + // now get the new state of the market by calling `createUpdatedPosition(order, orderPrice);` + PerpsMarket.load(order.request.marketId).recomputeFunding(orderPrice); - runtime.fillPrice = calculateFillPrice( - perpsMarketData.skew, - marketConfig.skewScale, - runtime.sizeDelta, - orderPrice - ); + (ctx, oldPosition, newPosition, fillPrice, orderFees) = createUpdatedPosition( + order, + orderPrice, + ctx + ); - runtime.orderFees = - calculateOrderFee( - runtime.sizeDelta, - runtime.fillPrice, - perpsMarketData.skew, - marketConfig.orderFees - ) + - settlementRewardCost(strategy); - - oldPosition = PerpsMarket.accountPosition(runtime.marketId, runtime.accountId); - runtime.newPositionSize = oldPosition.size + runtime.sizeDelta; - - // only account for negative pnl - runtime.currentAvailableMargin += MathUtil.min( - calculateFillPricePnl(runtime.fillPrice, orderPrice, runtime.sizeDelta), - 0 - ); + // add the additional settlement fee, which is not included as part of the updating position fee + orderFees += settlementRewardCost(strategy); + + // compute order fees and verify we can pay for them + // only account for negative pnl + currentAvailableMargin += MathUtil.min( + order.request.sizeDelta.mulDecimal( + // solhint-disable numcast/safe-cast + orderPrice.toInt() - uint256(newPosition.latestInteractionPrice).toInt() + ), + 0 + ); - if (runtime.currentAvailableMargin < runtime.orderFees.toInt()) { - revert InsufficientMargin(runtime.currentAvailableMargin, runtime.orderFees); - } + if (currentAvailableMargin < orderFees.toInt()) { + revert InsufficientMargin(currentAvailableMargin, orderFees); + } - PerpsMarket.validatePositionSize( - perpsMarketData, - marketConfig.maxMarketSize, - marketConfig.maxMarketValue, - orderPrice, - oldPosition.size, - runtime.newPositionSize - ); + // now that we have verified fees are sufficient, we can go ahead and remove from the available margin to simplify later calculation + currentAvailableMargin -= orderFees.toInt(); - runtime.totalRequiredMargin = - getRequiredMarginWithNewPosition( - account, - marketConfig, - runtime.marketId, - oldPosition.size, - runtime.newPositionSize, - runtime.fillPrice, - runtime.requiredInitialMargin - ) + - runtime.orderFees; - - if (runtime.currentAvailableMargin < runtime.totalRequiredMargin.toInt()) { - revert InsufficientMargin(runtime.currentAvailableMargin, runtime.totalRequiredMargin); - } + // check that the new account margin would be satisfied + (uint256 totalRequiredMargin, , uint256 possibleLiquidationReward) = PerpsAccount + .getAccountRequiredMargins(ctx, totalCollateralValueWithoutDiscount); - /// @dev if new position size is not 0, further credit validation required - if (runtime.newPositionSize != 0) { - /// @custom:magnitude determines if more market credit is required - /// when a position's magnitude is increased, more credit is required and risk increases - /// when a position's magnitude is decreased, less credit is required and risk decreases - uint256 newMagnitude = MathUtil.abs(runtime.newPositionSize); - uint256 oldMagnitude = MathUtil.abs(oldPosition.size); - - /// @custom:side reflects if position is long or short; if side changes, further validation required - /// given new position size cannot be zero, it is inconsequential if old size is zero; - /// magnitude will necessarily be larger - bool sameSide = runtime.newPositionSize > 0 == oldPosition.size > 0; - - // require validation if magnitude has increased or side has not remained the same - if (newMagnitude > oldMagnitude || !sameSide) { - int256 lockedCreditDelta = perpsMarketData.requiredCreditForSize( - newMagnitude.toInt() - oldMagnitude.toInt(), - PerpsPrice.Tolerance.DEFAULT + if ( + currentAvailableMargin < (totalRequiredMargin + possibleLiquidationReward).toInt() + ) { + revert InsufficientMargin( + currentAvailableMargin, + totalRequiredMargin + possibleLiquidationReward ); - GlobalPerpsMarket.load().validateMarketCapacity(lockedCreditDelta); } } - runtime.newPosition = Position.Data({ - marketId: runtime.marketId, - latestInteractionPrice: runtime.fillPrice.to128(), - latestInteractionFunding: perpsMarketData.lastFundingValue.to128(), - latestInterestAccrued: 0, - size: runtime.newPositionSize - }); - return (runtime.newPosition, runtime.orderFees, runtime.fillPrice, oldPosition); + // if the position is growing in magnitude, ensure market is not too big + // also verify that the credit capacity of the supermarket has not been exceeded + if (!MathUtil.isSameSideReducing(oldPosition.size, newPosition.size)) { + PerpsMarket.Data storage perpsMarketData = PerpsMarket.load(order.request.marketId); + perpsMarketData.validateGivenMarketSize( + ( + newPosition.size > 0 + ? perpsMarketData.getLongSize().toInt() + + newPosition.size - + MathUtil.max(0, oldPosition.size) + : perpsMarketData.getShortSize().toInt() - + newPosition.size + + MathUtil.min(0, oldPosition.size) + ).toUint(), + orderPrice + ); + + int256 lockedCreditDelta = perpsMarketData.requiredCreditForSize( + MathUtil.abs(order.request.sizeDelta).toInt(), + PerpsPrice.Tolerance.DEFAULT + ); + GlobalPerpsMarket.load().validateMarketCapacity(lockedCreditDelta); + } } /** @@ -401,16 +407,7 @@ library AsyncOrder { PerpsMarket.Data storage perpsMarketData = PerpsMarket.load(order.request.marketId); - PerpsMarketConfiguration.Data storage marketConfig = PerpsMarketConfiguration.load( - order.request.marketId - ); - - fillPrice = calculateFillPrice( - perpsMarketData.skew, - marketConfig.skewScale, - order.request.sizeDelta, - orderPrice - ); + fillPrice = perpsMarketData.calculateFillPrice(order.request.sizeDelta, orderPrice); Position.Data storage oldPosition = PerpsMarket.accountPosition( order.request.marketId, @@ -444,190 +441,6 @@ library AsyncOrder { return KeeperCosts.load().getSettlementKeeperCosts() + strategy.settlementReward; } - /** - * @notice Calculates the order fees. - */ - function calculateOrderFee( - int128 sizeDelta, - uint256 fillPrice, - int256 marketSkew, - OrderFee.Data storage orderFeeData - ) internal view returns (uint256) { - int256 notionalDiff = sizeDelta.mulDecimal(fillPrice.toInt()); - - // does this trade keep the skew on one side? - if (MathUtil.sameSide(marketSkew + sizeDelta, marketSkew)) { - // use a flat maker/taker fee for the entire size depending on whether the skew is increased or reduced. - // - // if the order is submitted on the same side as the skew (increasing it) - the taker fee is charged. - // otherwise if the order is opposite to the skew, the maker fee is charged. - - uint256 staticRate = MathUtil.sameSide(notionalDiff, marketSkew) - ? orderFeeData.takerFee - : orderFeeData.makerFee; - return MathUtil.abs(notionalDiff.mulDecimal(staticRate.toInt())); - } - - // this trade flips the skew. - // - // the proportion of size that moves in the direction after the flip should not be considered - // as a maker (reducing skew) as it's now taking (increasing skew) in the opposite direction. hence, - // a different fee is applied on the proportion increasing the skew. - - // The proportions are computed as follows: - // makerSize = abs(marketSkew) => since we are reversing the skew, the maker size is the current skew - // takerSize = abs(marketSkew + sizeDelta) => since we are reversing the skew, the taker size is the new skew - // - // we then multiply the sizes by the fill price to get the notional value of each side, and that times the fee rate for each side - - uint256 makerFee = MathUtil.abs(marketSkew).mulDecimal(fillPrice).mulDecimal( - orderFeeData.makerFee - ); - - uint256 takerFee = MathUtil.abs(marketSkew + sizeDelta).mulDecimal(fillPrice).mulDecimal( - orderFeeData.takerFee - ); - - return takerFee + makerFee; - } - - /** - * @notice Calculates the fill price for an order. - */ - function calculateFillPrice( - int256 skew, - uint256 skewScale, - int128 size, - uint256 price - ) internal pure returns (uint256) { - // How is the p/d-adjusted price calculated using an example: - // - // price = $1200 USD (oracle) - // size = 100 - // skew = 0 - // skew_scale = 1,000,000 (1M) - // - // Then, - // - // pd_before = 0 / 1,000,000 - // = 0 - // pd_after = (0 + 100) / 1,000,000 - // = 100 / 1,000,000 - // = 0.0001 - // - // price_before = 1200 * (1 + pd_before) - // = 1200 * (1 + 0) - // = 1200 - // price_after = 1200 * (1 + pd_after) - // = 1200 * (1 + 0.0001) - // = 1200 * (1.0001) - // = 1200.12 - // Finally, - // - // fill_price = (price_before + price_after) / 2 - // = (1200 + 1200.12) / 2 - // = 1200.06 - if (skewScale == 0) { - return price; - } - // calculate pd (premium/discount) before and after trade - int256 pdBefore = skew.divDecimal(skewScale.toInt()); - int256 newSkew = skew + size; - int256 pdAfter = newSkew.divDecimal(skewScale.toInt()); - - // calculate price before and after trade with pd applied - int256 priceBefore = price.toInt() + (price.toInt().mulDecimal(pdBefore)); - int256 priceAfter = price.toInt() + (price.toInt().mulDecimal(pdAfter)); - - // the fill price is the average of those prices - return (priceBefore + priceAfter).toUint().divDecimal(DecimalMath.UNIT * 2); - } - - struct RequiredMarginWithNewPositionRuntime { - uint256 newRequiredMargin; - uint256 oldRequiredMargin; - uint256 requiredMarginForNewPosition; - uint256 accumulatedLiquidationRewards; - uint256 maxNumberOfWindows; - uint256 numberOfWindows; - uint256 requiredRewardMargin; - } - - /** - * @notice PnL incurred from closing old position/opening new position based on fill price - */ - function calculateFillPricePnl( - uint256 fillPrice, - uint256 marketPrice, - int128 sizeDelta - ) internal pure returns (int256) { - return sizeDelta.mulDecimal(marketPrice.toInt() - fillPrice.toInt()); - } - - /** - * @notice After the required margins are calculated with the old position, this function replaces the - * old position initial margin with the new position initial margin requirements and returns them. - * @dev SIP-359: If the position is being reduced, required margin is 0. - */ - function getRequiredMarginWithNewPosition( - PerpsAccount.Data storage account, - PerpsMarketConfiguration.Data storage marketConfig, - uint128 marketId, - int128 oldPositionSize, - int128 newPositionSize, - uint256 fillPrice, - uint256 currentTotalInitialMargin - ) internal view returns (uint256) { - RequiredMarginWithNewPositionRuntime memory runtime; - - if (MathUtil.isSameSideReducing(oldPositionSize, newPositionSize)) { - return 0; - } - - // get initial margin requirement for the new position - (, , runtime.newRequiredMargin, ) = marketConfig.calculateRequiredMargins( - newPositionSize, - PerpsPrice.getCurrentPrice(marketId, PerpsPrice.Tolerance.DEFAULT) - ); - - // get initial margin of old position - (, , runtime.oldRequiredMargin, ) = marketConfig.calculateRequiredMargins( - oldPositionSize, - PerpsPrice.getCurrentPrice(marketId, PerpsPrice.Tolerance.DEFAULT) - ); - - // remove the old initial margin and add the new initial margin requirement - // this gets us our total required margin for new position - runtime.requiredMarginForNewPosition = - currentTotalInitialMargin + - runtime.newRequiredMargin - - runtime.oldRequiredMargin; - - (runtime.accumulatedLiquidationRewards, runtime.maxNumberOfWindows) = account - .getKeeperRewardsAndCosts( - marketId, - PerpsPrice.Tolerance.DEFAULT, - marketConfig.calculateFlagReward( - MathUtil.abs(newPositionSize).mulDecimal(fillPrice) - ) - ); - runtime.numberOfWindows = marketConfig.numberOfLiquidationWindows( - MathUtil.abs(newPositionSize) - ); - runtime.maxNumberOfWindows = MathUtil.max( - runtime.numberOfWindows, - runtime.maxNumberOfWindows - ); - - runtime.requiredRewardMargin = account.getPossibleLiquidationReward( - runtime.accumulatedLiquidationRewards, - runtime.maxNumberOfWindows - ); - - // this is the required margin for the new position (minus any order fees) - return runtime.requiredMarginForNewPosition + runtime.requiredRewardMargin; - } - function validateAcceptablePrice(Data storage order, uint256 fillPrice) internal view { if (acceptablePriceExceeded(order, fillPrice)) { revert AcceptablePriceExceeded(fillPrice, order.request.acceptablePrice); diff --git a/markets/perps-market/contracts/storage/GlobalPerpsMarket.sol b/markets/perps-market/contracts/storage/GlobalPerpsMarket.sol index ab52851b87..82e233d83f 100644 --- a/markets/perps-market/contracts/storage/GlobalPerpsMarket.sol +++ b/markets/perps-market/contracts/storage/GlobalPerpsMarket.sol @@ -178,8 +178,7 @@ library GlobalPerpsMarket { .valueInUsd( self.collateralAmounts[collateralId], spotMarket, - PerpsPrice.Tolerance.DEFAULT, - false + PerpsPrice.Tolerance.DEFAULT ); total += collateralValue; } diff --git a/markets/perps-market/contracts/storage/KeeperCosts.sol b/markets/perps-market/contracts/storage/KeeperCosts.sol index feb0621bd2..496c1f98dc 100644 --- a/markets/perps-market/contracts/storage/KeeperCosts.sol +++ b/markets/perps-market/contracts/storage/KeeperCosts.sol @@ -46,17 +46,10 @@ library KeeperCosts { function getFlagKeeperCosts( Data storage self, - uint128 accountId + uint256 numberOfUpdatedFeeds ) internal view returns (uint256 sUSDCost) { PerpsMarketFactory.Data storage factory = PerpsMarketFactory.load(); - PerpsAccount.Data storage account = PerpsAccount.load(accountId); - uint256 numberOfCollateralFeeds = account.activeCollateralTypes.contains(SNX_USD_MARKET_ID) - ? account.activeCollateralTypes.length() - 1 - : account.activeCollateralTypes.length(); - uint256 numberOfUpdatedFeeds = numberOfCollateralFeeds + - account.openPositionMarketIds.length(); - sUSDCost = _processWithRuntime( self.keeperCostNodeId, factory, diff --git a/markets/perps-market/contracts/storage/PerpsAccount.sol b/markets/perps-market/contracts/storage/PerpsAccount.sol index 9948ef6a53..14799f32b6 100644 --- a/markets/perps-market/contracts/storage/PerpsAccount.sol +++ b/markets/perps-market/contracts/storage/PerpsAccount.sol @@ -56,6 +56,13 @@ library PerpsAccount { uint256 debt; } + struct MemoryContext { + uint128 accountId; + PerpsPrice.Tolerance stalenessTolerance; + Position.Data[] positions; + uint256[] prices; + } + error InsufficientCollateralAvailableForWithdraw( int256 withdrawableMarginUsd, uint256 requestedMarginUsd @@ -163,32 +170,39 @@ library PerpsAccount { } function isEligibleForMarginLiquidation( - Data storage self, - PerpsPrice.Tolerance stalenessTolerance + MemoryContext memory ctx, + uint256 totalCollateralValueWithDiscount, + uint256 totalCollateralValueWithoutDiscount ) internal view returns (bool isEligible, int256 availableMargin) { // calculate keeper costs KeeperCosts.Data storage keeperCosts = KeeperCosts.load(); - uint256 totalLiquidationCost = keeperCosts.getFlagKeeperCosts(self.id) + + uint256 totalLiquidationCost = keeperCosts.getFlagKeeperCosts(ctx.accountId) + keeperCosts.getLiquidateKeeperCosts(); - uint256 totalCollateralValue = getTotalCollateralValue(self, stalenessTolerance, false); GlobalPerpsMarketConfiguration.Data storage globalConfig = GlobalPerpsMarketConfiguration .load(); uint256 liquidationRewardForKeeper = globalConfig.calculateCollateralLiquidateReward( - totalCollateralValue + totalCollateralValueWithoutDiscount ); int256 totalLiquidationReward = globalConfig - .keeperReward(liquidationRewardForKeeper, totalLiquidationCost, totalCollateralValue) + .keeperReward( + liquidationRewardForKeeper, + totalLiquidationCost, + totalCollateralValueWithoutDiscount + ) .toInt(); - availableMargin = getAvailableMargin(self, stalenessTolerance) - totalLiquidationReward; - isEligible = availableMargin < 0 && self.debt > 0; + availableMargin = + getAvailableMargin(ctx, totalCollateralValueWithDiscount) - + totalLiquidationReward; + isEligible = availableMargin < 0 && PerpsAccount.load(ctx.accountId).debt > 0; } function isEligibleForLiquidation( - Data storage self, - PerpsPrice.Tolerance stalenessTolerance + MemoryContext memory ctx, + uint256 totalCollateralValueWithDiscount, + uint256 totalCollateralValueWithoutDiscount ) internal view @@ -200,13 +214,13 @@ library PerpsAccount { uint256 liquidationReward ) { - availableMargin = getAvailableMargin(self, stalenessTolerance); + availableMargin = getAvailableMargin(ctx, totalCollateralValueWithDiscount); ( requiredInitialMargin, requiredMaintenanceMargin, liquidationReward - ) = getAccountRequiredMargins(self, stalenessTolerance); + ) = getAccountRequiredMargins(ctx, totalCollateralValueWithoutDiscount); isEligible = (requiredMaintenanceMargin + liquidationReward).toInt() > availableMargin; } @@ -218,7 +232,9 @@ library PerpsAccount { .liquidatableAccounts; if (!liquidatableAccounts.contains(self.id)) { - flagKeeperCost = KeeperCosts.load().getFlagKeeperCosts(self.id); + flagKeeperCost = KeeperCosts.load().getFlagKeeperCosts( + getNumberOfUpdatedFeedsRequired(self) + ); liquidatableAccounts.add(self.id); seizedMarginValue = seizeCollateral(self); @@ -300,7 +316,20 @@ library PerpsAccount { revert InsufficientSynthCollateral(collateralId, collateralAmount, amountToWithdraw); } - int256 withdrawableMarginUsd = getWithdrawableMargin(self, PerpsPrice.Tolerance.STRICT); + MemoryContext memory ctx = getOpenPositionsAndCurrentPrices( + self, + PerpsPrice.Tolerance.STRICT + ); + ( + uint256 totalCollateralValueWithDiscount, + uint256 totalCollateralValueWithoutDiscount + ) = getTotalCollateralValue(self, PerpsPrice.Tolerance.DEFAULT); + + int256 withdrawableMarginUsd = getWithdrawableMargin( + ctx, + totalCollateralValueWithoutDiscount, + totalCollateralValueWithDiscount + ); // Note: this can only happen if account is liquidatable if (withdrawableMarginUsd < 0) { revert AccountLiquidatable(self.id); @@ -313,8 +342,7 @@ library PerpsAccount { (amountToWithdrawUsd, ) = PerpsCollateralConfiguration.load(collateralId).valueInUsd( amountToWithdraw, spotMarket, - PerpsPrice.Tolerance.STRICT, - false + PerpsPrice.Tolerance.STRICT ); } @@ -333,67 +361,115 @@ library PerpsAccount { * @dev If the account has active positions, the withdrawable margin is the available margin - required margin - potential liquidation reward */ function getWithdrawableMargin( - Data storage self, - PerpsPrice.Tolerance stalenessTolerance + MemoryContext memory ctx, + uint256 totalNonDiscountedCollateralValue, + uint256 totalDiscountedCollateralValue ) internal view returns (int256 withdrawableMargin) { - bool hasActivePositions = hasOpenPositions(self); + PerpsAccount.Data storage account = load(ctx.accountId); + bool hasActivePositions = hasOpenPositions(account); // not allowed to withdraw until debt is paid off fully. - if (self.debt > 0) return 0; + if (account.debt > 0) return 0; if (hasActivePositions) { ( uint256 requiredInitialMargin, , uint256 liquidationReward - ) = getAccountRequiredMargins(self, stalenessTolerance); + ) = getAccountRequiredMargins(ctx, totalNonDiscountedCollateralValue); uint256 requiredMargin = requiredInitialMargin + liquidationReward; withdrawableMargin = - getAvailableMargin(self, stalenessTolerance) - + getAvailableMargin(ctx, totalDiscountedCollateralValue) - requiredMargin.toInt(); } else { - withdrawableMargin = getTotalCollateralValue(self, stalenessTolerance, false).toInt(); + withdrawableMargin = totalNonDiscountedCollateralValue.toInt(); } } function getTotalCollateralValue( Data storage self, - PerpsPrice.Tolerance stalenessTolerance, - bool useDiscountedValue - ) internal view returns (uint256) { - uint256 totalCollateralValue; + PerpsPrice.Tolerance stalenessTolerance + ) internal view returns (uint256 discounted, uint256 nonDiscounted) { ISpotMarketSystem spotMarket = PerpsMarketFactory.load().spotMarket; for (uint256 i = 1; i <= self.activeCollateralTypes.length(); i++) { uint128 collateralId = self.activeCollateralTypes.valueAt(i).to128(); uint256 amount = self.collateralAmounts[collateralId]; - uint256 amountToAdd; if (collateralId == SNX_USD_MARKET_ID) { - amountToAdd = amount; + discounted += amount; + nonDiscounted += amount; } else { - (amountToAdd, ) = PerpsCollateralConfiguration.load(collateralId).valueInUsd( - amount, - spotMarket, - stalenessTolerance, - useDiscountedValue - ); + (uint256 value, uint256 discount) = PerpsCollateralConfiguration + .load(collateralId) + .valueInUsd(amount, spotMarket, stalenessTolerance); + nonDiscounted += value; + discounted += value.mulDecimal(DecimalMath.UNIT - discount); } - totalCollateralValue += amountToAdd; } - return totalCollateralValue; } - function getAccountPnl( + /** + * @notice Retrieves current open positions and their corresponding market prices (given staleness tolerance) for the given account. + * These values are required inputs to many functions below. + */ + function getOpenPositionsAndCurrentPrices( Data storage self, PerpsPrice.Tolerance stalenessTolerance - ) internal view returns (int256 totalPnl) { + ) internal view returns (MemoryContext memory ctx) { uint256[] memory marketIds = self.openPositionMarketIds.values(); - uint256[] memory prices = PerpsPrice.getCurrentPrices(marketIds, stalenessTolerance); - for (uint256 i = 0; i < marketIds.length; i++) { - Position.Data storage position = PerpsMarket.load(marketIds[i].to128()).positions[ - self.id - ]; - (int256 pnl, , , , , ) = position.getPnl(prices[i]); + ctx = MemoryContext( + self.id, + stalenessTolerance, + new Position.Data[](marketIds.length), + PerpsPrice.getCurrentPrices(marketIds, stalenessTolerance) + ); + for (uint256 i = 0; i < ctx.positions.length; i++) { + ctx.positions[i] = PerpsMarket.load(marketIds[i].to128()).positions[self.id]; + } + } + + function findPositionByMarketId( + MemoryContext memory ctx, + uint128 marketId + ) internal pure returns (uint256 i) { + for (; i < ctx.positions.length; i++) { + if (ctx.positions[i].marketId == marketId) { + break; + } + } + } + + function upsertPosition( + MemoryContext memory ctx, + Position.Data memory newPosition + ) internal view returns (MemoryContext memory newCtx) { + uint256 oldPositionPos = PerpsAccount.findPositionByMarketId(ctx, newPosition.marketId); + if (oldPositionPos < ctx.positions.length) { + ctx.positions[oldPositionPos] = newPosition; + newCtx = ctx; + } else { + // we have to expand the size of the array + newCtx = MemoryContext( + ctx.accountId, + ctx.stalenessTolerance, + new Position.Data[](ctx.positions.length + 1), + new uint256[](ctx.positions.length + 1) + ); + for (uint256 i = 0; i < ctx.positions.length; i++) { + newCtx.positions[i] = ctx.positions[i]; + newCtx.prices[i] = ctx.prices[i]; + } + newCtx.positions[ctx.positions.length] = newPosition; + newCtx.prices[ctx.positions.length] = PerpsPrice.getCurrentPrice( + newPosition.marketId, + ctx.stalenessTolerance + ); + } + } + + function getAccountPnl(MemoryContext memory ctx) internal view returns (int256 totalPnl) { + for (uint256 i = 0; i < ctx.positions.length; i++) { + (int256 pnl, , , , , ) = ctx.positions[i].getPnl(ctx.prices[i]); totalPnl += pnl; } } @@ -404,29 +480,22 @@ library PerpsAccount { * @dev The total collateral value is always based on the discounted value of the collateral */ function getAvailableMargin( - Data storage self, - PerpsPrice.Tolerance stalenessTolerance + MemoryContext memory ctx, + uint256 totalCollateralValueWithDiscount ) internal view returns (int256) { - int256 totalCollateralValue = getTotalCollateralValue(self, stalenessTolerance, true) - .toInt(); - int256 accountPnl = getAccountPnl(self, stalenessTolerance); + int256 accountPnl = getAccountPnl(ctx); - return totalCollateralValue + accountPnl - self.debt.toInt(); + return + totalCollateralValueWithDiscount.toInt() + + accountPnl - + load(ctx.accountId).debt.toInt(); } function getTotalNotionalOpenInterest( - Data storage self - ) internal view returns (uint256 totalAccountOpenInterest) { - uint256[] memory marketIds = self.openPositionMarketIds.values(); - uint256[] memory prices = PerpsPrice.getCurrentPrices( - marketIds, - PerpsPrice.Tolerance.DEFAULT - ); - for (uint256 i = 0; i < marketIds.length; i++) { - Position.Data storage position = PerpsMarket.load(marketIds[i].to128()).positions[ - self.id - ]; - uint256 openInterest = position.getNotionalValue(prices[i]); + MemoryContext memory ctx + ) internal pure returns (uint256 totalAccountOpenInterest) { + for (uint256 i = 0; i < ctx.positions.length; i++) { + uint256 openInterest = ctx.positions[i].getNotionalValue(ctx.prices[i]); totalAccountOpenInterest += openInterest; } } @@ -437,8 +506,8 @@ library PerpsAccount { * @dev The maintenance margin is used to determine when to liquidate a position */ function getAccountRequiredMargins( - Data storage self, - PerpsPrice.Tolerance stalenessTolerance + MemoryContext memory ctx, + uint256 totalNonDiscountedCollateralValue ) internal view @@ -448,23 +517,18 @@ library PerpsAccount { uint256 possibleLiquidationReward ) { - uint256 openPositionMarketIdsLength = self.openPositionMarketIds.length(); - if (openPositionMarketIdsLength == 0) { + if (ctx.positions.length == 0) { return (0, 0, 0); } // use separate accounting for liquidation rewards so we can compare against global min/max liquidation reward values - uint256[] memory marketIds = self.openPositionMarketIds.values(); - uint256[] memory prices = PerpsPrice.getCurrentPrices(marketIds, stalenessTolerance); - for (uint256 i = 0; i < marketIds.length; i++) { - Position.Data storage position = PerpsMarket.load(marketIds[i].to128()).positions[ - self.id - ]; + for (uint256 i = 0; i < ctx.positions.length; i++) { + Position.Data memory position = ctx.positions[i]; PerpsMarketConfiguration.Data storage marketConfig = PerpsMarketConfiguration.load( - marketIds[i].to128() + position.marketId ); (, , uint256 positionInitialMargin, uint256 positionMaintenanceMargin) = marketConfig - .calculateRequiredMargins(position.size, prices[i]); + .calculateRequiredMargins(position.size, ctx.prices[i]); maintenanceMargin += positionMaintenanceMargin; initialMargin += positionInitialMargin; @@ -473,42 +537,42 @@ library PerpsAccount { ( uint256 accumulatedLiquidationRewards, uint256 maxNumberOfWindows - ) = getKeeperRewardsAndCosts(self, 0, stalenessTolerance, 0); + ) = getKeeperRewardsAndCosts(ctx, totalNonDiscountedCollateralValue); possibleLiquidationReward = getPossibleLiquidationReward( - self, accumulatedLiquidationRewards, - maxNumberOfWindows + maxNumberOfWindows, + totalNonDiscountedCollateralValue, + getNumberOfUpdatedFeedsRequired(load(ctx.accountId)) ); return (initialMargin, maintenanceMargin, possibleLiquidationReward); } + function getNumberOfUpdatedFeedsRequired( + Data storage self + ) internal view returns (uint256 numberOfUpdatedFeeds) { + uint256 numberOfCollateralFeeds = self.activeCollateralTypes.contains(SNX_USD_MARKET_ID) + ? self.activeCollateralTypes.length() - 1 + : self.activeCollateralTypes.length(); + numberOfUpdatedFeeds = numberOfCollateralFeeds + self.openPositionMarketIds.length(); + } + function getKeeperRewardsAndCosts( - Data storage self, - uint128 skipMarketId, - PerpsPrice.Tolerance stalenessTolerance, - uint256 newPositionFlagReward + MemoryContext memory ctx, + uint256 totalNonDiscountedCollateralValue ) internal view returns (uint256 accumulatedLiquidationRewards, uint256 maxNumberOfWindows) { - uint256 totalFlagReward = newPositionFlagReward; + uint256 totalFlagReward = 0; // use separate accounting for liquidation rewards so we can compare against global min/max liquidation reward values - uint256[] memory marketIds = self.openPositionMarketIds.values(); - uint256[] memory prices = PerpsPrice.getCurrentPrices( - marketIds, - PerpsPrice.Tolerance.DEFAULT - ); - for (uint256 i = 0; i < marketIds.length; i++) { - if (marketIds[i].to128() == skipMarketId) continue; - Position.Data storage position = PerpsMarket.load(marketIds[i].to128()).positions[ - self.id - ]; + for (uint256 i = 0; i < ctx.positions.length; i++) { + Position.Data memory position = ctx.positions[i]; PerpsMarketConfiguration.Data storage marketConfig = PerpsMarketConfiguration.load( - marketIds[i].to128() + position.marketId ); uint256 numberOfWindows = marketConfig.numberOfLiquidationWindows( MathUtil.abs(position.size) ); - uint256 notionalValue = MathUtil.abs(position.size).mulDecimal(prices[i]); + uint256 notionalValue = MathUtil.abs(position.size).mulDecimal(ctx.prices[i]); uint256 flagReward = marketConfig.calculateFlagReward(notionalValue); totalFlagReward += flagReward; @@ -516,28 +580,28 @@ library PerpsAccount { } GlobalPerpsMarketConfiguration.Data storage globalConfig = GlobalPerpsMarketConfiguration .load(); - uint256 totalCollateralValue = getTotalCollateralValue(self, stalenessTolerance, false); uint256 collateralReward = globalConfig.calculateCollateralLiquidateReward( - totalCollateralValue + totalNonDiscountedCollateralValue ); // Take the maximum between flag reward and collateral reward accumulatedLiquidationRewards += MathUtil.max(totalFlagReward, collateralReward); } function getPossibleLiquidationReward( - Data storage self, uint256 accumulatedLiquidationRewards, - uint256 numOfWindows + uint256 numOfWindows, + uint256 totalNonDiscountedCollateralValue, + uint256 numberOfUpdatedFeeds ) internal view returns (uint256 possibleLiquidationReward) { GlobalPerpsMarketConfiguration.Data storage globalConfig = GlobalPerpsMarketConfiguration .load(); KeeperCosts.Data storage keeperCosts = KeeperCosts.load(); - uint256 costOfFlagging = keeperCosts.getFlagKeeperCosts(self.id); + uint256 costOfFlagging = keeperCosts.getFlagKeeperCosts(numberOfUpdatedFeeds); uint256 costOfLiquidation = keeperCosts.getLiquidateKeeperCosts(); uint256 liquidateAndFlagCost = globalConfig.keeperReward( accumulatedLiquidationRewards, costOfFlagging + costOfLiquidation, - getTotalCollateralValue(self, PerpsPrice.Tolerance.DEFAULT, false) + totalNonDiscountedCollateralValue ); uint256 liquidateWindowsCosts = numOfWindows == 0 ? 0 @@ -571,29 +635,25 @@ library PerpsAccount { function liquidatePosition( Data storage self, - uint128 marketId, + Position.Data memory position, uint256 price ) internal returns ( uint128 amountToLiquidate, int128 newPositionSize, - int128 sizeDelta, - uint128 oldPositionAbsSize, MarketUpdate.Data memory marketUpdateData ) { - PerpsMarket.Data storage perpsMarket = PerpsMarket.load(marketId); - Position.Data storage position = perpsMarket.positions[self.id]; - + PerpsMarket.Data storage perpsMarket = PerpsMarket.load(position.marketId); perpsMarket.recomputeFunding(price); int128 oldPositionSize = position.size; - oldPositionAbsSize = MathUtil.abs128(oldPositionSize); + uint128 oldPositionAbsSize = MathUtil.abs128(oldPositionSize); amountToLiquidate = perpsMarket.maxLiquidatableAmount(oldPositionAbsSize); if (amountToLiquidate == 0) { - return (0, oldPositionSize, 0, oldPositionAbsSize, marketUpdateData); + return (0, oldPositionSize, marketUpdateData); } int128 amtToLiquidationInt = amountToLiquidate.toInt(); @@ -606,7 +666,7 @@ library PerpsAccount { Position.Data memory newPosition; if (newPositionSize != 0) { newPosition = Position.Data({ - marketId: marketId, + marketId: position.marketId, latestInteractionPrice: price.to128(), latestInteractionFunding: perpsMarket.lastFundingValue.to128(), latestInterestAccrued: 0, @@ -615,19 +675,12 @@ library PerpsAccount { } // update position markets - updateOpenPositions(self, marketId, newPositionSize); + updateOpenPositions(self, position.marketId, newPositionSize); // update market data marketUpdateData = perpsMarket.updatePositionData(self.id, newPosition); - sizeDelta = newPositionSize - oldPositionSize; - - return ( - amountToLiquidate, - newPositionSize, - sizeDelta, - oldPositionAbsSize, - marketUpdateData - ); + + return (amountToLiquidate, newPositionSize, marketUpdateData); } function hasOpenPositions(Data storage self) internal view returns (bool) { diff --git a/markets/perps-market/contracts/storage/PerpsCollateralConfiguration.sol b/markets/perps-market/contracts/storage/PerpsCollateralConfiguration.sol index 7870a1a549..46069e7ca4 100644 --- a/markets/perps-market/contracts/storage/PerpsCollateralConfiguration.sol +++ b/markets/perps-market/contracts/storage/PerpsCollateralConfiguration.sol @@ -129,12 +129,11 @@ library PerpsCollateralConfiguration { Data storage self, uint256 amount, ISpotMarketSystem spotMarket, - PerpsPrice.Tolerance stalenessTolerance, - bool useDiscount - ) internal view returns (uint256 collateralValueInUsd, uint256 discount) { + PerpsPrice.Tolerance stalenessTolerance + ) internal view returns (uint256 undiscountedCollateralValueInUsd, uint256 discount) { uint256 skewScale = spotMarket.getMarketSkewScale(self.id); - // only discount collateral if skew scale is set on spot market and useDiscount is set to true - if (useDiscount && skewScale != 0) { + // only discount collateral if skew scale is set on spot market + if (skewScale != 0) { uint256 impactOnSkew = amount.divDecimal(skewScale).mulDecimal(self.discountScalar); discount = ( MathUtil.min( @@ -150,9 +149,6 @@ library PerpsCollateralConfiguration { sellTxnType, Price.Tolerance(uint256(stalenessTolerance)) // solhint-disable-line numcast/safe-cast ); - uint256 valueWithoutDiscount = amount.mulDecimal(collateralPrice); - - // if discount is 0, this just gets multiplied by 1 - collateralValueInUsd = valueWithoutDiscount.mulDecimal(DecimalMath.UNIT - discount); + undiscountedCollateralValueInUsd = amount.mulDecimal(collateralPrice); } } diff --git a/markets/perps-market/contracts/storage/PerpsMarket.sol b/markets/perps-market/contracts/storage/PerpsMarket.sol index dcffd3f7a1..180c7523cc 100644 --- a/markets/perps-market/contracts/storage/PerpsMarket.sol +++ b/markets/perps-market/contracts/storage/PerpsMarket.sol @@ -6,6 +6,7 @@ import {DecimalMath} from "@synthetixio/core-contracts/contracts/utils/DecimalMa import {SafeCastU256, SafeCastI256, SafeCastU128} from "@synthetixio/core-contracts/contracts/utils/SafeCast.sol"; import {Position} from "./Position.sol"; import {AsyncOrder} from "./AsyncOrder.sol"; +import {OrderFee} from "./OrderFee.sol"; import {PerpsMarketConfiguration} from "./PerpsMarketConfiguration.sol"; import {MarketUpdate} from "./MarketUpdate.sol"; import {MathUtil} from "../utils/MathUtil.sol"; @@ -361,55 +362,40 @@ library PerpsMarket { return (block.timestamp - self.lastFundingTime).divDecimal(1 days).toInt(); } - function validatePositionSize( - Data storage self, - uint256 maxSize, - uint256 maxValue, - uint256 price, - int128 oldSize, - int128 newSize - ) internal view { - // Allow users to reduce an order no matter the market conditions. - bool isReducingInterest = MathUtil.isSameSideReducing(oldSize, newSize); - if (!isReducingInterest) { - int256 newSkew = self.skew - oldSize + newSize; - - int256 newMarketSize = self.size.toInt() - - MathUtil.abs(oldSize).toInt() + - MathUtil.abs(newSize).toInt(); - - int256 newSideSize; - if (0 < newSize) { - // long case: marketSize + skew - // = (|longSize| + |shortSize|) + (longSize + shortSize) - // = 2 * longSize - newSideSize = newMarketSize + newSkew; - } else { - // short case: marketSize - skew - // = (|longSize| + |shortSize|) - (longSize + shortSize) - // = 2 * -shortSize - newSideSize = newMarketSize - newSkew; - } + function getLongSize(Data storage self) internal view returns (uint256) { + return (self.size.toInt() + self.skew).toUint() / 2; + } - // newSideSize still includes an extra factor of 2 here, so we will divide by 2 in the actual condition - if (maxSize < MathUtil.abs(newSideSize / 2)) { - revert PerpsMarketConfiguration.MaxOpenInterestReached( - self.id, - maxSize, - newSideSize / 2 - ); - } + function getShortSize(Data storage self) internal view returns (uint256) { + return (self.size.toInt() - self.skew).toUint() / 2; + } - // same check but with value (size * price) - // note that if maxValue param is set to 0, this validation is skipped - if (maxValue > 0 && maxValue < MathUtil.abs(newSideSize / 2).mulDecimal(price)) { - revert PerpsMarketConfiguration.MaxUSDOpenInterestReached( - self.id, - maxValue, - newSideSize / 2, - price - ); - } + /** + * @notice ensures that the given market size (either in the long or short direction) does not exceed the maximum configured size. + * The size limitation is the same for long or short, so put the total size of the side you want to check. + * @param size the total size of the side you want to check against the limit. + */ + function validateGivenMarketSize(Data storage self, uint256 size, uint256 price) internal view { + PerpsMarketConfiguration.Data storage marketConfig = PerpsMarketConfiguration.load(self.id); + + if (marketConfig.maxMarketSize < size) { + revert PerpsMarketConfiguration.MaxOpenInterestReached( + self.id, + marketConfig.maxMarketSize, + size.toInt() + ); + } + + // same check but with value (size * price) + // note that if maxValue param is set to 0, this validation is skipped + uint256 maxMarketValue = marketConfig.maxMarketValue; + if (maxMarketValue > 0 && maxMarketValue < size.mulDecimal(price)) { + revert PerpsMarketConfiguration.MaxUSDOpenInterestReached( + self.id, + maxMarketValue, + size.toInt(), + price + ); } } @@ -466,4 +452,133 @@ library PerpsMarket { ) internal view returns (Position.Data storage position) { position = load(marketId).positions[accountId]; } + + /** + * @notice Calculates the order fees. + */ + function calculateOrderFee( + Data storage self, + int256 sizeDelta, + uint256 fillPrice + ) internal view returns (uint256) { + int256 marketSkew = self.skew; + OrderFee.Data storage orderFeeData = PerpsMarketConfiguration.load(self.id).orderFees; + int256 notionalDiff = sizeDelta.mulDecimal(fillPrice.toInt()); + + // does this trade keep the skew on one side? + if (MathUtil.sameSide(marketSkew + sizeDelta, marketSkew)) { + // use a flat maker/taker fee for the entire size depending on whether the skew is increased or reduced. + // + // if the order is submitted on the same side as the skew (increasing it) - the taker fee is charged. + // otherwise if the order is opposite to the skew, the maker fee is charged. + + uint256 staticRate = MathUtil.sameSide(notionalDiff, marketSkew) + ? orderFeeData.takerFee + : orderFeeData.makerFee; + return MathUtil.abs(notionalDiff.mulDecimal(staticRate.toInt())); + } + + // this trade flips the skew. + // + // the proportion of size that moves in the direction after the flip should not be considered + // as a maker (reducing skew) as it's now taking (increasing skew) in the opposite direction. hence, + // a different fee is applied on the proportion increasing the skew. + + // The proportions are computed as follows: + // makerSize = abs(marketSkew) => since we are reversing the skew, the maker size is the current skew + // takerSize = abs(marketSkew + sizeDelta) => since we are reversing the skew, the taker size is the new skew + // + // we then multiply the sizes by the fill price to get the notional value of each side, and that times the fee rate for each side + + uint256 makerFee = MathUtil.abs(marketSkew).mulDecimal(fillPrice).mulDecimal( + orderFeeData.makerFee + ); + + uint256 takerFee = MathUtil.abs(marketSkew + sizeDelta).mulDecimal(fillPrice).mulDecimal( + orderFeeData.takerFee + ); + + return takerFee + makerFee; + } + + /** + * @notice Calls `computeFillPrice` with the given size while filling in the current values for this market + */ + function calculateFillPrice( + Data storage self, + int128 size, + uint256 price + ) internal view returns (uint256) { + uint128 marketId = self.id; + return + computeFillPrice( + PerpsMarket.load(marketId).skew, + PerpsMarketConfiguration.load(marketId).skewScale, + price, + size + ); + } + + /** + * @notice Does the calculation to determine the fill price for an order. + */ + function computeFillPrice( + int256 skew, + uint256 skewScale, + uint256 price, + int128 size + ) internal pure returns (uint256) { + // How is the p/d-adjusted price calculated using an example: + // + // price = $1200 USD (oracle) + // size = 100 + // skew = 0 + // skew_scale = 1,000,000 (1M) + // + // Then, + // + // pd_before = 0 / 1,000,000 + // = 0 + // pd_after = (0 + 100) / 1,000,000 + // = 100 / 1,000,000 + // = 0.0001 + // + // price_before = 1200 * (1 + pd_before) + // = 1200 * (1 + 0) + // = 1200 + // price_after = 1200 * (1 + pd_after) + // = 1200 * (1 + 0.0001) + // = 1200 * (1.0001) + // = 1200.12 + // Finally, + // + // fill_price = (price_before + price_after) / 2 + // = (1200 + 1200.12) / 2 + // = 1200.06 + if (skewScale == 0) { + return price; + } + // calculate pd (premium/discount) before and after trade + int256 pdBefore = skew.divDecimal(skewScale.toInt()); + int256 newSkew = skew + size; + int256 pdAfter = newSkew.divDecimal(skewScale.toInt()); + + // calculate price before and after trade with pd applied + int256 priceBefore = price.toInt() + (price.toInt().mulDecimal(pdBefore)); + int256 priceAfter = price.toInt() + (price.toInt().mulDecimal(pdAfter)); + + // the fill price is the average of those prices + return (priceBefore + priceAfter).toUint().divDecimal(DecimalMath.UNIT * 2); + } + + /** + * @notice PnL incurred from closing old position/opening new position based on fill price + */ + function computeFillPricePnl( + uint256 fillPrice, + uint256 marketPrice, + int256 sizeDelta + ) internal pure returns (int256) { + return sizeDelta.mulDecimal(marketPrice.toInt() - fillPrice.toInt()); + } } diff --git a/markets/perps-market/contracts/storage/PerpsMarketFactory.sol b/markets/perps-market/contracts/storage/PerpsMarketFactory.sol index abbbddeb2d..89a42d198a 100644 --- a/markets/perps-market/contracts/storage/PerpsMarketFactory.sol +++ b/markets/perps-market/contracts/storage/PerpsMarketFactory.sol @@ -119,8 +119,7 @@ library PerpsMarketFactory { (synthValue, ) = PerpsCollateralConfiguration.load(collateralId).valueInUsd( amount, self.spotMarket, - PerpsPrice.Tolerance.DEFAULT, - false + PerpsPrice.Tolerance.DEFAULT ); PerpsCollateralConfiguration.loadValidLam(collateralId).distributeCollateral(synth, amount); diff --git a/markets/perps-market/contracts/storage/Position.sol b/markets/perps-market/contracts/storage/Position.sol index ffe5584c5b..7578a77497 100644 --- a/markets/perps-market/contracts/storage/Position.sol +++ b/markets/perps-market/contracts/storage/Position.sol @@ -64,7 +64,7 @@ library Position { } function getPnl( - Data storage self, + Data memory self, uint256 price ) internal @@ -91,7 +91,7 @@ library Position { } function interestAccrued( - Data storage self, + Data memory self, uint256 price ) internal view returns (uint256 chargedInterest) { uint256 nextInterestAccrued = InterestRate.load().calculateNextInterest(); @@ -102,7 +102,7 @@ library Position { } function getLockedNotionalValue( - Data storage self, + Data memory self, uint256 price ) internal view returns (uint256) { return @@ -111,7 +111,7 @@ library Position { ); } - function getNotionalValue(Data storage self, uint256 price) internal view returns (uint256) { + function getNotionalValue(Data memory self, uint256 price) internal pure returns (uint256) { return MathUtil.abs(self.size).mulDecimal(price); } } diff --git a/markets/perps-market/storage.dump.json b/markets/perps-market/storage.dump.json index dd3c60c9d6..b118ca387a 100644 --- a/markets/perps-market/storage.dump.json +++ b/markets/perps-market/storage.dump.json @@ -1,68 +1,4 @@ { - "contracts/modules/LiquidationModule.sol:LiquidationModule": { - "name": "LiquidationModule", - "kind": "contract", - "structs": { - "LiquidateAccountRuntime": [ - { - "type": "uint128", - "name": "accountId", - "size": 16, - "slot": "0", - "offset": 0 - }, - { - "type": "uint256", - "name": "totalFlaggingRewards", - "size": 32, - "slot": "1", - "offset": 0 - }, - { - "type": "uint256", - "name": "totalLiquidated", - "size": 32, - "slot": "2", - "offset": 0 - }, - { - "type": "bool", - "name": "accountFullyLiquidated", - "size": 1, - "slot": "3", - "offset": 0 - }, - { - "type": "uint256", - "name": "totalLiquidationCost", - "size": 32, - "slot": "4", - "offset": 0 - }, - { - "type": "uint256", - "name": "price", - "size": 32, - "slot": "5", - "offset": 0 - }, - { - "type": "uint128", - "name": "positionMarketId", - "size": 16, - "slot": "6", - "offset": 0 - }, - { - "type": "uint256", - "name": "loopIterator", - "size": 32, - "slot": "7", - "offset": 0 - } - ] - } - }, "contracts/storage/AsyncOrder.sol:AsyncOrder": { "name": "AsyncOrder", "kind": "library", @@ -163,200 +99,6 @@ "slot": "4", "offset": 0 } - ], - "SimulateDataRuntime": [ - { - "type": "bool", - "name": "isEligible", - "size": 1, - "slot": "0", - "offset": 0 - }, - { - "type": "int128", - "name": "sizeDelta", - "size": 16, - "slot": "0", - "offset": 1 - }, - { - "type": "uint128", - "name": "accountId", - "size": 16, - "slot": "1", - "offset": 0 - }, - { - "type": "uint128", - "name": "marketId", - "size": 16, - "slot": "1", - "offset": 16 - }, - { - "type": "uint256", - "name": "fillPrice", - "size": 32, - "slot": "2", - "offset": 0 - }, - { - "type": "uint256", - "name": "orderFees", - "size": 32, - "slot": "3", - "offset": 0 - }, - { - "type": "uint256", - "name": "availableMargin", - "size": 32, - "slot": "4", - "offset": 0 - }, - { - "type": "uint256", - "name": "currentLiquidationMargin", - "size": 32, - "slot": "5", - "offset": 0 - }, - { - "type": "uint256", - "name": "accumulatedLiquidationRewards", - "size": 32, - "slot": "6", - "offset": 0 - }, - { - "type": "int128", - "name": "newPositionSize", - "size": 16, - "slot": "7", - "offset": 0 - }, - { - "type": "uint256", - "name": "newNotionalValue", - "size": 32, - "slot": "8", - "offset": 0 - }, - { - "type": "int256", - "name": "currentAvailableMargin", - "size": 32, - "slot": "9", - "offset": 0 - }, - { - "type": "uint256", - "name": "requiredInitialMargin", - "size": 32, - "slot": "10", - "offset": 0 - }, - { - "type": "uint256", - "name": "initialRequiredMargin", - "size": 32, - "slot": "11", - "offset": 0 - }, - { - "type": "uint256", - "name": "totalRequiredMargin", - "size": 32, - "slot": "12", - "offset": 0 - }, - { - "type": "struct", - "name": "newPosition", - "members": [ - { - "type": "uint128", - "name": "marketId" - }, - { - "type": "int128", - "name": "size" - }, - { - "type": "uint128", - "name": "latestInteractionPrice" - }, - { - "type": "int128", - "name": "latestInteractionFunding" - }, - { - "type": "uint256", - "name": "latestInterestAccrued" - } - ], - "size": 96, - "slot": "13", - "offset": 0 - }, - { - "type": "bytes32", - "name": "trackingCode", - "size": 32, - "slot": "16", - "offset": 0 - } - ], - "RequiredMarginWithNewPositionRuntime": [ - { - "type": "uint256", - "name": "newRequiredMargin", - "size": 32, - "slot": "0", - "offset": 0 - }, - { - "type": "uint256", - "name": "oldRequiredMargin", - "size": 32, - "slot": "1", - "offset": 0 - }, - { - "type": "uint256", - "name": "requiredMarginForNewPosition", - "size": 32, - "slot": "2", - "offset": 0 - }, - { - "type": "uint256", - "name": "accumulatedLiquidationRewards", - "size": 32, - "slot": "3", - "offset": 0 - }, - { - "type": "uint256", - "name": "maxNumberOfWindows", - "size": 32, - "slot": "4", - "offset": 0 - }, - { - "type": "uint256", - "name": "numberOfWindows", - "size": 32, - "slot": "5", - "offset": 0 - }, - { - "type": "uint256", - "name": "requiredRewardMargin", - "size": 32, - "slot": "6", - "offset": 0 - } ] } }, @@ -910,6 +652,70 @@ "slot": "8", "offset": 0 } + ], + "MemoryContext": [ + { + "type": "uint128", + "name": "accountId", + "size": 16, + "slot": "0", + "offset": 0 + }, + { + "type": "enum", + "name": "stalenessTolerance", + "members": [ + "DEFAULT", + "STRICT", + "ONE_MONTH" + ], + "size": 1, + "slot": "0", + "offset": 16 + }, + { + "type": "array", + "name": "positions", + "value": { + "type": "struct", + "name": "Position.Data", + "members": [ + { + "type": "uint128", + "name": "marketId" + }, + { + "type": "int128", + "name": "size" + }, + { + "type": "uint128", + "name": "latestInteractionPrice" + }, + { + "type": "int128", + "name": "latestInteractionFunding" + }, + { + "type": "uint256", + "name": "latestInterestAccrued" + } + ] + }, + "size": 32, + "slot": "1", + "offset": 0 + }, + { + "type": "array", + "name": "prices", + "value": { + "type": "uint256" + }, + "size": 32, + "slot": "2", + "offset": 0 + } ] } }, diff --git a/protocol/synthetix/cannonfile.toml b/protocol/synthetix/cannonfile.toml index 3e2086c852..ba77be435f 100644 --- a/protocol/synthetix/cannonfile.toml +++ b/protocol/synthetix/cannonfile.toml @@ -133,7 +133,7 @@ fromCall.func = "owner" func = "upgradeTo" args = ["<%= contracts.CoreRouter.address %>"] factory.CoreProxy.abiOf = ["CoreRouter"] -factory.CoreProxy.artifact = "Proxy" +factory.CoreProxy.artifact = "contracts/Proxy.sol:Proxy" factory.CoreProxy.event = "Upgraded" factory.CoreProxy.arg = 0 factory.CoreProxy.highlight = true