From 9cd6a1846082ab1bf9a812b5e237e35a0c753eef Mon Sep 17 00:00:00 2001 From: Daniel Beal Date: Sun, 13 Oct 2024 09:53:19 +0900 Subject: [PATCH] add chainlink datastreams settlement strategy for perps market --- markets/perps-market/cannonfile.toml | 4 + .../IAsyncOrderSettlementChainlinkModule.sol | 74 ++++++++ .../external/IChainlinkDatastreamsERC7412.sol | 11 ++ .../AsyncOrderSettlementChainlinkModule.sol | 160 ++++++++++++++++++ .../contracts/storage/SettlementStrategy.sol | 3 +- markets/perps-market/storage.dump.json | 6 +- 6 files changed, 255 insertions(+), 3 deletions(-) create mode 100644 markets/perps-market/contracts/interfaces/IAsyncOrderSettlementChainlinkModule.sol create mode 100644 markets/perps-market/contracts/interfaces/external/IChainlinkDatastreamsERC7412.sol create mode 100644 markets/perps-market/contracts/modules/AsyncOrderSettlementChainlinkModule.sol diff --git a/markets/perps-market/cannonfile.toml b/markets/perps-market/cannonfile.toml index cd9fe00ac5..aba42702db 100644 --- a/markets/perps-market/cannonfile.toml +++ b/markets/perps-market/cannonfile.toml @@ -39,6 +39,9 @@ artifact = "PerpsMarketFactoryModule" [contract.AsyncOrderModule] artifact = "AsyncOrderModule" +[contract.AsyncOrderSettlementChainlinkModule] +artifact = "AsyncOrderSettlementChainlinkModule" + [contract.AsyncOrderSettlementPythModule] artifact = "AsyncOrderSettlementPythModule" @@ -85,6 +88,7 @@ contracts = [ "PerpsAccountModule", "PerpsMarketModule", "AsyncOrderModule", + "AsyncOrderSettlementChainlinkModule", "AsyncOrderSettlementPythModule", "AsyncOrderCancelModule", "FeatureFlagModule", diff --git a/markets/perps-market/contracts/interfaces/IAsyncOrderSettlementChainlinkModule.sol b/markets/perps-market/contracts/interfaces/IAsyncOrderSettlementChainlinkModule.sol new file mode 100644 index 0000000000..81cd1d8bb2 --- /dev/null +++ b/markets/perps-market/contracts/interfaces/IAsyncOrderSettlementChainlinkModule.sol @@ -0,0 +1,74 @@ +//SPDX-License-Identifier: MIT +pragma solidity >=0.8.11 <0.9.0; + +import {Position} from "../storage/Position.sol"; +import {MarketUpdate} from "../storage/MarketUpdate.sol"; + +interface IAsyncOrderSettlementChainlinkModule { + /** + * @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); + + // only used due to stack too deep during settlement + struct SettleOrderRuntime { + uint128 marketId; + uint128 accountId; + int128 sizeDelta; + int256 pnl; + uint256 chargedInterest; + int256 accruedFunding; + uint256 settlementReward; + uint256 fillPrice; + uint256 totalFees; + uint256 referralFees; + uint256 feeCollectorFees; + Position.Data newPosition; + MarketUpdate.Data updateData; + uint256 synthDeductionIterator; + uint128[] deductedSynthIds; + uint256[] deductedAmount; + int256 chargedAmount; + uint256 newAccountDebt; + } + + /** + * @notice Settles an offchain order using the offchain retrieved data from pyth. + * @param accountId The account id to settle the order + */ + function settleOrder(uint128 accountId) external; +} diff --git a/markets/perps-market/contracts/interfaces/external/IChainlinkDatastreamsERC7412.sol b/markets/perps-market/contracts/interfaces/external/IChainlinkDatastreamsERC7412.sol new file mode 100644 index 0000000000..e796c94abf --- /dev/null +++ b/markets/perps-market/contracts/interfaces/external/IChainlinkDatastreamsERC7412.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.11 <0.9.0; + +interface IChainlinkDatastreamsERC7412 { + error OracleDataRequired(address oracleContract, bytes oracleQuery, uint256 feeRequired); + + function getPriceForTimestamp( + bytes32 feedId, + uint32 forTimestamp + ) external view returns (int192); +} diff --git a/markets/perps-market/contracts/modules/AsyncOrderSettlementChainlinkModule.sol b/markets/perps-market/contracts/modules/AsyncOrderSettlementChainlinkModule.sol new file mode 100644 index 0000000000..a2772b952f --- /dev/null +++ b/markets/perps-market/contracts/modules/AsyncOrderSettlementChainlinkModule.sol @@ -0,0 +1,160 @@ +//SPDX-License-Identifier: MIT +pragma solidity >=0.8.11 <0.9.0; + +import {ERC2771Context} from "@synthetixio/core-contracts/contracts/utils/ERC2771Context.sol"; +import {FeatureFlag} from "@synthetixio/core-modules/contracts/storage/FeatureFlag.sol"; +import {IAsyncOrderSettlementChainlinkModule} from "../interfaces/IAsyncOrderSettlementChainlinkModule.sol"; +import {PerpsAccount, SNX_USD_MARKET_ID} from "../storage/PerpsAccount.sol"; +import {MathUtil} from "../utils/MathUtil.sol"; +import {Flags} from "../utils/Flags.sol"; +import {PerpsMarket} from "../storage/PerpsMarket.sol"; +import {AsyncOrder} from "../storage/AsyncOrder.sol"; +import {Position} from "../storage/Position.sol"; +import {GlobalPerpsMarket} from "../storage/GlobalPerpsMarket.sol"; +import {SettlementStrategy} from "../storage/SettlementStrategy.sol"; +import {PerpsMarketFactory} from "../storage/PerpsMarketFactory.sol"; +import {GlobalPerpsMarketConfiguration} from "../storage/GlobalPerpsMarketConfiguration.sol"; +import {IMarketEvents} from "../interfaces/IMarketEvents.sol"; +import {IAccountEvents} from "../interfaces/IAccountEvents.sol"; +import {KeeperCosts} from "../storage/KeeperCosts.sol"; +import {IChainlinkDatastreamsERC7412} from "../interfaces/external/IChainlinkDatastreamsERC7412.sol"; +import {SafeCastU256, SafeCastI256} from "@synthetixio/core-contracts/contracts/utils/SafeCast.sol"; + +/** + * @title Module for settling async orders using chainlink datastreams as price feed. + * @dev See IAsyncOrderSettlementChainlinkModule. + */ +contract AsyncOrderSettlementChainlinkModule is + IAsyncOrderSettlementChainlinkModule, + IMarketEvents, + IAccountEvents +{ + using SafeCastI256 for int256; + using SafeCastU256 for uint256; + using PerpsAccount for PerpsAccount.Data; + using PerpsMarket for PerpsMarket.Data; + using AsyncOrder for AsyncOrder.Data; + using PerpsMarketFactory for PerpsMarketFactory.Data; + using GlobalPerpsMarket for GlobalPerpsMarket.Data; + using GlobalPerpsMarketConfiguration for GlobalPerpsMarketConfiguration.Data; + using Position for Position.Data; + using KeeperCosts for KeeperCosts.Data; + + /** + * @inheritdoc IAsyncOrderSettlementChainlinkModule + */ + function settleOrder(uint128 accountId) external { + FeatureFlag.ensureAccessToFeature(Flags.PERPS_SYSTEM); + + ( + AsyncOrder.Data storage asyncOrder, + SettlementStrategy.Data storage settlementStrategy + ) = AsyncOrder.loadValid(accountId); + + int256 offchainPrice = IChainlinkDatastreamsERC7412( + settlementStrategy.priceVerificationContract + ).getPriceForTimestamp( + settlementStrategy.feedId, + (asyncOrder.commitmentTime + settlementStrategy.commitmentPriceDelay).to32() + ); + + _settleOrder(offchainPrice.toUint(), asyncOrder, settlementStrategy); + } + + /** + * @notice Settles an offchain order + * @param price provided by offchain oracle + * @param asyncOrder to be validated and settled + * @param settlementStrategy used to validate order and calculate settlement reward + */ + function _settleOrder( + uint256 price, + AsyncOrder.Data storage asyncOrder, + SettlementStrategy.Data storage settlementStrategy + ) private { + /// @dev runtime stores order settlement data; circumvents stack limitations + SettleOrderRuntime memory runtime; + + runtime.accountId = asyncOrder.request.accountId; + runtime.marketId = asyncOrder.request.marketId; + runtime.sizeDelta = asyncOrder.request.sizeDelta; + + GlobalPerpsMarket.load().checkLiquidation(runtime.accountId); + + Position.Data storage oldPosition; + + // validate order request can be settled; call reverts if not + (runtime.newPosition, runtime.totalFees, runtime.fillPrice, oldPosition) = asyncOrder + .validateRequest(settlementStrategy, price); + + // validate final fill price is acceptable relative to price specified by trader + asyncOrder.validateAcceptablePrice(runtime.fillPrice); + + PerpsMarketFactory.Data storage factory = PerpsMarketFactory.load(); + PerpsAccount.Data storage perpsAccount = PerpsAccount.load(runtime.accountId); + + // use actual fill price to calculate realized pnl + (runtime.pnl, , runtime.chargedInterest, runtime.accruedFunding, , ) = oldPosition.getPnl( + runtime.fillPrice + ); + + runtime.chargedAmount = runtime.pnl - runtime.totalFees.toInt(); + perpsAccount.charge(runtime.chargedAmount); + + emit AccountCharged(runtime.accountId, runtime.chargedAmount, perpsAccount.debt); + + // only update position state after pnl has been realized + runtime.updateData = PerpsMarket.loadValid(runtime.marketId).updatePositionData( + runtime.accountId, + runtime.newPosition + ); + perpsAccount.updateOpenPositions(runtime.marketId, runtime.newPosition.size); + + emit MarketUpdated( + runtime.updateData.marketId, + price, + runtime.updateData.skew, + runtime.updateData.size, + runtime.sizeDelta, + runtime.updateData.currentFundingRate, + runtime.updateData.currentFundingVelocity, + runtime.updateData.interestRate + ); + + runtime.settlementReward = AsyncOrder.settlementRewardCost(settlementStrategy); + + // if settlement reward is non-zero, pay keeper + if (runtime.settlementReward > 0) { + factory.withdrawMarketUsd(ERC2771Context._msgSender(), runtime.settlementReward); + } + + { + // order fees are total fees minus settlement reward + uint256 orderFees = runtime.totalFees - runtime.settlementReward; + GlobalPerpsMarketConfiguration.Data storage s = GlobalPerpsMarketConfiguration.load(); + s.collectFees(orderFees, asyncOrder.request.referrer, factory); + } + + // trader can now commit a new order + asyncOrder.reset(); + + /// @dev two events emitted to avoid stack limitations + emit InterestCharged(runtime.accountId, runtime.chargedInterest); + + emit OrderSettled( + runtime.marketId, + runtime.accountId, + runtime.fillPrice, + runtime.pnl, + runtime.accruedFunding, + runtime.sizeDelta, + runtime.newPosition.size, + runtime.totalFees, + runtime.referralFees, + runtime.feeCollectorFees, + runtime.settlementReward, + asyncOrder.request.trackingCode, + ERC2771Context._msgSender() + ); + } +} diff --git a/markets/perps-market/contracts/storage/SettlementStrategy.sol b/markets/perps-market/contracts/storage/SettlementStrategy.sol index 6bfed5bc1d..ce0d6b281a 100644 --- a/markets/perps-market/contracts/storage/SettlementStrategy.sol +++ b/markets/perps-market/contracts/storage/SettlementStrategy.sol @@ -49,6 +49,7 @@ library SettlementStrategy { } enum Type { - PYTH + PYTH, + CHAINLINK } } diff --git a/markets/perps-market/storage.dump.json b/markets/perps-market/storage.dump.json index 984b728925..e677880bf4 100644 --- a/markets/perps-market/storage.dump.json +++ b/markets/perps-market/storage.dump.json @@ -1244,7 +1244,8 @@ "type": "enum", "name": "strategyType", "members": [ - "PYTH" + "PYTH", + "CHAINLINK" ] }, { @@ -1513,7 +1514,8 @@ "type": "enum", "name": "strategyType", "members": [ - "PYTH" + "PYTH", + "CHAINLINK" ], "size": 1, "slot": "0",