瀏覽代碼

Merge pull request #2543 from pyth-network/evm-twap

feat(target_chains/ethereum): add twap
Daniel Chew 7 月之前
父節點
當前提交
6f0eb475bd

+ 1 - 1
target_chains/cosmwasm/examples/cw-contract/Cargo.lock

@@ -747,4 +747,4 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
 name = "zeroize"
 version = "1.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d68d9dcec5f9b43a30d38c49f91dfedfaac384cb8f085faca366c26207dd1619"
+checksum = "d68d9dcec5f9b43a30d38c49f91dfedfaac384cb8f085faca366c26207dd1619"

+ 174 - 2
target_chains/ethereum/contracts/contracts/pyth/Pyth.sol

@@ -7,11 +7,11 @@ import "@pythnetwork/pyth-sdk-solidity/AbstractPyth.sol";
 import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";
 
 import "@pythnetwork/pyth-sdk-solidity/PythErrors.sol";
+import "@pythnetwork/pyth-sdk-solidity/PythUtils.sol";
 import "./PythAccumulator.sol";
 import "./PythGetters.sol";
 import "./PythSetters.sol";
 import "./PythInternalStructs.sol";
-
 abstract contract Pyth is
     PythGetters,
     PythSetters,
@@ -308,6 +308,165 @@ abstract contract Pyth is
             );
     }
 
+    function processSingleTwapUpdate(
+        bytes calldata updateData
+    )
+        private
+        view
+        returns (
+            /// @return newOffset The next position in the update data after processing this TWAP update
+            /// @return twapPriceInfo The extracted time-weighted average price information
+            /// @return priceId The unique identifier for this price feed
+            uint newOffset,
+            PythStructs.TwapPriceInfo memory twapPriceInfo,
+            bytes32 priceId
+        )
+    {
+        UpdateType updateType;
+        uint offset;
+        bytes20 digest;
+        uint8 numUpdates;
+        bytes calldata encoded;
+        // Extract and validate the header for start data
+        (offset, updateType) = extractUpdateTypeFromAccumulatorHeader(
+            updateData
+        );
+
+        if (updateType != UpdateType.WormholeMerkle) {
+            revert PythErrors.InvalidUpdateData();
+        }
+
+        (
+            offset,
+            digest,
+            numUpdates,
+            encoded
+        ) = extractWormholeMerkleHeaderDigestAndNumUpdatesAndEncodedFromAccumulatorUpdate(
+            updateData,
+            offset
+        );
+
+        // Add additional validation before extracting TWAP price info
+        if (offset >= updateData.length) {
+            revert PythErrors.InvalidUpdateData();
+        }
+
+        // Extract start TWAP data with robust error checking
+        (offset, twapPriceInfo, priceId) = extractTwapPriceInfoFromMerkleProof(
+            digest,
+            encoded,
+            offset
+        );
+
+        if (offset != encoded.length) {
+            revert PythErrors.InvalidTwapUpdateData();
+        }
+        newOffset = offset;
+    }
+
+    function parseTwapPriceFeedUpdates(
+        bytes[] calldata updateData,
+        bytes32[] calldata priceIds
+    )
+        external
+        payable
+        override
+        returns (PythStructs.TwapPriceFeed[] memory twapPriceFeeds)
+    {
+        // TWAP requires exactly 2 updates - one for the start point and one for the end point
+        // to calculate the time-weighted average price between those two points
+        if (updateData.length != 2) {
+            revert PythErrors.InvalidUpdateData();
+        }
+
+        uint requiredFee = getUpdateFee(updateData);
+        if (msg.value < requiredFee) revert PythErrors.InsufficientFee();
+
+        unchecked {
+            twapPriceFeeds = new PythStructs.TwapPriceFeed[](priceIds.length);
+            for (uint i = 0; i < updateData.length - 1; i++) {
+                if (
+                    (updateData[i].length > 4 &&
+                        UnsafeCalldataBytesLib.toUint32(updateData[i], 0) ==
+                        ACCUMULATOR_MAGIC) &&
+                    (updateData[i + 1].length > 4 &&
+                        UnsafeCalldataBytesLib.toUint32(updateData[i + 1], 0) ==
+                        ACCUMULATOR_MAGIC)
+                ) {
+                    uint offsetStart;
+                    uint offsetEnd;
+                    bytes32 priceIdStart;
+                    bytes32 priceIdEnd;
+                    PythStructs.TwapPriceInfo memory twapPriceInfoStart;
+                    PythStructs.TwapPriceInfo memory twapPriceInfoEnd;
+                    (
+                        offsetStart,
+                        twapPriceInfoStart,
+                        priceIdStart
+                    ) = processSingleTwapUpdate(updateData[i]);
+                    (
+                        offsetEnd,
+                        twapPriceInfoEnd,
+                        priceIdEnd
+                    ) = processSingleTwapUpdate(updateData[i + 1]);
+
+                    if (priceIdStart != priceIdEnd)
+                        revert PythErrors.InvalidTwapUpdateDataSet();
+
+                    validateTwapPriceInfo(twapPriceInfoStart, twapPriceInfoEnd);
+
+                    uint k = findIndexOfPriceId(priceIds, priceIdStart);
+
+                    // If priceFeed[k].id != 0 then it means that there was a valid
+                    // update for priceIds[k] and we don't need to process this one.
+                    if (k == priceIds.length || twapPriceFeeds[k].id != 0) {
+                        continue;
+                    }
+
+                    twapPriceFeeds[k] = calculateTwap(
+                        priceIdStart,
+                        twapPriceInfoStart,
+                        twapPriceInfoEnd
+                    );
+                } else {
+                    revert PythErrors.InvalidUpdateData();
+                }
+            }
+
+            for (uint k = 0; k < priceIds.length; k++) {
+                if (twapPriceFeeds[k].id == 0) {
+                    revert PythErrors.PriceFeedNotFoundWithinRange();
+                }
+            }
+        }
+    }
+
+    function validateTwapPriceInfo(
+        PythStructs.TwapPriceInfo memory twapPriceInfoStart,
+        PythStructs.TwapPriceInfo memory twapPriceInfoEnd
+    ) private pure {
+        // First validate each individual price's uniqueness
+        if (
+            twapPriceInfoStart.prevPublishTime >= twapPriceInfoStart.publishTime
+        ) {
+            revert PythErrors.InvalidTwapUpdateData();
+        }
+        if (twapPriceInfoEnd.prevPublishTime >= twapPriceInfoEnd.publishTime) {
+            revert PythErrors.InvalidTwapUpdateData();
+        }
+
+        // Then validate the relationship between the two data points
+        if (twapPriceInfoStart.expo != twapPriceInfoEnd.expo) {
+            revert PythErrors.InvalidTwapUpdateDataSet();
+        }
+        if (twapPriceInfoStart.publishSlot > twapPriceInfoEnd.publishSlot) {
+            revert PythErrors.InvalidTwapUpdateDataSet();
+        }
+        if (twapPriceInfoStart.publishTime > twapPriceInfoEnd.publishTime) {
+            revert PythErrors.InvalidTwapUpdateDataSet();
+        }
+    }
+
     function parsePriceFeedUpdatesUnique(
         bytes[] calldata updateData,
         bytes32[] calldata priceIds,
@@ -397,6 +556,19 @@ abstract contract Pyth is
     }
 
     function version() public pure returns (string memory) {
-        return "1.4.4-alpha.1";
+        return "1.4.4-alpha.2";
+    }
+
+    function calculateTwap(
+        bytes32 priceId,
+        PythStructs.TwapPriceInfo memory twapPriceInfoStart,
+        PythStructs.TwapPriceInfo memory twapPriceInfoEnd
+    ) private pure returns (PythStructs.TwapPriceFeed memory) {
+        return
+            PythUtils.calculateTwap(
+                priceId,
+                twapPriceInfoStart,
+                twapPriceInfoEnd
+            );
     }
 }

+ 151 - 34
target_chains/ethereum/contracts/contracts/pyth/PythAccumulator.sol

@@ -25,7 +25,8 @@ abstract contract PythAccumulator is PythGetters, PythSetters, AbstractPyth {
     }
 
     enum MessageType {
-        PriceFeed
+        PriceFeed,
+        TwapPriceFeed
     }
 
     // This method is also used by batch attestation but moved here
@@ -228,46 +229,99 @@ abstract contract PythAccumulator is PythGetters, PythSetters, AbstractPyth {
             uint64 prevPublishTime
         )
     {
-        unchecked {
-            bytes calldata encodedMessage;
-            uint16 messageSize = UnsafeCalldataBytesLib.toUint16(
-                encoded,
-                offset
+        bytes calldata encodedMessage;
+        MessageType messageType;
+        (
+            encodedMessage,
+            messageType,
+            endOffset
+        ) = extractAndValidateEncodedMessage(encoded, offset, digest);
+
+        if (messageType == MessageType.PriceFeed) {
+            (priceInfo, priceId, prevPublishTime) = parsePriceFeedMessage(
+                encodedMessage,
+                1
             );
-            offset += 2;
+        } else revert PythErrors.InvalidUpdateData();
 
-            encodedMessage = UnsafeCalldataBytesLib.slice(
-                encoded,
-                offset,
-                messageSize
-            );
-            offset += messageSize;
+        return (endOffset, priceInfo, priceId, prevPublishTime);
+    }
 
-            bool valid;
-            (valid, endOffset) = MerkleTree.isProofValid(
-                encoded,
-                offset,
-                digest,
-                encodedMessage
+    function extractTwapPriceInfoFromMerkleProof(
+        bytes20 digest,
+        bytes calldata encoded,
+        uint offset
+    )
+        internal
+        pure
+        returns (
+            uint endOffset,
+            PythStructs.TwapPriceInfo memory twapPriceInfo,
+            bytes32 priceId
+        )
+    {
+        bytes calldata encodedMessage;
+        MessageType messageType;
+        (
+            encodedMessage,
+            messageType,
+            endOffset
+        ) = extractAndValidateEncodedMessage(encoded, offset, digest);
+
+        if (messageType == MessageType.TwapPriceFeed) {
+            (twapPriceInfo, priceId) = parseTwapPriceFeedMessage(
+                encodedMessage,
+                1
             );
-            if (!valid) {
-                revert PythErrors.InvalidUpdateData();
-            }
+        } else revert PythErrors.InvalidUpdateData();
 
-            MessageType messageType = MessageType(
-                UnsafeCalldataBytesLib.toUint8(encodedMessage, 0)
-            );
-            if (messageType == MessageType.PriceFeed) {
-                (priceInfo, priceId, prevPublishTime) = parsePriceFeedMessage(
-                    encodedMessage,
-                    1
-                );
-            } else {
-                revert PythErrors.InvalidUpdateData();
-            }
+        return (endOffset, twapPriceInfo, priceId);
+    }
+
+    function extractAndValidateEncodedMessage(
+        bytes calldata encoded,
+        uint offset,
+        bytes20 digest
+    )
+        private
+        pure
+        returns (
+            bytes calldata encodedMessage,
+            MessageType messageType,
+            uint endOffset
+        )
+    {
+        uint16 messageSize = UnsafeCalldataBytesLib.toUint16(encoded, offset);
+        offset += 2;
+
+        encodedMessage = UnsafeCalldataBytesLib.slice(
+            encoded,
+            offset,
+            messageSize
+        );
+        offset += messageSize;
 
-            return (endOffset, priceInfo, priceId, prevPublishTime);
+        bool valid;
+        (valid, endOffset) = MerkleTree.isProofValid(
+            encoded,
+            offset,
+            digest,
+            encodedMessage
+        );
+        if (!valid) {
+            revert PythErrors.InvalidUpdateData();
         }
+
+        messageType = MessageType(
+            UnsafeCalldataBytesLib.toUint8(encodedMessage, 0)
+        );
+        if (
+            messageType != MessageType.PriceFeed &&
+            messageType != MessageType.TwapPriceFeed
+        ) {
+            revert PythErrors.InvalidUpdateData();
+        }
+        return (encodedMessage, messageType, endOffset);
     }
 
     function parsePriceFeedMessage(
@@ -335,6 +389,69 @@ abstract contract PythAccumulator is PythGetters, PythSetters, AbstractPyth {
         }
     }
 
+    function parseTwapPriceFeedMessage(
+        bytes calldata encodedTwapPriceFeed,
+        uint offset
+    )
+        private
+        pure
+        returns (
+            PythStructs.TwapPriceInfo memory twapPriceInfo,
+            bytes32 priceId
+        )
+    {
+        unchecked {
+            priceId = UnsafeCalldataBytesLib.toBytes32(
+                encodedTwapPriceFeed,
+                offset
+            );
+            offset += 32;
+
+            twapPriceInfo.cumulativePrice = int128(
+                UnsafeCalldataBytesLib.toUint128(encodedTwapPriceFeed, offset)
+            );
+            offset += 16;
+
+            twapPriceInfo.cumulativeConf = UnsafeCalldataBytesLib.toUint128(
+                encodedTwapPriceFeed,
+                offset
+            );
+            offset += 16;
+
+            twapPriceInfo.numDownSlots = UnsafeCalldataBytesLib.toUint64(
+                encodedTwapPriceFeed,
+                offset
+            );
+            offset += 8;
+
+            twapPriceInfo.publishSlot = UnsafeCalldataBytesLib.toUint64(
+                encodedTwapPriceFeed,
+                offset
+            );
+            offset += 8;
+
+            twapPriceInfo.publishTime = UnsafeCalldataBytesLib.toUint64(
+                encodedTwapPriceFeed,
+                offset
+            );
+            offset += 8;
+
+            twapPriceInfo.prevPublishTime = UnsafeCalldataBytesLib.toUint64(
+                encodedTwapPriceFeed,
+                offset
+            );
+            offset += 8;
+
+            twapPriceInfo.expo = int32(
+                UnsafeCalldataBytesLib.toUint32(encodedTwapPriceFeed, offset)
+            );
+            offset += 4;
+
+            if (offset > encodedTwapPriceFeed.length)
+                revert PythErrors.InvalidUpdateData();
+        }
+    }
+
     function updatePriceInfosFromAccumulatorUpdate(
         bytes calldata accumulatorUpdate
     ) internal returns (uint8 numUpdates) {

+ 415 - 0
target_chains/ethereum/contracts/forge-test/Pyth.t.sol

@@ -11,6 +11,7 @@ import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";
 import "./utils/WormholeTestUtils.t.sol";
 import "./utils/PythTestUtils.t.sol";
 import "./utils/RandTestUtils.t.sol";
+import "forge-std/console.sol";
 
 contract PythTest is Test, WormholeTestUtils, PythTestUtils {
     IPyth public pyth;
@@ -25,8 +26,38 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils {
     // We will have less than 512 price for a foreseeable future.
     uint8 constant MERKLE_TREE_DEPTH = 9;
 
+    // Base TWAP messages that will be used as templates for tests
+    TwapPriceFeedMessage[1] baseTwapStartMessages;
+    TwapPriceFeedMessage[1] baseTwapEndMessages;
+    bytes32[1] basePriceIds;
+
     function setUp() public {
         pyth = IPyth(setUpPyth(setUpWormholeReceiver(NUM_GUARDIAN_SIGNERS)));
+
+        // Initialize base TWAP messages
+        basePriceIds[0] = bytes32(uint256(1));
+
+        baseTwapStartMessages[0] = TwapPriceFeedMessage({
+            priceId: basePriceIds[0],
+            cumulativePrice: 100_000, // Base cumulative value
+            cumulativeConf: 10_000, // Base cumulative conf
+            numDownSlots: 0,
+            publishSlot: 1000,
+            publishTime: 1000,
+            prevPublishTime: 900,
+            expo: -8
+        });
+
+        baseTwapEndMessages[0] = TwapPriceFeedMessage({
+            priceId: basePriceIds[0],
+            cumulativePrice: 210_000, // Increased by 110_000
+            cumulativeConf: 18_000, // Increased by 8_000
+            numDownSlots: 0,
+            publishSlot: 1100,
+            publishTime: 1100,
+            prevPublishTime: 1000,
+            expo: -8
+        });
     }
 
     function generateRandomPriceMessages(
@@ -100,6 +131,79 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils {
         );
     }
 
+    // This method divides messages into a couple of batches and creates
+    // twap updateData for them. It returns the updateData and the updateFee
+    function createBatchedTwapUpdateDataFromMessagesWithConfig(
+        PriceFeedMessage[] memory messages,
+        MerkleUpdateConfig memory config
+    ) public returns (bytes[] memory updateData, uint updateFee) {
+        require(messages.length >= 2, "At least 2 messages required for TWAP");
+
+        // Create TWAP messages from regular price feed messages
+        // For TWAP calculation, we need cumulative values that increase over time
+        TwapPriceFeedMessage[]
+            memory startTwapMessages = new TwapPriceFeedMessage[](1);
+        startTwapMessages[0].priceId = messages[0].priceId;
+        // For test purposes, we'll set cumulative values for start message
+        startTwapMessages[0].cumulativePrice = int128(messages[0].price) * 1000;
+        startTwapMessages[0].cumulativeConf = uint128(messages[0].conf) * 1000;
+        startTwapMessages[0].numDownSlots = 0; // No down slots for testing
+        startTwapMessages[0].expo = messages[0].expo;
+        startTwapMessages[0].publishTime = messages[0].publishTime;
+        startTwapMessages[0].prevPublishTime = messages[0].prevPublishTime;
+        startTwapMessages[0].publishSlot = 1000; // Start slot
+
+        TwapPriceFeedMessage[]
+            memory endTwapMessages = new TwapPriceFeedMessage[](1);
+        endTwapMessages[0].priceId = messages[1].priceId;
+        // For end message, make sure cumulative values are higher than start
+        endTwapMessages[0].cumulativePrice =
+            int128(messages[1].price) *
+            1000 +
+            startTwapMessages[0].cumulativePrice;
+        endTwapMessages[0].cumulativeConf =
+            uint128(messages[1].conf) *
+            1000 +
+            startTwapMessages[0].cumulativeConf;
+        endTwapMessages[0].numDownSlots = 0; // No down slots for testing
+        endTwapMessages[0].expo = messages[1].expo;
+        endTwapMessages[0].publishTime = messages[1].publishTime;
+        endTwapMessages[0].prevPublishTime = messages[1].prevPublishTime;
+        endTwapMessages[0].publishSlot = 1100; // End slot (100 slots after start)
+
+        // Create the updateData array with exactly 2 elements as required by parseTwapPriceFeedUpdates
+        updateData = new bytes[](2);
+        updateData[0] = generateWhMerkleTwapUpdateWithSource(
+            startTwapMessages,
+            config
+        );
+        updateData[1] = generateWhMerkleTwapUpdateWithSource(
+            endTwapMessages,
+            config
+        );
+
+        // Calculate the update fee
+        updateFee = pyth.getUpdateFee(updateData);
+    }
+
+    function createBatchedTwapUpdateDataFromMessages(
+        PriceFeedMessage[] memory messages
+    ) internal returns (bytes[] memory updateData, uint updateFee) {
+        (
+            updateData,
+            updateFee
+        ) = createBatchedTwapUpdateDataFromMessagesWithConfig(
+            messages,
+            MerkleUpdateConfig(
+                MERKLE_TREE_DEPTH,
+                NUM_GUARDIAN_SIGNERS,
+                SOURCE_EMITTER_CHAIN_ID,
+                SOURCE_EMITTER_ADDRESS,
+                false
+            )
+        );
+    }
+
     /// Testing parsePriceFeedUpdates method.
     function testParsePriceFeedUpdatesWorks(uint seed) public {
         setRandSeed(seed);
@@ -309,4 +413,315 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils {
             MAX_UINT64
         );
     }
+
+    function testParseTwapPriceFeedUpdates() public {
+        bytes32[] memory priceIds = new bytes32[](1);
+        priceIds[0] = basePriceIds[0];
+
+        // Create update data directly from base TWAP messages
+        bytes[] memory updateData = new bytes[](2);
+        TwapPriceFeedMessage[]
+            memory startMessages = new TwapPriceFeedMessage[](1);
+        TwapPriceFeedMessage[] memory endMessages = new TwapPriceFeedMessage[](
+            1
+        );
+        startMessages[0] = baseTwapStartMessages[0];
+        endMessages[0] = baseTwapEndMessages[0];
+
+        updateData[0] = generateWhMerkleTwapUpdateWithSource(
+            startMessages,
+            MerkleUpdateConfig(
+                MERKLE_TREE_DEPTH,
+                NUM_GUARDIAN_SIGNERS,
+                SOURCE_EMITTER_CHAIN_ID,
+                SOURCE_EMITTER_ADDRESS,
+                false
+            )
+        );
+        updateData[1] = generateWhMerkleTwapUpdateWithSource(
+            endMessages,
+            MerkleUpdateConfig(
+                MERKLE_TREE_DEPTH,
+                NUM_GUARDIAN_SIGNERS,
+                SOURCE_EMITTER_CHAIN_ID,
+                SOURCE_EMITTER_ADDRESS,
+                false
+            )
+        );
+
+        uint updateFee = pyth.getUpdateFee(updateData);
+
+        // Parse the TWAP updates
+        PythStructs.TwapPriceFeed[] memory twapPriceFeeds = pyth
+            .parseTwapPriceFeedUpdates{value: updateFee}(updateData, priceIds);
+
+        // Validate results
+        assertEq(twapPriceFeeds[0].id, basePriceIds[0]);
+        assertEq(
+            twapPriceFeeds[0].startTime,
+            baseTwapStartMessages[0].publishTime
+        );
+        assertEq(twapPriceFeeds[0].endTime, baseTwapEndMessages[0].publishTime);
+        assertEq(twapPriceFeeds[0].twap.expo, baseTwapStartMessages[0].expo);
+
+        // Expected TWAP price: (210_000 - 100_000) / (1100 - 1000) = 1100
+        assertEq(twapPriceFeeds[0].twap.price, int64(1100));
+
+        // Expected TWAP conf: (18_000 - 10_000) / (1100 - 1000) = 80
+        assertEq(twapPriceFeeds[0].twap.conf, uint64(80));
+
+        // Validate the downSlotsRatio is 0 in our test implementation
+        assertEq(twapPriceFeeds[0].downSlotsRatio, uint32(0));
+    }
+
+    function testParseTwapPriceFeedUpdatesRevertsWithInvalidUpdateDataLength()
+        public
+    {
+        bytes32[] memory priceIds = new bytes32[](1);
+        priceIds[0] = bytes32(uint256(1));
+
+        // Create invalid update data with wrong length
+        bytes[] memory updateData = new bytes[](1); // Should be 2
+        updateData[0] = new bytes(1);
+
+        vm.expectRevert(PythErrors.InvalidUpdateData.selector);
+        pyth.parseTwapPriceFeedUpdates{value: 0}(updateData, priceIds);
+    }
+
+    function testParseTwapPriceFeedUpdatesRevertsWithMismatchedPriceIds()
+        public
+    {
+        bytes32[] memory priceIds = new bytes32[](1);
+        priceIds[0] = bytes32(uint256(1));
+
+        // Copy base messages
+        TwapPriceFeedMessage[]
+            memory startMessages = new TwapPriceFeedMessage[](1);
+        TwapPriceFeedMessage[] memory endMessages = new TwapPriceFeedMessage[](
+            1
+        );
+        startMessages[0] = baseTwapStartMessages[0];
+        endMessages[0] = baseTwapEndMessages[0];
+
+        // Change end message priceId to create mismatch
+        endMessages[0].priceId = bytes32(uint256(2));
+
+        // Create update data
+        bytes[] memory updateData = new bytes[](2);
+        updateData[0] = generateWhMerkleTwapUpdateWithSource(
+            startMessages,
+            MerkleUpdateConfig(
+                MERKLE_TREE_DEPTH,
+                NUM_GUARDIAN_SIGNERS,
+                SOURCE_EMITTER_CHAIN_ID,
+                SOURCE_EMITTER_ADDRESS,
+                false
+            )
+        );
+        updateData[1] = generateWhMerkleTwapUpdateWithSource(
+            endMessages,
+            MerkleUpdateConfig(
+                MERKLE_TREE_DEPTH,
+                NUM_GUARDIAN_SIGNERS,
+                SOURCE_EMITTER_CHAIN_ID,
+                SOURCE_EMITTER_ADDRESS,
+                false
+            )
+        );
+
+        uint updateFee = pyth.getUpdateFee(updateData);
+
+        vm.expectRevert(PythErrors.InvalidTwapUpdateDataSet.selector);
+        pyth.parseTwapPriceFeedUpdates{value: updateFee}(updateData, priceIds);
+    }
+
+    function testParseTwapPriceFeedUpdatesRevertsWithInvalidTimeOrdering()
+        public
+    {
+        bytes32[] memory priceIds = new bytes32[](1);
+        priceIds[0] = bytes32(uint256(1));
+
+        // Copy base messages
+        TwapPriceFeedMessage[]
+            memory startMessages = new TwapPriceFeedMessage[](1);
+        TwapPriceFeedMessage[] memory endMessages = new TwapPriceFeedMessage[](
+            1
+        );
+        startMessages[0] = baseTwapStartMessages[0];
+        endMessages[0] = baseTwapEndMessages[0];
+
+        // Modify times to create invalid ordering
+        startMessages[0].publishTime = 1100;
+        startMessages[0].publishSlot = 1100;
+        endMessages[0].publishTime = 1000;
+        endMessages[0].publishSlot = 1000;
+        endMessages[0].prevPublishTime = 900;
+
+        // Create update data
+        bytes[] memory updateData = new bytes[](2);
+        updateData[0] = generateWhMerkleTwapUpdateWithSource(
+            startMessages,
+            MerkleUpdateConfig(
+                MERKLE_TREE_DEPTH,
+                NUM_GUARDIAN_SIGNERS,
+                SOURCE_EMITTER_CHAIN_ID,
+                SOURCE_EMITTER_ADDRESS,
+                false
+            )
+        );
+        updateData[1] = generateWhMerkleTwapUpdateWithSource(
+            endMessages,
+            MerkleUpdateConfig(
+                MERKLE_TREE_DEPTH,
+                NUM_GUARDIAN_SIGNERS,
+                SOURCE_EMITTER_CHAIN_ID,
+                SOURCE_EMITTER_ADDRESS,
+                false
+            )
+        );
+
+        uint updateFee = pyth.getUpdateFee(updateData);
+
+        vm.expectRevert(PythErrors.InvalidTwapUpdateDataSet.selector);
+        pyth.parseTwapPriceFeedUpdates{value: updateFee}(updateData, priceIds);
+    }
+
+    function testParseTwapPriceFeedUpdatesRevertsWithMismatchedExponents()
+        public
+    {
+        bytes32[] memory priceIds = new bytes32[](1);
+        priceIds[0] = bytes32(uint256(1));
+
+        // Copy base messages
+        TwapPriceFeedMessage[]
+            memory startMessages = new TwapPriceFeedMessage[](1);
+        TwapPriceFeedMessage[] memory endMessages = new TwapPriceFeedMessage[](
+            1
+        );
+        startMessages[0] = baseTwapStartMessages[0];
+        endMessages[0] = baseTwapEndMessages[0];
+
+        // Change end message expo to create mismatch
+        endMessages[0].expo = -6;
+
+        // Create update data
+        bytes[] memory updateData = new bytes[](2);
+        updateData[0] = generateWhMerkleTwapUpdateWithSource(
+            startMessages,
+            MerkleUpdateConfig(
+                MERKLE_TREE_DEPTH,
+                NUM_GUARDIAN_SIGNERS,
+                SOURCE_EMITTER_CHAIN_ID,
+                SOURCE_EMITTER_ADDRESS,
+                false
+            )
+        );
+        updateData[1] = generateWhMerkleTwapUpdateWithSource(
+            endMessages,
+            MerkleUpdateConfig(
+                MERKLE_TREE_DEPTH,
+                NUM_GUARDIAN_SIGNERS,
+                SOURCE_EMITTER_CHAIN_ID,
+                SOURCE_EMITTER_ADDRESS,
+                false
+            )
+        );
+
+        uint updateFee = pyth.getUpdateFee(updateData);
+
+        vm.expectRevert(PythErrors.InvalidTwapUpdateDataSet.selector);
+        pyth.parseTwapPriceFeedUpdates{value: updateFee}(updateData, priceIds);
+    }
+
+    function testParseTwapPriceFeedUpdatesRevertsWithInvalidPrevPublishTime()
+        public
+    {
+        bytes32[] memory priceIds = new bytes32[](1);
+        priceIds[0] = bytes32(uint256(1));
+
+        // Copy base messages
+        TwapPriceFeedMessage[]
+            memory startMessages = new TwapPriceFeedMessage[](1);
+        TwapPriceFeedMessage[] memory endMessages = new TwapPriceFeedMessage[](
+            1
+        );
+        startMessages[0] = baseTwapStartMessages[0];
+        endMessages[0] = baseTwapEndMessages[0];
+
+        // Set invalid prevPublishTime (greater than publishTime)
+        startMessages[0].prevPublishTime = 1100;
+
+        // Create update data
+        bytes[] memory updateData = new bytes[](2);
+        updateData[0] = generateWhMerkleTwapUpdateWithSource(
+            startMessages,
+            MerkleUpdateConfig(
+                MERKLE_TREE_DEPTH,
+                NUM_GUARDIAN_SIGNERS,
+                SOURCE_EMITTER_CHAIN_ID,
+                SOURCE_EMITTER_ADDRESS,
+                false
+            )
+        );
+        updateData[1] = generateWhMerkleTwapUpdateWithSource(
+            endMessages,
+            MerkleUpdateConfig(
+                MERKLE_TREE_DEPTH,
+                NUM_GUARDIAN_SIGNERS,
+                SOURCE_EMITTER_CHAIN_ID,
+                SOURCE_EMITTER_ADDRESS,
+                false
+            )
+        );
+
+        uint updateFee = pyth.getUpdateFee(updateData);
+
+        vm.expectRevert(PythErrors.InvalidTwapUpdateData.selector);
+        pyth.parseTwapPriceFeedUpdates{value: updateFee}(updateData, priceIds);
+    }
+
+    function testParseTwapPriceFeedUpdatesRevertsWithInsufficientFee() public {
+        bytes32[] memory priceIds = new bytes32[](1);
+        priceIds[0] = bytes32(uint256(1));
+
+        // Copy base messages
+        TwapPriceFeedMessage[]
+            memory startMessages = new TwapPriceFeedMessage[](1);
+        TwapPriceFeedMessage[] memory endMessages = new TwapPriceFeedMessage[](
+            1
+        );
+        startMessages[0] = baseTwapStartMessages[0];
+        endMessages[0] = baseTwapEndMessages[0];
+
+        // Create update data
+        bytes[] memory updateData = new bytes[](2);
+        updateData[0] = generateWhMerkleTwapUpdateWithSource(
+            startMessages,
+            MerkleUpdateConfig(
+                MERKLE_TREE_DEPTH,
+                NUM_GUARDIAN_SIGNERS,
+                SOURCE_EMITTER_CHAIN_ID,
+                SOURCE_EMITTER_ADDRESS,
+                false
+            )
+        );
+        updateData[1] = generateWhMerkleTwapUpdateWithSource(
+            endMessages,
+            MerkleUpdateConfig(
+                MERKLE_TREE_DEPTH,
+                NUM_GUARDIAN_SIGNERS,
+                SOURCE_EMITTER_CHAIN_ID,
+                SOURCE_EMITTER_ADDRESS,
+                false
+            )
+        );
+
+        uint updateFee = pyth.getUpdateFee(updateData);
+
+        vm.expectRevert(PythErrors.InsufficientFee.selector);
+        pyth.parseTwapPriceFeedUpdates{value: updateFee - 1}(
+            updateData,
+            priceIds
+        );
+    }
 }

+ 92 - 0
target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol

@@ -73,6 +73,17 @@ abstract contract PythTestUtils is Test, WormholeTestUtils, RandTestUtils {
         uint64 emaConf;
     }
 
+    struct TwapPriceFeedMessage {
+        bytes32 priceId;
+        int128 cumulativePrice;
+        uint128 cumulativeConf;
+        uint64 numDownSlots;
+        uint64 publishSlot;
+        uint64 publishTime;
+        uint64 prevPublishTime;
+        int32 expo;
+    }
+
     struct MerkleUpdateConfig {
         uint8 depth;
         uint8 numSigners;
@@ -101,6 +112,28 @@ abstract contract PythTestUtils is Test, WormholeTestUtils, RandTestUtils {
         }
     }
 
+    function encodeTwapPriceFeedMessages(
+        TwapPriceFeedMessage[] memory twapPriceFeedMessages
+    ) internal pure returns (bytes[] memory encodedTwapPriceFeedMessages) {
+        encodedTwapPriceFeedMessages = new bytes[](
+            twapPriceFeedMessages.length
+        );
+
+        for (uint i = 0; i < twapPriceFeedMessages.length; i++) {
+            encodedTwapPriceFeedMessages[i] = abi.encodePacked(
+                uint8(PythAccumulator.MessageType.TwapPriceFeed),
+                twapPriceFeedMessages[i].priceId,
+                twapPriceFeedMessages[i].cumulativePrice,
+                twapPriceFeedMessages[i].cumulativeConf,
+                twapPriceFeedMessages[i].numDownSlots,
+                twapPriceFeedMessages[i].publishSlot,
+                twapPriceFeedMessages[i].publishTime,
+                twapPriceFeedMessages[i].prevPublishTime,
+                twapPriceFeedMessages[i].expo
+            );
+        }
+    }
+
     function generateWhMerkleUpdateWithSource(
         PriceFeedMessage[] memory priceFeedMessages,
         MerkleUpdateConfig memory config
@@ -159,6 +192,65 @@ abstract contract PythTestUtils is Test, WormholeTestUtils, RandTestUtils {
         }
     }
 
+    function generateWhMerkleTwapUpdateWithSource(
+        TwapPriceFeedMessage[] memory twapPriceFeedMessages,
+        MerkleUpdateConfig memory config
+    ) internal returns (bytes memory whMerkleTwapUpdateData) {
+        bytes[]
+            memory encodedTwapPriceFeedMessages = encodeTwapPriceFeedMessages(
+                twapPriceFeedMessages
+            );
+
+        (bytes20 rootDigest, bytes[] memory proofs) = MerkleTree
+            .constructProofs(encodedTwapPriceFeedMessages, config.depth);
+
+        bytes memory wormholePayload = abi.encodePacked(
+            uint32(0x41555756), // PythAccumulator.ACCUMULATOR_WORMHOLE_MAGIC
+            uint8(PythAccumulator.UpdateType.WormholeMerkle),
+            uint64(0), // Slot, not used in target networks
+            uint32(0), // Ring size, not used in target networks
+            rootDigest
+        );
+
+        bytes memory wormholeMerkleVaa = generateVaa(
+            0,
+            config.source_chain_id,
+            config.source_emitter_address,
+            0,
+            wormholePayload,
+            config.numSigners
+        );
+
+        if (config.brokenVaa) {
+            uint mutPos = getRandUint() % wormholeMerkleVaa.length;
+
+            // mutate the random position by 1 bit
+            wormholeMerkleVaa[mutPos] = bytes1(
+                uint8(wormholeMerkleVaa[mutPos]) ^ 1
+            );
+        }
+
+        whMerkleTwapUpdateData = abi.encodePacked(
+            uint32(0x504e4155), // PythAccumulator.ACCUMULATOR_MAGIC
+            uint8(1), // major version
+            uint8(0), // minor version
+            uint8(0), // trailing header size
+            uint8(PythAccumulator.UpdateType.WormholeMerkle),
+            uint16(wormholeMerkleVaa.length),
+            wormholeMerkleVaa,
+            uint8(twapPriceFeedMessages.length)
+        );
+
+        for (uint i = 0; i < twapPriceFeedMessages.length; i++) {
+            whMerkleTwapUpdateData = abi.encodePacked(
+                whMerkleTwapUpdateData,
+                uint16(encodedTwapPriceFeedMessages[i].length),
+                encodedTwapPriceFeedMessages[i],
+                proofs[i]
+            );
+        }
+    }
+
     function generateWhMerkleUpdate(
         PriceFeedMessage[] memory priceFeedMessages,
         uint8 depth,

+ 10 - 0
target_chains/ethereum/sdk/solidity/AbstractPyth.sol

@@ -135,4 +135,14 @@ abstract contract AbstractPyth is IPyth {
         virtual
         override
         returns (PythStructs.PriceFeed[] memory priceFeeds);
+
+    function parseTwapPriceFeedUpdates(
+        bytes[] calldata updateData,
+        bytes32[] calldata priceIds
+    )
+        external
+        payable
+        virtual
+        override
+        returns (PythStructs.TwapPriceFeed[] memory twapPriceFeeds);
 }

+ 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 consecutive price updates 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 exactly two updates 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 updateData array.
+    ///
+    /// @dev Reverts if:
+    /// - The transferred fee is not sufficient
+    /// - The updateData is invalid or malformed
+    /// - The updateData array does not contain exactly 2 updates
+    /// - There is no update for any of the given `priceIds`
+    /// - The time ordering between data points is invalid (start time must be before end time)
+    /// @param updateData Array containing exactly two price updates (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 downSlotsRatio Down slot ratio of the TWAP.
+    event TwapPriceFeedUpdate(
+        bytes32 indexed id,
+        uint64 startTime,
+        uint64 endTime,
+        int64 twapPrice,
+        uint64 twapConf,
+        uint32 downSlotsRatio
+    );
 }

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

@@ -4,6 +4,7 @@ pragma solidity ^0.8.0;
 import "./AbstractPyth.sol";
 import "./PythStructs.sol";
 import "./PythErrors.sol";
+import "./PythUtils.sol";
 
 contract MockPyth is AbstractPyth {
     mapping(bytes32 => PythStructs.PriceFeed) priceFeeds;
@@ -160,6 +161,106 @@ contract MockPyth is AbstractPyth {
             );
     }
 
+    function parseTwapPriceFeedUpdates(
+        bytes[] calldata updateData,
+        bytes32[] calldata priceIds
+    )
+        external
+        payable
+        override
+        returns (PythStructs.TwapPriceFeed[] memory twapPriceFeeds)
+    {
+        uint requiredFee = getUpdateFee(updateData);
+        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);
+        }
+
+        return twapPriceFeeds;
+    }
+
+    // You can create this data either by calling createTwapPriceFeedUpdateData.
+    // @note: The updateData expected here is different from the one used in the main contract.
+    // In particular, the expected format is:
+    // [
+    //     abi.encode(
+    //         bytes32 id,
+    //         PythStructs.TwapPriceInfo startInfo,
+    //         PythStructs.TwapPriceInfo endInfo
+    //     )
+    // ]
+    function processTwapPriceFeed(
+        bytes[] calldata updateData,
+        bytes32 priceId,
+        uint index,
+        PythStructs.TwapPriceFeed[] memory twapPriceFeeds
+    ) private {
+        // Decode TWAP feed directly
+        PythStructs.TwapPriceFeed memory twapFeed = abi.decode(
+            updateData[0],
+            (PythStructs.TwapPriceFeed)
+        );
+
+        // Validate ID matches
+        if (twapFeed.id != priceId)
+            revert PythErrors.InvalidTwapUpdateDataSet();
+
+        // Store the TWAP feed
+        twapPriceFeeds[index] = twapFeed;
+
+        // Emit event
+        emit TwapPriceFeedUpdate(
+            priceId,
+            twapFeed.startTime,
+            twapFeed.endTime,
+            twapFeed.twap.price,
+            twapFeed.twap.conf,
+            twapFeed.downSlotsRatio
+        );
+    }
+
+    /**
+     * @notice Creates TWAP price feed update data with simplified parameters for testing
+     * @param id The price feed ID
+     * @param startTime Start time of the TWAP
+     * @param endTime End time of the TWAP
+     * @param price The price value
+     * @param conf The confidence interval
+     * @param expo Price exponent
+     * @param downSlotsRatio Down slots ratio
+     * @return twapData Encoded TWAP price feed data ready for parseTwapPriceFeedUpdates
+     */
+    function createTwapPriceFeedUpdateData(
+        bytes32 id,
+        uint64 startTime,
+        uint64 endTime,
+        int64 price,
+        uint64 conf,
+        int32 expo,
+        uint32 downSlotsRatio
+    ) public pure returns (bytes memory twapData) {
+        PythStructs.Price memory twapPrice = PythStructs.Price({
+            price: price,
+            conf: conf,
+            expo: expo,
+            publishTime: endTime
+        });
+
+        PythStructs.TwapPriceFeed memory twapFeed = PythStructs.TwapPriceFeed({
+            id: id,
+            startTime: startTime,
+            endTime: endTime,
+            twap: twapPrice,
+            downSlotsRatio: downSlotsRatio
+        });
+
+        twapData = abi.encode(twapFeed);
+    }
+
     function createPriceFeedUpdateData(
         bytes32 id,
         int64 price,

+ 4 - 0
target_chains/ethereum/sdk/solidity/PythErrors.sol

@@ -45,4 +45,8 @@ library PythErrors {
     // The wormhole address to set in SetWormholeAddress governance is invalid.
     // Signature: 0x13d3ed82
     error InvalidWormholeAddressToSet();
+    // The twap update data is invalid.
+    error InvalidTwapUpdateData();
+    // The twap update data set is invalid.
+    error InvalidTwapUpdateDataSet();
 }

+ 35 - 0
target_chains/ethereum/sdk/solidity/PythStructs.sol

@@ -30,4 +30,39 @@ contract PythStructs {
         // Latest available exponentially-weighted moving average price
         Price emaPrice;
     }
+
+    struct TwapPriceFeed {
+        // The price ID.
+        bytes32 id;
+        // Start time of the TWAP
+        uint64 startTime;
+        // End time of the TWAP
+        uint64 endTime;
+        // TWAP price
+        Price twap;
+        // Down slot ratio represents the ratio of price feed updates that were missed or unavailable
+        // during the TWAP period, expressed as a fixed-point number between 0 and 1e6 (100%).
+        // For example:
+        //   - 0 means all price updates were available
+        //   - 500_000 means 50% of updates were missed
+        //   - 1_000_000 means all updates were missed
+        // This can be used to assess the quality/reliability of the TWAP calculation.
+        // Applications should define a maximum acceptable ratio (e.g. 100000 for 10%)
+        // and revert if downSlotsRatio exceeds it.
+        uint32 downSlotsRatio;
+    }
+
+    // Information used to calculate time-weighted average prices (TWAP)
+    struct TwapPriceInfo {
+        // slot 1
+        int128 cumulativePrice;
+        uint128 cumulativeConf;
+        // slot 2
+        uint64 numDownSlots;
+        uint64 publishSlot;
+        uint64 publishTime;
+        uint64 prevPublishTime;
+        // slot 3
+        int32 expo;
+    }
 }

+ 52 - 0
target_chains/ethereum/sdk/solidity/PythUtils.sol

@@ -1,6 +1,8 @@
 // SPDX-License-Identifier: Apache-2.0
 pragma solidity ^0.8.0;
 
+import "./PythStructs.sol";
+
 library PythUtils {
     /// @notice Converts a Pyth price to a uint256 with a target number of decimals
     /// @param price The Pyth price
@@ -31,4 +33,54 @@ library PythUtils {
                 10 ** uint32(priceDecimals - targetDecimals);
         }
     }
+
+    /// @notice Calculates TWAP from two price points
+    /// @dev The calculation is done by taking the difference of cumulative values and dividing by the time difference
+    /// @param priceId The price feed ID
+    /// @param twapPriceInfoStart The starting price point
+    /// @param twapPriceInfoEnd The ending price point
+    /// @return twapPriceFeed The calculated TWAP price feed
+    function calculateTwap(
+        bytes32 priceId,
+        PythStructs.TwapPriceInfo memory twapPriceInfoStart,
+        PythStructs.TwapPriceInfo memory twapPriceInfoEnd
+    ) public 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 by dividing
+        // the difference in cumulative values by the number of slots between data points
+        int128 twapPrice = priceDiff / int128(uint128(slotDiff));
+        uint128 twapConf = confDiff / uint128(slotDiff);
+
+        // The conversion from int128 to int64 is safe because:
+        // 1. Individual prices fit within int64 by protocol design
+        // 2. TWAP is essentially an average price over time (cumulativePrice₂-cumulativePrice₁)/slotDiff
+        // 3. This average must be within the range of individual prices that went into the calculation
+        // We use int128 only as an intermediate type to safely handle cumulative sums
+        twapPriceFeed.twap.price = int64(twapPrice);
+        twapPriceFeed.twap.conf = uint64(twapConf);
+        twapPriceFeed.twap.expo = twapPriceInfoStart.expo;
+        twapPriceFeed.twap.publishTime = twapPriceInfoEnd.publishTime;
+
+        // Calculate downSlotsRatio as a value between 0 and 1,000,000
+        // 0 means no slots were missed, 1,000,000 means all slots were missed
+        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.downSlotsRatio = uint32(downSlotsRatio);
+
+        return twapPriceFeed;
+    }
 }

+ 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": "downSlotsRatio",
+        "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": "uint64",
+            "name": "startTime",
+            "type": "uint64"
+          },
+          {
+            "internalType": "uint64",
+            "name": "endTime",
+            "type": "uint64"
+          },
+          {
+            "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": "downSlotsRatio",
+            "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": "downSlotsRatio",
+        "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": "uint64",
+            "name": "startTime",
+            "type": "uint64"
+          },
+          {
+            "internalType": "uint64",
+            "name": "endTime",
+            "type": "uint64"
+          },
+          {
+            "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": "downSlotsRatio",
+            "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": "downSlotsRatio",
+        "type": "uint32"
+      }
+    ],
+    "name": "TwapPriceFeedUpdate",
+    "type": "event"
   }
 ]

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

@@ -25,6 +25,11 @@
     "name": "InvalidArgument",
     "type": "error"
   },
+  {
+    "inputs": [],
+    "name": "InvalidTwapUpdateDataSet",
+    "type": "error"
+  },
   {
     "inputs": [],
     "name": "NoFreshUpdate",
@@ -76,6 +81,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": "downSlotsRatio",
+        "type": "uint32"
+      }
+    ],
+    "name": "TwapPriceFeedUpdate",
+    "type": "event"
+  },
   {
     "inputs": [
       {
@@ -130,6 +178,55 @@
     "stateMutability": "pure",
     "type": "function"
   },
+  {
+    "inputs": [
+      {
+        "internalType": "bytes32",
+        "name": "id",
+        "type": "bytes32"
+      },
+      {
+        "internalType": "uint64",
+        "name": "startTime",
+        "type": "uint64"
+      },
+      {
+        "internalType": "uint64",
+        "name": "endTime",
+        "type": "uint64"
+      },
+      {
+        "internalType": "int64",
+        "name": "price",
+        "type": "int64"
+      },
+      {
+        "internalType": "uint64",
+        "name": "conf",
+        "type": "uint64"
+      },
+      {
+        "internalType": "int32",
+        "name": "expo",
+        "type": "int32"
+      },
+      {
+        "internalType": "uint32",
+        "name": "downSlotsRatio",
+        "type": "uint32"
+      }
+    ],
+    "name": "createTwapPriceFeedUpdateData",
+    "outputs": [
+      {
+        "internalType": "bytes",
+        "name": "twapData",
+        "type": "bytes"
+      }
+    ],
+    "stateMutability": "pure",
+    "type": "function"
+  },
   {
     "inputs": [
       {
@@ -608,6 +705,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": "uint64",
+            "name": "startTime",
+            "type": "uint64"
+          },
+          {
+            "internalType": "uint64",
+            "name": "endTime",
+            "type": "uint64"
+          },
+          {
+            "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": "downSlotsRatio",
+            "type": "uint32"
+          }
+        ],
+        "internalType": "struct PythStructs.TwapPriceFeed[]",
+        "name": "twapPriceFeeds",
+        "type": "tuple[]"
+      }
+    ],
+    "stateMutability": "payable",
+    "type": "function"
+  },
   {
     "inputs": [
       {

+ 10 - 0
target_chains/ethereum/sdk/solidity/abis/PythErrors.json

@@ -24,6 +24,16 @@
     "name": "InvalidGovernanceTarget",
     "type": "error"
   },
+  {
+    "inputs": [],
+    "name": "InvalidTwapUpdateData",
+    "type": "error"
+  },
+  {
+    "inputs": [],
+    "name": "InvalidTwapUpdateDataSet",
+    "type": "error"
+  },
   {
     "inputs": [],
     "name": "InvalidUpdateData",

+ 152 - 0
target_chains/ethereum/sdk/solidity/abis/PythUtils.json

@@ -1,4 +1,156 @@
 [
+  {
+    "inputs": [
+      {
+        "internalType": "bytes32",
+        "name": "priceId",
+        "type": "bytes32"
+      },
+      {
+        "components": [
+          {
+            "internalType": "int128",
+            "name": "cumulativePrice",
+            "type": "int128"
+          },
+          {
+            "internalType": "uint128",
+            "name": "cumulativeConf",
+            "type": "uint128"
+          },
+          {
+            "internalType": "uint64",
+            "name": "numDownSlots",
+            "type": "uint64"
+          },
+          {
+            "internalType": "uint64",
+            "name": "publishSlot",
+            "type": "uint64"
+          },
+          {
+            "internalType": "uint64",
+            "name": "publishTime",
+            "type": "uint64"
+          },
+          {
+            "internalType": "uint64",
+            "name": "prevPublishTime",
+            "type": "uint64"
+          },
+          {
+            "internalType": "int32",
+            "name": "expo",
+            "type": "int32"
+          }
+        ],
+        "internalType": "struct PythStructs.TwapPriceInfo",
+        "name": "twapPriceInfoStart",
+        "type": "tuple"
+      },
+      {
+        "components": [
+          {
+            "internalType": "int128",
+            "name": "cumulativePrice",
+            "type": "int128"
+          },
+          {
+            "internalType": "uint128",
+            "name": "cumulativeConf",
+            "type": "uint128"
+          },
+          {
+            "internalType": "uint64",
+            "name": "numDownSlots",
+            "type": "uint64"
+          },
+          {
+            "internalType": "uint64",
+            "name": "publishSlot",
+            "type": "uint64"
+          },
+          {
+            "internalType": "uint64",
+            "name": "publishTime",
+            "type": "uint64"
+          },
+          {
+            "internalType": "uint64",
+            "name": "prevPublishTime",
+            "type": "uint64"
+          },
+          {
+            "internalType": "int32",
+            "name": "expo",
+            "type": "int32"
+          }
+        ],
+        "internalType": "struct PythStructs.TwapPriceInfo",
+        "name": "twapPriceInfoEnd",
+        "type": "tuple"
+      }
+    ],
+    "name": "calculateTwap",
+    "outputs": [
+      {
+        "components": [
+          {
+            "internalType": "bytes32",
+            "name": "id",
+            "type": "bytes32"
+          },
+          {
+            "internalType": "uint64",
+            "name": "startTime",
+            "type": "uint64"
+          },
+          {
+            "internalType": "uint64",
+            "name": "endTime",
+            "type": "uint64"
+          },
+          {
+            "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": "downSlotsRatio",
+            "type": "uint32"
+          }
+        ],
+        "internalType": "struct PythStructs.TwapPriceFeed",
+        "name": "twapPriceFeed",
+        "type": "tuple"
+      }
+    ],
+    "stateMutability": "pure",
+    "type": "function"
+  },
   {
     "inputs": [
       {

+ 1 - 1
target_chains/fuel/contracts/Cargo.lock

@@ -7261,4 +7261,4 @@ checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa"
 dependencies = [
  "cc",
  "pkg-config",
-]
+]

+ 1 - 1
target_chains/near/example/Cargo.lock

@@ -3824,4 +3824,4 @@ dependencies = [
  "flate2",
  "thiserror",
  "time",
-]
+]