From 31d0b06c4c1440acca25064ef417e81adfcd0b66 Mon Sep 17 00:00:00 2001 From: Daniel Beal Date: Tue, 26 Nov 2024 08:23:20 -0800 Subject: [PATCH 01/15] 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 02/15] 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 03/15] 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 04/15] 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 05/15] 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 06/15] 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 07/15] 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 + ); } } From 4d3dd12b80b6bfc21d2aa48d19e11a53d45f50b0 Mon Sep 17 00:00:00 2001 From: Daniel Beal Date: Tue, 3 Dec 2024 06:16:45 -0800 Subject: [PATCH 08/15] starting the book order module doesnt quite compile yet, but just trying to get to a starting point. --- .../contracts/interfaces/IBookOrderModule.sol | 68 ++++ .../contracts/modules/BookOrderModule.sol | 143 ++++++++ markets/perps-market/storage.dump.json | 322 ++++++++++++++---- 3 files changed, 469 insertions(+), 64 deletions(-) create mode 100644 markets/perps-market/contracts/interfaces/IBookOrderModule.sol create mode 100644 markets/perps-market/contracts/modules/BookOrderModule.sol diff --git a/markets/perps-market/contracts/interfaces/IBookOrderModule.sol b/markets/perps-market/contracts/interfaces/IBookOrderModule.sol new file mode 100644 index 0000000000..fc192f0e99 --- /dev/null +++ b/markets/perps-market/contracts/interfaces/IBookOrderModule.sol @@ -0,0 +1,68 @@ +//SPDX-License-Identifier: MIT +pragma solidity >=0.8.11 <0.9.0; + +import {AsyncOrder} from "../storage/AsyncOrder.sol"; +import {SettlementStrategy} from "../storage/SettlementStrategy.sol"; + +/** + * @title Module for processing orders from the offchain orderbook + */ +interface IBookOrderModule { + /** + * @notice An order being settled by the orderbook. + */ + struct BookOrder { + /** + * @dev Order account id. + */ + uint128 accountId; + /** + * @dev Order size delta (of asset units expressed in decimal 18 digits). It can be positive or negative. + */ + int128 sizeDelta; + /** + * @dev The price that should be used to fill the order + */ + uint256 orderPrice; + /** + * @dev The price that should be used for this order. + * It should be signed by a trusted price provider for the perps market. + * This field is optional and can be 0x. If this is the case, the next order(s) must be of oposite magnitude to match this order. + */ + bytes signedPriceData; + /** + * @dev An optional code provided by frontends to assist with tracking the source of volume and fees. + */ + bytes32 trackingCode; + } + + /** + * @notice Indicates a summary as to the operation state of a subbmitted order for settlement + */ + enum OrderStatus { + FILLED, + CANCELLED + } + + /** + * @notice Returned by `settleBookOrders` to indicate the result of a submitted order for settlement + */ + struct BookOrderSettleStatus { + /** + * @dev The result of the order + */ + OrderStatus status; + } + + /** + * @notice Called by the offchain orderbook to settle a prevoiusly placed order onchain. Any orders submitted to this function will be processed as if they happened simultaneously, at the prices given by the orderbook. + * If an order is found to be unfillable (ex. insufficient account liquidity), it will be returned in the `statuses` return field. + * @param marketId the market for which all of the following orders should be operated on + * @param orders the list of orders to settle + * @return statuses the result of the `orders` supplied to this function. + */ + function settleBookOrders( + uint128 marketId, + BookOrder[] memory orders + ) external returns (BookOrderSettleStatus[] memory statuses); +} diff --git a/markets/perps-market/contracts/modules/BookOrderModule.sol b/markets/perps-market/contracts/modules/BookOrderModule.sol new file mode 100644 index 0000000000..70deb81425 --- /dev/null +++ b/markets/perps-market/contracts/modules/BookOrderModule.sol @@ -0,0 +1,143 @@ +//SPDX-License-Identifier: MIT +pragma solidity >=0.8.11 <0.9.0; + +import {SafeCastU256} from "@synthetixio/core-contracts/contracts/utils/SafeCast.sol"; +import {ERC2771Context} from "@synthetixio/core-contracts/contracts/utils/ERC2771Context.sol"; +import {ParameterError} from "@synthetixio/core-contracts/contracts/errors/ParameterError.sol"; +import {FeatureFlag} from "@synthetixio/core-modules/contracts/storage/FeatureFlag.sol"; +import {Account} from "@synthetixio/main/contracts/storage/Account.sol"; +import {AccountRBAC} from "@synthetixio/main/contracts/storage/AccountRBAC.sol"; +import {IBookOrderModule} from "../interfaces/IBookOrderModule.sol"; +import {PerpsMarket} from "../storage/PerpsMarket.sol"; +import {PerpsAccount} from "../storage/PerpsAccount.sol"; +import {AsyncOrder} from "../storage/AsyncOrder.sol"; +import {Position} from "../storage/Position.sol"; +import {PerpsPrice} from "../storage/PerpsPrice.sol"; +import {MarketUpdate} from "../storage/MarketUpdate.sol"; +import {GlobalPerpsMarket} from "../storage/GlobalPerpsMarket.sol"; +import {PerpsMarketConfiguration} from "../storage/PerpsMarketConfiguration.sol"; +import {SettlementStrategy} from "../storage/SettlementStrategy.sol"; +import {GlobalPerpsMarketConfiguration} from "../storage/GlobalPerpsMarketConfiguration.sol"; +import {PerpsMarketFactory} from "../storage/PerpsMarketFactory.sol"; +import {Flags} from "../utils/Flags.sol"; + +/** + * @title Module for processing orders from an off-chain orderbook. + * @dev See IBookOrderModule. + */ +contract BookOrderModule is IBookOrderModule { + using AsyncOrder for AsyncOrder.Data; + using PerpsAccount for PerpsAccount.Data; + using PerpsMarket for PerpsMarket.Data; + using GlobalPerpsMarket for GlobalPerpsMarket.Data; + using GlobalPerpsMarketConfiguration for GlobalPerpsMarketConfiguration.Data; + using Position for Position.Data; + using SafeCastU256 for uint256; + + struct AccumulatedOrderData { + uint256 orderFee; + int256 sizeDelta; + uint256 orderCount; + } + + /** + * @inheritdoc IBookOrderModule + */ + function settleBookOrders( + uint128 marketId, + BookOrder[] memory orders + ) external override returns (BookOrderSettleStatus[] memory cancelledOrders) { + FeatureFlag.ensureAccessToFeature(Flags.PERPS_SYSTEM); + PerpsMarket.Data storage market = PerpsMarket.loadValid(marketId); + + // loop 1: figure out the big picture change on the market + int256 newMarketSkew = market.skew; + uint256 marketSkewScale = PerpsMarketConfiguration.load(marketId).skewScale; + for (uint256 i = 0; i < orders.length; i++) { + newMarketSkew += orders[i].sizeDelta; + } + + // TODO: verify total market size (?) + + // loop 2: apply the order changes to account + PerpsAccount.MemoryContext memory ctx; + Position.Data memory oldPosition; + Position.Data memory curPosition; + AccumulatedOrderData memory accumOrderData; + int256 accumulatedSizeDelta; + uint256 totalCollectedFees; + for (uint256 i = 0; i < orders.length; i++) { + BookOrder memory order = orders[i]; + if (order.accountId > ctx.accountId) { + if (ctx.accountId > 0) { + // charge the funding fee, the order fee, and whatever pnl has been accumulated from the last position. + (int256 pnl, , uint256 chargedInterest, int256 accruedFunding, , ) = oldPosition + .getPnl(order.orderPrice); + + PerpsAccount.load(order.accountId).charge( + pnl - accumOrderData.orderFee.toInt() + ); + totalCollectedFees += accumOrderData.orderFee; + ( + uint256 totalNonDiscountedCollateralValue, + uint256 totalDiscountedCollateralValue + ) = PerpsAccount.load(order.accountId).getTotalCollateralValue( + PerpsPrice.Tolerance.DEFAULT + ); + + // verify that the account is in valid state. should have required initial margin + // if account is not in valid state, immediately close their orders (they will still be charged fees) + // TODO: check + (uint256 requiredInitialMargin, , uint256 liquidationReward) = PerpsAccount + .getAccountRequiredMargins(ctx, totalNonDiscountedCollateralValue); + + if ( + totalDiscountedCollateralValue < requiredInitialMargin + liquidationReward + ) { + // cancel order because it does not work with the user's margin. + } else { + // commit order to the user's account + MarketUpdate.Data memory updateData = market.updatePositionData( + ctx.accountId, + curPosition + ); + PerpsAccount.load(order.accountId).updateOpenPositions( + marketId, + curPosition.size + ); + } + } + + // load the new account + // Check if commitment.accountId is valid + GlobalPerpsMarket.load().checkLiquidation(order.accountId); + ctx = PerpsAccount.load(order.accountId).getOpenPositionsAndCurrentPrices( + PerpsPrice.Tolerance.DEFAULT + ); + oldPosition = ctx.positions[PerpsAccount.findPositionByMarketId(ctx, marketId)]; + curPosition = oldPosition; + accumOrderData = AccumulatedOrderData(0, 0, 0); + } else if (order.accountId < ctx.accountId) { + // order ids must be supplied in strictly ascending order + revert ParameterError.InvalidParameter( + "orders", + "order's accountId must be increasing" + ); + } + + curPosition.size += order.sizeDelta; + accumOrderData.sizeDelta += order.sizeDelta; + accumOrderData.orderFee += market.calculateOrderFee(order.sizeDelta, order.orderPrice); + // TODO: emit event for each order settled (alternatively, if we decide otherwise, we can emit just one event after they are aggregated together) + } + + // send collected fees to the fee collector and etc. + GlobalPerpsMarketConfiguration.load().collectFees( + totalCollectedFees, + address(0), + PerpsMarketFactory.load() + ); + + // TODO: emit event + } +} diff --git a/markets/perps-market/storage.dump.json b/markets/perps-market/storage.dump.json index b118ca387a..dd3c60c9d6 100644 --- a/markets/perps-market/storage.dump.json +++ b/markets/perps-market/storage.dump.json @@ -1,4 +1,68 @@ { + "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", @@ -99,6 +163,200 @@ "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 + } ] } }, @@ -652,70 +910,6 @@ "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 428cfa5567d49978d0c7b068c32f3eb60f1c4814 Mon Sep 17 00:00:00 2001 From: Daniel Beal Date: Thu, 5 Dec 2024 22:16:57 +0900 Subject: [PATCH 09/15] add book order type to perps market still very initial feeling implementation. enough to get the demo out. can clean up later. --- markets/perps-market/cannonfile.test.toml | 4 + markets/perps-market/cannonfile.toml | 4 + .../contracts/interfaces/IBookOrderModule.sol | 2 + .../contracts/modules/BookOrderModule.sol | 202 ++++++++---- .../test/integration/Orders/BookOrder.test.ts | 288 ++++++++++++++++++ 5 files changed, 440 insertions(+), 60 deletions(-) create mode 100644 markets/perps-market/test/integration/Orders/BookOrder.test.ts diff --git a/markets/perps-market/cannonfile.test.toml b/markets/perps-market/cannonfile.test.toml index 9c9fc21ca4..3353348d30 100644 --- a/markets/perps-market/cannonfile.test.toml +++ b/markets/perps-market/cannonfile.test.toml @@ -33,6 +33,9 @@ artifact = "PerpsMarketFactoryModule" [contract.AsyncOrderModule] artifact = "AsyncOrderModule" +[contract.BookOrderModule] +artifact = "BookOrderModule" + [contract.AsyncOrderSettlementPythModule] artifact = "AsyncOrderSettlementPythModule" @@ -79,6 +82,7 @@ contracts = [ "PerpsAccountModule", "PerpsMarketModule", "AsyncOrderModule", + "BookOrderModule", "AsyncOrderSettlementPythModule", "AsyncOrderCancelModule", "FeatureFlagModule", diff --git a/markets/perps-market/cannonfile.toml b/markets/perps-market/cannonfile.toml index cd9fe00ac5..90d35876b8 100644 --- a/markets/perps-market/cannonfile.toml +++ b/markets/perps-market/cannonfile.toml @@ -39,6 +39,9 @@ artifact = "PerpsMarketFactoryModule" [contract.AsyncOrderModule] artifact = "AsyncOrderModule" +[contract.BookOrderModule] +artifact = "BookOrderModule" + [contract.AsyncOrderSettlementPythModule] artifact = "AsyncOrderSettlementPythModule" @@ -85,6 +88,7 @@ contracts = [ "PerpsAccountModule", "PerpsMarketModule", "AsyncOrderModule", + "BookOrderModule", "AsyncOrderSettlementPythModule", "AsyncOrderCancelModule", "FeatureFlagModule", diff --git a/markets/perps-market/contracts/interfaces/IBookOrderModule.sol b/markets/perps-market/contracts/interfaces/IBookOrderModule.sol index fc192f0e99..18e951b8ab 100644 --- a/markets/perps-market/contracts/interfaces/IBookOrderModule.sol +++ b/markets/perps-market/contracts/interfaces/IBookOrderModule.sol @@ -36,6 +36,8 @@ interface IBookOrderModule { bytes32 trackingCode; } + event BookOrderSettled(uint128 indexed marketId, BookOrder[] orders, uint256 totalCollectedFees); + /** * @notice Indicates a summary as to the operation state of a subbmitted order for settlement */ diff --git a/markets/perps-market/contracts/modules/BookOrderModule.sol b/markets/perps-market/contracts/modules/BookOrderModule.sol index 70deb81425..95660c936d 100644 --- a/markets/perps-market/contracts/modules/BookOrderModule.sol +++ b/markets/perps-market/contracts/modules/BookOrderModule.sol @@ -8,6 +8,8 @@ import {FeatureFlag} from "@synthetixio/core-modules/contracts/storage/FeatureFl import {Account} from "@synthetixio/main/contracts/storage/Account.sol"; import {AccountRBAC} from "@synthetixio/main/contracts/storage/AccountRBAC.sol"; import {IBookOrderModule} from "../interfaces/IBookOrderModule.sol"; +import {IAccountEvents} from "../interfaces/IAccountEvents.sol"; +import {IMarketEvents} from "../interfaces/IMarketEvents.sol"; import {PerpsMarket} from "../storage/PerpsMarket.sol"; import {PerpsAccount} from "../storage/PerpsAccount.sol"; import {AsyncOrder} from "../storage/AsyncOrder.sol"; @@ -25,7 +27,7 @@ import {Flags} from "../utils/Flags.sol"; * @title Module for processing orders from an off-chain orderbook. * @dev See IBookOrderModule. */ -contract BookOrderModule is IBookOrderModule { +contract BookOrderModule is IBookOrderModule, IAccountEvents, IMarketEvents { using AsyncOrder for AsyncOrder.Data; using PerpsAccount for PerpsAccount.Data; using PerpsMarket for PerpsMarket.Data; @@ -34,12 +36,55 @@ contract BookOrderModule is IBookOrderModule { using Position for Position.Data; using SafeCastU256 for uint256; + /** + * @notice Gets fired when a new order is settled. + * @param marketId Id of the market used for the trade. + * @param accountId Id of the account used for the trade. + * @param fillPrice Price at which the order was settled. + * @param pnl Pnl of the previous closed position. + * @param accruedFunding Accrued funding of the previous closed position. + * @param sizeDelta Size delta from order. + * @param newSize New size of the position after settlement. + * @param totalFees Amount of fees collected by the protocol. + * @param referralFees Amount of fees collected by the referrer. + * @param collectedFees Amount of fees collected by fee collector. + * @param settlementReward reward to sender for settling order. + * @param trackingCode Optional code for integrator tracking purposes. + * @param settler address of the settler of the order. + */ + event OrderSettled( + uint128 indexed marketId, + uint128 indexed accountId, + uint256 fillPrice, + int256 pnl, + int256 accruedFunding, + int128 sizeDelta, + int128 newSize, + uint256 totalFees, + uint256 referralFees, + uint256 collectedFees, + uint256 settlementReward, + bytes32 indexed trackingCode, + address settler + ); + + /** + * @notice Gets fired after order settles and includes the interest charged to the account. + * @param accountId Id of the account used for the trade. + * @param interest interest charges + */ + event InterestCharged(uint128 indexed accountId, uint256 interest); + struct AccumulatedOrderData { uint256 orderFee; int256 sizeDelta; uint256 orderCount; + uint256 price; } + event DoneLoop(uint128 accountId); + event ItsGreater(uint128 accountId, uint128 cmpAccountId); + /** * @inheritdoc IBookOrderModule */ @@ -51,73 +96,36 @@ contract BookOrderModule is IBookOrderModule { PerpsMarket.Data storage market = PerpsMarket.loadValid(marketId); // loop 1: figure out the big picture change on the market - int256 newMarketSkew = market.skew; - uint256 marketSkewScale = PerpsMarketConfiguration.load(marketId).skewScale; - for (uint256 i = 0; i < orders.length; i++) { - newMarketSkew += orders[i].sizeDelta; - } + uint256 marketSkewScale = PerpsMarketConfiguration.load(marketId).skewScale; + { + int256 newMarketSkew = market.skew; + for (uint256 i = 0; i < orders.length; i++) { + newMarketSkew += orders[i].sizeDelta; + } + } // TODO: verify total market size (?) // loop 2: apply the order changes to account PerpsAccount.MemoryContext memory ctx; - Position.Data memory oldPosition; Position.Data memory curPosition; AccumulatedOrderData memory accumOrderData; - int256 accumulatedSizeDelta; uint256 totalCollectedFees; for (uint256 i = 0; i < orders.length; i++) { - BookOrder memory order = orders[i]; - if (order.accountId > ctx.accountId) { - if (ctx.accountId > 0) { - // charge the funding fee, the order fee, and whatever pnl has been accumulated from the last position. - (int256 pnl, , uint256 chargedInterest, int256 accruedFunding, , ) = oldPosition - .getPnl(order.orderPrice); - - PerpsAccount.load(order.accountId).charge( - pnl - accumOrderData.orderFee.toInt() - ); - totalCollectedFees += accumOrderData.orderFee; - ( - uint256 totalNonDiscountedCollateralValue, - uint256 totalDiscountedCollateralValue - ) = PerpsAccount.load(order.accountId).getTotalCollateralValue( - PerpsPrice.Tolerance.DEFAULT - ); - - // verify that the account is in valid state. should have required initial margin - // if account is not in valid state, immediately close their orders (they will still be charged fees) - // TODO: check - (uint256 requiredInitialMargin, , uint256 liquidationReward) = PerpsAccount - .getAccountRequiredMargins(ctx, totalNonDiscountedCollateralValue); - - if ( - totalDiscountedCollateralValue < requiredInitialMargin + liquidationReward - ) { - // cancel order because it does not work with the user's margin. - } else { - // commit order to the user's account - MarketUpdate.Data memory updateData = market.updatePositionData( - ctx.accountId, - curPosition - ); - PerpsAccount.load(order.accountId).updateOpenPositions( - marketId, - curPosition.size - ); - } - } + if (orders[i].accountId > ctx.accountId) { + curPosition.latestInteractionPrice = accumOrderData.price.to128(); + totalCollectedFees += _applyAggregatedAccountPosition(marketId, ctx, curPosition, accumOrderData); // load the new account - // Check if commitment.accountId is valid - GlobalPerpsMarket.load().checkLiquidation(order.accountId); - ctx = PerpsAccount.load(order.accountId).getOpenPositionsAndCurrentPrices( + GlobalPerpsMarket.load().checkLiquidation(orders[i].accountId); + ctx = PerpsAccount.load(orders[i].accountId).getOpenPositionsAndCurrentPrices( PerpsPrice.Tolerance.DEFAULT ); - oldPosition = ctx.positions[PerpsAccount.findPositionByMarketId(ctx, marketId)]; - curPosition = oldPosition; - accumOrderData = AccumulatedOrderData(0, 0, 0); - } else if (order.accountId < ctx.accountId) { + // todo: is the below line necessary? in the tests I have been finding it is + ctx.accountId = orders[i].accountId; + accumOrderData = AccumulatedOrderData(0, 0, 0, 0); + curPosition = market.positions[ctx.accountId]; + } else if (orders[i].accountId < ctx.accountId) { // order ids must be supplied in strictly ascending order revert ParameterError.InvalidParameter( "orders", @@ -125,12 +133,17 @@ contract BookOrderModule is IBookOrderModule { ); } - curPosition.size += order.sizeDelta; - accumOrderData.sizeDelta += order.sizeDelta; - accumOrderData.orderFee += market.calculateOrderFee(order.sizeDelta, order.orderPrice); - // TODO: emit event for each order settled (alternatively, if we decide otherwise, we can emit just one event after they are aggregated together) + curPosition.size += orders[i].sizeDelta; + accumOrderData.sizeDelta += orders[i].sizeDelta; + accumOrderData.orderFee += market.calculateOrderFee(orders[i].sizeDelta, orders[i].orderPrice); + + // the first received price for the orders for an account will be used as the settling price for the previous order. Least gamable that way. + accumOrderData.price = accumOrderData.price == 0 ? orders[i].orderPrice : accumOrderData.price; } + curPosition.latestInteractionPrice = accumOrderData.price.to128(); + totalCollectedFees += _applyAggregatedAccountPosition(marketId, ctx, curPosition, accumOrderData); + // send collected fees to the fee collector and etc. GlobalPerpsMarketConfiguration.load().collectFees( totalCollectedFees, @@ -138,6 +151,75 @@ contract BookOrderModule is IBookOrderModule { PerpsMarketFactory.load() ); - // TODO: emit event + emit BookOrderSettled(marketId, orders, totalCollectedFees); } + + function _applyAggregatedAccountPosition(uint128 marketId, PerpsAccount.MemoryContext memory ctx, Position.Data memory pos, AccumulatedOrderData memory accumOrderData) internal returns (uint256) { + if (ctx.accountId == 0) { + return 0; + } + Position.Data memory oldPosition = PerpsMarket.load(marketId).positions[ctx.accountId]; + // charge the funding fee from the previously held position, the order fee, and whatever pnl has been accumulated from the last position. + (int256 pnl, , uint256 chargedInterest, int256 accruedFunding, , ) = + oldPosition.getPnl(accumOrderData.price); + + PerpsAccount.load(ctx.accountId).charge( + pnl - accumOrderData.orderFee.toInt() + ); + + emit AccountCharged(ctx.accountId, pnl - accumOrderData.orderFee.toInt(), PerpsAccount.load(ctx.accountId).debt); + + MarketUpdate.Data memory updateData; + { + PerpsMarket.Data storage market = PerpsMarket.load(marketId); + + // we recompute to the price of the first order the user set. if they set multiple trades in te timeframe, its as if they fully close their order for a short period of time + // between the first order and the last order they place + market.recomputeFunding(accumOrderData.price); + + // skip verifications for the account having minimum collateral. + // this is because they are undertaken by the orderbook and cancelling them would be unnecessary complication + // commit order to the user's account + updateData = market.updatePositionData( + ctx.accountId, + pos + ); + } + + PerpsAccount.load(ctx.accountId).updateOpenPositions( + marketId, + pos.size + ); + + emit MarketUpdated( + updateData.marketId, + accumOrderData.price, + updateData.skew, + PerpsMarket.load(marketId).size, + pos.size - oldPosition.size, + updateData.currentFundingRate, + updateData.currentFundingVelocity, + updateData.interestRate + ); + + return accumOrderData.orderFee; + + emit InterestCharged(ctx.accountId, chargedInterest); + + emit OrderSettled( + marketId, + ctx.accountId, + accumOrderData.price, + pnl, + accruedFunding, + pos.size - oldPosition.size, + pos.size, + accumOrderData.orderFee, + 0, // referral fees + 0, // TODO: fee collector fees + 0, // settlement reward + "", // TODO: tracking code, may not have ever + ERC2771Context._msgSender() + ); + } } diff --git a/markets/perps-market/test/integration/Orders/BookOrder.test.ts b/markets/perps-market/test/integration/Orders/BookOrder.test.ts new file mode 100644 index 0000000000..beefa818f1 --- /dev/null +++ b/markets/perps-market/test/integration/Orders/BookOrder.test.ts @@ -0,0 +1,288 @@ +import { ethers } from 'ethers'; +import { DEFAULT_SETTLEMENT_STRATEGY, bn, bootstrapMarkets } from '../bootstrap'; +import { fastForwardTo } from '@synthetixio/core-utils/utils/hardhat/rpc'; +import { snapshotCheckpoint } from '@synthetixio/core-utils/utils/mocha/snapshot'; +import { SynthMarkets } from '@synthetixio/spot-market/test/common'; +import { DepositCollateralData, depositCollateral } from '../helpers'; +import assertBn from '@synthetixio/core-utils/utils/assertions/assert-bignumber'; +import assertEvent from '@synthetixio/core-utils/utils/assertions/assert-event'; +import assertRevert from '@synthetixio/core-utils/utils/assertions/assert-revert'; +import { getTxTime } from '@synthetixio/core-utils/src/utils/hardhat/rpc'; +import { calculateFillPrice, calculatePricePnl } from '../helpers/fillPrice'; +import { wei } from '@synthetixio/wei'; +import { calcCurrentFundingVelocity } from '../helpers/funding-calcs'; +import { deepEqual } from 'assert/strict'; + +describe.only('Settle Orderbook order', () => { + const orderFees = { + makerFee: wei(0.0003), // 3bps + takerFee: wei(0.0008), // 8bps + }; + const { systems, owner, perpsMarkets, synthMarkets, provider, trader1, keeper } = + bootstrapMarkets({ + synthMarkets: [ + { + name: 'Bitcoin', + token: 'snxBTC', + buyPrice: bn(10_000), + sellPrice: bn(10_000), + }, + ], + perpsMarkets: [ + { + requestedMarketId: 25, + name: 'Ether', + token: 'snxETH', + price: bn(1000), + fundingParams: { skewScale: bn(100_000), maxFundingVelocity: bn(10) }, + orderFees: { + makerFee: orderFees.makerFee.toBN(), + takerFee: orderFees.takerFee.toBN(), + }, + }, + ], + traderAccountIds: [2, 3], + }); + let ethMarketId: ethers.BigNumber; + let ethSettlementStrategyId: ethers.BigNumber; + let btcSynth: SynthMarkets[number]; + + before('identify actors', async () => { + ethMarketId = perpsMarkets()[0].marketId(); + ethSettlementStrategyId = perpsMarkets()[0].strategyId(); + btcSynth = synthMarkets()[0]; + }); + + before('set Pyth Benchmark Price data', async () => { + const offChainPrice = bn(1000); + + // set Pyth setBenchmarkPrice + await systems().MockPythERC7412Wrapper.setBenchmarkPrice(offChainPrice); + }); + + before('deposit collateral', async () => { + depositCollateral({ + systems, + trader: trader1, + accountId: () => 2, + collaterals: [ + { + snxUSDAmount: () => bn(10_000), + }, + ], + }); + + depositCollateral({ + systems, + trader: trader1, + accountId: () => 3, + collaterals: [ + { + snxUSDAmount: () => bn(10_000), + }, + ], + }); + }); + + before('set fee collector and referral', async () => { + await systems().FeeCollectorMock.mockSetFeeRatio(bn(1)); + await systems() + .PerpsMarket.connect(owner()) + .setFeeCollector(systems().FeeCollectorMock.address); + }); + + const restore = snapshotCheckpoint(provider); + + it.skip('fails if not called by orderbook', async () => { + // for this test we consider `keeper` to be the orderbook + // but it cna be a different address from the actual keeper + }); + + it('fails when the orders are not increasing account id order', async () => { + await assertRevert( + systems() + .PerpsMarket.connect(keeper()) + .settleBookOrders(ethMarketId, [ + { + accountId: 2, + sizeDelta: bn(1), + orderPrice: bn(1050), + signedPriceData: '0x', + trackingCode: ethers.utils.formatBytes32String(''), + }, + { + accountId: 3, + sizeDelta: bn(3), + orderPrice: bn(1100), + signedPriceData: '0x', + trackingCode: ethers.utils.formatBytes32String(''), + }, + { + accountId: 2, + sizeDelta: bn(-5), + orderPrice: bn(1300), + signedPriceData: '0x', + trackingCode: ethers.utils.formatBytes32String(''), + }, + ]), + 'InvalidParameter("orders"', + systems().PerpsMarket + ); + }); + + let tx: ethers.ContractTransaction; + describe('1 order 1 account', async () => { + before(restore); + before('run orderbook order', async () => { + tx = await systems() + .PerpsMarket.connect(keeper()) + .settleBookOrders(ethMarketId, [ + { + accountId: 2, + sizeDelta: bn(1), + orderPrice: bn(1050), + signedPriceData: '0x', + trackingCode: ethers.utils.formatBytes32String(''), + }, + ]); + }); + + it('updates the account size', async () => { + const [, , size] = await systems().PerpsMarket.getOpenPosition(2, ethMarketId); + assertBn.equal(size, bn(1)); + }); + + it('charges fees and deposits them to the RD', async () => { + const balance = await systems().USD.balanceOf(systems().FeeCollectorMock.address); + assertBn.equal(balance, bn(0.84)); + }); + + it('charges the account with pnl (which is just fees right now)', async () => { + const amount = await systems().PerpsMarket.getCollateralAmount(2, 0); + assertBn.equal(amount, bn(9999.16)); + const debted = await systems().PerpsMarket.debt(2); + assertBn.equal(debted, bn(0)); + }); + + it('emits account events', async () => { + await assertEvent(tx, 'BookOrderSettled', systems().PerpsMarket); + }); + + describe('run another order', () => { + before('run another orderbook order', async () => { + tx = await systems() + .PerpsMarket.connect(keeper()) + .settleBookOrders(ethMarketId, [ + { + accountId: 2, + sizeDelta: bn(2), + orderPrice: bn(1100), + signedPriceData: '0x', + trackingCode: ethers.utils.formatBytes32String(''), + }, + ]); + }); + + it('changes the account size again', async () => { + const [, , size] = await systems().PerpsMarket.getOpenPosition(2, ethMarketId); + assertBn.equal(size, bn(3)); + }); + + it('charges the account with pnl', async () => { + const amount = await systems().PerpsMarket.getCollateralAmount(2, 0); + // eth went up 50 dollars so we should have 50 more minus fees + assertBn.equal(amount, bn(10047.4)); + }); + }); + }); + + describe('3 orders 1 account', async () => { + before(restore); + before('run orderbook order', async () => { + tx = await systems() + .PerpsMarket.connect(keeper()) + .settleBookOrders(ethMarketId, [ + { + accountId: 2, + sizeDelta: bn(1), + orderPrice: bn(1050), + signedPriceData: '0x', + trackingCode: ethers.utils.formatBytes32String(''), + }, + { + accountId: 2, + sizeDelta: bn(3), + orderPrice: bn(1100), + signedPriceData: '0x', + trackingCode: ethers.utils.formatBytes32String(''), + }, + { + accountId: 2, + sizeDelta: bn(-5), + orderPrice: bn(1300), + signedPriceData: '0x', + trackingCode: ethers.utils.formatBytes32String(''), + }, + ]); + + it('updates the account size', async () => { + const [, , size] = await systems().PerpsMarket.getOpenPosition(2, ethMarketId); + assertBn.equal(size, bn(-1)); + }); + + it('charges fees and deposits them to the RD', async () => { + const balance = await systems().USD.balanceOf(systems().FeeCollectorMock.address); + assertBn.equal(balance, bn(6.08)); + }); + + it('emits account events', async () => { + await assertEvent(tx, 'BookOrderSettled', systems().PerpsMarket); + }); + }); + }); + + describe('3 orders 2 accounts', async () => { + before(restore); + before('run orderbook order', async () => { + tx = await systems() + .PerpsMarket.connect(keeper()) + .settleBookOrders(ethMarketId, [ + { + accountId: 2, + sizeDelta: bn(1), + orderPrice: bn(1050), + signedPriceData: '0x', + trackingCode: ethers.utils.formatBytes32String(''), + }, + { + accountId: 2, + sizeDelta: bn(3), + orderPrice: bn(1100), + signedPriceData: '0x', + trackingCode: ethers.utils.formatBytes32String(''), + }, + { + accountId: 3, + sizeDelta: bn(-5), + orderPrice: bn(1300), + signedPriceData: '0x', + trackingCode: ethers.utils.formatBytes32String(''), + }, + ]); + }); + + it('updates the account size', async () => { + const [, , size] = await systems().PerpsMarket.getOpenPosition(2, ethMarketId); + assertBn.equal(size, bn(4)); + }); + + it('charges fees and deposits them to the RD', async () => { + const balance = await systems().USD.balanceOf(systems().FeeCollectorMock.address); + assertBn.equal(balance, bn(6.08)); + }); + + it('emits account events', async () => { + await assertEvent(tx, 'BookOrderSettled', systems().PerpsMarket); + }); + }); +}); From db2b5c067cde105e4d795c6f1b21497c6e3441b5 Mon Sep 17 00:00:00 2001 From: Daniel Beal Date: Sat, 7 Dec 2024 23:31:15 +0900 Subject: [PATCH 10/15] a position bulk read function on account is needed --- .../interfaces/IPerpsAccountModule.sol | 25 +++++++++++++++ .../contracts/modules/PerpsAccountModule.sol | 32 +++++++++++++++++++ .../contracts/storage/Position.sol | 2 +- .../Account/ModifyCollateral.withdraw.test.ts | 20 +++++++----- 4 files changed, 70 insertions(+), 9 deletions(-) diff --git a/markets/perps-market/contracts/interfaces/IPerpsAccountModule.sol b/markets/perps-market/contracts/interfaces/IPerpsAccountModule.sol index 43e9521f2f..4933c79fd3 100644 --- a/markets/perps-market/contracts/interfaces/IPerpsAccountModule.sol +++ b/markets/perps-market/contracts/interfaces/IPerpsAccountModule.sol @@ -104,6 +104,31 @@ interface IPerpsAccountModule { uint128 marketId ) external view returns (int128 positionSize); + /** + * @notice Position with additional fields returned by the `getAccountFullPositionInfo` function + */ + struct DetailedPosition { + uint128 marketId; + int256 size; + int256 pnl; + int256 accruedFunding; + uint256 chargedInterest; + uint256 currentPrice; + uint256 entryPrice; + uint256 requiredInitialMargin; + uint256 requiredMaintenanceMargin; + string marketName; + string marketSymbol; + } + + /** + * @notice Returns detailed information about all the positions currently opened by an account. + * @param accountId Id of account to get positions for + */ + function getAccountFullPositionInfo( + uint128 accountId + ) external view returns (DetailedPosition[] memory); + /** * @notice Gets the available margin of an account. It can be negative due to pnl. * @param accountId Id of the account. diff --git a/markets/perps-market/contracts/modules/PerpsAccountModule.sol b/markets/perps-market/contracts/modules/PerpsAccountModule.sol index 75f932054b..5f8a078fa5 100644 --- a/markets/perps-market/contracts/modules/PerpsAccountModule.sol +++ b/markets/perps-market/contracts/modules/PerpsAccountModule.sol @@ -21,6 +21,7 @@ import {MathUtil} from "../utils/MathUtil.sol"; import {Flags} from "../utils/Flags.sol"; import {SafeCastU256, SafeCastI256} from "@synthetixio/core-contracts/contracts/utils/SafeCast.sol"; import {PerpsCollateralConfiguration} from "../storage/PerpsCollateralConfiguration.sol"; +import {PerpsMarketConfiguration} from "../storage/PerpsMarketConfiguration.sol"; /** * @title Module to manage accounts @@ -34,6 +35,8 @@ contract PerpsAccountModule is IPerpsAccountModule { using SafeCastI256 for int256; using GlobalPerpsMarket for GlobalPerpsMarket.Data; using PerpsMarketFactory for PerpsMarketFactory.Data; + using Position for Position.Data; + using PerpsMarketConfiguration for PerpsMarketConfiguration.Data; /** * @inheritdoc IPerpsAccountModule @@ -174,6 +177,35 @@ contract PerpsAccountModule is IPerpsAccountModule { positionSize = perpsMarket.positions[accountId].size; } + /** + * @inheritdoc IPerpsAccountModule + */ + function getAccountFullPositionInfo( + uint128 accountId + ) external view override returns (DetailedPosition[] memory detailedPositions) { + PerpsAccount.MemoryContext memory ctx = PerpsAccount.load(accountId).getOpenPositionsAndCurrentPrices( + PerpsPrice.Tolerance.DEFAULT + ); + + detailedPositions = new DetailedPosition[](ctx.positions.length); + for (uint256 i = 0;i < ctx.positions.length;i++) { + detailedPositions[i].size = ctx.positions[i].size; + detailedPositions[i].currentPrice = ctx.prices[i]; + (, detailedPositions[i].pnl, , detailedPositions[i].chargedInterest, detailedPositions[i].accruedFunding, , ) = + ctx.positions[i].getPositionData(ctx.prices[i]); + + // NOTE: we consider the position to be re-entered when it is interacted with + detailedPositions[i].entryPrice = ctx.positions[i].latestInteractionPrice; + + (, , detailedPositions[i].requiredInitialMargin, detailedPositions[i].requiredMaintenanceMargin) = PerpsMarketConfiguration.load(ctx.positions[i].marketId) + .calculateRequiredMargins(ctx.positions[i].size, ctx.prices[i]); + PerpsMarket.Data storage market = PerpsMarket.load(ctx.positions[i].marketId); + detailedPositions[i].marketId = ctx.positions[i].marketId; + detailedPositions[i].marketName = market.name; + detailedPositions[i].marketSymbol = market.symbol; + } + } + /** * @inheritdoc IPerpsAccountModule */ diff --git a/markets/perps-market/contracts/storage/Position.sol b/markets/perps-market/contracts/storage/Position.sol index 7578a77497..c8acf9c121 100644 --- a/markets/perps-market/contracts/storage/Position.sol +++ b/markets/perps-market/contracts/storage/Position.sol @@ -37,7 +37,7 @@ library Position { } function getPositionData( - Data storage self, + Data memory self, uint256 price ) internal diff --git a/markets/perps-market/test/integration/Account/ModifyCollateral.withdraw.test.ts b/markets/perps-market/test/integration/Account/ModifyCollateral.withdraw.test.ts index b14dcd837d..142d0edc89 100644 --- a/markets/perps-market/test/integration/Account/ModifyCollateral.withdraw.test.ts +++ b/markets/perps-market/test/integration/Account/ModifyCollateral.withdraw.test.ts @@ -294,16 +294,20 @@ describe('ModifyCollateral Withdraw', () => { ); }); it('has correct pnl, given our position changed the skew', async () => { - const [btcPnl] = await systems().PerpsMarket.getOpenPosition( - trader1AccountId, - perpsMarkets()[0].marketId() + const openPositions = + await systems().PerpsMarket.getAccountFullPositionInfo(trader1AccountId); + assertBn.equal( + openPositions.find( + (p: any) => p.marketId.toNumber() === perpsMarkets()[0].marketId().toNumber() + )!.pnl, + bn(-600) ); - assertBn.equal(btcPnl, bn(-600)); - const [ethPnl] = await systems().PerpsMarket.getOpenPosition( - trader1AccountId, - perpsMarkets()[1].marketId() + assertBn.equal( + openPositions.find( + (p: any) => p.marketId.toNumber() === perpsMarkets()[1].marketId().toNumber() + )!.pnl, + bn(-400) ); - assertBn.equal(ethPnl, bn(-400)); }); it('has correct available margin', async () => { assertBn.equal( From 60a96bb2b381674dfe5dd2cdfdfc0f79563c9dd6 Mon Sep 17 00:00:00 2001 From: Daniel Beal Date: Mon, 9 Dec 2024 20:41:10 +0900 Subject: [PATCH 11/15] more orderbook integration --- markets/perps-market/cannonfile.test.toml | 6 +- markets/perps-market/cannonfile.toml | 2 +- .../contracts/interfaces/IBookOrderModule.sol | 6 +- .../interfaces/IPerpsAccountModule.sol | 42 +-- .../contracts/modules/BookOrderModule.sol | 161 ++++----- .../contracts/modules/PerpsAccountModule.sol | 56 ++-- markets/perps-market/storage.dump.json | 306 +++++------------- 7 files changed, 228 insertions(+), 351 deletions(-) diff --git a/markets/perps-market/cannonfile.test.toml b/markets/perps-market/cannonfile.test.toml index 3353348d30..3be795a16d 100644 --- a/markets/perps-market/cannonfile.test.toml +++ b/markets/perps-market/cannonfile.test.toml @@ -14,11 +14,11 @@ defaultValue = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266" defaultValue = "" [setting.synthetixPackage] -defaultValue = "synthetix:<%= package.version %>-testable" +defaultValue = "synthetix:3.9.1-testable" depends = [] [setting.spotMarketPackage] -defaultValue = "synthetix-spot-market:<%= package.version %>-testable" +defaultValue = "synthetix-spot-market:3.9.1-testable" depends = [] [import.synthetix] @@ -82,7 +82,7 @@ contracts = [ "PerpsAccountModule", "PerpsMarketModule", "AsyncOrderModule", - "BookOrderModule", + "BookOrderModule", "AsyncOrderSettlementPythModule", "AsyncOrderCancelModule", "FeatureFlagModule", diff --git a/markets/perps-market/cannonfile.toml b/markets/perps-market/cannonfile.toml index 90d35876b8..16004de25e 100644 --- a/markets/perps-market/cannonfile.toml +++ b/markets/perps-market/cannonfile.toml @@ -88,7 +88,7 @@ contracts = [ "PerpsAccountModule", "PerpsMarketModule", "AsyncOrderModule", - "BookOrderModule", + "BookOrderModule", "AsyncOrderSettlementPythModule", "AsyncOrderCancelModule", "FeatureFlagModule", diff --git a/markets/perps-market/contracts/interfaces/IBookOrderModule.sol b/markets/perps-market/contracts/interfaces/IBookOrderModule.sol index 18e951b8ab..fd48a4d605 100644 --- a/markets/perps-market/contracts/interfaces/IBookOrderModule.sol +++ b/markets/perps-market/contracts/interfaces/IBookOrderModule.sol @@ -36,7 +36,11 @@ interface IBookOrderModule { bytes32 trackingCode; } - event BookOrderSettled(uint128 indexed marketId, BookOrder[] orders, uint256 totalCollectedFees); + event BookOrderSettled( + uint128 indexed marketId, + BookOrder[] orders, + uint256 totalCollectedFees + ); /** * @notice Indicates a summary as to the operation state of a subbmitted order for settlement diff --git a/markets/perps-market/contracts/interfaces/IPerpsAccountModule.sol b/markets/perps-market/contracts/interfaces/IPerpsAccountModule.sol index 4933c79fd3..704bad2676 100644 --- a/markets/perps-market/contracts/interfaces/IPerpsAccountModule.sol +++ b/markets/perps-market/contracts/interfaces/IPerpsAccountModule.sol @@ -104,27 +104,27 @@ interface IPerpsAccountModule { uint128 marketId ) external view returns (int128 positionSize); - /** - * @notice Position with additional fields returned by the `getAccountFullPositionInfo` function - */ - struct DetailedPosition { - uint128 marketId; - int256 size; - int256 pnl; - int256 accruedFunding; - uint256 chargedInterest; - uint256 currentPrice; - uint256 entryPrice; - uint256 requiredInitialMargin; - uint256 requiredMaintenanceMargin; - string marketName; - string marketSymbol; - } - - /** - * @notice Returns detailed information about all the positions currently opened by an account. - * @param accountId Id of account to get positions for - */ + /** + * @notice Position with additional fields returned by the `getAccountFullPositionInfo` function + */ + struct DetailedPosition { + uint128 marketId; + int256 size; + int256 pnl; + int256 accruedFunding; + uint256 chargedInterest; + uint256 currentPrice; + uint256 entryPrice; + uint256 requiredInitialMargin; + uint256 requiredMaintenanceMargin; + string marketName; + string marketSymbol; + } + + /** + * @notice Returns detailed information about all the positions currently opened by an account. + * @param accountId Id of account to get positions for + */ function getAccountFullPositionInfo( uint128 accountId ) external view returns (DetailedPosition[] memory); diff --git a/markets/perps-market/contracts/modules/BookOrderModule.sol b/markets/perps-market/contracts/modules/BookOrderModule.sol index 95660c936d..d50fdc47d3 100644 --- a/markets/perps-market/contracts/modules/BookOrderModule.sol +++ b/markets/perps-market/contracts/modules/BookOrderModule.sol @@ -79,11 +79,11 @@ contract BookOrderModule is IBookOrderModule, IAccountEvents, IMarketEvents { uint256 orderFee; int256 sizeDelta; uint256 orderCount; - uint256 price; + uint256 price; } - event DoneLoop(uint128 accountId); - event ItsGreater(uint128 accountId, uint128 cmpAccountId); + event DoneLoop(uint128 accountId); + event ItsGreater(uint128 accountId, uint128 cmpAccountId); /** * @inheritdoc IBookOrderModule @@ -96,13 +96,13 @@ contract BookOrderModule is IBookOrderModule, IAccountEvents, IMarketEvents { PerpsMarket.Data storage market = PerpsMarket.loadValid(marketId); // loop 1: figure out the big picture change on the market - uint256 marketSkewScale = PerpsMarketConfiguration.load(marketId).skewScale; - { - int256 newMarketSkew = market.skew; - for (uint256 i = 0; i < orders.length; i++) { - newMarketSkew += orders[i].sizeDelta; - } - } + uint256 marketSkewScale = PerpsMarketConfiguration.load(marketId).skewScale; + { + int256 newMarketSkew = market.skew; + for (uint256 i = 0; i < orders.length; i++) { + newMarketSkew += orders[i].sizeDelta; + } + } // TODO: verify total market size (?) @@ -113,18 +113,23 @@ contract BookOrderModule is IBookOrderModule, IAccountEvents, IMarketEvents { uint256 totalCollectedFees; for (uint256 i = 0; i < orders.length; i++) { if (orders[i].accountId > ctx.accountId) { - curPosition.latestInteractionPrice = accumOrderData.price.to128(); - totalCollectedFees += _applyAggregatedAccountPosition(marketId, ctx, curPosition, accumOrderData); + curPosition.latestInteractionPrice = accumOrderData.price.to128(); + totalCollectedFees += _applyAggregatedAccountPosition( + marketId, + ctx, + curPosition, + accumOrderData + ); // load the new account GlobalPerpsMarket.load().checkLiquidation(orders[i].accountId); ctx = PerpsAccount.load(orders[i].accountId).getOpenPositionsAndCurrentPrices( PerpsPrice.Tolerance.DEFAULT ); - // todo: is the below line necessary? in the tests I have been finding it is - ctx.accountId = orders[i].accountId; + // todo: is the below line necessary? in the tests I have been finding it is + ctx.accountId = orders[i].accountId; accumOrderData = AccumulatedOrderData(0, 0, 0, 0); - curPosition = market.positions[ctx.accountId]; + curPosition = market.positions[ctx.accountId]; } else if (orders[i].accountId < ctx.accountId) { // order ids must be supplied in strictly ascending order revert ParameterError.InvalidParameter( @@ -135,14 +140,24 @@ contract BookOrderModule is IBookOrderModule, IAccountEvents, IMarketEvents { curPosition.size += orders[i].sizeDelta; accumOrderData.sizeDelta += orders[i].sizeDelta; - accumOrderData.orderFee += market.calculateOrderFee(orders[i].sizeDelta, orders[i].orderPrice); - - // the first received price for the orders for an account will be used as the settling price for the previous order. Least gamable that way. - accumOrderData.price = accumOrderData.price == 0 ? orders[i].orderPrice : accumOrderData.price; + accumOrderData.orderFee += market.calculateOrderFee( + orders[i].sizeDelta, + orders[i].orderPrice + ); + + // the first received price for the orders for an account will be used as the settling price for the previous order. Least gamable that way. + accumOrderData.price = accumOrderData.price == 0 + ? orders[i].orderPrice + : accumOrderData.price; } - curPosition.latestInteractionPrice = accumOrderData.price.to128(); - totalCollectedFees += _applyAggregatedAccountPosition(marketId, ctx, curPosition, accumOrderData); + curPosition.latestInteractionPrice = accumOrderData.price.to128(); + totalCollectedFees += _applyAggregatedAccountPosition( + marketId, + ctx, + curPosition, + accumOrderData + ); // send collected fees to the fee collector and etc. GlobalPerpsMarketConfiguration.load().collectFees( @@ -151,58 +166,60 @@ contract BookOrderModule is IBookOrderModule, IAccountEvents, IMarketEvents { PerpsMarketFactory.load() ); - emit BookOrderSettled(marketId, orders, totalCollectedFees); + emit BookOrderSettled(marketId, orders, totalCollectedFees); } - function _applyAggregatedAccountPosition(uint128 marketId, PerpsAccount.MemoryContext memory ctx, Position.Data memory pos, AccumulatedOrderData memory accumOrderData) internal returns (uint256) { - if (ctx.accountId == 0) { - return 0; - } - Position.Data memory oldPosition = PerpsMarket.load(marketId).positions[ctx.accountId]; - // charge the funding fee from the previously held position, the order fee, and whatever pnl has been accumulated from the last position. - (int256 pnl, , uint256 chargedInterest, int256 accruedFunding, , ) = - oldPosition.getPnl(accumOrderData.price); - - PerpsAccount.load(ctx.accountId).charge( - pnl - accumOrderData.orderFee.toInt() - ); - - emit AccountCharged(ctx.accountId, pnl - accumOrderData.orderFee.toInt(), PerpsAccount.load(ctx.accountId).debt); - - MarketUpdate.Data memory updateData; - { - PerpsMarket.Data storage market = PerpsMarket.load(marketId); - - // we recompute to the price of the first order the user set. if they set multiple trades in te timeframe, its as if they fully close their order for a short period of time - // between the first order and the last order they place - market.recomputeFunding(accumOrderData.price); - - // skip verifications for the account having minimum collateral. - // this is because they are undertaken by the orderbook and cancelling them would be unnecessary complication - // commit order to the user's account - updateData = market.updatePositionData( - ctx.accountId, - pos - ); - } - - PerpsAccount.load(ctx.accountId).updateOpenPositions( - marketId, - pos.size - ); - - emit MarketUpdated( - updateData.marketId, - accumOrderData.price, - updateData.skew, - PerpsMarket.load(marketId).size, - pos.size - oldPosition.size, - updateData.currentFundingRate, - updateData.currentFundingVelocity, - updateData.interestRate - ); - - return accumOrderData.orderFee; + function _applyAggregatedAccountPosition( + uint128 marketId, + PerpsAccount.MemoryContext memory ctx, + Position.Data memory pos, + AccumulatedOrderData memory accumOrderData + ) internal returns (uint256) { + if (ctx.accountId == 0) { + return 0; + } + Position.Data memory oldPosition = PerpsMarket.load(marketId).positions[ctx.accountId]; + // charge the funding fee from the previously held position, the order fee, and whatever pnl has been accumulated from the last position. + (int256 pnl, , uint256 chargedInterest, int256 accruedFunding, , ) = oldPosition.getPnl( + accumOrderData.price + ); + + PerpsAccount.load(ctx.accountId).charge(pnl - accumOrderData.orderFee.toInt()); + + emit AccountCharged( + ctx.accountId, + pnl - accumOrderData.orderFee.toInt(), + PerpsAccount.load(ctx.accountId).debt + ); + + MarketUpdate.Data memory updateData; + { + PerpsMarket.Data storage market = PerpsMarket.load(marketId); + + // we recompute to the price of the first order the user set. if they set multiple trades in te timeframe, its as if they fully close their order for a short period of time + // between the first order and the last order they place + market.recomputeFunding(accumOrderData.price); + + // skip verifications for the account having minimum collateral. + // this is because they are undertaken by the orderbook and cancelling them would be unnecessary complication + // commit order to the user's account + updateData = market.updatePositionData(ctx.accountId, pos); + } + + PerpsAccount.load(ctx.accountId).updateOpenPositions(marketId, pos.size); + + emit MarketUpdated( + updateData.marketId, + accumOrderData.price, + updateData.skew, + PerpsMarket.load(marketId).size, + pos.size - oldPosition.size, + updateData.currentFundingRate, + updateData.currentFundingVelocity, + updateData.interestRate + ); + + return accumOrderData.orderFee; emit InterestCharged(ctx.accountId, chargedInterest); @@ -221,5 +238,5 @@ contract BookOrderModule is IBookOrderModule, IAccountEvents, IMarketEvents { "", // TODO: tracking code, may not have ever ERC2771Context._msgSender() ); - } + } } diff --git a/markets/perps-market/contracts/modules/PerpsAccountModule.sol b/markets/perps-market/contracts/modules/PerpsAccountModule.sol index 5f8a078fa5..381f3eb4e5 100644 --- a/markets/perps-market/contracts/modules/PerpsAccountModule.sol +++ b/markets/perps-market/contracts/modules/PerpsAccountModule.sol @@ -183,27 +183,41 @@ contract PerpsAccountModule is IPerpsAccountModule { function getAccountFullPositionInfo( uint128 accountId ) external view override returns (DetailedPosition[] memory detailedPositions) { - PerpsAccount.MemoryContext memory ctx = PerpsAccount.load(accountId).getOpenPositionsAndCurrentPrices( - PerpsPrice.Tolerance.DEFAULT - ); - - detailedPositions = new DetailedPosition[](ctx.positions.length); - for (uint256 i = 0;i < ctx.positions.length;i++) { - detailedPositions[i].size = ctx.positions[i].size; - detailedPositions[i].currentPrice = ctx.prices[i]; - (, detailedPositions[i].pnl, , detailedPositions[i].chargedInterest, detailedPositions[i].accruedFunding, , ) = - ctx.positions[i].getPositionData(ctx.prices[i]); - - // NOTE: we consider the position to be re-entered when it is interacted with - detailedPositions[i].entryPrice = ctx.positions[i].latestInteractionPrice; - - (, , detailedPositions[i].requiredInitialMargin, detailedPositions[i].requiredMaintenanceMargin) = PerpsMarketConfiguration.load(ctx.positions[i].marketId) - .calculateRequiredMargins(ctx.positions[i].size, ctx.prices[i]); - PerpsMarket.Data storage market = PerpsMarket.load(ctx.positions[i].marketId); - detailedPositions[i].marketId = ctx.positions[i].marketId; - detailedPositions[i].marketName = market.name; - detailedPositions[i].marketSymbol = market.symbol; - } + PerpsAccount.MemoryContext memory ctx = PerpsAccount + .load(accountId) + .getOpenPositionsAndCurrentPrices(PerpsPrice.Tolerance.DEFAULT); + + detailedPositions = new DetailedPosition[](ctx.positions.length); + for (uint256 i = 0; i < ctx.positions.length; i++) { + detailedPositions[i].size = ctx.positions[i].size; + detailedPositions[i].currentPrice = ctx.prices[i]; + ( + , + detailedPositions[i].pnl, + , + detailedPositions[i].chargedInterest, + detailedPositions[i].accruedFunding, + , + + ) = ctx.positions[i].getPositionData(ctx.prices[i]); + + // NOTE: we consider the position to be re-entered when it is interacted with + detailedPositions[i].entryPrice = ctx.positions[i].latestInteractionPrice; + + ( + , + , + detailedPositions[i].requiredInitialMargin, + detailedPositions[i].requiredMaintenanceMargin + ) = PerpsMarketConfiguration.load(ctx.positions[i].marketId).calculateRequiredMargins( + ctx.positions[i].size, + ctx.prices[i] + ); + PerpsMarket.Data storage market = PerpsMarket.load(ctx.positions[i].marketId); + detailedPositions[i].marketId = ctx.positions[i].marketId; + detailedPositions[i].marketName = market.name; + detailedPositions[i].marketSymbol = market.symbol; + } } /** diff --git a/markets/perps-market/storage.dump.json b/markets/perps-market/storage.dump.json index dd3c60c9d6..9de8cc2992 100644 --- a/markets/perps-market/storage.dump.json +++ b/markets/perps-market/storage.dump.json @@ -1,63 +1,35 @@ { - "contracts/modules/LiquidationModule.sol:LiquidationModule": { - "name": "LiquidationModule", + "contracts/modules/BookOrderModule.sol:BookOrderModule": { + "name": "BookOrderModule", "kind": "contract", "structs": { - "LiquidateAccountRuntime": [ + "AccumulatedOrderData": [ { - "type": "uint128", - "name": "accountId", - "size": 16, + "type": "uint256", + "name": "orderFee", + "size": 32, "slot": "0", "offset": 0 }, { - "type": "uint256", - "name": "totalFlaggingRewards", + "type": "int256", + "name": "sizeDelta", "size": 32, "slot": "1", "offset": 0 }, { "type": "uint256", - "name": "totalLiquidated", + "name": "orderCount", "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", + "slot": "3", "offset": 0 } ] @@ -163,200 +135,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 +688,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 e8e5d145626d597c8d6a87f0d05cb78b8b1645f4 Mon Sep 17 00:00:00 2001 From: Daniel Beal Date: Thu, 19 Dec 2024 12:03:09 +0900 Subject: [PATCH 12/15] fix some state bugs in the new modules --- .../contracts/modules/BookOrderModule.sol | 13 ++++++++++--- .../perps-market/contracts/storage/PerpsAccount.sol | 3 ++- markets/perps-market/package.json | 2 +- yarn.lock | 6 ++++++ 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/markets/perps-market/contracts/modules/BookOrderModule.sol b/markets/perps-market/contracts/modules/BookOrderModule.sol index d50fdc47d3..d5aa31a7b9 100644 --- a/markets/perps-market/contracts/modules/BookOrderModule.sol +++ b/markets/perps-market/contracts/modules/BookOrderModule.sol @@ -121,8 +121,15 @@ contract BookOrderModule is IBookOrderModule, IAccountEvents, IMarketEvents { accumOrderData ); - // load the new account GlobalPerpsMarket.load().checkLiquidation(orders[i].accountId); + + // load and verify existance of the new account + if (PerpsAccount.load(orders[i].accountId).id == 0) { + // TODO: what to do if account doesnt exist + // for now to make debugging easy accounts can be created out of thin air + PerpsAccount.load(orders[i].accountId).id = orders[i].accountId; + } + ctx = PerpsAccount.load(orders[i].accountId).getOpenPositionsAndCurrentPrices( PerpsPrice.Tolerance.DEFAULT ); @@ -219,8 +226,6 @@ contract BookOrderModule is IBookOrderModule, IAccountEvents, IMarketEvents { updateData.interestRate ); - return accumOrderData.orderFee; - emit InterestCharged(ctx.accountId, chargedInterest); emit OrderSettled( @@ -238,5 +243,7 @@ contract BookOrderModule is IBookOrderModule, IAccountEvents, IMarketEvents { "", // TODO: tracking code, may not have ever ERC2771Context._msgSender() ); + + return accumOrderData.orderFee; } } diff --git a/markets/perps-market/contracts/storage/PerpsAccount.sol b/markets/perps-market/contracts/storage/PerpsAccount.sol index 14799f32b6..46ea0feccc 100644 --- a/markets/perps-market/contracts/storage/PerpsAccount.sol +++ b/markets/perps-market/contracts/storage/PerpsAccount.sol @@ -417,6 +417,7 @@ library PerpsAccount { PerpsPrice.Tolerance stalenessTolerance ) internal view returns (MemoryContext memory ctx) { uint256[] memory marketIds = self.openPositionMarketIds.values(); + uint128 accountId = self.id; ctx = MemoryContext( self.id, stalenessTolerance, @@ -424,7 +425,7 @@ library PerpsAccount { PerpsPrice.getCurrentPrices(marketIds, stalenessTolerance) ); for (uint256 i = 0; i < ctx.positions.length; i++) { - ctx.positions[i] = PerpsMarket.load(marketIds[i].to128()).positions[self.id]; + ctx.positions[i] = PerpsMarket.load(marketIds[i].to128()).positions[accountId]; } } diff --git a/markets/perps-market/package.json b/markets/perps-market/package.json index dbed731579..c251af370c 100644 --- a/markets/perps-market/package.json +++ b/markets/perps-market/package.json @@ -1,6 +1,6 @@ { "name": "@synthetixio/perps-market", - "version": "3.10.1", + "version": "3.11.0-orderbook", "description": "Perps Market implementation", "publishConfig": { "access": "public" diff --git a/yarn.lock b/yarn.lock index 34d3e52a67..89d04a3b68 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8307,6 +8307,12 @@ __metadata: languageName: node linkType: hard +"forge-std@workspace:markets/treasury-market/lib/forge-std": + version: 0.0.0-use.local + resolution: "forge-std@workspace:markets/treasury-market/lib/forge-std" + languageName: unknown + linkType: soft + "form-data-encoder@npm:^2.1.2": version: 2.1.4 resolution: "form-data-encoder@npm:2.1.4" From a2f49b86b476f0338825d647d3f7e092b460add6 Mon Sep 17 00:00:00 2001 From: Daniel Beal Date: Mon, 27 Jan 2025 14:25:52 +0900 Subject: [PATCH 13/15] remove unnecessary files, fix yarn --- markets/treasury-market/storage.dump.json | 38 ----------------------- yarn.lock | 6 ---- 2 files changed, 44 deletions(-) delete mode 100644 markets/treasury-market/storage.dump.json diff --git a/markets/treasury-market/storage.dump.json b/markets/treasury-market/storage.dump.json deleted file mode 100644 index e97cfc0f34..0000000000 --- a/markets/treasury-market/storage.dump.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "contracts/TreasuryMarket.sol:TreasuryMarket": { - "name": "TreasuryMarket", - "kind": "contract", - "structs": { - "LoanInfo": [ - { - "type": "uint64", - "name": "startTime", - "size": 8, - "slot": "0", - "offset": 0 - }, - { - "type": "uint32", - "name": "power", - "size": 4, - "slot": "0", - "offset": 8 - }, - { - "type": "uint32", - "name": "duration", - "size": 4, - "slot": "0", - "offset": 12 - }, - { - "type": "uint128", - "name": "loanAmount", - "size": 16, - "slot": "0", - "offset": 16 - } - ] - } - } -} diff --git a/yarn.lock b/yarn.lock index 89d04a3b68..34d3e52a67 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8307,12 +8307,6 @@ __metadata: languageName: node linkType: hard -"forge-std@workspace:markets/treasury-market/lib/forge-std": - version: 0.0.0-use.local - resolution: "forge-std@workspace:markets/treasury-market/lib/forge-std" - languageName: unknown - linkType: soft - "form-data-encoder@npm:^2.1.2": version: 2.1.4 resolution: "form-data-encoder@npm:2.1.4" From e28eb2abd52829cf16e5ed5001c8014c648c5d87 Mon Sep 17 00:00:00 2001 From: Daniel Beal Date: Mon, 27 Jan 2025 15:16:42 +0900 Subject: [PATCH 14/15] fix test results --- .../Account/ModifyCollateral.withdraw.test.ts | 9 ++++++--- .../test/integration/Orders/BookOrder.test.ts | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/markets/perps-market/test/integration/Account/ModifyCollateral.withdraw.test.ts b/markets/perps-market/test/integration/Account/ModifyCollateral.withdraw.test.ts index efd469880e..db43672b9f 100644 --- a/markets/perps-market/test/integration/Account/ModifyCollateral.withdraw.test.ts +++ b/markets/perps-market/test/integration/Account/ModifyCollateral.withdraw.test.ts @@ -297,9 +297,12 @@ describe('ModifyCollateral Withdraw', () => { const [collateralIds, collateralAmounts, debt] = await systems().PerpsMarket.getAccountAllCollateralAmounts(trader1AccountId); - expect(collateralIds).toHaveLength(1); - expect(collateralAmounts).toEqual([]); - expect(debt).toEqual(0); + assertBn.equal(collateralIds.length, 2); + assertBn.equal(collateralIds[0], 0); + assertBn.equal(collateralIds[1], 2); + assertBn.equal(collateralAmounts[0], '18000000000000000000000'); + assertBn.equal(collateralAmounts[1], '1000000000000000000'); + assertBn.equal(debt, 0); }); it('has correct pnl, given our position changed the skew', async () => { const openPositions = diff --git a/markets/perps-market/test/integration/Orders/BookOrder.test.ts b/markets/perps-market/test/integration/Orders/BookOrder.test.ts index 920a78277a..a2fb35dceb 100644 --- a/markets/perps-market/test/integration/Orders/BookOrder.test.ts +++ b/markets/perps-market/test/integration/Orders/BookOrder.test.ts @@ -64,7 +64,7 @@ describe('Settle Orderbook order', () => { await depositCollateral({ systems, - trader: trader1, + trader: trader2, accountId: () => 3, collaterals: [ { From 4280d13c056a3980c531cd44f28cd48f3d2548cd Mon Sep 17 00:00:00 2001 From: Daniel Beal Date: Fri, 31 Jan 2025 22:26:59 +0900 Subject: [PATCH 15/15] add function to get the current order mode also, reduce time to 15 seconds for now --- .../contracts/interfaces/IBookOrderModule.sol | 14 +++++++++ .../contracts/modules/BookOrderModule.sol | 13 ++++++++- .../contracts/storage/PerpsAccount.sol | 2 +- .../test/integration/Orders/BookOrder.test.ts | 29 +++++++++++++++++++ 4 files changed, 56 insertions(+), 2 deletions(-) diff --git a/markets/perps-market/contracts/interfaces/IBookOrderModule.sol b/markets/perps-market/contracts/interfaces/IBookOrderModule.sol index fd48a4d605..0988131598 100644 --- a/markets/perps-market/contracts/interfaces/IBookOrderModule.sol +++ b/markets/perps-market/contracts/interfaces/IBookOrderModule.sol @@ -60,6 +60,20 @@ interface IBookOrderModule { OrderStatus status; } + /** + * @notice Set the current order mode to BOOK + * @param accountId the account id to set to BOOK + * @param useBook whether or not to set hte mode to BOOK. If not BOOK, it will be ONCHAIN + */ + function setBookMode(uint128 accountId, bool useBook) external; + + /** + * @notice Get the current order mode + * @param accountId the account id to pull data for + * @return the current order mode + */ + function getOrderMode(uint128 accountId) external view returns (bytes16); + /** * @notice Called by the offchain orderbook to settle a prevoiusly placed order onchain. Any orders submitted to this function will be processed as if they happened simultaneously, at the prices given by the orderbook. * If an order is found to be unfillable (ex. insufficient account liquidity), it will be returned in the `statuses` return field. diff --git a/markets/perps-market/contracts/modules/BookOrderModule.sol b/markets/perps-market/contracts/modules/BookOrderModule.sol index 29a74acc5f..79da920f2e 100644 --- a/markets/perps-market/contracts/modules/BookOrderModule.sol +++ b/markets/perps-market/contracts/modules/BookOrderModule.sol @@ -89,7 +89,10 @@ contract BookOrderModule is IBookOrderModule, IAccountEvents, IMarketEvents { error IncorrectAccountMode(uint128 accountId, bytes16 mode); - function setBookMode(uint128 accountId, bool useBook) external { + /** + * @inheritdoc IBookOrderModule + */ + function setBookMode(uint128 accountId, bool useBook) external override { FeatureFlag.ensureAccessToFeature(Flags.PERPS_SYSTEM); Account.exists(accountId); @@ -107,6 +110,14 @@ contract BookOrderModule is IBookOrderModule, IAccountEvents, IMarketEvents { emit AccountOrderModeChanged(accountId, "BOOK"); } + /** + * @inheritdoc IBookOrderModule + */ + function getOrderMode(uint128 accountId) external view override returns (bytes16) { + PerpsAccount.Data storage perpsAccount = PerpsAccount.load(accountId); + return perpsAccount.getOrderMode(); + } + /** * @inheritdoc IBookOrderModule */ diff --git a/markets/perps-market/contracts/storage/PerpsAccount.sol b/markets/perps-market/contracts/storage/PerpsAccount.sol index 5b7290c609..8edb9b2e25 100644 --- a/markets/perps-market/contracts/storage/PerpsAccount.sol +++ b/markets/perps-market/contracts/storage/PerpsAccount.sol @@ -89,7 +89,7 @@ library PerpsAccount { error NonexistentDebt(uint128 accountId); - uint256 constant ORDER_MODE_CHANGE_GRACE_PERIOD = 180; // 3 minutes + uint256 constant ORDER_MODE_CHANGE_GRACE_PERIOD = 15; // seconds function load(uint128 id) internal pure returns (Data storage account) { bytes32 s = keccak256(abi.encode("io.synthetix.perps-market.Account", id)); diff --git a/markets/perps-market/test/integration/Orders/BookOrder.test.ts b/markets/perps-market/test/integration/Orders/BookOrder.test.ts index a2fb35dceb..254c625ebb 100644 --- a/markets/perps-market/test/integration/Orders/BookOrder.test.ts +++ b/markets/perps-market/test/integration/Orders/BookOrder.test.ts @@ -1,10 +1,12 @@ import { ethers } from 'ethers'; +import assert from 'assert/strict'; import { bn, bootstrapMarkets } from '../bootstrap'; import { snapshotCheckpoint } from '@synthetixio/core-utils/utils/mocha/snapshot'; import { depositCollateral } from '../helpers'; import assertBn from '@synthetixio/core-utils/utils/assertions/assert-bignumber'; import assertEvent from '@synthetixio/core-utils/utils/assertions/assert-event'; import assertRevert from '@synthetixio/core-utils/utils/assertions/assert-revert'; +import { fastForwardTo, getTime } from '@synthetixio/core-utils/utils/hardhat/rpc'; import { wei } from '@synthetixio/wei'; describe('Settle Orderbook order', () => { @@ -91,6 +93,33 @@ describe('Settle Orderbook order', () => { // but it cna be a different address from the actual keeper }); + it('has correct order mode', async () => { + console.log(await systems().PerpsMarket.getOrderMode(3)); + assert( + ethers.utils.parseBytes32String( + (await systems().PerpsMarket.getOrderMode(3)) + '00000000000000000000000000000000' + ) === 'RECENTLY_CHANGED' + ); + assert( + ethers.utils.parseBytes32String( + (await systems().PerpsMarket.getOrderMode(5)) + '00000000000000000000000000000000' + ) === '' + ); + + await fastForwardTo((await getTime(provider())) + 1000, provider()); + + assert( + ethers.utils.parseBytes32String( + (await systems().PerpsMarket.getOrderMode(3)) + '00000000000000000000000000000000' + ) === 'BOOK' + ); + assert( + ethers.utils.parseBytes32String( + (await systems().PerpsMarket.getOrderMode(5)) + '00000000000000000000000000000000' + ) === '' + ); + }); + it('fails when the orders are not increasing account id order', async () => { await assertRevert( systems()