From 31d0b06c4c1440acca25064ef417e81adfcd0b66 Mon Sep 17 00:00:00 2001 From: Daniel Beal Date: Tue, 26 Nov 2024 08:23:20 -0800 Subject: [PATCH 1/7] refactor complete and code compiles --- .../contracts/modules/AsyncOrderModule.sol | 115 +++-- .../AsyncOrderSettlementPythModule.sol | 2 +- .../contracts/modules/LiquidationModule.sol | 19 +- .../contracts/modules/PerpsAccountModule.sol | 33 +- .../contracts/modules/PerpsMarketModule.sol | 4 +- .../contracts/storage/AsyncOrder.sol | 405 ++++-------------- .../contracts/storage/GlobalPerpsMarket.sol | 3 +- .../contracts/storage/KeeperCosts.sol | 9 +- .../contracts/storage/PerpsAccount.sol | 203 +++++---- .../storage/PerpsCollateralConfiguration.sol | 14 +- .../contracts/storage/PerpsMarket.sol | 204 +++++++-- .../contracts/storage/PerpsMarketFactory.sol | 3 +- .../contracts/storage/Position.sol | 8 +- protocol/synthetix/cannonfile.toml | 2 +- 14 files changed, 482 insertions(+), 542 deletions(-) diff --git a/markets/perps-market/contracts/modules/AsyncOrderModule.sol b/markets/perps-market/contracts/modules/AsyncOrderModule.sol index a7b5ad970c..2dece954b3 100644 --- a/markets/perps-market/contracts/modules/AsyncOrderModule.sol +++ b/markets/perps-market/contracts/modules/AsyncOrderModule.sol @@ -104,22 +104,43 @@ 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) - ); + _computeOrderFeesWithPrice(marketId, sizeDelta, PerpsPrice.getCurrentPrice(marketId, PerpsPrice.Tolerance.DEFAULT)); } /** * @inheritdoc IAsyncOrderModule */ - function computeOrderFeesWithPrice( + function computeOrderFeesWithPrice(uint128 marketId, int128 sizeDelta, uint256 price) external view override returns (uint256 orderFees, uint256 fillPrice) { + _computeOrderFeesWithPrice(marketId, sizeDelta, price); + } + + function _computeOrderFeesWithPrice( uint128 marketId, int128 sizeDelta, uint256 price - ) external view override returns (uint256 orderFees, uint256 fillPrice) { - (orderFees, fillPrice) = _computeOrderFees(marketId, sizeDelta, 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) + ) + ); + + PerpsMarket.Data storage perpsMarketData = PerpsMarket.load(order.request.marketId); + 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); + + (Position.Data[] memory positions, uint256[] memory prices) = account.getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); + (,,, fillPrice, orderFees) = order.createUpdatedPosition(PerpsMarketConfiguration.load(marketId).settlementStrategies[0], price, positions); } /** @@ -140,13 +161,7 @@ contract AsyncOrderModule is IAsyncOrderModule { uint128 marketId, int128 sizeDelta ) external view override returns (uint256 requiredMargin) { - return - _requiredMarginForOrder( - accountId, - marketId, - sizeDelta, - PerpsPrice.getCurrentPrice(marketId, PerpsPrice.Tolerance.DEFAULT) - ); + return _requiredMarginForOrderWithPrice(accountId, marketId, sizeDelta, PerpsPrice.getCurrentPrice(marketId, PerpsPrice.Tolerance.DEFAULT)); } function requiredMarginForOrderWithPrice( @@ -155,59 +170,43 @@ 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( - PerpsPrice.Tolerance.DEFAULT - ); - (uint256 orderFees, uint256 fillPrice) = _computeOrderFees(marketId, sizeDelta, orderPrice); + PerpsMarket.Data storage perpsMarketData = PerpsMarket.load(order.request.marketId); + PerpsAccount.Data storage account = PerpsAccount.load(order.request.accountId); - return - AsyncOrder.getRequiredMarginWithNewPosition( - account, - marketConfig, - marketId, - oldPosition.size, - oldPosition.size + sizeDelta, - fillPrice, - currentInitialMargin - ) + orderFees; - } + // probably should be doing this but cant because the interface (view) doesn't allow it + //perpsMarketData.recomputeFunding(orderPrice); - 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 - ); + (Position.Data[] memory positions, uint256[] memory prices) = account.getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); - orderFees = AsyncOrder.calculateOrderFee( - sizeDelta, - fillPrice, - skew, - marketConfig.orderFees - ); + SettlementStrategy.Data storage strategy = PerpsMarketConfiguration + .loadValidSettlementStrategy(marketId, 0); + (positions, , , , ) = order.createUpdatedPosition(PerpsMarketConfiguration.load(marketId).settlementStrategies[0], price, positions); + + (uint256 totalCollateralValueWithDiscount, uint256 totalCollateralValueWithoutDiscount) = account.getTotalCollateralValue(PerpsPrice.Tolerance.DEFAULT); + + (requiredMargin,, ) = account.getAccountRequiredMargins(positions, prices, totalCollateralValueWithoutDiscount); } } 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..5298ae1113 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. @@ -48,13 +49,16 @@ contract LiquidationModule is ILiquidationModule, IMarketEvents { .liquidatableAccounts; PerpsAccount.Data storage account = PerpsAccount.load(accountId); if (!liquidatableAccounts.contains(accountId)) { + (Position.Data[] memory positions, uint256[] memory prices) = account.getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); + (uint256 totalCollateralValueWithDiscount, uint256 totalCollateralValueWithoutDiscount) = account.getTotalCollateralValue(PerpsPrice.Tolerance.DEFAULT); + ( bool isEligible, int256 availableMargin, , uint256 requiredMaintenaceMargin, uint256 expectedLiquidationReward - ) = account.isEligibleForLiquidation(PerpsPrice.Tolerance.STRICT); + ) = account.isEligibleForLiquidation(positions, prices, totalCollateralValueWithDiscount, totalCollateralValueWithoutDiscount); if (isEligible) { (uint256 flagCost, uint256 seizedMarginValue) = account.flagForLiquidation(); @@ -87,7 +91,9 @@ contract LiquidationModule is ILiquidationModule, IMarketEvents { revert AccountHasOpenPositions(accountId); } - (bool isEligible, ) = account.isEligibleForMarginLiquidation(PerpsPrice.Tolerance.STRICT); + (Position.Data[] memory positions, uint256[] memory prices) = account.getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); + (uint256 totalCollateralValueWithDiscount, uint256 totalCollateralValueWithoutDiscount) = account.getTotalCollateralValue(PerpsPrice.Tolerance.DEFAULT); + (bool isEligible, ) = account.isEligibleForMarginLiquidation(positions, prices, totalCollateralValueWithDiscount, totalCollateralValueWithoutDiscount); if (isEligible) { // margin is sent to liquidation rewards distributor in getMarginLiquidationCostAndSeizeMargin uint256 marginLiquidateCost = KeeperCosts.load().getFlagKeeperCosts(account.id); @@ -174,8 +180,11 @@ contract LiquidationModule is ILiquidationModule, IMarketEvents { return true; } + PerpsAccount.Data storage account = PerpsAccount.load(accountId); + (Position.Data[] memory positions, uint256[] memory prices) = account.getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); + (uint256 totalCollateralValueWithDiscount, uint256 totalCollateralValueWithoutDiscount) = account.getTotalCollateralValue(PerpsPrice.Tolerance.DEFAULT); (isEligible, , , , ) = PerpsAccount.load(accountId).isEligibleForLiquidation( - PerpsPrice.Tolerance.DEFAULT + positions, prices, totalCollateralValueWithDiscount, totalCollateralValueWithoutDiscount ); } @@ -186,7 +195,9 @@ contract LiquidationModule is ILiquidationModule, IMarketEvents { if (account.hasOpenPositions()) { return false; } else { - (isEligible, ) = account.isEligibleForMarginLiquidation(PerpsPrice.Tolerance.DEFAULT); + (Position.Data[] memory positions, uint256[] memory prices) = account.getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); + (uint256 totalCollateralValueWithDiscount, uint256 totalCollateralValueWithoutDiscount) = account.getTotalCollateralValue(PerpsPrice.Tolerance.DEFAULT); + (isEligible, ) = account.isEligibleForMarginLiquidation(positions, prices, totalCollateralValueWithDiscount, totalCollateralValueWithoutDiscount); } } diff --git a/markets/perps-market/contracts/modules/PerpsAccountModule.sol b/markets/perps-market/contracts/modules/PerpsAccountModule.sol index 2d906b1a7c..427a4fa54c 100644 --- a/markets/perps-market/contracts/modules/PerpsAccountModule.sol +++ b/markets/perps-market/contracts/modules/PerpsAccountModule.sol @@ -121,11 +121,10 @@ contract PerpsAccountModule is IPerpsAccountModule { /** * @inheritdoc IPerpsAccountModule */ - function totalCollateralValue(uint128 accountId) external view override returns (uint256) { - return + function totalCollateralValue(uint128 accountId) external view override returns (uint256 totalValue) { + (totalValue, ) = PerpsAccount.load(accountId).getTotalCollateralValue( - PerpsPrice.Tolerance.DEFAULT, - false + PerpsPrice.Tolerance.DEFAULT ); } @@ -133,7 +132,9 @@ contract PerpsAccountModule is IPerpsAccountModule { * @inheritdoc IPerpsAccountModule */ function totalAccountOpenInterest(uint128 accountId) external view override returns (uint256) { - return PerpsAccount.load(accountId).getTotalNotionalOpenInterest(); + PerpsAccount.Data storage account = PerpsAccount.load(accountId); + (Position.Data[] memory positions, uint256[] memory prices) = account.getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); + return PerpsAccount.getTotalNotionalOpenInterest(positions, prices); } /** @@ -176,8 +177,13 @@ contract PerpsAccountModule is IPerpsAccountModule { function getAvailableMargin( uint128 accountId ) external view override returns (int256 availableMargin) { + PerpsAccount.Data storage account = PerpsAccount.load(accountId); + (Position.Data[] memory positions, uint256[] memory prices) = account.getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); + (uint256 totalCollateralValueWithDiscount,) = account.getTotalCollateralValue(PerpsPrice.Tolerance.DEFAULT); availableMargin = PerpsAccount.load(accountId).getAvailableMargin( - PerpsPrice.Tolerance.DEFAULT + positions, + prices, + totalCollateralValueWithDiscount ); } @@ -188,7 +194,14 @@ 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); + (Position.Data[] memory positions, uint256[] memory prices) = account.getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); + (uint256 totalCollateralValueWithDiscount, uint256 totalCollateralValueWithoutDiscount) = account.getTotalCollateralValue(PerpsPrice.Tolerance.DEFAULT); + withdrawableMargin = account.getWithdrawableMargin( + positions, + prices, + totalCollateralValueWithDiscount, + totalCollateralValueWithoutDiscount + ); } /** @@ -211,8 +224,12 @@ contract PerpsAccountModule is IPerpsAccountModule { return (0, 0, 0); } + (Position.Data[] memory positions, uint256[] memory prices) = account.getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); + (uint256 totalCollateralValueWithDiscount, uint256 totalCollateralValueWithoutDiscount) = account.getTotalCollateralValue(PerpsPrice.Tolerance.DEFAULT); (requiredInitialMargin, requiredMaintenanceMargin, maxLiquidationReward) = account - .getAccountRequiredMargins(PerpsPrice.Tolerance.DEFAULT); + .getAccountRequiredMargins( + positions, prices, 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..3b09343596 100644 --- a/markets/perps-market/contracts/modules/PerpsMarketModule.sol +++ b/markets/perps-market/contracts/modules/PerpsMarketModule.sol @@ -75,9 +75,7 @@ contract PerpsMarketModule is IPerpsMarketModule { uint256 price ) external view override returns (uint256) { return - AsyncOrder.calculateFillPrice( - PerpsMarket.load(marketId).skew, - PerpsMarketConfiguration.load(marketId).skewScale, + 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..7dece7fe4e 100644 --- a/markets/perps-market/contracts/storage/AsyncOrder.sol +++ b/markets/perps-market/contracts/storage/AsyncOrder.sol @@ -225,30 +225,42 @@ 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, + SettlementStrategy.Data storage strategy, + uint256 orderPrice, + Position.Data[] memory positions + ) internal view returns (Position.Data[] memory newPositions, Position.Data memory oldPosition, Position.Data memory newPosition, uint256 fillPrice, uint256 orderFees) { + PerpsMarket.Data storage perpsMarketData = PerpsMarket.load(order.request.marketId); + PerpsMarketConfiguration.Data storage marketConfig = PerpsMarketConfiguration.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 + newPositions = PerpsAccount.upsertPosition(positions, newPosition); + + orderFees = + perpsMarketData.calculateOrderFee( + newPosition.size - oldPosition.size, + fillPrice + ) + settlementRewardCost(strategy); } /** - * @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 +274,83 @@ 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); + (Position.Data[] memory positions, uint256[] memory prices) = PerpsAccount.load(order.request.accountId).getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); + (uint256 totalCollateralValueWithDiscount, uint256 totalCollateralValueWithoutDiscount) = PerpsAccount.load(order.request.accountId).getTotalCollateralValue(PerpsPrice.Tolerance.DEFAULT); - ( - runtime.isEligible, - runtime.currentAvailableMargin, - runtime.requiredInitialMargin, - , + // 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 + { + PerpsAccount.Data storage account = PerpsAccount.load(order.request.accountId); - ) = account.isEligibleForLiquidation(PerpsPrice.Tolerance.DEFAULT); + int256 currentAvailableMargin; + { + bool isEligibleForLiquidation; + ( + isEligibleForLiquidation, + currentAvailableMargin, + , + , - if (runtime.isEligible) { - revert PerpsAccount.AccountLiquidatable(runtime.accountId); - } + ) = account.isEligibleForLiquidation(positions, prices, totalCollateralValueWithDiscount, totalCollateralValueWithoutDiscount); - PerpsMarket.Data storage perpsMarketData = PerpsMarket.load(runtime.marketId); - perpsMarketData.recomputeFunding(orderPrice); + 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 - ); + (positions, oldPosition, newPosition, fillPrice, orderFees) = createUpdatedPosition(order, strategy, orderPrice, positions); - 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 - ); + // compute order fees and verify we can pay for them + // only account for negative pnl + currentAvailableMargin += MathUtil.min( + order.request.sizeDelta.mulDecimal(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,, ) = account.getAccountRequiredMargins( + positions, + prices, + 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 - ); - GlobalPerpsMarket.load().validateMarketCapacity(lockedCreditDelta); + if (currentAvailableMargin < totalRequiredMargin.toInt()) { + revert InsufficientMargin(currentAvailableMargin, totalRequiredMargin); } } - 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,13 +372,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, + fillPrice = perpsMarketData.calculateFillPrice( order.request.sizeDelta, orderPrice ); @@ -444,190 +409,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..96d8d95be1 100644 --- a/markets/perps-market/contracts/storage/PerpsAccount.sol +++ b/markets/perps-market/contracts/storage/PerpsAccount.sol @@ -164,31 +164,36 @@ library PerpsAccount { function isEligibleForMarginLiquidation( Data storage self, - PerpsPrice.Tolerance stalenessTolerance + Position.Data[] memory positions, + uint256[] memory prices, + 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) + 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; + availableMargin = getAvailableMargin(self, positions, prices, totalCollateralValueWithDiscount) - totalLiquidationReward; isEligible = availableMargin < 0 && self.debt > 0; } function isEligibleForLiquidation( Data storage self, - PerpsPrice.Tolerance stalenessTolerance + Position.Data[] memory positions, + uint256[] memory prices, + uint256 totalCollateralValueWithDiscount, + uint256 totalCollateralValueWithoutDiscount ) internal view @@ -200,13 +205,13 @@ library PerpsAccount { uint256 liquidationReward ) { - availableMargin = getAvailableMargin(self, stalenessTolerance); + availableMargin = getAvailableMargin(self, positions, prices, totalCollateralValueWithDiscount); ( requiredInitialMargin, requiredMaintenanceMargin, liquidationReward - ) = getAccountRequiredMargins(self, stalenessTolerance); + ) = getAccountRequiredMargins(self, positions, prices, totalCollateralValueWithoutDiscount); isEligible = (requiredMaintenanceMargin + liquidationReward).toInt() > availableMargin; } @@ -300,7 +305,10 @@ library PerpsAccount { revert InsufficientSynthCollateral(collateralId, collateralAmount, amountToWithdraw); } - int256 withdrawableMarginUsd = getWithdrawableMargin(self, PerpsPrice.Tolerance.STRICT); + (Position.Data[] memory positions, uint256[] memory prices) = getOpenPositionsAndCurrentPrices(self, PerpsPrice.Tolerance.STRICT); + (uint256 totalCollateralValueWithDiscount, uint256 totalCollateralValueWithoutDiscount) = getTotalCollateralValue(self, PerpsPrice.Tolerance.DEFAULT); + + int256 withdrawableMarginUsd = getWithdrawableMargin(self, positions, prices, totalCollateralValueWithoutDiscount, totalCollateralValueWithDiscount); // Note: this can only happen if account is liquidatable if (withdrawableMarginUsd < 0) { revert AccountLiquidatable(self.id); @@ -313,8 +321,7 @@ library PerpsAccount { (amountToWithdrawUsd, ) = PerpsCollateralConfiguration.load(collateralId).valueInUsd( amountToWithdraw, spotMarket, - PerpsPrice.Tolerance.STRICT, - false + PerpsPrice.Tolerance.STRICT ); } @@ -334,7 +341,10 @@ library PerpsAccount { */ function getWithdrawableMargin( Data storage self, - PerpsPrice.Tolerance stalenessTolerance + Position.Data[] memory positions, + uint256[] memory prices, + uint256 totalNonDiscountedCollateralValue, + uint256 totalDiscountedCollateralValue ) internal view returns (int256 withdrawableMargin) { bool hasActivePositions = hasOpenPositions(self); @@ -346,54 +356,86 @@ library PerpsAccount { uint256 requiredInitialMargin, , uint256 liquidationReward - ) = getAccountRequiredMargins(self, stalenessTolerance); + ) = getAccountRequiredMargins(self, positions, prices, totalNonDiscountedCollateralValue); uint256 requiredMargin = requiredInitialMargin + liquidationReward; withdrawableMargin = - getAvailableMargin(self, stalenessTolerance) - + getAvailableMargin(self, positions, prices, 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) { + PerpsPrice.Tolerance stalenessTolerance + ) internal view returns (uint256 discounted, uint256 nonDiscounted) { uint256 totalCollateralValue; 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( + (uint256 value, uint256 discount) = PerpsCollateralConfiguration.load(collateralId).valueInUsd( amount, spotMarket, - stalenessTolerance, - useDiscountedValue + stalenessTolerance ); + nonDiscounted += value; + discounted += value.mulDecimal(DecimalMath.UNIT - discount); + } + } + } + + /** + * @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 (Position.Data[] memory positions, uint256[] memory prices) { + uint256[] memory marketIds = self.openPositionMarketIds.values(); + + positions = new Position.Data[](marketIds.length); + for (uint256 i = 0;i < positions.length;i++) { + positions[i] = PerpsMarket.load(marketIds[i].to128()).positions[self.id]; + } + + prices = PerpsPrice.getCurrentPrices(marketIds, stalenessTolerance); + } + + function findPositionByMarketId(Position.Data[] memory positions, uint128 marketId) internal pure returns (uint256 i) { + for (;i < positions.length;i++) { + if (positions[i].marketId == marketId) { + break; + } + } + } + + function upsertPosition(Position.Data[] memory positions, Position.Data memory newPosition) internal pure returns (Position.Data[] memory) { + uint256 oldPositionPos = PerpsAccount.findPositionByMarketId(positions, newPosition.marketId); + if (oldPositionPos < positions.length) { + positions[oldPositionPos] = newPosition; + return positions; + } else { + // we have to expand the size of the array + Position.Data[] memory newPositions = new Position.Data[](positions.length); + for (uint256 i = 0;i < positions.length;i++) { + } - totalCollateralValue += amountToAdd; + newPositions[positions.length] = newPosition; + return newPositions; } - return totalCollateralValue; } function getAccountPnl( - Data storage self, - PerpsPrice.Tolerance stalenessTolerance + Position.Data[] memory positions, + uint256[] memory prices ) internal view returns (int256 totalPnl) { - 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]); + for (uint256 i = 0; i < positions.length; i++) { + (int256 pnl, , , , , ) = positions[i].getPnl(prices[i]); totalPnl += pnl; } } @@ -405,28 +447,21 @@ library PerpsAccount { */ function getAvailableMargin( Data storage self, - PerpsPrice.Tolerance stalenessTolerance + Position.Data[] memory positions, + uint256[] memory prices, + uint256 totalCollateralValueWithDiscount ) internal view returns (int256) { - int256 totalCollateralValue = getTotalCollateralValue(self, stalenessTolerance, true) - .toInt(); - int256 accountPnl = getAccountPnl(self, stalenessTolerance); + int256 accountPnl = getAccountPnl(positions, prices); - return totalCollateralValue + accountPnl - self.debt.toInt(); + return totalCollateralValueWithDiscount.toInt() + accountPnl - self.debt.toInt(); } function getTotalNotionalOpenInterest( - Data storage self + Position.Data[] memory positions, + uint256[] memory prices ) 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]); + for (uint256 i = 0; i < positions.length; i++) { + uint256 openInterest = positions[i].getNotionalValue(prices[i]); totalAccountOpenInterest += openInterest; } } @@ -438,7 +473,9 @@ library PerpsAccount { */ function getAccountRequiredMargins( Data storage self, - PerpsPrice.Tolerance stalenessTolerance + Position.Data[] memory positions, + uint256[] memory prices, + uint256 totalNonDiscountedCollateralValue ) internal view @@ -448,20 +485,15 @@ library PerpsAccount { uint256 possibleLiquidationReward ) { - uint256 openPositionMarketIdsLength = self.openPositionMarketIds.length(); - if (openPositionMarketIdsLength == 0) { + if (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 < positions.length; i++) { + Position.Data memory position = positions[i]; PerpsMarketConfiguration.Data storage marketConfig = PerpsMarketConfiguration.load( - marketIds[i].to128() + position.marketId ); (, , uint256 positionInitialMargin, uint256 positionMaintenanceMargin) = marketConfig .calculateRequiredMargins(position.size, prices[i]); @@ -473,36 +505,36 @@ library PerpsAccount { ( uint256 accumulatedLiquidationRewards, uint256 maxNumberOfWindows - ) = getKeeperRewardsAndCosts(self, 0, stalenessTolerance, 0); + ) = getKeeperRewardsAndCosts(positions, prices, totalNonDiscountedCollateralValue); possibleLiquidationReward = getPossibleLiquidationReward( - self, accumulatedLiquidationRewards, - maxNumberOfWindows + maxNumberOfWindows, + totalNonDiscountedCollateralValue, + getNumberOfUpdatedFeedsRequired(self) ); 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 + Position.Data[] memory positions, + uint256[] memory prices, + 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 < positions.length; i++) { + Position.Data memory position = positions[i]; PerpsMarketConfiguration.Data storage marketConfig = PerpsMarketConfiguration.load( - marketIds[i].to128() + position.marketId ); uint256 numberOfWindows = marketConfig.numberOfLiquidationWindows( MathUtil.abs(position.size) @@ -516,28 +548,29 @@ 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 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..321a844458 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,44 @@ library PerpsMarket { return (block.timestamp - self.lastFundingTime).divDecimal(1 days).toInt(); } - function validatePositionSize( + function getLongSize(Data storage self) internal view returns (uint256) { + return (self.size.toInt() + self.skew).toUint() / 2; + } + + function getShortSize(Data storage self) internal view returns (uint256) { + return (self.size.toInt() - self.skew).toUint() / 2; + } + + /** + * @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 maxSize, - uint256 maxValue, - uint256 price, - int128 oldSize, - int128 newSize + uint256 size, + uint256 price ) 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; - } + PerpsMarketConfiguration.Data storage marketConfig = PerpsMarketConfiguration.load(self.id); - // 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 - ); - } + 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 - if (maxValue > 0 && maxValue < MathUtil.abs(newSideSize / 2).mulDecimal(price)) { - revert PerpsMarketConfiguration.MaxUSDOpenInterestReached( - self.id, - maxValue, - newSideSize / 2, - price - ); - } + // 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 +456,128 @@ 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..48173783e5 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 view returns (uint256) { return MathUtil.abs(self.size).mulDecimal(price); } } 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 From 0dcd1ea249e03ee040b87183ae412611013e4d64 Mon Sep 17 00:00:00 2001 From: Daniel Beal Date: Wed, 27 Nov 2024 01:26:45 -0800 Subject: [PATCH 2/7] fix lints and compiler warnings --- .../contracts/mocks/FeeCollectorMock.sol | 2 +- .../contracts/modules/AsyncOrderModule.sol | 66 +++++++---- .../contracts/modules/LiquidationModule.sol | 58 ++++++++-- .../contracts/modules/PerpsAccountModule.sol | 40 ++++--- .../contracts/modules/PerpsMarketModule.sol | 6 +- .../contracts/storage/AsyncOrder.sol | 103 ++++++++++++------ .../contracts/storage/PerpsAccount.sol | 91 +++++++++++----- .../contracts/storage/PerpsMarket.sol | 25 +++-- .../contracts/storage/Position.sol | 2 +- 9 files changed, 260 insertions(+), 133 deletions(-) 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 2dece954b3..25077b56a0 100644 --- a/markets/perps-market/contracts/modules/AsyncOrderModule.sol +++ b/markets/perps-market/contracts/modules/AsyncOrderModule.sol @@ -104,14 +104,23 @@ contract AsyncOrderModule is IAsyncOrderModule { uint128 marketId, int128 sizeDelta ) external view override returns (uint256 orderFees, uint256 fillPrice) { - _computeOrderFeesWithPrice(marketId, sizeDelta, PerpsPrice.getCurrentPrice(marketId, PerpsPrice.Tolerance.DEFAULT)); + return + _computeOrderFeesWithPrice( + marketId, + sizeDelta, + PerpsPrice.getCurrentPrice(marketId, PerpsPrice.Tolerance.DEFAULT) + ); } /** * @inheritdoc IAsyncOrderModule */ - function computeOrderFeesWithPrice(uint128 marketId, int128 sizeDelta, uint256 price) external view override returns (uint256 orderFees, uint256 fillPrice) { - _computeOrderFeesWithPrice(marketId, sizeDelta, price); + function computeOrderFeesWithPrice( + uint128 marketId, + int128 sizeDelta, + uint256 price + ) external view override returns (uint256 orderFees, uint256 fillPrice) { + return _computeOrderFeesWithPrice(marketId, sizeDelta, price); } function _computeOrderFeesWithPrice( @@ -122,25 +131,22 @@ contract AsyncOrderModule is IAsyncOrderModule { // create a fake order commitment request AsyncOrder.Data memory order = AsyncOrder.Data( 0, - AsyncOrder.OrderCommitmentRequest( - marketId, - 0, - sizeDelta, - 0, - 0, - bytes32(0), - address(0) - ) + AsyncOrder.OrderCommitmentRequest(marketId, 0, sizeDelta, 0, 0, bytes32(0), address(0)) ); - PerpsMarket.Data storage perpsMarketData = PerpsMarket.load(order.request.marketId); 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); - (Position.Data[] memory positions, uint256[] memory prices) = account.getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); - (,,, fillPrice, orderFees) = order.createUpdatedPosition(PerpsMarketConfiguration.load(marketId).settlementStrategies[0], price, positions); + (Position.Data[] memory positions, ) = account.getOpenPositionsAndCurrentPrices( + PerpsPrice.Tolerance.DEFAULT + ); + (, , , fillPrice, orderFees) = order.createUpdatedPosition( + PerpsMarketConfiguration.load(marketId).settlementStrategies[0], + price, + positions + ); } /** @@ -161,7 +167,13 @@ contract AsyncOrderModule is IAsyncOrderModule { uint128 marketId, int128 sizeDelta ) external view override returns (uint256 requiredMargin) { - return _requiredMarginForOrderWithPrice(accountId, marketId, sizeDelta, PerpsPrice.getCurrentPrice(marketId, PerpsPrice.Tolerance.DEFAULT)); + return + _requiredMarginForOrderWithPrice( + accountId, + marketId, + sizeDelta, + PerpsPrice.getCurrentPrice(marketId, PerpsPrice.Tolerance.DEFAULT) + ); } function requiredMarginForOrderWithPrice( @@ -193,20 +205,28 @@ contract AsyncOrderModule is IAsyncOrderModule { ) ); - PerpsMarket.Data storage perpsMarketData = PerpsMarket.load(order.request.marketId); 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); - (Position.Data[] memory positions, uint256[] memory prices) = account.getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); + (Position.Data[] memory positions, uint256[] memory prices) = account + .getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); - SettlementStrategy.Data storage strategy = PerpsMarketConfiguration - .loadValidSettlementStrategy(marketId, 0); - (positions, , , , ) = order.createUpdatedPosition(PerpsMarketConfiguration.load(marketId).settlementStrategies[0], price, positions); + (positions, , , , ) = order.createUpdatedPosition( + PerpsMarketConfiguration.load(marketId).settlementStrategies[0], + price, + positions + ); - (uint256 totalCollateralValueWithDiscount, uint256 totalCollateralValueWithoutDiscount) = account.getTotalCollateralValue(PerpsPrice.Tolerance.DEFAULT); + (, uint256 totalCollateralValueWithoutDiscount) = account.getTotalCollateralValue( + PerpsPrice.Tolerance.DEFAULT + ); - (requiredMargin,, ) = account.getAccountRequiredMargins(positions, prices, totalCollateralValueWithoutDiscount); + (requiredMargin, , ) = account.getAccountRequiredMargins( + positions, + prices, + totalCollateralValueWithoutDiscount + ); } } diff --git a/markets/perps-market/contracts/modules/LiquidationModule.sol b/markets/perps-market/contracts/modules/LiquidationModule.sol index 5298ae1113..e238abc541 100644 --- a/markets/perps-market/contracts/modules/LiquidationModule.sol +++ b/markets/perps-market/contracts/modules/LiquidationModule.sol @@ -49,8 +49,12 @@ contract LiquidationModule is ILiquidationModule, IMarketEvents { .liquidatableAccounts; PerpsAccount.Data storage account = PerpsAccount.load(accountId); if (!liquidatableAccounts.contains(accountId)) { - (Position.Data[] memory positions, uint256[] memory prices) = account.getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); - (uint256 totalCollateralValueWithDiscount, uint256 totalCollateralValueWithoutDiscount) = account.getTotalCollateralValue(PerpsPrice.Tolerance.DEFAULT); + (Position.Data[] memory positions, uint256[] memory prices) = account + .getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); + ( + uint256 totalCollateralValueWithDiscount, + uint256 totalCollateralValueWithoutDiscount + ) = account.getTotalCollateralValue(PerpsPrice.Tolerance.DEFAULT); ( bool isEligible, @@ -58,7 +62,12 @@ contract LiquidationModule is ILiquidationModule, IMarketEvents { , uint256 requiredMaintenaceMargin, uint256 expectedLiquidationReward - ) = account.isEligibleForLiquidation(positions, prices, totalCollateralValueWithDiscount, totalCollateralValueWithoutDiscount); + ) = account.isEligibleForLiquidation( + positions, + prices, + totalCollateralValueWithDiscount, + totalCollateralValueWithoutDiscount + ); if (isEligible) { (uint256 flagCost, uint256 seizedMarginValue) = account.flagForLiquidation(); @@ -91,9 +100,18 @@ contract LiquidationModule is ILiquidationModule, IMarketEvents { revert AccountHasOpenPositions(accountId); } - (Position.Data[] memory positions, uint256[] memory prices) = account.getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); - (uint256 totalCollateralValueWithDiscount, uint256 totalCollateralValueWithoutDiscount) = account.getTotalCollateralValue(PerpsPrice.Tolerance.DEFAULT); - (bool isEligible, ) = account.isEligibleForMarginLiquidation(positions, prices, totalCollateralValueWithDiscount, totalCollateralValueWithoutDiscount); + (Position.Data[] memory positions, uint256[] memory prices) = account + .getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); + ( + uint256 totalCollateralValueWithDiscount, + uint256 totalCollateralValueWithoutDiscount + ) = account.getTotalCollateralValue(PerpsPrice.Tolerance.DEFAULT); + (bool isEligible, ) = account.isEligibleForMarginLiquidation( + positions, + prices, + totalCollateralValueWithDiscount, + totalCollateralValueWithoutDiscount + ); if (isEligible) { // margin is sent to liquidation rewards distributor in getMarginLiquidationCostAndSeizeMargin uint256 marginLiquidateCost = KeeperCosts.load().getFlagKeeperCosts(account.id); @@ -181,10 +199,17 @@ contract LiquidationModule is ILiquidationModule, IMarketEvents { } PerpsAccount.Data storage account = PerpsAccount.load(accountId); - (Position.Data[] memory positions, uint256[] memory prices) = account.getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); - (uint256 totalCollateralValueWithDiscount, uint256 totalCollateralValueWithoutDiscount) = account.getTotalCollateralValue(PerpsPrice.Tolerance.DEFAULT); + (Position.Data[] memory positions, uint256[] memory prices) = account + .getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); + ( + uint256 totalCollateralValueWithDiscount, + uint256 totalCollateralValueWithoutDiscount + ) = account.getTotalCollateralValue(PerpsPrice.Tolerance.DEFAULT); (isEligible, , , , ) = PerpsAccount.load(accountId).isEligibleForLiquidation( - positions, prices, totalCollateralValueWithDiscount, totalCollateralValueWithoutDiscount + positions, + prices, + totalCollateralValueWithDiscount, + totalCollateralValueWithoutDiscount ); } @@ -195,9 +220,18 @@ contract LiquidationModule is ILiquidationModule, IMarketEvents { if (account.hasOpenPositions()) { return false; } else { - (Position.Data[] memory positions, uint256[] memory prices) = account.getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); - (uint256 totalCollateralValueWithDiscount, uint256 totalCollateralValueWithoutDiscount) = account.getTotalCollateralValue(PerpsPrice.Tolerance.DEFAULT); - (isEligible, ) = account.isEligibleForMarginLiquidation(positions, prices, totalCollateralValueWithDiscount, totalCollateralValueWithoutDiscount); + (Position.Data[] memory positions, uint256[] memory prices) = account + .getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); + ( + uint256 totalCollateralValueWithDiscount, + uint256 totalCollateralValueWithoutDiscount + ) = account.getTotalCollateralValue(PerpsPrice.Tolerance.DEFAULT); + (isEligible, ) = account.isEligibleForMarginLiquidation( + positions, + prices, + totalCollateralValueWithDiscount, + totalCollateralValueWithoutDiscount + ); } } diff --git a/markets/perps-market/contracts/modules/PerpsAccountModule.sol b/markets/perps-market/contracts/modules/PerpsAccountModule.sol index 427a4fa54c..9a534af9ad 100644 --- a/markets/perps-market/contracts/modules/PerpsAccountModule.sol +++ b/markets/perps-market/contracts/modules/PerpsAccountModule.sol @@ -121,11 +121,12 @@ contract PerpsAccountModule is IPerpsAccountModule { /** * @inheritdoc IPerpsAccountModule */ - function totalCollateralValue(uint128 accountId) external view override returns (uint256 totalValue) { - (totalValue, ) = - PerpsAccount.load(accountId).getTotalCollateralValue( - PerpsPrice.Tolerance.DEFAULT - ); + function totalCollateralValue( + uint128 accountId + ) external view override returns (uint256 totalValue) { + (totalValue, ) = PerpsAccount.load(accountId).getTotalCollateralValue( + PerpsPrice.Tolerance.DEFAULT + ); } /** @@ -133,7 +134,8 @@ contract PerpsAccountModule is IPerpsAccountModule { */ function totalAccountOpenInterest(uint128 accountId) external view override returns (uint256) { PerpsAccount.Data storage account = PerpsAccount.load(accountId); - (Position.Data[] memory positions, uint256[] memory prices) = account.getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); + (Position.Data[] memory positions, uint256[] memory prices) = account + .getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); return PerpsAccount.getTotalNotionalOpenInterest(positions, prices); } @@ -178,8 +180,11 @@ contract PerpsAccountModule is IPerpsAccountModule { uint128 accountId ) external view override returns (int256 availableMargin) { PerpsAccount.Data storage account = PerpsAccount.load(accountId); - (Position.Data[] memory positions, uint256[] memory prices) = account.getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); - (uint256 totalCollateralValueWithDiscount,) = account.getTotalCollateralValue(PerpsPrice.Tolerance.DEFAULT); + (Position.Data[] memory positions, uint256[] memory prices) = account + .getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); + (uint256 totalCollateralValueWithDiscount, ) = account.getTotalCollateralValue( + PerpsPrice.Tolerance.DEFAULT + ); availableMargin = PerpsAccount.load(accountId).getAvailableMargin( positions, prices, @@ -194,8 +199,12 @@ contract PerpsAccountModule is IPerpsAccountModule { uint128 accountId ) external view override returns (int256 withdrawableMargin) { PerpsAccount.Data storage account = PerpsAccount.load(accountId); - (Position.Data[] memory positions, uint256[] memory prices) = account.getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); - (uint256 totalCollateralValueWithDiscount, uint256 totalCollateralValueWithoutDiscount) = account.getTotalCollateralValue(PerpsPrice.Tolerance.DEFAULT); + (Position.Data[] memory positions, uint256[] memory prices) = account + .getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); + ( + uint256 totalCollateralValueWithDiscount, + uint256 totalCollateralValueWithoutDiscount + ) = account.getTotalCollateralValue(PerpsPrice.Tolerance.DEFAULT); withdrawableMargin = account.getWithdrawableMargin( positions, prices, @@ -224,12 +233,13 @@ contract PerpsAccountModule is IPerpsAccountModule { return (0, 0, 0); } - (Position.Data[] memory positions, uint256[] memory prices) = account.getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); - (uint256 totalCollateralValueWithDiscount, uint256 totalCollateralValueWithoutDiscount) = account.getTotalCollateralValue(PerpsPrice.Tolerance.DEFAULT); + (Position.Data[] memory positions, uint256[] memory prices) = account + .getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); + (, uint256 totalCollateralValueWithoutDiscount) = account.getTotalCollateralValue( + PerpsPrice.Tolerance.DEFAULT + ); (requiredInitialMargin, requiredMaintenanceMargin, maxLiquidationReward) = account - .getAccountRequiredMargins( - positions, prices, totalCollateralValueWithoutDiscount - ); + .getAccountRequiredMargins(positions, prices, 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 3b09343596..302c2c7b75 100644 --- a/markets/perps-market/contracts/modules/PerpsMarketModule.sol +++ b/markets/perps-market/contracts/modules/PerpsMarketModule.sol @@ -74,11 +74,7 @@ contract PerpsMarketModule is IPerpsMarketModule { int128 orderSize, uint256 price ) external view override returns (uint256) { - return - PerpsMarket.load(marketId).calculateFillPrice( - 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 7dece7fe4e..43f1e4f0de 100644 --- a/markets/perps-market/contracts/storage/AsyncOrder.sol +++ b/markets/perps-market/contracts/storage/AsyncOrder.sol @@ -229,34 +229,39 @@ library AsyncOrder { * Useful for validation or various getters on the modules. */ function createUpdatedPosition( - Data memory order, + Data memory order, SettlementStrategy.Data storage strategy, - uint256 orderPrice, + uint256 orderPrice, Position.Data[] memory positions - ) internal view returns (Position.Data[] memory newPositions, Position.Data memory oldPosition, Position.Data memory newPosition, uint256 fillPrice, uint256 orderFees) { + ) + internal + view + returns ( + Position.Data[] memory newPositions, + Position.Data memory oldPosition, + Position.Data memory newPosition, + uint256 fillPrice, + uint256 orderFees + ) + { PerpsMarket.Data storage perpsMarketData = PerpsMarket.load(order.request.marketId); - PerpsMarketConfiguration.Data storage marketConfig = PerpsMarketConfiguration.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 - }); + 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 newPositions = PerpsAccount.upsertPosition(positions, newPosition); orderFees = - perpsMarketData.calculateOrderFee( - newPosition.size - oldPosition.size, - fillPrice - ) + settlementRewardCost(strategy); + perpsMarketData.calculateOrderFee(newPosition.size - oldPosition.size, fillPrice) + + settlementRewardCost(strategy); } /** @@ -274,13 +279,28 @@ library AsyncOrder { Data storage order, SettlementStrategy.Data storage strategy, uint256 orderPrice - ) internal returns (Position.Data memory newPosition, uint256 orderFees, uint256 fillPrice, Position.Data memory oldPosition) { + ) + internal + returns ( + Position.Data memory newPosition, + uint256 orderFees, + uint256 fillPrice, + Position.Data memory oldPosition + ) + { if (order.request.sizeDelta == 0) { revert ZeroSizeOrder(); } - (Position.Data[] memory positions, uint256[] memory prices) = PerpsAccount.load(order.request.accountId).getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); - (uint256 totalCollateralValueWithDiscount, uint256 totalCollateralValueWithoutDiscount) = PerpsAccount.load(order.request.accountId).getTotalCollateralValue(PerpsPrice.Tolerance.DEFAULT); + (Position.Data[] memory positions, uint256[] memory prices) = PerpsAccount + .load(order.request.accountId) + .getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); + ( + uint256 totalCollateralValueWithDiscount, + uint256 totalCollateralValueWithoutDiscount + ) = PerpsAccount.load(order.request.accountId).getTotalCollateralValue( + PerpsPrice.Tolerance.DEFAULT + ); // 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 @@ -290,13 +310,13 @@ library AsyncOrder { int256 currentAvailableMargin; { bool isEligibleForLiquidation; - ( - isEligibleForLiquidation, - currentAvailableMargin, - , - , - - ) = account.isEligibleForLiquidation(positions, prices, totalCollateralValueWithDiscount, totalCollateralValueWithoutDiscount); + (isEligibleForLiquidation, currentAvailableMargin, , , ) = account + .isEligibleForLiquidation( + positions, + prices, + totalCollateralValueWithDiscount, + totalCollateralValueWithoutDiscount + ); if (isEligibleForLiquidation) { revert PerpsAccount.AccountLiquidatable(order.request.accountId); @@ -306,12 +326,20 @@ library AsyncOrder { // now get the new state of the market by calling `createUpdatedPosition(order, orderPrice);` PerpsMarket.load(order.request.marketId).recomputeFunding(orderPrice); - (positions, oldPosition, newPosition, fillPrice, orderFees) = createUpdatedPosition(order, strategy, orderPrice, positions); + (positions, oldPosition, newPosition, fillPrice, orderFees) = createUpdatedPosition( + order, + strategy, + orderPrice, + positions + ); // compute order fees and verify we can pay for them // only account for negative pnl currentAvailableMargin += MathUtil.min( - order.request.sizeDelta.mulDecimal(orderPrice.toInt() - uint256(newPosition.latestInteractionPrice).toInt()), + order.request.sizeDelta.mulDecimal( + // solhint-disable numcast/safe-cast + orderPrice.toInt() - uint256(newPosition.latestInteractionPrice).toInt() + ), 0 ); @@ -323,7 +351,7 @@ library AsyncOrder { currentAvailableMargin -= orderFees.toInt(); // check that the new account margin would be satisfied - (uint256 totalRequiredMargin,, ) = account.getAccountRequiredMargins( + (uint256 totalRequiredMargin, , ) = account.getAccountRequiredMargins( positions, prices, totalCollateralValueWithoutDiscount @@ -339,9 +367,15 @@ library AsyncOrder { 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(), + ( + 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 ); @@ -372,10 +406,7 @@ library AsyncOrder { PerpsMarket.Data storage perpsMarketData = PerpsMarket.load(order.request.marketId); - fillPrice = perpsMarketData.calculateFillPrice( - order.request.sizeDelta, - orderPrice - ); + fillPrice = perpsMarketData.calculateFillPrice(order.request.sizeDelta, orderPrice); Position.Data storage oldPosition = PerpsMarket.accountPosition( order.request.marketId, diff --git a/markets/perps-market/contracts/storage/PerpsAccount.sol b/markets/perps-market/contracts/storage/PerpsAccount.sol index 96d8d95be1..19bf3c9cbe 100644 --- a/markets/perps-market/contracts/storage/PerpsAccount.sol +++ b/markets/perps-market/contracts/storage/PerpsAccount.sol @@ -181,10 +181,16 @@ library PerpsAccount { ); int256 totalLiquidationReward = globalConfig - .keeperReward(liquidationRewardForKeeper, totalLiquidationCost, totalCollateralValueWithoutDiscount) + .keeperReward( + liquidationRewardForKeeper, + totalLiquidationCost, + totalCollateralValueWithoutDiscount + ) .toInt(); - availableMargin = getAvailableMargin(self, positions, prices, totalCollateralValueWithDiscount) - totalLiquidationReward; + availableMargin = + getAvailableMargin(self, positions, prices, totalCollateralValueWithDiscount) - + totalLiquidationReward; isEligible = availableMargin < 0 && self.debt > 0; } @@ -205,7 +211,12 @@ library PerpsAccount { uint256 liquidationReward ) { - availableMargin = getAvailableMargin(self, positions, prices, totalCollateralValueWithDiscount); + availableMargin = getAvailableMargin( + self, + positions, + prices, + totalCollateralValueWithDiscount + ); ( requiredInitialMargin, @@ -305,10 +316,22 @@ library PerpsAccount { revert InsufficientSynthCollateral(collateralId, collateralAmount, amountToWithdraw); } - (Position.Data[] memory positions, uint256[] memory prices) = getOpenPositionsAndCurrentPrices(self, PerpsPrice.Tolerance.STRICT); - (uint256 totalCollateralValueWithDiscount, uint256 totalCollateralValueWithoutDiscount) = getTotalCollateralValue(self, PerpsPrice.Tolerance.DEFAULT); - - int256 withdrawableMarginUsd = getWithdrawableMargin(self, positions, prices, totalCollateralValueWithoutDiscount, totalCollateralValueWithDiscount); + ( + Position.Data[] memory positions, + uint256[] memory prices + ) = getOpenPositionsAndCurrentPrices(self, PerpsPrice.Tolerance.STRICT); + ( + uint256 totalCollateralValueWithDiscount, + uint256 totalCollateralValueWithoutDiscount + ) = getTotalCollateralValue(self, PerpsPrice.Tolerance.DEFAULT); + + int256 withdrawableMarginUsd = getWithdrawableMargin( + self, + positions, + prices, + totalCollateralValueWithoutDiscount, + totalCollateralValueWithDiscount + ); // Note: this can only happen if account is liquidatable if (withdrawableMarginUsd < 0) { revert AccountLiquidatable(self.id); @@ -356,7 +379,12 @@ library PerpsAccount { uint256 requiredInitialMargin, , uint256 liquidationReward - ) = getAccountRequiredMargins(self, positions, prices, totalNonDiscountedCollateralValue); + ) = getAccountRequiredMargins( + self, + positions, + prices, + totalNonDiscountedCollateralValue + ); uint256 requiredMargin = requiredInitialMargin + liquidationReward; withdrawableMargin = getAvailableMargin(self, positions, prices, totalDiscountedCollateralValue) - @@ -370,7 +398,6 @@ library PerpsAccount { Data storage self, PerpsPrice.Tolerance stalenessTolerance ) internal view returns (uint256 discounted, uint256 nonDiscounted) { - uint256 totalCollateralValue; ISpotMarketSystem spotMarket = PerpsMarketFactory.load().spotMarket; for (uint256 i = 1; i <= self.activeCollateralTypes.length(); i++) { uint128 collateralId = self.activeCollateralTypes.valueAt(i).to128(); @@ -380,11 +407,9 @@ library PerpsAccount { discounted += amount; nonDiscounted += amount; } else { - (uint256 value, uint256 discount) = PerpsCollateralConfiguration.load(collateralId).valueInUsd( - amount, - spotMarket, - stalenessTolerance - ); + (uint256 value, uint256 discount) = PerpsCollateralConfiguration + .load(collateralId) + .valueInUsd(amount, spotMarket, stalenessTolerance); nonDiscounted += value; discounted += value.mulDecimal(DecimalMath.UNIT - discount); } @@ -395,36 +420,46 @@ library PerpsAccount { * @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 (Position.Data[] memory positions, uint256[] memory prices) { + function getOpenPositionsAndCurrentPrices( + Data storage self, + PerpsPrice.Tolerance stalenessTolerance + ) internal view returns (Position.Data[] memory positions, uint256[] memory prices) { uint256[] memory marketIds = self.openPositionMarketIds.values(); positions = new Position.Data[](marketIds.length); - for (uint256 i = 0;i < positions.length;i++) { + for (uint256 i = 0; i < positions.length; i++) { positions[i] = PerpsMarket.load(marketIds[i].to128()).positions[self.id]; } prices = PerpsPrice.getCurrentPrices(marketIds, stalenessTolerance); } - function findPositionByMarketId(Position.Data[] memory positions, uint128 marketId) internal pure returns (uint256 i) { - for (;i < positions.length;i++) { + function findPositionByMarketId( + Position.Data[] memory positions, + uint128 marketId + ) internal pure returns (uint256 i) { + for (; i < positions.length; i++) { if (positions[i].marketId == marketId) { break; } } } - function upsertPosition(Position.Data[] memory positions, Position.Data memory newPosition) internal pure returns (Position.Data[] memory) { - uint256 oldPositionPos = PerpsAccount.findPositionByMarketId(positions, newPosition.marketId); + function upsertPosition( + Position.Data[] memory positions, + Position.Data memory newPosition + ) internal pure returns (Position.Data[] memory) { + uint256 oldPositionPos = PerpsAccount.findPositionByMarketId( + positions, + newPosition.marketId + ); if (oldPositionPos < positions.length) { positions[oldPositionPos] = newPosition; return positions; } else { // we have to expand the size of the array Position.Data[] memory newPositions = new Position.Data[](positions.length); - for (uint256 i = 0;i < positions.length;i++) { - - } + for (uint256 i = 0; i < positions.length; i++) {} newPositions[positions.length] = newPosition; return newPositions; } @@ -459,7 +494,7 @@ library PerpsAccount { function getTotalNotionalOpenInterest( Position.Data[] memory positions, uint256[] memory prices - ) internal view returns (uint256 totalAccountOpenInterest) { + ) internal pure returns (uint256 totalAccountOpenInterest) { for (uint256 i = 0; i < positions.length; i++) { uint256 openInterest = positions[i].getNotionalValue(prices[i]); totalAccountOpenInterest += openInterest; @@ -516,12 +551,13 @@ library PerpsAccount { return (initialMargin, maintenanceMargin, possibleLiquidationReward); } - function getNumberOfUpdatedFeedsRequired(Data storage self) internal view returns (uint256 numberOfUpdatedFeeds) { + 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(); + numberOfUpdatedFeeds = numberOfCollateralFeeds + self.openPositionMarketIds.length(); } function getKeeperRewardsAndCosts( @@ -560,7 +596,6 @@ library PerpsAccount { uint256 numOfWindows, uint256 totalNonDiscountedCollateralValue, uint256 numberOfUpdatedFeeds - ) internal view returns (uint256 possibleLiquidationReward) { GlobalPerpsMarketConfiguration.Data storage globalConfig = GlobalPerpsMarketConfiguration .load(); diff --git a/markets/perps-market/contracts/storage/PerpsMarket.sol b/markets/perps-market/contracts/storage/PerpsMarket.sol index 321a844458..180c7523cc 100644 --- a/markets/perps-market/contracts/storage/PerpsMarket.sol +++ b/markets/perps-market/contracts/storage/PerpsMarket.sol @@ -375,11 +375,7 @@ library PerpsMarket { * 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 { + function validateGivenMarketSize(Data storage self, uint256 size, uint256 price) internal view { PerpsMarketConfiguration.Data storage marketConfig = PerpsMarketConfiguration.load(self.id); if (marketConfig.maxMarketSize < size) { @@ -508,14 +504,19 @@ library PerpsMarket { /** * @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) { + 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 - ); + return + computeFillPrice( + PerpsMarket.load(marketId).skew, + PerpsMarketConfiguration.load(marketId).skewScale, + price, + size + ); } /** diff --git a/markets/perps-market/contracts/storage/Position.sol b/markets/perps-market/contracts/storage/Position.sol index 48173783e5..7578a77497 100644 --- a/markets/perps-market/contracts/storage/Position.sol +++ b/markets/perps-market/contracts/storage/Position.sol @@ -111,7 +111,7 @@ library Position { ); } - function getNotionalValue(Data memory 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); } } From 1ad4259aa935c92da694407e41f946d1b0fa74d9 Mon Sep 17 00:00:00 2001 From: Daniel Beal Date: Wed, 27 Nov 2024 04:08:46 -0800 Subject: [PATCH 3/7] very close seems like just having an issue with keeper rewards mismatch --- .../contracts/modules/AsyncOrderModule.sol | 15 +- .../contracts/modules/LiquidationModule.sol | 148 +++++++---------- .../contracts/modules/PerpsAccountModule.sol | 28 ++-- .../contracts/storage/AsyncOrder.sol | 24 ++- .../contracts/storage/PerpsAccount.sol | 157 ++++++++---------- 5 files changed, 162 insertions(+), 210 deletions(-) diff --git a/markets/perps-market/contracts/modules/AsyncOrderModule.sol b/markets/perps-market/contracts/modules/AsyncOrderModule.sol index 25077b56a0..368660d2d5 100644 --- a/markets/perps-market/contracts/modules/AsyncOrderModule.sol +++ b/markets/perps-market/contracts/modules/AsyncOrderModule.sol @@ -139,13 +139,13 @@ contract AsyncOrderModule is IAsyncOrderModule { // probably should be doing this but cant because the interface (view) doesn't allow it //perpsMarketData.recomputeFunding(orderPrice); - (Position.Data[] memory positions, ) = account.getOpenPositionsAndCurrentPrices( + PerpsAccount.MemoryContext memory ctx = account.getOpenPositionsAndCurrentPrices( PerpsPrice.Tolerance.DEFAULT ); (, , , fillPrice, orderFees) = order.createUpdatedPosition( PerpsMarketConfiguration.load(marketId).settlementStrategies[0], price, - positions + ctx ); } @@ -210,22 +210,21 @@ contract AsyncOrderModule is IAsyncOrderModule { // probably should be doing this but cant because the interface (view) doesn't allow it //perpsMarketData.recomputeFunding(orderPrice); - (Position.Data[] memory positions, uint256[] memory prices) = account + PerpsAccount.MemoryContext memory ctx = account .getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); - (positions, , , , ) = order.createUpdatedPosition( + (ctx,,,, ) = order.createUpdatedPosition( PerpsMarketConfiguration.load(marketId).settlementStrategies[0], price, - positions + ctx ); (, uint256 totalCollateralValueWithoutDiscount) = account.getTotalCollateralValue( PerpsPrice.Tolerance.DEFAULT ); - (requiredMargin, , ) = account.getAccountRequiredMargins( - positions, - prices, + (requiredMargin, , ) = PerpsAccount.getAccountRequiredMargins( + ctx, totalCollateralValueWithoutDiscount ); } diff --git a/markets/perps-market/contracts/modules/LiquidationModule.sol b/markets/perps-market/contracts/modules/LiquidationModule.sol index e238abc541..73fe85a5c3 100644 --- a/markets/perps-market/contracts/modules/LiquidationModule.sol +++ b/markets/perps-market/contracts/modules/LiquidationModule.sol @@ -48,13 +48,13 @@ 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)) { - (Position.Data[] memory positions, uint256[] memory prices) = account - .getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); ( uint256 totalCollateralValueWithDiscount, uint256 totalCollateralValueWithoutDiscount - ) = account.getTotalCollateralValue(PerpsPrice.Tolerance.DEFAULT); + ) = account.getTotalCollateralValue(PerpsPrice.Tolerance.STRICT); ( bool isEligible, @@ -62,9 +62,8 @@ contract LiquidationModule is ILiquidationModule, IMarketEvents { , uint256 requiredMaintenaceMargin, uint256 expectedLiquidationReward - ) = account.isEligibleForLiquidation( - positions, - prices, + ) = PerpsAccount.isEligibleForLiquidation( + ctx, totalCollateralValueWithDiscount, totalCollateralValueWithoutDiscount ); @@ -80,12 +79,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); } } @@ -100,15 +99,14 @@ contract LiquidationModule is ILiquidationModule, IMarketEvents { revert AccountHasOpenPositions(accountId); } - (Position.Data[] memory positions, uint256[] memory prices) = account - .getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); + PerpsAccount.MemoryContext memory ctx = account + .getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.STRICT); ( uint256 totalCollateralValueWithDiscount, uint256 totalCollateralValueWithoutDiscount - ) = account.getTotalCollateralValue(PerpsPrice.Tolerance.DEFAULT); - (bool isEligible, ) = account.isEligibleForMarginLiquidation( - positions, - prices, + ) = account.getTotalCollateralValue(PerpsPrice.Tolerance.STRICT); + (bool isEligible, ) = PerpsAccount.isEligibleForMarginLiquidation( + ctx, totalCollateralValueWithDiscount, totalCollateralValueWithoutDiscount ); @@ -119,7 +117,7 @@ contract LiquidationModule is ILiquidationModule, IMarketEvents { // keeper is rewarded in _liquidateAccount liquidationReward = _liquidateAccount( - account, + ctx, marginLiquidateCost, seizedMarginValue, true @@ -156,7 +154,7 @@ 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); } } @@ -178,7 +176,7 @@ 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); } } @@ -199,15 +197,14 @@ contract LiquidationModule is ILiquidationModule, IMarketEvents { } PerpsAccount.Data storage account = PerpsAccount.load(accountId); - (Position.Data[] memory positions, uint256[] memory prices) = account + PerpsAccount.MemoryContext memory ctx = account .getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); ( uint256 totalCollateralValueWithDiscount, uint256 totalCollateralValueWithoutDiscount ) = account.getTotalCollateralValue(PerpsPrice.Tolerance.DEFAULT); - (isEligible, , , , ) = PerpsAccount.load(accountId).isEligibleForLiquidation( - positions, - prices, + (isEligible, , , , ) = PerpsAccount.isEligibleForLiquidation( + ctx, totalCollateralValueWithDiscount, totalCollateralValueWithoutDiscount ); @@ -220,15 +217,14 @@ contract LiquidationModule is ILiquidationModule, IMarketEvents { if (account.hasOpenPositions()) { return false; } else { - (Position.Data[] memory positions, uint256[] memory prices) = account + PerpsAccount.MemoryContext memory ctx = account .getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); ( uint256 totalCollateralValueWithDiscount, uint256 totalCollateralValueWithoutDiscount ) = account.getTotalCollateralValue(PerpsPrice.Tolerance.DEFAULT); - (isEligible, ) = account.isEligibleForMarginLiquidation( - positions, - prices, + (isEligible, ) = PerpsAccount.isEligibleForMarginLiquidation( + ctx, totalCollateralValueWithDiscount, totalCollateralValueWithoutDiscount ); @@ -256,123 +252,107 @@ 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, + PerpsAccount.MemoryContext memory ctx, 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]; + //PerpsAccount.MemoryContext memory ctx = account + // .getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.STRICT); + uint256 i; + uint256 totalLiquidated; + 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 ); } + uint256 totalFlaggingRewards; + 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 ); } - runtime.totalLiquidationCost = + bool accountFullyLiquidated; + + 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 9a534af9ad..bff165c345 100644 --- a/markets/perps-market/contracts/modules/PerpsAccountModule.sol +++ b/markets/perps-market/contracts/modules/PerpsAccountModule.sol @@ -134,9 +134,9 @@ contract PerpsAccountModule is IPerpsAccountModule { */ function totalAccountOpenInterest(uint128 accountId) external view override returns (uint256) { PerpsAccount.Data storage account = PerpsAccount.load(accountId); - (Position.Data[] memory positions, uint256[] memory prices) = account + PerpsAccount.MemoryContext memory ctx = account .getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); - return PerpsAccount.getTotalNotionalOpenInterest(positions, prices); + return PerpsAccount.getTotalNotionalOpenInterest(ctx); } /** @@ -180,14 +180,13 @@ contract PerpsAccountModule is IPerpsAccountModule { uint128 accountId ) external view override returns (int256 availableMargin) { PerpsAccount.Data storage account = PerpsAccount.load(accountId); - (Position.Data[] memory positions, uint256[] memory prices) = account + PerpsAccount.MemoryContext memory ctx = account .getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); (uint256 totalCollateralValueWithDiscount, ) = account.getTotalCollateralValue( PerpsPrice.Tolerance.DEFAULT ); - availableMargin = PerpsAccount.load(accountId).getAvailableMargin( - positions, - prices, + availableMargin = PerpsAccount.getAvailableMargin( + ctx, totalCollateralValueWithDiscount ); } @@ -199,17 +198,16 @@ contract PerpsAccountModule is IPerpsAccountModule { uint128 accountId ) external view override returns (int256 withdrawableMargin) { PerpsAccount.Data storage account = PerpsAccount.load(accountId); - (Position.Data[] memory positions, uint256[] memory prices) = account + PerpsAccount.MemoryContext memory ctx = account .getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); ( uint256 totalCollateralValueWithDiscount, uint256 totalCollateralValueWithoutDiscount ) = account.getTotalCollateralValue(PerpsPrice.Tolerance.DEFAULT); - withdrawableMargin = account.getWithdrawableMargin( - positions, - prices, - totalCollateralValueWithDiscount, - totalCollateralValueWithoutDiscount + withdrawableMargin = PerpsAccount.getWithdrawableMargin( + ctx, + totalCollateralValueWithoutDiscount, + totalCollateralValueWithDiscount ); } @@ -233,13 +231,13 @@ contract PerpsAccountModule is IPerpsAccountModule { return (0, 0, 0); } - (Position.Data[] memory positions, uint256[] memory prices) = account + PerpsAccount.MemoryContext memory ctx = account .getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); (, uint256 totalCollateralValueWithoutDiscount) = account.getTotalCollateralValue( PerpsPrice.Tolerance.DEFAULT ); - (requiredInitialMargin, requiredMaintenanceMargin, maxLiquidationReward) = account - .getAccountRequiredMargins(positions, prices, totalCollateralValueWithoutDiscount); + (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/storage/AsyncOrder.sol b/markets/perps-market/contracts/storage/AsyncOrder.sol index 43f1e4f0de..1ad1a22e65 100644 --- a/markets/perps-market/contracts/storage/AsyncOrder.sol +++ b/markets/perps-market/contracts/storage/AsyncOrder.sol @@ -232,12 +232,12 @@ library AsyncOrder { Data memory order, SettlementStrategy.Data storage strategy, uint256 orderPrice, - Position.Data[] memory positions + PerpsAccount.MemoryContext memory ctx ) internal view returns ( - Position.Data[] memory newPositions, + PerpsAccount.MemoryContext memory newCtx, Position.Data memory oldPosition, Position.Data memory newPosition, uint256 fillPrice, @@ -257,7 +257,7 @@ library AsyncOrder { }); // update the account positions list, so we can now conveniently recompute required margin - newPositions = PerpsAccount.upsertPosition(positions, newPosition); + newCtx = PerpsAccount.upsertPosition(ctx, newPosition); orderFees = perpsMarketData.calculateOrderFee(newPosition.size - oldPosition.size, fillPrice) + @@ -292,7 +292,7 @@ library AsyncOrder { revert ZeroSizeOrder(); } - (Position.Data[] memory positions, uint256[] memory prices) = PerpsAccount + PerpsAccount.MemoryContext memory ctx = PerpsAccount .load(order.request.accountId) .getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); ( @@ -305,15 +305,12 @@ library AsyncOrder { // 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 { - PerpsAccount.Data storage account = PerpsAccount.load(order.request.accountId); - int256 currentAvailableMargin; { bool isEligibleForLiquidation; - (isEligibleForLiquidation, currentAvailableMargin, , , ) = account + (isEligibleForLiquidation, currentAvailableMargin, , , ) = PerpsAccount .isEligibleForLiquidation( - positions, - prices, + ctx, totalCollateralValueWithDiscount, totalCollateralValueWithoutDiscount ); @@ -326,11 +323,11 @@ library AsyncOrder { // now get the new state of the market by calling `createUpdatedPosition(order, orderPrice);` PerpsMarket.load(order.request.marketId).recomputeFunding(orderPrice); - (positions, oldPosition, newPosition, fillPrice, orderFees) = createUpdatedPosition( + (ctx, oldPosition, newPosition, fillPrice, orderFees) = createUpdatedPosition( order, strategy, orderPrice, - positions + ctx ); // compute order fees and verify we can pay for them @@ -351,9 +348,8 @@ library AsyncOrder { currentAvailableMargin -= orderFees.toInt(); // check that the new account margin would be satisfied - (uint256 totalRequiredMargin, , ) = account.getAccountRequiredMargins( - positions, - prices, + (uint256 totalRequiredMargin, , ) = PerpsAccount.getAccountRequiredMargins( + ctx, totalCollateralValueWithoutDiscount ); diff --git a/markets/perps-market/contracts/storage/PerpsAccount.sol b/markets/perps-market/contracts/storage/PerpsAccount.sol index 19bf3c9cbe..b54b04a8e5 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,15 +170,13 @@ library PerpsAccount { } function isEligibleForMarginLiquidation( - Data storage self, - Position.Data[] memory positions, - uint256[] memory prices, + 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(); GlobalPerpsMarketConfiguration.Data storage globalConfig = GlobalPerpsMarketConfiguration @@ -189,15 +194,13 @@ library PerpsAccount { .toInt(); availableMargin = - getAvailableMargin(self, positions, prices, totalCollateralValueWithDiscount) - + getAvailableMargin(ctx, totalCollateralValueWithDiscount) - totalLiquidationReward; - isEligible = availableMargin < 0 && self.debt > 0; + isEligible = availableMargin < 0 && PerpsAccount.load(ctx.accountId).debt > 0; } function isEligibleForLiquidation( - Data storage self, - Position.Data[] memory positions, - uint256[] memory prices, + MemoryContext memory ctx, uint256 totalCollateralValueWithDiscount, uint256 totalCollateralValueWithoutDiscount ) @@ -212,9 +215,7 @@ library PerpsAccount { ) { availableMargin = getAvailableMargin( - self, - positions, - prices, + ctx, totalCollateralValueWithDiscount ); @@ -222,7 +223,7 @@ library PerpsAccount { requiredInitialMargin, requiredMaintenanceMargin, liquidationReward - ) = getAccountRequiredMargins(self, positions, prices, totalCollateralValueWithoutDiscount); + ) = getAccountRequiredMargins(ctx, totalCollateralValueWithoutDiscount); isEligible = (requiredMaintenanceMargin + liquidationReward).toInt() > availableMargin; } @@ -316,19 +317,14 @@ library PerpsAccount { revert InsufficientSynthCollateral(collateralId, collateralAmount, amountToWithdraw); } - ( - Position.Data[] memory positions, - uint256[] memory prices - ) = getOpenPositionsAndCurrentPrices(self, PerpsPrice.Tolerance.STRICT); + MemoryContext memory ctx = getOpenPositionsAndCurrentPrices(self, PerpsPrice.Tolerance.STRICT); ( uint256 totalCollateralValueWithDiscount, uint256 totalCollateralValueWithoutDiscount ) = getTotalCollateralValue(self, PerpsPrice.Tolerance.DEFAULT); int256 withdrawableMarginUsd = getWithdrawableMargin( - self, - positions, - prices, + ctx, totalCollateralValueWithoutDiscount, totalCollateralValueWithDiscount ); @@ -363,16 +359,15 @@ 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, - Position.Data[] memory positions, - uint256[] memory prices, + 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) { ( @@ -380,14 +375,12 @@ library PerpsAccount { , uint256 liquidationReward ) = getAccountRequiredMargins( - self, - positions, - prices, + ctx, totalNonDiscountedCollateralValue ); uint256 requiredMargin = requiredInitialMargin + liquidationReward; withdrawableMargin = - getAvailableMargin(self, positions, prices, totalDiscountedCollateralValue) - + getAvailableMargin(ctx, totalDiscountedCollateralValue) - requiredMargin.toInt(); } else { withdrawableMargin = totalNonDiscountedCollateralValue.toInt(); @@ -423,54 +416,53 @@ library PerpsAccount { function getOpenPositionsAndCurrentPrices( Data storage self, PerpsPrice.Tolerance stalenessTolerance - ) internal view returns (Position.Data[] memory positions, uint256[] memory prices) { + ) internal view returns (MemoryContext memory ctx) { uint256[] memory marketIds = self.openPositionMarketIds.values(); - - positions = new Position.Data[](marketIds.length); - for (uint256 i = 0; i < positions.length; i++) { - positions[i] = PerpsMarket.load(marketIds[i].to128()).positions[self.id]; + 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]; } - - prices = PerpsPrice.getCurrentPrices(marketIds, stalenessTolerance); } function findPositionByMarketId( - Position.Data[] memory positions, + MemoryContext memory ctx, uint128 marketId ) internal pure returns (uint256 i) { - for (; i < positions.length; i++) { - if (positions[i].marketId == marketId) { + for (; i < ctx.positions.length; i++) { + if (ctx.positions[i].marketId == marketId) { break; } } } function upsertPosition( - Position.Data[] memory positions, + MemoryContext memory ctx, Position.Data memory newPosition - ) internal pure returns (Position.Data[] memory) { + ) internal view returns (MemoryContext memory newCtx) { uint256 oldPositionPos = PerpsAccount.findPositionByMarketId( - positions, + ctx, newPosition.marketId ); - if (oldPositionPos < positions.length) { - positions[oldPositionPos] = newPosition; - return positions; + if (oldPositionPos < ctx.positions.length) { + ctx.positions[oldPositionPos] = newPosition; + newCtx = ctx; } else { // we have to expand the size of the array - Position.Data[] memory newPositions = new Position.Data[](positions.length); - for (uint256 i = 0; i < positions.length; i++) {} - newPositions[positions.length] = newPosition; - return newPositions; + 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( - Position.Data[] memory positions, - uint256[] memory prices + MemoryContext memory ctx ) internal view returns (int256 totalPnl) { - for (uint256 i = 0; i < positions.length; i++) { - (int256 pnl, , , , , ) = positions[i].getPnl(prices[i]); + for (uint256 i = 0; i < ctx.positions.length; i++) { + (int256 pnl, , , , , ) = ctx.positions[i].getPnl(ctx.prices[i]); totalPnl += pnl; } } @@ -481,22 +473,19 @@ library PerpsAccount { * @dev The total collateral value is always based on the discounted value of the collateral */ function getAvailableMargin( - Data storage self, - Position.Data[] memory positions, - uint256[] memory prices, + MemoryContext memory ctx, uint256 totalCollateralValueWithDiscount ) internal view returns (int256) { - int256 accountPnl = getAccountPnl(positions, prices); + int256 accountPnl = getAccountPnl(ctx); - return totalCollateralValueWithDiscount.toInt() + accountPnl - self.debt.toInt(); + return totalCollateralValueWithDiscount.toInt() + accountPnl - load(ctx.accountId).debt.toInt(); } function getTotalNotionalOpenInterest( - Position.Data[] memory positions, - uint256[] memory prices + MemoryContext memory ctx ) internal pure returns (uint256 totalAccountOpenInterest) { - for (uint256 i = 0; i < positions.length; i++) { - uint256 openInterest = positions[i].getNotionalValue(prices[i]); + for (uint256 i = 0; i < ctx.positions.length; i++) { + uint256 openInterest = ctx.positions[i].getNotionalValue(ctx.prices[i]); totalAccountOpenInterest += openInterest; } } @@ -507,9 +496,7 @@ library PerpsAccount { * @dev The maintenance margin is used to determine when to liquidate a position */ function getAccountRequiredMargins( - Data storage self, - Position.Data[] memory positions, - uint256[] memory prices, + MemoryContext memory ctx, uint256 totalNonDiscountedCollateralValue ) internal @@ -520,18 +507,18 @@ library PerpsAccount { uint256 possibleLiquidationReward ) { - if (positions.length == 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 - for (uint256 i = 0; i < positions.length; i++) { - Position.Data memory position = positions[i]; + for (uint256 i = 0; i < ctx.positions.length; i++) { + Position.Data memory position = ctx.positions[i]; PerpsMarketConfiguration.Data storage marketConfig = PerpsMarketConfiguration.load( position.marketId ); (, , uint256 positionInitialMargin, uint256 positionMaintenanceMargin) = marketConfig - .calculateRequiredMargins(position.size, prices[i]); + .calculateRequiredMargins(position.size, ctx.prices[i]); maintenanceMargin += positionMaintenanceMargin; initialMargin += positionInitialMargin; @@ -540,12 +527,12 @@ library PerpsAccount { ( uint256 accumulatedLiquidationRewards, uint256 maxNumberOfWindows - ) = getKeeperRewardsAndCosts(positions, prices, totalNonDiscountedCollateralValue); + ) = getKeeperRewardsAndCosts(ctx, totalNonDiscountedCollateralValue); possibleLiquidationReward = getPossibleLiquidationReward( accumulatedLiquidationRewards, maxNumberOfWindows, totalNonDiscountedCollateralValue, - getNumberOfUpdatedFeedsRequired(self) + getNumberOfUpdatedFeedsRequired(load(ctx.accountId)) ); return (initialMargin, maintenanceMargin, possibleLiquidationReward); @@ -561,14 +548,13 @@ library PerpsAccount { } function getKeeperRewardsAndCosts( - Position.Data[] memory positions, - uint256[] memory prices, + MemoryContext memory ctx, uint256 totalNonDiscountedCollateralValue ) internal view returns (uint256 accumulatedLiquidationRewards, uint256 maxNumberOfWindows) { uint256 totalFlagReward = 0; // use separate accounting for liquidation rewards so we can compare against global min/max liquidation reward values - for (uint256 i = 0; i < positions.length; i++) { - Position.Data memory position = positions[i]; + for (uint256 i = 0; i < ctx.positions.length; i++) { + Position.Data memory position = ctx.positions[i]; PerpsMarketConfiguration.Data storage marketConfig = PerpsMarketConfiguration.load( position.marketId ); @@ -576,7 +562,7 @@ library PerpsAccount { 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; @@ -639,29 +625,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(); @@ -674,7 +656,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, @@ -683,17 +665,14 @@ 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 ); } From 1b6e92246e054b0e98cf1dce762e1608ac20bcad Mon Sep 17 00:00:00 2001 From: Daniel Beal Date: Wed, 27 Nov 2024 05:23:11 -0800 Subject: [PATCH 4/7] tests might be all passing my computer is having issues running them all --- .../contracts/modules/AsyncOrderModule.sol | 7 ++- .../contracts/modules/LiquidationModule.sol | 63 ++++++++++++------- .../contracts/modules/PerpsAccountModule.sol | 25 ++++---- .../contracts/storage/PerpsAccount.sol | 58 +++++++++-------- 4 files changed, 90 insertions(+), 63 deletions(-) diff --git a/markets/perps-market/contracts/modules/AsyncOrderModule.sol b/markets/perps-market/contracts/modules/AsyncOrderModule.sol index 368660d2d5..690c872c1b 100644 --- a/markets/perps-market/contracts/modules/AsyncOrderModule.sol +++ b/markets/perps-market/contracts/modules/AsyncOrderModule.sol @@ -210,10 +210,11 @@ contract AsyncOrderModule is IAsyncOrderModule { // 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); + PerpsAccount.MemoryContext memory ctx = account.getOpenPositionsAndCurrentPrices( + PerpsPrice.Tolerance.DEFAULT + ); - (ctx,,,, ) = order.createUpdatedPosition( + (ctx, , , , ) = order.createUpdatedPosition( PerpsMarketConfiguration.load(marketId).settlementStrategies[0], price, ctx diff --git a/markets/perps-market/contracts/modules/LiquidationModule.sol b/markets/perps-market/contracts/modules/LiquidationModule.sol index 73fe85a5c3..2c491bc233 100644 --- a/markets/perps-market/contracts/modules/LiquidationModule.sol +++ b/markets/perps-market/contracts/modules/LiquidationModule.sol @@ -48,8 +48,9 @@ contract LiquidationModule is ILiquidationModule, IMarketEvents { .load() .liquidatableAccounts; PerpsAccount.Data storage account = PerpsAccount.load(accountId); - PerpsAccount.MemoryContext memory ctx = account - .getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.STRICT); + PerpsAccount.MemoryContext memory ctx = account.getOpenPositionsAndCurrentPrices( + PerpsPrice.Tolerance.STRICT + ); if (!liquidatableAccounts.contains(accountId)) { ( uint256 totalCollateralValueWithDiscount, @@ -99,8 +100,9 @@ contract LiquidationModule is ILiquidationModule, IMarketEvents { revert AccountHasOpenPositions(accountId); } - PerpsAccount.MemoryContext memory ctx = account - .getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.STRICT); + PerpsAccount.MemoryContext memory ctx = account.getOpenPositionsAndCurrentPrices( + PerpsPrice.Tolerance.STRICT + ); ( uint256 totalCollateralValueWithDiscount, uint256 totalCollateralValueWithoutDiscount @@ -154,7 +156,14 @@ contract LiquidationModule is ILiquidationModule, IMarketEvents { for (uint256 i = 0; i < numberOfAccountsToLiquidate; i++) { uint128 accountId = liquidatableAccounts[i].to128(); - liquidationReward += _liquidateAccount(PerpsAccount.load(accountId).getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.STRICT), 0, 0, false); + liquidationReward += _liquidateAccount( + PerpsAccount.load(accountId).getOpenPositionsAndCurrentPrices( + PerpsPrice.Tolerance.STRICT + ), + 0, + 0, + false + ); } } @@ -176,7 +185,14 @@ contract LiquidationModule is ILiquidationModule, IMarketEvents { continue; } - liquidationReward += _liquidateAccount(PerpsAccount.load(accountId).getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.STRICT), 0, 0, false); + liquidationReward += _liquidateAccount( + PerpsAccount.load(accountId).getOpenPositionsAndCurrentPrices( + PerpsPrice.Tolerance.STRICT + ), + 0, + 0, + false + ); } } @@ -197,8 +213,9 @@ contract LiquidationModule is ILiquidationModule, IMarketEvents { } PerpsAccount.Data storage account = PerpsAccount.load(accountId); - PerpsAccount.MemoryContext memory ctx = account - .getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); + PerpsAccount.MemoryContext memory ctx = account.getOpenPositionsAndCurrentPrices( + PerpsPrice.Tolerance.DEFAULT + ); ( uint256 totalCollateralValueWithDiscount, uint256 totalCollateralValueWithoutDiscount @@ -217,8 +234,9 @@ contract LiquidationModule is ILiquidationModule, IMarketEvents { if (account.hasOpenPositions()) { return false; } else { - PerpsAccount.MemoryContext memory ctx = account - .getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); + PerpsAccount.MemoryContext memory ctx = account.getOpenPositionsAndCurrentPrices( + PerpsPrice.Tolerance.DEFAULT + ); ( uint256 totalCollateralValueWithDiscount, uint256 totalCollateralValueWithoutDiscount @@ -261,13 +279,12 @@ contract LiquidationModule is ILiquidationModule, IMarketEvents { uint256 totalCollateralValue, bool positionFlagged ) internal returns (uint256 keeperLiquidationReward) { - //PerpsAccount.MemoryContext memory ctx = account // .getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.STRICT); uint256 i; uint256 totalLiquidated; - for (i = 0;i < ctx.positions.length;i++) { + for (i = 0; i < ctx.positions.length; i++) { ( uint256 amountLiquidated, int128 newPositionSize, @@ -300,24 +317,26 @@ contract LiquidationModule is ILiquidationModule, IMarketEvents { } uint256 totalFlaggingRewards; - for (uint256 j = 0;j <= MathUtil.min(i, ctx.positions.length - 1);j++) { + 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])); + totalFlaggingRewards += PerpsMarketConfiguration + .load(ctx.positions[j].marketId) + .calculateFlagReward( + MathUtil.abs(ctx.positions[j].size).mulDecimal(ctx.prices[j]) + ); } } if ( ERC2771Context._msgSender() != - PerpsMarketConfiguration.load(ctx.positions[MathUtil.min(i, ctx.positions.length - 1)].marketId).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() @@ -331,8 +350,7 @@ contract LiquidationModule is ILiquidationModule, IMarketEvents { bool accountFullyLiquidated; - uint256 totalLiquidationCost = - KeeperCosts.load().getLiquidateKeeperCosts() + + uint256 totalLiquidationCost = KeeperCosts.load().getLiquidateKeeperCosts() + costOfFlagExecution; if (positionFlagged || totalLiquidated > 0) { keeperLiquidationReward = _processLiquidationRewards( @@ -340,7 +358,8 @@ contract LiquidationModule is ILiquidationModule, IMarketEvents { totalLiquidationCost, totalCollateralValue ); - accountFullyLiquidated = PerpsAccount.load(ctx.accountId).openPositionMarketIds.length() == 0; + accountFullyLiquidated = + PerpsAccount.load(ctx.accountId).openPositionMarketIds.length() == 0; if ( accountFullyLiquidated && GlobalPerpsMarket.load().liquidatableAccounts.contains(ctx.accountId) diff --git a/markets/perps-market/contracts/modules/PerpsAccountModule.sol b/markets/perps-market/contracts/modules/PerpsAccountModule.sol index bff165c345..0254e8f64e 100644 --- a/markets/perps-market/contracts/modules/PerpsAccountModule.sol +++ b/markets/perps-market/contracts/modules/PerpsAccountModule.sol @@ -134,8 +134,9 @@ contract PerpsAccountModule is IPerpsAccountModule { */ function totalAccountOpenInterest(uint128 accountId) external view override returns (uint256) { PerpsAccount.Data storage account = PerpsAccount.load(accountId); - PerpsAccount.MemoryContext memory ctx = account - .getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); + PerpsAccount.MemoryContext memory ctx = account.getOpenPositionsAndCurrentPrices( + PerpsPrice.Tolerance.DEFAULT + ); return PerpsAccount.getTotalNotionalOpenInterest(ctx); } @@ -180,15 +181,13 @@ contract PerpsAccountModule is IPerpsAccountModule { uint128 accountId ) external view override returns (int256 availableMargin) { PerpsAccount.Data storage account = PerpsAccount.load(accountId); - PerpsAccount.MemoryContext memory ctx = account - .getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); - (uint256 totalCollateralValueWithDiscount, ) = account.getTotalCollateralValue( + PerpsAccount.MemoryContext memory ctx = account.getOpenPositionsAndCurrentPrices( PerpsPrice.Tolerance.DEFAULT ); - availableMargin = PerpsAccount.getAvailableMargin( - ctx, - totalCollateralValueWithDiscount + (uint256 totalCollateralValueWithDiscount, ) = account.getTotalCollateralValue( + PerpsPrice.Tolerance.DEFAULT ); + availableMargin = PerpsAccount.getAvailableMargin(ctx, totalCollateralValueWithDiscount); } /** @@ -198,8 +197,9 @@ contract PerpsAccountModule is IPerpsAccountModule { uint128 accountId ) external view override returns (int256 withdrawableMargin) { PerpsAccount.Data storage account = PerpsAccount.load(accountId); - PerpsAccount.MemoryContext memory ctx = account - .getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); + PerpsAccount.MemoryContext memory ctx = account.getOpenPositionsAndCurrentPrices( + PerpsPrice.Tolerance.DEFAULT + ); ( uint256 totalCollateralValueWithDiscount, uint256 totalCollateralValueWithoutDiscount @@ -231,8 +231,9 @@ contract PerpsAccountModule is IPerpsAccountModule { return (0, 0, 0); } - PerpsAccount.MemoryContext memory ctx = account - .getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); + PerpsAccount.MemoryContext memory ctx = account.getOpenPositionsAndCurrentPrices( + PerpsPrice.Tolerance.DEFAULT + ); (, uint256 totalCollateralValueWithoutDiscount) = account.getTotalCollateralValue( PerpsPrice.Tolerance.DEFAULT ); diff --git a/markets/perps-market/contracts/storage/PerpsAccount.sol b/markets/perps-market/contracts/storage/PerpsAccount.sol index b54b04a8e5..14799f32b6 100644 --- a/markets/perps-market/contracts/storage/PerpsAccount.sol +++ b/markets/perps-market/contracts/storage/PerpsAccount.sol @@ -214,10 +214,7 @@ library PerpsAccount { uint256 liquidationReward ) { - availableMargin = getAvailableMargin( - ctx, - totalCollateralValueWithDiscount - ); + availableMargin = getAvailableMargin(ctx, totalCollateralValueWithDiscount); ( requiredInitialMargin, @@ -235,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); @@ -317,7 +316,10 @@ library PerpsAccount { revert InsufficientSynthCollateral(collateralId, collateralAmount, amountToWithdraw); } - MemoryContext memory ctx = getOpenPositionsAndCurrentPrices(self, PerpsPrice.Tolerance.STRICT); + MemoryContext memory ctx = getOpenPositionsAndCurrentPrices( + self, + PerpsPrice.Tolerance.STRICT + ); ( uint256 totalCollateralValueWithDiscount, uint256 totalCollateralValueWithoutDiscount @@ -374,10 +376,7 @@ library PerpsAccount { uint256 requiredInitialMargin, , uint256 liquidationReward - ) = getAccountRequiredMargins( - ctx, - totalNonDiscountedCollateralValue - ); + ) = getAccountRequiredMargins(ctx, totalNonDiscountedCollateralValue); uint256 requiredMargin = requiredInitialMargin + liquidationReward; withdrawableMargin = getAvailableMargin(ctx, totalDiscountedCollateralValue) - @@ -418,7 +417,12 @@ library PerpsAccount { PerpsPrice.Tolerance stalenessTolerance ) internal view returns (MemoryContext memory ctx) { uint256[] memory marketIds = self.openPositionMarketIds.values(); - ctx = MemoryContext(self.id, stalenessTolerance, new Position.Data[](marketIds.length), PerpsPrice.getCurrentPrices(marketIds, stalenessTolerance)); + 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]; } @@ -439,28 +443,31 @@ library PerpsAccount { MemoryContext memory ctx, Position.Data memory newPosition ) internal view returns (MemoryContext memory newCtx) { - uint256 oldPositionPos = PerpsAccount.findPositionByMarketId( - ctx, - newPosition.marketId - ); + 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)); + 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); + newCtx.prices[ctx.positions.length] = PerpsPrice.getCurrentPrice( + newPosition.marketId, + ctx.stalenessTolerance + ); } } - function getAccountPnl( - MemoryContext memory ctx - ) internal view returns (int256 totalPnl) { + 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; @@ -478,7 +485,10 @@ library PerpsAccount { ) internal view returns (int256) { int256 accountPnl = getAccountPnl(ctx); - return totalCollateralValueWithDiscount.toInt() + accountPnl - load(ctx.accountId).debt.toInt(); + return + totalCollateralValueWithDiscount.toInt() + + accountPnl - + load(ctx.accountId).debt.toInt(); } function getTotalNotionalOpenInterest( @@ -670,11 +680,7 @@ library PerpsAccount { // update market data marketUpdateData = perpsMarket.updatePositionData(self.id, newPosition); - return ( - amountToLiquidate, - newPositionSize, - marketUpdateData - ); + return (amountToLiquidate, newPositionSize, marketUpdateData); } function hasOpenPositions(Data storage self) internal view returns (bool) { From ac8f6cf2c5ec9c204c7dfc6cd694d775e10b3d57 Mon Sep 17 00:00:00 2001 From: Daniel Beal Date: Wed, 27 Nov 2024 09:47:33 -0800 Subject: [PATCH 5/7] fix more tests --- .../contracts/modules/AsyncOrderModule.sol | 12 +---- .../contracts/modules/LiquidationModule.sol | 44 +++++++++++++------ .../contracts/modules/PerpsAccountModule.sol | 2 +- .../contracts/storage/AsyncOrder.sol | 12 ++--- 4 files changed, 40 insertions(+), 30 deletions(-) diff --git a/markets/perps-market/contracts/modules/AsyncOrderModule.sol b/markets/perps-market/contracts/modules/AsyncOrderModule.sol index 690c872c1b..e9cfb660fa 100644 --- a/markets/perps-market/contracts/modules/AsyncOrderModule.sol +++ b/markets/perps-market/contracts/modules/AsyncOrderModule.sol @@ -142,11 +142,7 @@ contract AsyncOrderModule is IAsyncOrderModule { PerpsAccount.MemoryContext memory ctx = account.getOpenPositionsAndCurrentPrices( PerpsPrice.Tolerance.DEFAULT ); - (, , , fillPrice, orderFees) = order.createUpdatedPosition( - PerpsMarketConfiguration.load(marketId).settlementStrategies[0], - price, - ctx - ); + (, , , fillPrice, orderFees) = order.createUpdatedPosition(price, ctx); } /** @@ -214,11 +210,7 @@ contract AsyncOrderModule is IAsyncOrderModule { PerpsPrice.Tolerance.DEFAULT ); - (ctx, , , , ) = order.createUpdatedPosition( - PerpsMarketConfiguration.load(marketId).settlementStrategies[0], - price, - ctx - ); + (ctx, , , , ) = order.createUpdatedPosition(price, ctx); (, uint256 totalCollateralValueWithoutDiscount) = account.getTotalCollateralValue( PerpsPrice.Tolerance.DEFAULT diff --git a/markets/perps-market/contracts/modules/LiquidationModule.sol b/markets/perps-market/contracts/modules/LiquidationModule.sol index 2c491bc233..d7aa6c935b 100644 --- a/markets/perps-market/contracts/modules/LiquidationModule.sol +++ b/markets/perps-market/contracts/modules/LiquidationModule.sol @@ -114,7 +114,9 @@ contract LiquidationModule is ILiquidationModule, IMarketEvents { ); 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 @@ -270,20 +272,11 @@ contract LiquidationModule is ILiquidationModule, IMarketEvents { ); } - /** - * @dev liquidates an account - */ - function _liquidateAccount( + function _liquidateAccountPositions( PerpsAccount.MemoryContext memory ctx, - uint256 costOfFlagExecution, - uint256 totalCollateralValue, - bool positionFlagged - ) internal returns (uint256 keeperLiquidationReward) { - //PerpsAccount.MemoryContext memory ctx = account - // .getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.STRICT); - + uint256 totalCollateralValue + ) internal returns (uint256 totalLiquidated, uint256 totalFlaggingRewards) { uint256 i; - uint256 totalLiquidated; for (i = 0; i < ctx.positions.length; i++) { ( uint256 amountLiquidated, @@ -316,7 +309,6 @@ contract LiquidationModule is ILiquidationModule, IMarketEvents { ); } - uint256 totalFlaggingRewards; for (uint256 j = 0; j <= MathUtil.min(i, ctx.positions.length - 1); j++) { // using oldPositionAbsSize to calculate flag reward if ( @@ -348,6 +340,30 @@ contract LiquidationModule is ILiquidationModule, IMarketEvents { ); } + 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; uint256 totalLiquidationCost = KeeperCosts.load().getLiquidateKeeperCosts() + diff --git a/markets/perps-market/contracts/modules/PerpsAccountModule.sol b/markets/perps-market/contracts/modules/PerpsAccountModule.sol index 0254e8f64e..75f932054b 100644 --- a/markets/perps-market/contracts/modules/PerpsAccountModule.sol +++ b/markets/perps-market/contracts/modules/PerpsAccountModule.sol @@ -124,7 +124,7 @@ contract PerpsAccountModule is IPerpsAccountModule { function totalCollateralValue( uint128 accountId ) external view override returns (uint256 totalValue) { - (totalValue, ) = PerpsAccount.load(accountId).getTotalCollateralValue( + (, totalValue) = PerpsAccount.load(accountId).getTotalCollateralValue( PerpsPrice.Tolerance.DEFAULT ); } diff --git a/markets/perps-market/contracts/storage/AsyncOrder.sol b/markets/perps-market/contracts/storage/AsyncOrder.sol index 1ad1a22e65..daa465bcc3 100644 --- a/markets/perps-market/contracts/storage/AsyncOrder.sol +++ b/markets/perps-market/contracts/storage/AsyncOrder.sol @@ -230,7 +230,6 @@ library AsyncOrder { */ function createUpdatedPosition( Data memory order, - SettlementStrategy.Data storage strategy, uint256 orderPrice, PerpsAccount.MemoryContext memory ctx ) @@ -259,9 +258,10 @@ library AsyncOrder { // 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) + - settlementRewardCost(strategy); + orderFees = perpsMarketData.calculateOrderFee( + newPosition.size - oldPosition.size, + fillPrice + ); } /** @@ -325,11 +325,13 @@ library AsyncOrder { (ctx, oldPosition, newPosition, fillPrice, orderFees) = createUpdatedPosition( order, - strategy, orderPrice, ctx ); + // 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( From d21b30a4bcd123d6c7073d881c8491af71f97442 Mon Sep 17 00:00:00 2001 From: Daniel Beal Date: Wed, 27 Nov 2024 09:48:25 -0800 Subject: [PATCH 6/7] update storage dump --- markets/perps-market/storage.dump.json | 322 +++++-------------------- 1 file changed, 64 insertions(+), 258 deletions(-) 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 + } ] } }, From 9194c939bafb450cfba55c10e53972c3e875af8a Mon Sep 17 00:00:00 2001 From: Daniel Beal Date: Wed, 27 Nov 2024 21:30:12 -0800 Subject: [PATCH 7/7] probably all tests pass --- .../contracts/modules/AsyncOrderModule.sol | 30 +++++++++++++++++-- .../contracts/storage/AsyncOrder.sol | 17 ++++++----- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/markets/perps-market/contracts/modules/AsyncOrderModule.sol b/markets/perps-market/contracts/modules/AsyncOrderModule.sol index e9cfb660fa..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"; /** @@ -158,6 +159,20 @@ 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, @@ -210,15 +225,26 @@ contract AsyncOrderModule is IAsyncOrderModule { PerpsPrice.Tolerance.DEFAULT ); - (ctx, , , , ) = order.createUpdatedPosition(price, ctx); + uint256 orderFees; + Position.Data memory oldPosition; + Position.Data memory newPosition; + (ctx, oldPosition, newPosition, , orderFees) = order.createUpdatedPosition(price, ctx); + + // 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 ); - (requiredMargin, , ) = PerpsAccount.getAccountRequiredMargins( + uint256 possibleLiquidationReward; + (requiredMargin, , possibleLiquidationReward) = PerpsAccount.getAccountRequiredMargins( ctx, totalCollateralValueWithoutDiscount ); + + return requiredMargin + possibleLiquidationReward + orderFees; } } diff --git a/markets/perps-market/contracts/storage/AsyncOrder.sol b/markets/perps-market/contracts/storage/AsyncOrder.sol index daa465bcc3..7e66c45aa9 100644 --- a/markets/perps-market/contracts/storage/AsyncOrder.sol +++ b/markets/perps-market/contracts/storage/AsyncOrder.sol @@ -350,13 +350,16 @@ library AsyncOrder { currentAvailableMargin -= orderFees.toInt(); // check that the new account margin would be satisfied - (uint256 totalRequiredMargin, , ) = PerpsAccount.getAccountRequiredMargins( - ctx, - totalCollateralValueWithoutDiscount - ); - - if (currentAvailableMargin < totalRequiredMargin.toInt()) { - revert InsufficientMargin(currentAvailableMargin, totalRequiredMargin); + (uint256 totalRequiredMargin, , uint256 possibleLiquidationReward) = PerpsAccount + .getAccountRequiredMargins(ctx, totalCollateralValueWithoutDiscount); + + if ( + currentAvailableMargin < (totalRequiredMargin + possibleLiquidationReward).toInt() + ) { + revert InsufficientMargin( + currentAvailableMargin, + totalRequiredMargin + possibleLiquidationReward + ); } }