From df26ebd071f7298ba415397244014d5b9ce6635b Mon Sep 17 00:00:00 2001 From: Joey <5688912+bachstatter@users.noreply.github.com> Date: Wed, 5 Jul 2023 12:42:57 +1000 Subject: [PATCH] MarketUpdated event (#1697) * Add tracking code to runtime * Add market size and market skew to * Update test asserting the event * Add MarketUpdated event. Note that for an event to be emitted from a library, it needs to be defined in two places: - The library itself, and - The interface of the contract that is using the library. * We now hit a size limit AsyncOrderModule - Creating a AsyncOrderViewModule * Dont forget to update cannon files * Add currentFundingRate and currentFundingVelocity to MarketUpdated event * Revert IAsyncOrderViewModule * Use AsyncOrderSettlementModule like spot market * Let call site emit MarketUpdated event to avoid emitting events from library --- markets/perps-market/cannonfile.test.toml | 5 + markets/perps-market/cannonfile.toml | 5 + .../interfaces/IAsyncOrderModule.sol | 36 +-- .../IAsyncOrderSettlementModule.sol | 42 ++++ .../contracts/interfaces/IMarketEvents.sol | 13 ++ .../contracts/modules/AsyncOrderModule.sol | 167 -------------- .../modules/AsyncOrderSettlementModule.sol | 215 ++++++++++++++++++ .../contracts/storage/AsyncOrder.sol | 1 + .../contracts/storage/PerpsMarket.sol | 25 +- .../Orders/OffchainAsyncOrder.settle.test.ts | 56 ++++- .../test/integration/helpers/funding-calcs.ts | 22 ++ 11 files changed, 378 insertions(+), 209 deletions(-) create mode 100644 markets/perps-market/contracts/interfaces/IAsyncOrderSettlementModule.sol create mode 100644 markets/perps-market/contracts/interfaces/IMarketEvents.sol create mode 100644 markets/perps-market/contracts/modules/AsyncOrderSettlementModule.sol create mode 100644 markets/perps-market/test/integration/helpers/funding-calcs.ts diff --git a/markets/perps-market/cannonfile.test.toml b/markets/perps-market/cannonfile.test.toml index a0eb043c6c..2cd216bc57 100644 --- a/markets/perps-market/cannonfile.test.toml +++ b/markets/perps-market/cannonfile.test.toml @@ -28,6 +28,9 @@ artifact = "PerpsMarketFactoryModule" [contract.AsyncOrderModule] artifact = "AsyncOrderModule" +[contract.AsyncOrderSettlementModule] +artifact = "AsyncOrderSettlementModule" + [contract.AtomicOrderModule] artifact = "AtomicOrderModule" @@ -73,6 +76,7 @@ contracts = [ "PerpsMarketModule", "AtomicOrderModule", "AsyncOrderModule", + "AsyncOrderSettlementModule", "FeatureFlagModule", "LimitOrderModule", "LiquidationModule", @@ -85,6 +89,7 @@ depends = [ "contract.PerpsMarketFactoryModule", "contract.AtomicOrderModule", "contract.AsyncOrderModule", + "contract.AsyncOrderSettlementModule", "contract.PerpsAccountModule", "contract.PerpsMarketModule", "contract.FeatureFlagModule", diff --git a/markets/perps-market/cannonfile.toml b/markets/perps-market/cannonfile.toml index 5ef089dac4..7607f61041 100644 --- a/markets/perps-market/cannonfile.toml +++ b/markets/perps-market/cannonfile.toml @@ -28,6 +28,9 @@ artifact = "PerpsMarketFactoryModule" [contract.AsyncOrderModule] artifact = "AsyncOrderModule" +[contract.AsyncOrderSettlementModule] +artifact = "AsyncOrderSettlementModule" + [contract.AtomicOrderModule] artifact = "AtomicOrderModule" @@ -73,6 +76,7 @@ contracts = [ "PerpsMarketModule", "AtomicOrderModule", "AsyncOrderModule", + "AsyncOrderSettlementModule", "FeatureFlagModule", "LimitOrderModule", "LiquidationModule", @@ -85,6 +89,7 @@ depends = [ "contract.PerpsMarketFactoryModule", "contract.AtomicOrderModule", "contract.AsyncOrderModule", + "contract.AsyncOrderSettlementModule", "contract.PerpsAccountModule", "contract.PerpsMarketModule", "contract.FeatureFlagModule", diff --git a/markets/perps-market/contracts/interfaces/IAsyncOrderModule.sol b/markets/perps-market/contracts/interfaces/IAsyncOrderModule.sol index 32cfee4eee..5ebfd07873 100644 --- a/markets/perps-market/contracts/interfaces/IAsyncOrderModule.sol +++ b/markets/perps-market/contracts/interfaces/IAsyncOrderModule.sol @@ -20,18 +20,6 @@ interface IAsyncOrderModule { address sender ); - event OrderSettled( - uint128 indexed marketId, - uint128 indexed accountId, - uint256 fillPrice, - int256 accountPnlRealized, - int128 newSize, - uint256 collectedFees, - uint256 settelementReward, - bytes32 indexed trackingCode, - address settler - ); - event OrderCanceled( uint128 indexed marketId, uint128 indexed accountId, @@ -40,35 +28,15 @@ interface IAsyncOrderModule { ); error OrderAlreadyCommitted(uint128 marketId, uint128 accountId); - error SettlementStrategyNotFound(SettlementStrategy.Type strategyType); - error OffchainLookup( - address sender, - string[] urls, - bytes callData, - bytes4 callbackFunction, - bytes extraData - ); function commitOrder( AsyncOrder.OrderCommitmentRequest memory commitment ) external returns (AsyncOrder.Data memory retOrder, uint fees); - function cancelOrder(uint128 marketId, uint128 accountId) external; - - // only used due to stack too deep during settlement - struct SettleOrderRuntime { - uint128 marketId; - uint128 accountId; - int128 newPositionSize; - int256 pnl; - uint256 pnlUint; - uint256 amountToDeposit; - uint256 settlementReward; - bytes32 trackingCode; - } - function getOrder( uint128 marketId, uint128 accountId ) external returns (AsyncOrder.Data memory); + + function cancelOrder(uint128 marketId, uint128 accountId) external; } diff --git a/markets/perps-market/contracts/interfaces/IAsyncOrderSettlementModule.sol b/markets/perps-market/contracts/interfaces/IAsyncOrderSettlementModule.sol new file mode 100644 index 0000000000..012d8da767 --- /dev/null +++ b/markets/perps-market/contracts/interfaces/IAsyncOrderSettlementModule.sol @@ -0,0 +1,42 @@ +//SPDX-License-Identifier: MIT +pragma solidity >=0.8.11 <0.9.0; +import {SettlementStrategy} from "../storage/SettlementStrategy.sol"; + +interface IAsyncOrderSettlementModule { + error SettlementStrategyNotFound(SettlementStrategy.Type strategyType); + error OffchainLookup( + address sender, + string[] urls, + bytes callData, + bytes4 callbackFunction, + bytes extraData + ); + + event OrderSettled( + uint128 indexed marketId, + uint128 indexed accountId, + uint256 fillPrice, + int256 accountPnlRealized, + int128 newSize, + uint256 collectedFees, + uint256 settelementReward, + bytes32 indexed trackingCode, + address settler + ); + + // only used due to stack too deep during settlement + struct SettleOrderRuntime { + uint128 marketId; + uint128 accountId; + int128 newPositionSize; + int256 pnl; + uint256 pnlUint; + uint256 amountToDeposit; + uint256 settlementReward; + bytes32 trackingCode; + } + + function settle(uint128 marketId, uint128 accountId) external view; + + function settlePythOrder(bytes calldata result, bytes calldata extraData) external payable; +} diff --git a/markets/perps-market/contracts/interfaces/IMarketEvents.sol b/markets/perps-market/contracts/interfaces/IMarketEvents.sol new file mode 100644 index 0000000000..4e34731757 --- /dev/null +++ b/markets/perps-market/contracts/interfaces/IMarketEvents.sol @@ -0,0 +1,13 @@ +//SPDX-License-Identifier: MIT +pragma solidity >=0.8.11 <0.9.0; + +interface IMarketEvents { + event MarketUpdated( + uint128 marketId, + int256 skew, + uint256 size, + int256 sizeDelta, + int256 currentFundingRate, + int256 currentFundingVelocity + ); +} diff --git a/markets/perps-market/contracts/modules/AsyncOrderModule.sol b/markets/perps-market/contracts/modules/AsyncOrderModule.sol index 65063ff294..49be03d74e 100644 --- a/markets/perps-market/contracts/modules/AsyncOrderModule.sol +++ b/markets/perps-market/contracts/modules/AsyncOrderModule.sol @@ -34,8 +34,6 @@ contract AsyncOrderModule is IAsyncOrderModule { using SafeCastU256 for uint256; using SafeCastI256 for int256; - int256 public constant PRECISION = 18; - function commitOrder( AsyncOrder.OrderCommitmentRequest memory commitment ) external override returns (AsyncOrder.Data memory retOrder, uint fees) { @@ -86,46 +84,6 @@ contract AsyncOrderModule is IAsyncOrderModule { return (order, feesAccrued); } - function settle(uint128 marketId, uint128 accountId) external view { - GlobalPerpsMarket.load().checkLiquidation(accountId); - ( - AsyncOrder.Data storage order, - SettlementStrategy.Data storage settlementStrategy - ) = _performOrderValidityChecks(marketId, accountId); - - _settleOffchain(order, settlementStrategy); - } - - function settlePythOrder(bytes calldata result, bytes calldata extraData) external payable { - (uint128 marketId, uint128 asyncOrderId) = abi.decode(extraData, (uint128, uint128)); - ( - AsyncOrder.Data storage order, - SettlementStrategy.Data storage settlementStrategy - ) = _performOrderValidityChecks(marketId, asyncOrderId); - - bytes32[] memory priceIds = new bytes32[](1); - priceIds[0] = settlementStrategy.feedId; - - bytes[] memory updateData = new bytes[](1); - updateData[0] = result; - - IPythVerifier.PriceFeed[] memory priceFeeds = IPythVerifier( - settlementStrategy.priceVerificationContract - ).parsePriceFeedUpdates{value: msg.value}( - updateData, - priceIds, - order.settlementTime.to64(), - (order.settlementTime + settlementStrategy.priceWindowDuration).to64() - ); - - IPythVerifier.PriceFeed memory pythData = priceFeeds[0]; - uint offchainPrice = _getScaledPrice(pythData.price.price, pythData.price.expo).toUint(); - - settlementStrategy.checkPriceDeviation(offchainPrice, PerpsPrice.getCurrentPrice(marketId)); - - _settleOrder(offchainPrice, order, settlementStrategy); - } - function getOrder( uint128 marketId, uint128 accountId @@ -143,129 +101,4 @@ contract AsyncOrderModule is IAsyncOrderModule { order.reset(); emit OrderCanceled(marketId, accountId, order.settlementTime, order.acceptablePrice); } - - function _settleOffchain( - AsyncOrder.Data storage asyncOrder, - SettlementStrategy.Data storage settlementStrategy - ) private view returns (uint, int256, uint256) { - string[] memory urls = new string[](1); - urls[0] = settlementStrategy.url; - - bytes4 selector; - if (settlementStrategy.strategyType == SettlementStrategy.Type.PYTH) { - selector = AsyncOrderModule.settlePythOrder.selector; - } else { - revert SettlementStrategyNotFound(settlementStrategy.strategyType); - } - - // see EIP-3668: https://eips.ethereum.org/EIPS/eip-3668 - revert OffchainLookup( - address(this), - urls, - abi.encodePacked(settlementStrategy.feedId, _getTimeInBytes(asyncOrder.settlementTime)), - selector, - abi.encode(asyncOrder.marketId, asyncOrder.accountId) // extraData that gets sent to callback for validation - ); - } - - function _settleOrder( - uint256 price, - AsyncOrder.Data storage asyncOrder, - SettlementStrategy.Data storage settlementStrategy - ) private { - SettleOrderRuntime memory runtime; - - runtime.accountId = asyncOrder.accountId; - runtime.marketId = asyncOrder.marketId; - - // check if account is flagged - GlobalPerpsMarket.load().checkLiquidation(runtime.accountId); - ( - Position.Data memory newPosition, - uint totalFees, - uint fillPrice, - Position.Data storage oldPosition - ) = asyncOrder.validateOrder(settlementStrategy, price); - - runtime.newPositionSize = newPosition.size; - - PerpsMarketFactory.Data storage factory = PerpsMarketFactory.load(); - PerpsAccount.Data storage perpsAccount = PerpsAccount.load(runtime.accountId); - // use fill price to calculate realized pnl - (runtime.pnl, , , ) = oldPosition.getPnl(fillPrice); - - runtime.pnlUint = MathUtil.abs(runtime.pnl); - if (runtime.pnl > 0) { - factory.synthetix.withdrawMarketUsd(runtime.marketId, address(this), runtime.pnlUint); - perpsAccount.addCollateralAmount(SNX_USD_MARKET_ID, runtime.pnlUint); - } else if (runtime.pnl < 0) { - perpsAccount.deductFromAccount(runtime.pnlUint); - runtime.amountToDeposit = runtime.pnlUint; - // all gets deposited below with fees - } - - // after pnl is realized, update position - PerpsMarket.loadValid(runtime.marketId).updatePositionData(runtime.accountId, newPosition); - - perpsAccount.updatePositionMarkets(runtime.marketId, runtime.newPositionSize); - perpsAccount.deductFromAccount(totalFees); - - runtime.settlementReward = settlementStrategy.settlementReward; - runtime.amountToDeposit += totalFees - runtime.settlementReward; - if (runtime.settlementReward > 0) { - // pay keeper - factory.usdToken.transfer(msg.sender, runtime.settlementReward); - } - - if (runtime.amountToDeposit > 0) { - // deposit into market manager - factory.depositToMarketManager(runtime.marketId, runtime.amountToDeposit); - } - - // exctracted from asyncOrder before order is reset - bytes32 trackingCode = asyncOrder.trackingCode; - - asyncOrder.reset(); - - // emit event - emit OrderSettled( - runtime.marketId, - runtime.accountId, - fillPrice, - runtime.pnl, - runtime.newPositionSize, - totalFees, - runtime.settlementReward, - trackingCode, - msg.sender - ); - } - - function _performOrderValidityChecks( - uint128 marketId, - uint128 accountId - ) private view returns (AsyncOrder.Data storage, SettlementStrategy.Data storage) { - AsyncOrder.Data storage order = PerpsMarket.loadValid(marketId).asyncOrders[accountId]; - SettlementStrategy.Data storage settlementStrategy = PerpsMarketConfiguration - .load(marketId) - .settlementStrategies[order.settlementStrategyId]; - - order.checkValidity(); - order.checkWithinSettlementWindow(settlementStrategy); - - return (order, settlementStrategy); - } - - function _getTimeInBytes(uint256 settlementTime) private pure returns (bytes8) { - bytes32 settlementTimeBytes = bytes32(abi.encode(settlementTime)); - - // get last 8 bytes - return bytes8(settlementTimeBytes << 192); - } - - // borrowed from PythNode.sol - function _getScaledPrice(int64 price, int32 expo) private pure returns (int256) { - int256 factor = PRECISION + expo; - return factor > 0 ? price.upscale(factor.toUint()) : price.downscale((-factor).toUint()); - } } diff --git a/markets/perps-market/contracts/modules/AsyncOrderSettlementModule.sol b/markets/perps-market/contracts/modules/AsyncOrderSettlementModule.sol new file mode 100644 index 0000000000..084a3884a5 --- /dev/null +++ b/markets/perps-market/contracts/modules/AsyncOrderSettlementModule.sol @@ -0,0 +1,215 @@ +//SPDX-License-Identifier: MIT +pragma solidity >=0.8.11 <0.9.0; + +import {IAsyncOrderSettlementModule} from "../interfaces/IAsyncOrderSettlementModule.sol"; +import {SafeCastU256, SafeCastI256} from "@synthetixio/core-contracts/contracts/utils/SafeCast.sol"; +import {DecimalMath} from "@synthetixio/core-contracts/contracts/utils/DecimalMath.sol"; +import {Account} from "@synthetixio/main/contracts/storage/Account.sol"; +import {AccountRBAC} from "@synthetixio/main/contracts/storage/AccountRBAC.sol"; +import {IPythVerifier} from "../interfaces/external/IPythVerifier.sol"; +import {IAsyncOrderModule} from "../interfaces/IAsyncOrderModule.sol"; +import {PerpsAccount, SNX_USD_MARKET_ID} from "../storage/PerpsAccount.sol"; +import {MathUtil} from "../utils/MathUtil.sol"; +import {PerpsMarket} from "../storage/PerpsMarket.sol"; +import {AsyncOrder} from "../storage/AsyncOrder.sol"; +import {Position} from "../storage/Position.sol"; +import {PerpsPrice} from "../storage/PerpsPrice.sol"; +import {GlobalPerpsMarket} from "../storage/GlobalPerpsMarket.sol"; +import {PerpsMarketConfiguration} from "../storage/PerpsMarketConfiguration.sol"; +import {SettlementStrategy} from "../storage/SettlementStrategy.sol"; +import {PerpsMarketFactory} from "../storage/PerpsMarketFactory.sol"; +import {IMarketEvents} from "../interfaces/IMarketEvents.sol"; + +contract AsyncOrderSettlementModule is IAsyncOrderSettlementModule, IMarketEvents { + using DecimalMath for int256; + using DecimalMath for uint256; + using DecimalMath for int64; + using PerpsPrice for PerpsPrice.Data; + using PerpsAccount for PerpsAccount.Data; + using PerpsMarket for PerpsMarket.Data; + using AsyncOrder for AsyncOrder.Data; + using SettlementStrategy for SettlementStrategy.Data; + using PerpsMarketFactory for PerpsMarketFactory.Data; + using GlobalPerpsMarket for GlobalPerpsMarket.Data; + using PerpsMarketConfiguration for PerpsMarketConfiguration.Data; + using Position for Position.Data; + using SafeCastU256 for uint256; + using SafeCastI256 for int256; + + int256 public constant PRECISION = 18; + + function settle(uint128 marketId, uint128 accountId) external view { + GlobalPerpsMarket.load().checkLiquidation(accountId); + ( + AsyncOrder.Data storage order, + SettlementStrategy.Data storage settlementStrategy + ) = _performOrderValidityChecks(marketId, accountId); + + _settleOffchain(order, settlementStrategy); + } + + function settlePythOrder(bytes calldata result, bytes calldata extraData) external payable { + (uint128 marketId, uint128 asyncOrderId) = abi.decode(extraData, (uint128, uint128)); + ( + AsyncOrder.Data storage order, + SettlementStrategy.Data storage settlementStrategy + ) = _performOrderValidityChecks(marketId, asyncOrderId); + + bytes32[] memory priceIds = new bytes32[](1); + priceIds[0] = settlementStrategy.feedId; + + bytes[] memory updateData = new bytes[](1); + updateData[0] = result; + + IPythVerifier.PriceFeed[] memory priceFeeds = IPythVerifier( + settlementStrategy.priceVerificationContract + ).parsePriceFeedUpdates{value: msg.value}( + updateData, + priceIds, + order.settlementTime.to64(), + (order.settlementTime + settlementStrategy.priceWindowDuration).to64() + ); + + IPythVerifier.PriceFeed memory pythData = priceFeeds[0]; + uint offchainPrice = _getScaledPrice(pythData.price.price, pythData.price.expo).toUint(); + + settlementStrategy.checkPriceDeviation(offchainPrice, PerpsPrice.getCurrentPrice(marketId)); + + _settleOrder(offchainPrice, order, settlementStrategy); + } + + function _settleOffchain( + AsyncOrder.Data storage asyncOrder, + SettlementStrategy.Data storage settlementStrategy + ) private view returns (uint, int256, uint256) { + string[] memory urls = new string[](1); + urls[0] = settlementStrategy.url; + + bytes4 selector; + if (settlementStrategy.strategyType == SettlementStrategy.Type.PYTH) { + selector = AsyncOrderSettlementModule.settlePythOrder.selector; + } else { + revert SettlementStrategyNotFound(settlementStrategy.strategyType); + } + + // see EIP-3668: https://eips.ethereum.org/EIPS/eip-3668 + revert OffchainLookup( + address(this), + urls, + abi.encodePacked(settlementStrategy.feedId, _getTimeInBytes(asyncOrder.settlementTime)), + selector, + abi.encode(asyncOrder.marketId, asyncOrder.accountId) // extraData that gets sent to callback for validation + ); + } + + function _settleOrder( + uint256 price, + AsyncOrder.Data storage asyncOrder, + SettlementStrategy.Data storage settlementStrategy + ) private { + SettleOrderRuntime memory runtime; + + runtime.accountId = asyncOrder.accountId; + runtime.marketId = asyncOrder.marketId; + + // check if account is flagged + GlobalPerpsMarket.load().checkLiquidation(runtime.accountId); + ( + Position.Data memory newPosition, + uint totalFees, + uint fillPrice, + Position.Data storage oldPosition + ) = asyncOrder.validateOrder(settlementStrategy, price); + + runtime.newPositionSize = newPosition.size; + + PerpsMarketFactory.Data storage factory = PerpsMarketFactory.load(); + PerpsAccount.Data storage perpsAccount = PerpsAccount.load(runtime.accountId); + // use fill price to calculate realized pnl + (runtime.pnl, , , ) = oldPosition.getPnl(fillPrice); + + runtime.pnlUint = MathUtil.abs(runtime.pnl); + if (runtime.pnl > 0) { + factory.synthetix.withdrawMarketUsd(runtime.marketId, address(this), runtime.pnlUint); + perpsAccount.addCollateralAmount(SNX_USD_MARKET_ID, runtime.pnlUint); + } else if (runtime.pnl < 0) { + perpsAccount.deductFromAccount(runtime.pnlUint); + runtime.amountToDeposit = runtime.pnlUint; + // all gets deposited below with fees + } + + // after pnl is realized, update position on the perps market, this will also update the position. + PerpsMarket.MarketUpdateData memory updateData = PerpsMarket + .loadValid(runtime.marketId) + .updatePositionData(runtime.accountId, newPosition); + emit MarketUpdated( + updateData.marketId, + updateData.skew, + updateData.size, + updateData.sizeDelta, + updateData.currentFundingRate, + updateData.currentFundingVelocity + ); + + perpsAccount.updatePositionMarkets(runtime.marketId, runtime.newPositionSize); + perpsAccount.deductFromAccount(totalFees); + + runtime.settlementReward = settlementStrategy.settlementReward; + runtime.amountToDeposit += totalFees - runtime.settlementReward; + if (runtime.settlementReward > 0) { + // pay keeper + factory.usdToken.transfer(msg.sender, runtime.settlementReward); + } + + if (runtime.amountToDeposit > 0) { + // deposit into market manager + factory.depositToMarketManager(runtime.marketId, runtime.amountToDeposit); + } + + // exctracted from asyncOrder before order is reset + runtime.trackingCode = asyncOrder.trackingCode; + + asyncOrder.reset(); + + // emit event + emit OrderSettled( + runtime.marketId, + runtime.accountId, + fillPrice, + runtime.pnl, + runtime.newPositionSize, + totalFees, + runtime.settlementReward, + runtime.trackingCode, + msg.sender + ); + } + + function _performOrderValidityChecks( + uint128 marketId, + uint128 accountId + ) private view returns (AsyncOrder.Data storage, SettlementStrategy.Data storage) { + AsyncOrder.Data storage order = PerpsMarket.loadValid(marketId).asyncOrders[accountId]; + SettlementStrategy.Data storage settlementStrategy = PerpsMarketConfiguration + .load(marketId) + .settlementStrategies[order.settlementStrategyId]; + + order.checkValidity(); + order.checkWithinSettlementWindow(settlementStrategy); + + return (order, settlementStrategy); + } + + function _getTimeInBytes(uint256 settlementTime) private pure returns (bytes8) { + bytes32 settlementTimeBytes = bytes32(abi.encode(settlementTime)); + + // get last 8 bytes + return bytes8(settlementTimeBytes << 192); + } + + // borrowed from PythNode.sol + function _getScaledPrice(int64 price, int32 expo) private pure returns (int256) { + int256 factor = PRECISION + expo; + return factor > 0 ? price.upscale(factor.toUint()) : price.downscale((-factor).toUint()); + } +} diff --git a/markets/perps-market/contracts/storage/AsyncOrder.sol b/markets/perps-market/contracts/storage/AsyncOrder.sol index 2f117c30af..75d5793d99 100644 --- a/markets/perps-market/contracts/storage/AsyncOrder.sol +++ b/markets/perps-market/contracts/storage/AsyncOrder.sol @@ -131,6 +131,7 @@ library AsyncOrder { uint initialRequiredMargin; uint totalRequiredMargin; Position.Data newPosition; + bytes32 trackingCode; } function validateOrder( diff --git a/markets/perps-market/contracts/storage/PerpsMarket.sol b/markets/perps-market/contracts/storage/PerpsMarket.sol index 27312f38c3..c977b43fa0 100644 --- a/markets/perps-market/contracts/storage/PerpsMarket.sol +++ b/markets/perps-market/contracts/storage/PerpsMarket.sol @@ -133,17 +133,40 @@ library PerpsMarket { } } + struct MarketUpdateData { + uint128 marketId; + int256 skew; + uint256 size; + int256 sizeDelta; + int256 currentFundingRate; + int256 currentFundingVelocity; + } + + /** + * @dev If you call this method, please ensure you emit an event so offchain solution can index market state history properly + */ function updatePositionData( Data storage self, uint128 accountId, Position.Data memory newPosition - ) internal { + ) internal returns (MarketUpdateData memory) { Position.Data storage oldPosition = self.positions[accountId]; int128 oldPositionSize = oldPosition.size; self.size = (self.size + MathUtil.abs(newPosition.size)) - MathUtil.abs(oldPositionSize); self.skew += newPosition.size - oldPositionSize; oldPosition.updatePosition(newPosition); + int128 sizeDelta = newPosition.size - oldPositionSize; + // TODO add current market debt + return + MarketUpdateData( + self.id, + self.skew, + self.size, + sizeDelta, + self.lastFundingRate, + currentFundingVelocity(self) + ); } function loadWithVerifiedOwner( diff --git a/markets/perps-market/test/integration/Orders/OffchainAsyncOrder.settle.test.ts b/markets/perps-market/test/integration/Orders/OffchainAsyncOrder.settle.test.ts index a1a347f08b..23f864f967 100644 --- a/markets/perps-market/test/integration/Orders/OffchainAsyncOrder.settle.test.ts +++ b/markets/perps-market/test/integration/Orders/OffchainAsyncOrder.settle.test.ts @@ -8,6 +8,8 @@ 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 { wei } from '@synthetixio/wei'; +import { calcCurrentFundingVelocity } from '../helpers/funding-calcs'; describe('Settle Offchain Async Order test', () => { const { systems, perpsMarkets, synthMarkets, provider, trader1, keeper } = bootstrapMarkets({ @@ -24,7 +26,7 @@ describe('Settle Offchain Async Order test', () => { name: 'Ether', token: 'snxETH', price: bn(1000), - fundingParams: { skewScale: bn(100_000), maxFundingVelocity: bn(0) }, + fundingParams: { skewScale: bn(100_000), maxFundingVelocity: bn(10) }, }, ], traderAccountIds: [2, 3], @@ -379,16 +381,56 @@ describe('Settle Offchain Async Order test', () => { .settlePythOrder(pythPriceData, extraData, { value: updateFee }); }); - it('emits event', async () => { + it('emits event settle event', async () => { // TODO Calculate the correct fill price instead of hardcoding + + const accountId = 2; const fillPrice = bn(1000.005); + const pnl = 0; + const newPositionSize = bn(1); + const totalFees = DEFAULT_SETTLEMENT_STRATEGY.settlementReward; + const settlementReward = DEFAULT_SETTLEMENT_STRATEGY.settlementReward; + const trackingCode = `"${ethers.constants.HashZero}"`; + const msgSender = `"${await keeper().getAddress()}"`; + const params = [ + ethMarketId, + accountId, + fillPrice, + pnl, + newPositionSize, + totalFees, + settlementReward, + trackingCode, + msgSender, + ]; + await assertEvent( + settleTx, + `OrderSettled(${params.join(', ')})`, + systems().PerpsMarket + ); + }); + + it('emits market updated event', async () => { + const marketSize = bn(1); + const marketSkew = bn(1); + const sizeDelta = bn(1); + const currentFundingRate = bn(0); + const currentFundingVelocity = calcCurrentFundingVelocity({ + skew: wei(1), + skewScale: wei(100_000), + maxFundingVelocity: wei(10), + }); + const params = [ + ethMarketId, + marketSkew, + marketSize, + sizeDelta, + currentFundingRate, + currentFundingVelocity.toBN(), // Funding rates should be tested more thoroughly elsewhre + ]; await assertEvent( settleTx, - `OrderSettled(${ethMarketId}, 2, ${fillPrice}, 0, ${bn(1)}, ${ - DEFAULT_SETTLEMENT_STRATEGY.settlementReward - }, ${DEFAULT_SETTLEMENT_STRATEGY.settlementReward}, "${ - ethers.constants.HashZero - }", "${await keeper().getAddress()}")`, + `MarketUpdated(${params.join(', ')})`, systems().PerpsMarket ); }); diff --git a/markets/perps-market/test/integration/helpers/funding-calcs.ts b/markets/perps-market/test/integration/helpers/funding-calcs.ts new file mode 100644 index 0000000000..7af4079b6d --- /dev/null +++ b/markets/perps-market/test/integration/helpers/funding-calcs.ts @@ -0,0 +1,22 @@ +import Wei, { wei } from '@synthetixio/wei'; + +export function calcCurrentFundingVelocity({ + skew, + skewScale, + maxFundingVelocity, +}: { + skew: Wei; + skewScale: Wei; + maxFundingVelocity: Wei; +}) { + // Avoid a panic due to div by zero. Return 0 immediately. + if (skewScale.eq(0)) { + return wei(0); + } + + // Ensures the proportionalSkew is between -1 and 1. + const pSkew = wei(skew).div(skewScale); + const pSkewBounded = Wei.max(Wei.min(pSkew, wei(1)), wei(-1)); + + return pSkewBounded.mul(maxFundingVelocity); +}