Преглед на файлове

feat: add parseTwapPriceFeedUpdates function to IPyth and MockPyth

Daniel Chew преди 7 месеца
родител
ревизия
98ab843c7f

+ 3 - 3
target_chains/ethereum/contracts/contracts/pyth/Pyth.sol

@@ -478,13 +478,13 @@ abstract contract Pyth is
         PythInternalStructs.TwapPriceInfo memory twapPriceInfoStart,
         PythInternalStructs.TwapPriceInfo memory twapPriceInfoEnd
     ) private pure {
-        // First validate each individual data point's internal consistency
+        // First validate each individual price's uniqueness
         if (
-            twapPriceInfoStart.prevPublishTime > twapPriceInfoStart.publishTime
+            twapPriceInfoStart.prevPublishTime >= twapPriceInfoStart.publishTime
         ) {
             revert PythErrors.InvalidTwapUpdateData();
         }
-        if (twapPriceInfoEnd.prevPublishTime > twapPriceInfoEnd.publishTime) {
+        if (twapPriceInfoEnd.prevPublishTime >= twapPriceInfoEnd.publishTime) {
             revert PythErrors.InvalidTwapUpdateData();
         }
 

+ 26 - 0
target_chains/ethereum/sdk/solidity/IPyth.sol

@@ -119,6 +119,32 @@ interface IPyth is IPythEvents {
         uint64 maxPublishTime
     ) external payable returns (PythStructs.PriceFeed[] memory priceFeeds);
 
+    /// @notice Parse time-weighted average price (TWAP) from two sets of price update data for the given `priceIds`.
+    ///
+    /// This method calculates TWAP between two data points by processing the difference in cumulative price values
+    /// divided by the time period. It requires two sets of update data that contain valid price information
+    /// for all the requested price IDs.
+    ///
+    /// This method requires the caller to pay a fee in wei; the required fee can be computed by calling
+    /// `getUpdateFee` with the length of either updateData array (both arrays must have the same length).
+    ///
+    /// @dev Reverts if:
+    /// - The transferred fee is not sufficient
+    /// - The updateData is invalid or malformed
+    /// - There is no update for any of the given `priceIds`
+    /// - The two update datasets are not comparable (different number of updates or mismatched price IDs)
+    /// - The time ordering between data points is invalid (start time must be before end time)
+    /// @param updateData Array containing two arrays of price update data (start and end points for TWAP calculation)
+    /// @param priceIds Array of price ids to calculate TWAP for
+    /// @return twapPriceFeeds Array of TWAP price feeds corresponding to the given `priceIds` (with the same order)
+    function parseTwapPriceFeedUpdates(
+        bytes[][] calldata updateData,
+        bytes32[] calldata priceIds
+    )
+        external
+        payable
+        returns (PythStructs.TwapPriceFeed[] memory twapPriceFeeds);
+
     /// @notice Similar to `parsePriceFeedUpdates` but ensures the updates returned are
     /// the first updates published in minPublishTime. That is, if there are multiple updates for a given timestamp,
     /// this method will return the first update. This method may store the price updates on-chain, if they

+ 16 - 0
target_chains/ethereum/sdk/solidity/IPythEvents.sol

@@ -15,4 +15,20 @@ interface IPythEvents {
         int64 price,
         uint64 conf
     );
+
+    /// @dev Emitted when the TWAP price feed with `id` has received a fresh update.
+    /// @param id The Pyth Price Feed ID.
+    /// @param startTime Start time of the TWAP.
+    /// @param endTime End time of the TWAP.
+    /// @param twapPrice Price of the TWAP.
+    /// @param twapConf Confidence interval of the TWAP.
+    /// @param downSlotRatio Down slot ratio of the TWAP.
+    event TwapPriceFeedUpdate(
+        bytes32 indexed id,
+        uint64 startTime,
+        uint64 endTime,
+        int64 twapPrice,
+        uint64 twapConf,
+        uint32 downSlotRatio
+    );
 }

+ 193 - 0
target_chains/ethereum/sdk/solidity/MockPyth.sol

@@ -11,6 +11,19 @@ contract MockPyth is AbstractPyth {
     uint singleUpdateFeeInWei;
     uint validTimePeriod;
 
+    // Mock structure for TWAP price information
+    struct MockTwapPriceInfo {
+        int32 expo;
+        int64 price;
+        uint64 conf;
+        uint64 publishTime;
+        uint64 prevPublishTime;
+        uint64 publishSlot;
+        int128 cumulativePrice;
+        uint128 cumulativeConf;
+        uint64 numDownSlots;
+    }
+
     constructor(uint _validTimePeriod, uint _singleUpdateFeeInWei) {
         singleUpdateFeeInWei = _singleUpdateFeeInWei;
         validTimePeriod = _validTimePeriod;
@@ -160,6 +173,186 @@ contract MockPyth is AbstractPyth {
             );
     }
 
+    function parseTwapPriceFeedUpdates(
+        bytes[][] calldata updateData,
+        bytes32[] calldata priceIds
+    )
+        external
+        payable
+        returns (PythStructs.TwapPriceFeed[] memory twapPriceFeeds)
+    {
+        // Validate inputs and fee
+        if (updateData.length != 2) revert PythErrors.InvalidUpdateData();
+
+        uint requiredFee = getUpdateFee(updateData[0]);
+        if (requiredFee != getUpdateFee(updateData[1]))
+            revert PythErrors.InvalidUpdateData();
+        if (msg.value < requiredFee) revert PythErrors.InsufficientFee();
+
+        twapPriceFeeds = new PythStructs.TwapPriceFeed[](priceIds.length);
+
+        // Process each price ID
+        for (uint i = 0; i < priceIds.length; i++) {
+            processTwapPriceFeed(updateData, priceIds[i], i, twapPriceFeeds);
+        }
+    }
+
+    function processTwapPriceFeed(
+        bytes[][] calldata updateData,
+        bytes32 priceId,
+        uint index,
+        PythStructs.TwapPriceFeed[] memory twapPriceFeeds
+    ) private {
+        // Find start price feed
+        PythStructs.PriceFeed memory startFeed;
+        uint64 startPrevPublishTime;
+        bool foundStart = false;
+
+        for (uint j = 0; j < updateData[0].length; j++) {
+            (startFeed, startPrevPublishTime) = abi.decode(
+                updateData[0][j],
+                (PythStructs.PriceFeed, uint64)
+            );
+
+            if (startFeed.id == priceId) {
+                foundStart = true;
+                break;
+            }
+        }
+
+        if (!foundStart) revert PythErrors.PriceFeedNotFoundWithinRange();
+
+        // Find end price feed
+        PythStructs.PriceFeed memory endFeed;
+        uint64 endPrevPublishTime;
+        bool foundEnd = false;
+
+        for (uint j = 0; j < updateData[1].length; j++) {
+            (endFeed, endPrevPublishTime) = abi.decode(
+                updateData[1][j],
+                (PythStructs.PriceFeed, uint64)
+            );
+
+            if (endFeed.id == priceId) {
+                foundEnd = true;
+                break;
+            }
+        }
+
+        if (!foundEnd) revert PythErrors.PriceFeedNotFoundWithinRange();
+
+        // Validate time ordering
+        if (startFeed.price.publishTime >= endFeed.price.publishTime) {
+            revert PythErrors.InvalidTwapUpdateDataSet();
+        }
+
+        // Convert to MockTwapPriceInfo
+        MockTwapPriceInfo memory startInfo = createMockTwapInfo(
+            startFeed,
+            startPrevPublishTime
+        );
+        MockTwapPriceInfo memory endInfo = createMockTwapInfo(
+            endFeed,
+            endPrevPublishTime
+        );
+
+        if (startInfo.publishSlot >= endInfo.publishSlot) {
+            revert PythErrors.InvalidTwapUpdateDataSet();
+        }
+
+        // Calculate and store TWAP
+        twapPriceFeeds[index] = calculateTwap(priceId, startInfo, endInfo);
+
+        // Emit event in a separate function to reduce stack depth
+        emitTwapUpdate(
+            priceId,
+            startInfo.publishTime,
+            endInfo.publishTime,
+            twapPriceFeeds[index]
+        );
+    }
+
+    function emitTwapUpdate(
+        bytes32 priceId,
+        uint64 startTime,
+        uint64 endTime,
+        PythStructs.TwapPriceFeed memory twapFeed
+    ) private {
+        emit TwapPriceFeedUpdate(
+            priceId,
+            startTime,
+            endTime,
+            twapFeed.twap.price,
+            twapFeed.twap.conf,
+            twapFeed.downSlotRatio
+        );
+    }
+
+    function createMockTwapInfo(
+        PythStructs.PriceFeed memory feed,
+        uint64 prevPublishTime
+    ) internal pure returns (MockTwapPriceInfo memory mockInfo) {
+        mockInfo.expo = feed.price.expo;
+        mockInfo.price = feed.price.price;
+        mockInfo.conf = feed.price.conf;
+        mockInfo.publishTime = uint64(feed.price.publishTime);
+        mockInfo.prevPublishTime = prevPublishTime;
+
+        // Use publishTime as publishSlot in mock implementation
+        mockInfo.publishSlot = uint64(feed.price.publishTime);
+
+        // Create mock cumulative values for demonstration
+        // In a real implementation, these would accumulate over time
+        mockInfo.cumulativePrice =
+            int128(feed.price.price) *
+            int128(uint128(mockInfo.publishSlot));
+        mockInfo.cumulativeConf =
+            uint128(feed.price.conf) *
+            uint128(mockInfo.publishSlot);
+
+        // Default to 0 down slots for mock
+        mockInfo.numDownSlots = 0;
+
+        return mockInfo;
+    }
+
+    function calculateTwap(
+        bytes32 priceId,
+        MockTwapPriceInfo memory twapPriceInfoStart,
+        MockTwapPriceInfo memory twapPriceInfoEnd
+    ) internal pure returns (PythStructs.TwapPriceFeed memory twapPriceFeed) {
+        twapPriceFeed.id = priceId;
+        twapPriceFeed.startTime = twapPriceInfoStart.publishTime;
+        twapPriceFeed.endTime = twapPriceInfoEnd.publishTime;
+
+        // Calculate differences between start and end points for slots and cumulative values
+        uint64 slotDiff = twapPriceInfoEnd.publishSlot -
+            twapPriceInfoStart.publishSlot;
+        int128 priceDiff = twapPriceInfoEnd.cumulativePrice -
+            twapPriceInfoStart.cumulativePrice;
+        uint128 confDiff = twapPriceInfoEnd.cumulativeConf -
+            twapPriceInfoStart.cumulativeConf;
+
+        // Calculate time-weighted average price (TWAP) and confidence
+        int128 twapPrice = priceDiff / int128(uint128(slotDiff));
+        uint128 twapConf = confDiff / uint128(slotDiff);
+
+        twapPriceFeed.twap.price = int64(twapPrice);
+        twapPriceFeed.twap.conf = uint64(twapConf);
+        twapPriceFeed.twap.expo = twapPriceInfoStart.expo;
+        twapPriceFeed.twap.publishTime = twapPriceInfoEnd.publishTime;
+
+        // Calculate downSlotRatio as a value between 0 and 1,000,000
+        uint64 totalDownSlots = twapPriceInfoEnd.numDownSlots -
+            twapPriceInfoStart.numDownSlots;
+        uint64 downSlotsRatio = (totalDownSlots * 1_000_000) / slotDiff;
+
+        // Safely downcast to uint32 (sufficient for value range 0-1,000,000)
+        twapPriceFeed.downSlotRatio = uint32(downSlotsRatio);
+
+        return twapPriceFeed;
+    }
+
     function createPriceFeedUpdateData(
         bytes32 id,
         int64 price,

+ 116 - 0
target_chains/ethereum/sdk/solidity/abis/AbstractPyth.json

@@ -45,6 +45,49 @@
     "name": "PriceFeedUpdate",
     "type": "event"
   },
+  {
+    "anonymous": false,
+    "inputs": [
+      {
+        "indexed": true,
+        "internalType": "bytes32",
+        "name": "id",
+        "type": "bytes32"
+      },
+      {
+        "indexed": false,
+        "internalType": "uint64",
+        "name": "startTime",
+        "type": "uint64"
+      },
+      {
+        "indexed": false,
+        "internalType": "uint64",
+        "name": "endTime",
+        "type": "uint64"
+      },
+      {
+        "indexed": false,
+        "internalType": "int64",
+        "name": "twapPrice",
+        "type": "int64"
+      },
+      {
+        "indexed": false,
+        "internalType": "uint64",
+        "name": "twapConf",
+        "type": "uint64"
+      },
+      {
+        "indexed": false,
+        "internalType": "uint32",
+        "name": "downSlotRatio",
+        "type": "uint32"
+      }
+    ],
+    "name": "TwapPriceFeedUpdate",
+    "type": "event"
+  },
   {
     "inputs": [
       {
@@ -523,6 +566,79 @@
     "stateMutability": "payable",
     "type": "function"
   },
+  {
+    "inputs": [
+      {
+        "internalType": "bytes[][]",
+        "name": "updateData",
+        "type": "bytes[][]"
+      },
+      {
+        "internalType": "bytes32[]",
+        "name": "priceIds",
+        "type": "bytes32[]"
+      }
+    ],
+    "name": "parseTwapPriceFeedUpdates",
+    "outputs": [
+      {
+        "components": [
+          {
+            "internalType": "bytes32",
+            "name": "id",
+            "type": "bytes32"
+          },
+          {
+            "internalType": "uint256",
+            "name": "startTime",
+            "type": "uint256"
+          },
+          {
+            "internalType": "uint256",
+            "name": "endTime",
+            "type": "uint256"
+          },
+          {
+            "components": [
+              {
+                "internalType": "int64",
+                "name": "price",
+                "type": "int64"
+              },
+              {
+                "internalType": "uint64",
+                "name": "conf",
+                "type": "uint64"
+              },
+              {
+                "internalType": "int32",
+                "name": "expo",
+                "type": "int32"
+              },
+              {
+                "internalType": "uint256",
+                "name": "publishTime",
+                "type": "uint256"
+              }
+            ],
+            "internalType": "struct PythStructs.Price",
+            "name": "twap",
+            "type": "tuple"
+          },
+          {
+            "internalType": "uint32",
+            "name": "downSlotRatio",
+            "type": "uint32"
+          }
+        ],
+        "internalType": "struct PythStructs.TwapPriceFeed[]",
+        "name": "twapPriceFeeds",
+        "type": "tuple[]"
+      }
+    ],
+    "stateMutability": "payable",
+    "type": "function"
+  },
   {
     "inputs": [
       {

+ 116 - 0
target_chains/ethereum/sdk/solidity/abis/IPyth.json

@@ -30,6 +30,49 @@
     "name": "PriceFeedUpdate",
     "type": "event"
   },
+  {
+    "anonymous": false,
+    "inputs": [
+      {
+        "indexed": true,
+        "internalType": "bytes32",
+        "name": "id",
+        "type": "bytes32"
+      },
+      {
+        "indexed": false,
+        "internalType": "uint64",
+        "name": "startTime",
+        "type": "uint64"
+      },
+      {
+        "indexed": false,
+        "internalType": "uint64",
+        "name": "endTime",
+        "type": "uint64"
+      },
+      {
+        "indexed": false,
+        "internalType": "int64",
+        "name": "twapPrice",
+        "type": "int64"
+      },
+      {
+        "indexed": false,
+        "internalType": "uint64",
+        "name": "twapConf",
+        "type": "uint64"
+      },
+      {
+        "indexed": false,
+        "internalType": "uint32",
+        "name": "downSlotRatio",
+        "type": "uint32"
+      }
+    ],
+    "name": "TwapPriceFeedUpdate",
+    "type": "event"
+  },
   {
     "inputs": [
       {
@@ -413,6 +456,79 @@
     "stateMutability": "payable",
     "type": "function"
   },
+  {
+    "inputs": [
+      {
+        "internalType": "bytes[][]",
+        "name": "updateData",
+        "type": "bytes[][]"
+      },
+      {
+        "internalType": "bytes32[]",
+        "name": "priceIds",
+        "type": "bytes32[]"
+      }
+    ],
+    "name": "parseTwapPriceFeedUpdates",
+    "outputs": [
+      {
+        "components": [
+          {
+            "internalType": "bytes32",
+            "name": "id",
+            "type": "bytes32"
+          },
+          {
+            "internalType": "uint256",
+            "name": "startTime",
+            "type": "uint256"
+          },
+          {
+            "internalType": "uint256",
+            "name": "endTime",
+            "type": "uint256"
+          },
+          {
+            "components": [
+              {
+                "internalType": "int64",
+                "name": "price",
+                "type": "int64"
+              },
+              {
+                "internalType": "uint64",
+                "name": "conf",
+                "type": "uint64"
+              },
+              {
+                "internalType": "int32",
+                "name": "expo",
+                "type": "int32"
+              },
+              {
+                "internalType": "uint256",
+                "name": "publishTime",
+                "type": "uint256"
+              }
+            ],
+            "internalType": "struct PythStructs.Price",
+            "name": "twap",
+            "type": "tuple"
+          },
+          {
+            "internalType": "uint32",
+            "name": "downSlotRatio",
+            "type": "uint32"
+          }
+        ],
+        "internalType": "struct PythStructs.TwapPriceFeed[]",
+        "name": "twapPriceFeeds",
+        "type": "tuple[]"
+      }
+    ],
+    "stateMutability": "payable",
+    "type": "function"
+  },
   {
     "inputs": [
       {

+ 43 - 0
target_chains/ethereum/sdk/solidity/abis/IPythEvents.json

@@ -29,5 +29,48 @@
     ],
     "name": "PriceFeedUpdate",
     "type": "event"
+  },
+  {
+    "anonymous": false,
+    "inputs": [
+      {
+        "indexed": true,
+        "internalType": "bytes32",
+        "name": "id",
+        "type": "bytes32"
+      },
+      {
+        "indexed": false,
+        "internalType": "uint64",
+        "name": "startTime",
+        "type": "uint64"
+      },
+      {
+        "indexed": false,
+        "internalType": "uint64",
+        "name": "endTime",
+        "type": "uint64"
+      },
+      {
+        "indexed": false,
+        "internalType": "int64",
+        "name": "twapPrice",
+        "type": "int64"
+      },
+      {
+        "indexed": false,
+        "internalType": "uint64",
+        "name": "twapConf",
+        "type": "uint64"
+      },
+      {
+        "indexed": false,
+        "internalType": "uint32",
+        "name": "downSlotRatio",
+        "type": "uint32"
+      }
+    ],
+    "name": "TwapPriceFeedUpdate",
+    "type": "event"
   }
 ]

+ 126 - 0
target_chains/ethereum/sdk/solidity/abis/MockPyth.json

@@ -25,6 +25,16 @@
     "name": "InvalidArgument",
     "type": "error"
   },
+  {
+    "inputs": [],
+    "name": "InvalidTwapUpdateDataSet",
+    "type": "error"
+  },
+  {
+    "inputs": [],
+    "name": "InvalidUpdateData",
+    "type": "error"
+  },
   {
     "inputs": [],
     "name": "NoFreshUpdate",
@@ -76,6 +86,49 @@
     "name": "PriceFeedUpdate",
     "type": "event"
   },
+  {
+    "anonymous": false,
+    "inputs": [
+      {
+        "indexed": true,
+        "internalType": "bytes32",
+        "name": "id",
+        "type": "bytes32"
+      },
+      {
+        "indexed": false,
+        "internalType": "uint64",
+        "name": "startTime",
+        "type": "uint64"
+      },
+      {
+        "indexed": false,
+        "internalType": "uint64",
+        "name": "endTime",
+        "type": "uint64"
+      },
+      {
+        "indexed": false,
+        "internalType": "int64",
+        "name": "twapPrice",
+        "type": "int64"
+      },
+      {
+        "indexed": false,
+        "internalType": "uint64",
+        "name": "twapConf",
+        "type": "uint64"
+      },
+      {
+        "indexed": false,
+        "internalType": "uint32",
+        "name": "downSlotRatio",
+        "type": "uint32"
+      }
+    ],
+    "name": "TwapPriceFeedUpdate",
+    "type": "event"
+  },
   {
     "inputs": [
       {
@@ -608,6 +661,79 @@
     "stateMutability": "payable",
     "type": "function"
   },
+  {
+    "inputs": [
+      {
+        "internalType": "bytes[][]",
+        "name": "updateData",
+        "type": "bytes[][]"
+      },
+      {
+        "internalType": "bytes32[]",
+        "name": "priceIds",
+        "type": "bytes32[]"
+      }
+    ],
+    "name": "parseTwapPriceFeedUpdates",
+    "outputs": [
+      {
+        "components": [
+          {
+            "internalType": "bytes32",
+            "name": "id",
+            "type": "bytes32"
+          },
+          {
+            "internalType": "uint256",
+            "name": "startTime",
+            "type": "uint256"
+          },
+          {
+            "internalType": "uint256",
+            "name": "endTime",
+            "type": "uint256"
+          },
+          {
+            "components": [
+              {
+                "internalType": "int64",
+                "name": "price",
+                "type": "int64"
+              },
+              {
+                "internalType": "uint64",
+                "name": "conf",
+                "type": "uint64"
+              },
+              {
+                "internalType": "int32",
+                "name": "expo",
+                "type": "int32"
+              },
+              {
+                "internalType": "uint256",
+                "name": "publishTime",
+                "type": "uint256"
+              }
+            ],
+            "internalType": "struct PythStructs.Price",
+            "name": "twap",
+            "type": "tuple"
+          },
+          {
+            "internalType": "uint32",
+            "name": "downSlotRatio",
+            "type": "uint32"
+          }
+        ],
+        "internalType": "struct PythStructs.TwapPriceFeed[]",
+        "name": "twapPriceFeeds",
+        "type": "tuple[]"
+      }
+    ],
+    "stateMutability": "payable",
+    "type": "function"
+  },
   {
     "inputs": [
       {