diff --git a/target_chains/cosmwasm/examples/cw-contract/Cargo.lock b/target_chains/cosmwasm/examples/cw-contract/Cargo.lock index 7a8a4c0d3f..c255e5b155 100644 --- a/target_chains/cosmwasm/examples/cw-contract/Cargo.lock +++ b/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" \ No newline at end of file diff --git a/target_chains/ethereum/contracts/contracts/pyth/Pyth.sol b/target_chains/ethereum/contracts/contracts/pyth/Pyth.sol index 4e7be0bbff..dbc2be966c 100644 --- a/target_chains/ethereum/contracts/contracts/pyth/Pyth.sol +++ b/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 + ); } } diff --git a/target_chains/ethereum/contracts/contracts/pyth/PythAccumulator.sol b/target_chains/ethereum/contracts/contracts/pyth/PythAccumulator.sol index 2779cec497..caa0c49920 100644 --- a/target_chains/ethereum/contracts/contracts/pyth/PythAccumulator.sol +++ b/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) { diff --git a/target_chains/ethereum/contracts/forge-test/Pyth.t.sol b/target_chains/ethereum/contracts/forge-test/Pyth.t.sol index bd072df0a2..b95c0e1dd0 100644 --- a/target_chains/ethereum/contracts/forge-test/Pyth.t.sol +++ b/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 + ); + } } diff --git a/target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol b/target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol index 53294bf900..d3cb7d9f76 100644 --- a/target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol +++ b/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, diff --git a/target_chains/ethereum/contracts/package.json b/target_chains/ethereum/contracts/package.json index 6f6ef476c4..ab12685d81 100644 --- a/target_chains/ethereum/contracts/package.json +++ b/target_chains/ethereum/contracts/package.json @@ -1,6 +1,6 @@ { "name": "@pythnetwork/pyth-evm-contract", - "version": "1.4.4-alpha.1", + "version": "1.4.4-alpha.2", "description": "", "private": "true", "devDependencies": { diff --git a/target_chains/ethereum/sdk/solidity/AbstractPyth.sol b/target_chains/ethereum/sdk/solidity/AbstractPyth.sol index 691b4804c3..295bc78264 100644 --- a/target_chains/ethereum/sdk/solidity/AbstractPyth.sol +++ b/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); } diff --git a/target_chains/ethereum/sdk/solidity/IPyth.sol b/target_chains/ethereum/sdk/solidity/IPyth.sol index c94e8c3ed8..3d3cae7148 100644 --- a/target_chains/ethereum/sdk/solidity/IPyth.sol +++ b/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 diff --git a/target_chains/ethereum/sdk/solidity/IPythEvents.sol b/target_chains/ethereum/sdk/solidity/IPythEvents.sol index a293776162..661158c5aa 100644 --- a/target_chains/ethereum/sdk/solidity/IPythEvents.sol +++ b/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 + ); } diff --git a/target_chains/ethereum/sdk/solidity/MockPyth.sol b/target_chains/ethereum/sdk/solidity/MockPyth.sol index dcb8d7864a..77aa86bb6f 100644 --- a/target_chains/ethereum/sdk/solidity/MockPyth.sol +++ b/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, diff --git a/target_chains/ethereum/sdk/solidity/PythErrors.sol b/target_chains/ethereum/sdk/solidity/PythErrors.sol index 2b5457740e..ad98f11e30 100644 --- a/target_chains/ethereum/sdk/solidity/PythErrors.sol +++ b/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(); } diff --git a/target_chains/ethereum/sdk/solidity/PythStructs.sol b/target_chains/ethereum/sdk/solidity/PythStructs.sol index b3d2ee2c6a..04feddfdce 100644 --- a/target_chains/ethereum/sdk/solidity/PythStructs.sol +++ b/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; + } } diff --git a/target_chains/ethereum/sdk/solidity/PythUtils.sol b/target_chains/ethereum/sdk/solidity/PythUtils.sol index 04b7f51f34..91af5952c4 100644 --- a/target_chains/ethereum/sdk/solidity/PythUtils.sol +++ b/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; + } } diff --git a/target_chains/ethereum/sdk/solidity/abis/AbstractPyth.json b/target_chains/ethereum/sdk/solidity/abis/AbstractPyth.json index 7b6724065f..0e7ccf59bb 100644 --- a/target_chains/ethereum/sdk/solidity/abis/AbstractPyth.json +++ b/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": [ { diff --git a/target_chains/ethereum/sdk/solidity/abis/IPyth.json b/target_chains/ethereum/sdk/solidity/abis/IPyth.json index 0b5fa851b2..1bfe2c1e98 100644 --- a/target_chains/ethereum/sdk/solidity/abis/IPyth.json +++ b/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": [ { diff --git a/target_chains/ethereum/sdk/solidity/abis/IPythEvents.json b/target_chains/ethereum/sdk/solidity/abis/IPythEvents.json index 1a89732089..b192fe28a1 100644 --- a/target_chains/ethereum/sdk/solidity/abis/IPythEvents.json +++ b/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" } ] diff --git a/target_chains/ethereum/sdk/solidity/abis/MockPyth.json b/target_chains/ethereum/sdk/solidity/abis/MockPyth.json index a917b56a28..1f5ebb3c32 100644 --- a/target_chains/ethereum/sdk/solidity/abis/MockPyth.json +++ b/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": [ { diff --git a/target_chains/ethereum/sdk/solidity/abis/PythErrors.json b/target_chains/ethereum/sdk/solidity/abis/PythErrors.json index f8fdc192ce..def11cb07a 100644 --- a/target_chains/ethereum/sdk/solidity/abis/PythErrors.json +++ b/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", diff --git a/target_chains/ethereum/sdk/solidity/abis/PythUtils.json b/target_chains/ethereum/sdk/solidity/abis/PythUtils.json index c30f138950..eee5c3ed69 100644 --- a/target_chains/ethereum/sdk/solidity/abis/PythUtils.json +++ b/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": [ { diff --git a/target_chains/fuel/contracts/Cargo.lock b/target_chains/fuel/contracts/Cargo.lock index b82ed9002b..0ae3a77fd2 100644 --- a/target_chains/fuel/contracts/Cargo.lock +++ b/target_chains/fuel/contracts/Cargo.lock @@ -7261,4 +7261,4 @@ checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" dependencies = [ "cc", "pkg-config", -] +] \ No newline at end of file diff --git a/target_chains/near/example/Cargo.lock b/target_chains/near/example/Cargo.lock index 568198a246..caecca2123 100644 --- a/target_chains/near/example/Cargo.lock +++ b/target_chains/near/example/Cargo.lock @@ -3824,4 +3824,4 @@ dependencies = [ "flate2", "thiserror", "time", -] +] \ No newline at end of file