diff --git a/target_chains/ethereum/contracts/contracts/pulse/Scheduler.sol b/target_chains/ethereum/contracts/contracts/pulse/Scheduler.sol index eb3c53e6e2..6c02e20a3d 100644 --- a/target_chains/ethereum/contracts/contracts/pulse/Scheduler.sol +++ b/target_chains/ethereum/contracts/contracts/pulse/Scheduler.sol @@ -308,11 +308,14 @@ abstract contract Scheduler is IScheduler, SchedulerState, SchedulerConstants { ( PythStructs.PriceFeed[] memory priceFeeds, uint64[] memory slots - ) = pyth.parsePriceFeedUpdatesWithSlotsStrict{value: pythFee}( + ) = pyth.parsePriceFeedUpdatesWithConfig{value: pythFee}( updateData, params.priceIds, 0, // We enforce the past max validity ourselves in _validateShouldUpdatePrices - curTime + FUTURE_TIMESTAMP_MAX_VALIDITY_PERIOD + curTime + FUTURE_TIMESTAMP_MAX_VALIDITY_PERIOD, + false, + true, + false ); // Verify all price feeds have the same Pythnet slot. diff --git a/target_chains/ethereum/contracts/contracts/pyth/Pyth.sol b/target_chains/ethereum/contracts/contracts/pyth/Pyth.sol index 0df97396ed..b4a1df3f14 100644 --- a/target_chains/ethereum/contracts/contracts/pyth/Pyth.sol +++ b/target_chains/ethereum/contracts/contracts/pyth/Pyth.sol @@ -317,34 +317,36 @@ abstract contract Pyth is return merkleData.numUpdates; } - function parsePriceFeedUpdatesInternal( + function parsePriceFeedUpdatesWithConfig( bytes[] calldata updateData, bytes32[] calldata priceIds, uint64 minAllowedPublishTime, uint64 maxAllowedPublishTime, bool checkUniqueness, - bool checkUpdateDataIsMinimal + bool checkUpdateDataIsMinimal, + bool storeUpdatesIfFresh ) - internal + public + payable returns ( PythStructs.PriceFeed[] memory priceFeeds, uint64[] memory slots ) { - { - uint requiredFee = getUpdateFee(updateData); - if (msg.value < requiredFee) revert PythErrors.InsufficientFee(); - } + if (msg.value < getUpdateFee(updateData)) + revert PythErrors.InsufficientFee(); // Create the context struct that holds all shared parameters - PythInternalStructs.UpdateParseContext memory context; - context.priceIds = priceIds; - context.minAllowedPublishTime = minAllowedPublishTime; - context.maxAllowedPublishTime = maxAllowedPublishTime; - context.checkUniqueness = checkUniqueness; - context.checkUpdateDataIsMinimal = checkUpdateDataIsMinimal; - context.priceFeeds = new PythStructs.PriceFeed[](priceIds.length); - context.slots = new uint64[](priceIds.length); + PythInternalStructs.UpdateParseContext + memory context = PythInternalStructs.UpdateParseContext({ + priceIds: priceIds, + minAllowedPublishTime: minAllowedPublishTime, + maxAllowedPublishTime: maxAllowedPublishTime, + checkUniqueness: checkUniqueness, + checkUpdateDataIsMinimal: checkUpdateDataIsMinimal, + priceFeeds: new PythStructs.PriceFeed[](priceIds.length), + slots: new uint64[](priceIds.length) + }); // Track total updates for minimal update data check uint64 totalUpdatesAcrossBlobs = 0; @@ -358,6 +360,23 @@ abstract contract Pyth is context ); } + + for (uint j = 0; j < priceIds.length; j++) { + PythStructs.PriceFeed memory pf = context.priceFeeds[j]; + if (storeUpdatesIfFresh && pf.id != 0) { + updateLatestPriceIfNecessary( + priceIds[j], + PythInternalStructs.PriceInfo({ + publishTime: uint64(pf.price.publishTime), + expo: pf.price.expo, + price: pf.price.price, + conf: pf.price.conf, + emaPrice: pf.emaPrice.price, + emaConf: pf.emaPrice.conf + }) + ); + } + } } // In minimal update data mode, revert if we have more or less updates than price IDs @@ -378,6 +397,7 @@ abstract contract Pyth is // Return results return (context.priceFeeds, context.slots); } + function parsePriceFeedUpdates( bytes[] calldata updateData, bytes32[] calldata priceIds, @@ -389,41 +409,17 @@ abstract contract Pyth is override returns (PythStructs.PriceFeed[] memory priceFeeds) { - (priceFeeds, ) = parsePriceFeedUpdatesInternal( + (priceFeeds, ) = parsePriceFeedUpdatesWithConfig( updateData, priceIds, minPublishTime, maxPublishTime, false, + false, false ); } - function parsePriceFeedUpdatesWithSlotsStrict( - bytes[] calldata updateData, - bytes32[] calldata priceIds, - uint64 minPublishTime, - uint64 maxPublishTime - ) - external - payable - override - returns ( - PythStructs.PriceFeed[] memory priceFeeds, - uint64[] memory slots - ) - { - return - parsePriceFeedUpdatesInternal( - updateData, - priceIds, - minPublishTime, - maxPublishTime, - false, - true - ); - } - function extractTwapPriceInfos( bytes calldata updateData ) @@ -624,12 +620,13 @@ abstract contract Pyth is override returns (PythStructs.PriceFeed[] memory priceFeeds) { - (priceFeeds, ) = parsePriceFeedUpdatesInternal( + (priceFeeds, ) = parsePriceFeedUpdatesWithConfig( updateData, priceIds, minPublishTime, maxPublishTime, true, + false, false ); } @@ -703,7 +700,7 @@ abstract contract Pyth is } function version() public pure returns (string memory) { - return "1.4.5"; + return "1.4.5-alpha.1"; } /// @notice Calculates TWAP from two price points diff --git a/target_chains/ethereum/contracts/forge-test/GasBenchmark.t.sol b/target_chains/ethereum/contracts/forge-test/GasBenchmark.t.sol index ee82c3280f..063a5c2e6a 100644 --- a/target_chains/ethereum/contracts/forge-test/GasBenchmark.t.sol +++ b/target_chains/ethereum/contracts/forge-test/GasBenchmark.t.sol @@ -240,6 +240,36 @@ contract GasBenchmark is Test, WormholeTestUtils, PythTestUtils { ); } + function testBenchmarkParsePriceFeedUpdatesWithConfigTrue() public { + bytes32[] memory ids = new bytes32[](1); + ids[0] = priceIds[0]; + + pyth.parsePriceFeedUpdatesWithConfig{value: freshPricesUpdateFee[0]}( + freshPricesUpdateData[0], + ids, + 0, + 50, + false, + true, // check minimal + false + ); + } + + function testBenchmarkParsePriceFeedUpdatesWithConfigFalse() public { + bytes32[] memory ids = new bytes32[](1); + ids[0] = priceIds[0]; + + pyth.parsePriceFeedUpdatesWithConfig{value: freshPricesUpdateFee[0]}( + freshPricesUpdateData[0], // contains only priceIds[0] + ids, + 0, + 50, + false, + true, // check minimal + false + ); + } + function testBenchmarkParsePriceFeedUpdatesUniqueFor() public { bytes32[] memory ids = new bytes32[](1); ids[0] = priceIds[0]; diff --git a/target_chains/ethereum/contracts/forge-test/Pyth.t.sol b/target_chains/ethereum/contracts/forge-test/Pyth.t.sol index b883ba9e58..e7b41accff 100644 --- a/target_chains/ethereum/contracts/forge-test/Pyth.t.sol +++ b/target_chains/ethereum/contracts/forge-test/Pyth.t.sol @@ -278,7 +278,92 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils { } } - function testParsePriceFeedUpdatesWithSlotsStrictWorks(uint seed) public { + function testParsePriceFeedUpdatesWithConfigIfStorageTrue( + uint seed + ) public { + setRandSeed(seed); + uint numMessages = 1 + (getRandUint() % 10); + ( + bytes32[] memory priceIds, + PriceFeedMessage[] memory messages + ) = generateRandomPriceMessages(numMessages); + + ( + bytes[] memory updateData, + uint updateFee + ) = createBatchedUpdateDataFromMessages(messages); + + (PythStructs.PriceFeed[] memory priceFeeds, ) = pyth + .parsePriceFeedUpdatesWithConfig{value: updateFee}( + updateData, + priceIds, + 0, + MAX_UINT64, + false, + true, + true + ); + + for (uint i = 0; i < numMessages; i++) { + // Validating that returned priceIds are equal + assertEq(priceFeeds[i].id, priceIds[i]); + assertEq(priceFeeds[i].price.price, messages[i].price); + assertEq(priceFeeds[i].price.conf, messages[i].conf); + assertEq(priceFeeds[i].price.expo, messages[i].expo); + assertEq(priceFeeds[i].price.publishTime, messages[i].publishTime); + assertEq(priceFeeds[i].emaPrice.price, messages[i].emaPrice); + assertEq(priceFeeds[i].emaPrice.conf, messages[i].emaConf); + assertEq(priceFeeds[i].emaPrice.expo, messages[i].expo); + assertEq( + priceFeeds[i].emaPrice.publishTime, + messages[i].publishTime + ); + + // Validating that prices are stored on chain + PythStructs.Price memory curPrice = pyth.getPriceUnsafe( + messages[i].priceId + ); + + assertEq(priceFeeds[i].price.price, curPrice.price); + assertEq(priceFeeds[i].price.conf, curPrice.conf); + assertEq(priceFeeds[i].price.expo, curPrice.expo); + assertEq(priceFeeds[i].price.publishTime, curPrice.publishTime); + } + } + + function testParsePriceFeedUpdatesWithConfigIfStorageFalse( + uint seed + ) public { + setRandSeed(seed); + uint numMessages = 1 + (getRandUint() % 10); + ( + bytes32[] memory priceIds, + PriceFeedMessage[] memory messages + ) = generateRandomPriceMessages(numMessages); + + ( + bytes[] memory updateData, + uint updateFee + ) = createBatchedUpdateDataFromMessages(messages); + + pyth.parsePriceFeedUpdatesWithConfig{value: updateFee}( + updateData, + priceIds, + 0, + MAX_UINT64, + false, + true, + false + ); + + // validate that stored prices of each priceId are still unpopulated + for (uint i = 0; i < numMessages; i++) { + vm.expectRevert(PythErrors.PriceFeedNotFound.selector); + pyth.getPriceUnsafe(priceIds[i]); + } + } + + function testParsePriceFeedUpdatesWithConfigWorks(uint seed) public { setRandSeed(seed); uint numMessages = 1 + (getRandUint() % 10); ( @@ -293,11 +378,14 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils { ( PythStructs.PriceFeed[] memory priceFeeds, uint64[] memory slots - ) = pyth.parsePriceFeedUpdatesWithSlotsStrict{value: updateFee}( + ) = pyth.parsePriceFeedUpdatesWithConfig{value: updateFee}( updateData, priceIds, 0, - MAX_UINT64 + MAX_UINT64, + false, + true, + false ); assertEq(priceFeeds.length, numMessages); @@ -434,7 +522,7 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils { ); } - function testParsePriceFeedUpdatesWithSlotsStrictRevertsWithExcessUpdateData() + function testParsePriceFeedUpdatesWithConfigRevertsWithExcessUpdateData() public { // Create a price update with more price updates than requested price IDs @@ -459,15 +547,18 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils { // Should revert in strict mode vm.expectRevert(PythErrors.InvalidArgument.selector); - pyth.parsePriceFeedUpdatesWithSlotsStrict{value: updateFee}( + pyth.parsePriceFeedUpdatesWithConfig{value: updateFee}( updateData, requestedPriceIds, 0, - MAX_UINT64 + MAX_UINT64, + false, + true, + false ); } - function testParsePriceFeedUpdatesWithSlotsStrictRevertsWithFewerUpdateData() + function testParsePriceFeedUpdatesWithConfigRevertsWithFewerUpdateData() public { // Create a price update with fewer price updates than requested price IDs @@ -496,11 +587,14 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils { // Should revert in strict mode because we have fewer updates than price IDs vm.expectRevert(PythErrors.InvalidArgument.selector); - pyth.parsePriceFeedUpdatesWithSlotsStrict{value: updateFee}( + pyth.parsePriceFeedUpdatesWithConfig{value: updateFee}( updateData, requestedPriceIds, 0, - MAX_UINT64 + MAX_UINT64, + false, + true, + false ); } diff --git a/target_chains/ethereum/contracts/forge-test/utils/MockPriceFeedTestUtils.sol b/target_chains/ethereum/contracts/forge-test/utils/MockPriceFeedTestUtils.sol index 7488047587..4f650ae0b9 100644 --- a/target_chains/ethereum/contracts/forge-test/utils/MockPriceFeedTestUtils.sol +++ b/target_chains/ethereum/contracts/forge-test/utils/MockPriceFeedTestUtils.sol @@ -187,7 +187,7 @@ abstract contract MockPriceFeedTestUtils is Test { pyth, expectedFee, abi.encodeWithSelector( - IPyth.parsePriceFeedUpdatesWithSlotsStrict.selector + IPyth.parsePriceFeedUpdatesWithConfig.selector ), abi.encode(priceFeeds, slots) ); diff --git a/target_chains/ethereum/sdk/solidity/AbstractPyth.sol b/target_chains/ethereum/sdk/solidity/AbstractPyth.sol index dfa60ceb0b..295bc78264 100644 --- a/target_chains/ethereum/sdk/solidity/AbstractPyth.sol +++ b/target_chains/ethereum/sdk/solidity/AbstractPyth.sol @@ -136,21 +136,6 @@ abstract contract AbstractPyth is IPyth { override returns (PythStructs.PriceFeed[] memory priceFeeds); - function parsePriceFeedUpdatesWithSlotsStrict( - bytes[] calldata updateData, - bytes32[] calldata priceIds, - uint64 minPublishTime, - uint64 maxPublishTime - ) - external - payable - virtual - override - returns ( - PythStructs.PriceFeed[] memory priceFeeds, - uint64[] memory slots - ); - function parseTwapPriceFeedUpdates( bytes[] calldata updateData, bytes32[] calldata priceIds diff --git a/target_chains/ethereum/sdk/solidity/IPyth.sol b/target_chains/ethereum/sdk/solidity/IPyth.sol index 5418504429..2f0eef94c2 100644 --- a/target_chains/ethereum/sdk/solidity/IPyth.sol +++ b/target_chains/ethereum/sdk/solidity/IPyth.sol @@ -126,6 +126,43 @@ interface IPyth is IPythEvents { uint64 maxPublishTime ) external payable returns (PythStructs.PriceFeed[] memory priceFeeds); + /// @notice Parse `updateData` and return price feeds of the given `priceIds` if they are all published + /// within `minPublishTime` and `maxPublishTime,` but choose to store price updates if `storeUpdatesIfFresh`. + /// + /// You can use this method if you want to use a Pyth price at a fixed time and not the most recent price; + /// otherwise, please consider using `updatePriceFeeds`. This method may store the price updates on-chain, if they + /// are more recent than the current stored prices. + /// + /// This method requires the caller to pay a fee in wei; the required fee can be computed by calling + /// `getUpdateFee` with the length of the `updateData` array. + /// + /// This method will eventually allow the caller to determine whether parsed price feeds should update + /// the stored values as well. + /// + /// @dev Reverts if the transferred fee is not sufficient or the updateData is invalid or there is + /// no update for any of the given `priceIds` within the given time range. + /// @param updateData Array of price update data. + /// @param priceIds Array of price ids. + /// @param minAllowedPublishTime minimum acceptable publishTime for the given `priceIds`. + /// @param maxAllowedPublishTime maximum acceptable publishTime for the given `priceIds`. + /// @param storeUpdatesIfFresh flag for the parse function to + /// @return priceFeeds Array of the price feeds corresponding to the given `priceIds` (with the same order). + function parsePriceFeedUpdatesWithConfig( + bytes[] calldata updateData, + bytes32[] calldata priceIds, + uint64 minAllowedPublishTime, + uint64 maxAllowedPublishTime, + bool checkUniqueness, + bool checkUpdateDataIsMinimal, + bool storeUpdatesIfFresh + ) + external + payable + returns ( + PythStructs.PriceFeed[] memory priceFeeds, + uint64[] memory slots + ); + /// @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 @@ -171,24 +208,4 @@ interface IPyth is IPythEvents { uint64 minPublishTime, uint64 maxPublishTime ) external payable returns (PythStructs.PriceFeed[] memory priceFeeds); - - /// @dev Same as `parsePriceFeedUpdates`, but checks that the updateData is minimal and also returns the Pythnet slots. - /// @param updateData Array of price update data. - /// @param priceIds Array of price ids. - /// @param minPublishTime minimum acceptable publishTime for the given `priceIds`. - /// @param maxPublishTime maximum acceptable publishTime for the given `priceIds`. - /// @return priceFeeds Array of the price feeds corresponding to the given `priceIds` (with the same order). - /// @return slots Array of the Pythnet slot corresponding to the given `priceIds` (with the same order). - function parsePriceFeedUpdatesWithSlotsStrict( - bytes[] calldata updateData, - bytes32[] calldata priceIds, - uint64 minPublishTime, - uint64 maxPublishTime - ) - external - payable - returns ( - PythStructs.PriceFeed[] memory priceFeeds, - uint64[] memory slots - ); } diff --git a/target_chains/ethereum/sdk/solidity/MockPyth.sol b/target_chains/ethereum/sdk/solidity/MockPyth.sol index e0a59f9cdd..31fd72d3d3 100644 --- a/target_chains/ethereum/sdk/solidity/MockPyth.sol +++ b/target_chains/ethereum/sdk/solidity/MockPyth.sol @@ -86,14 +86,17 @@ contract MockPyth is AbstractPyth { return singleUpdateFeeInWei * updateData.length; } - function parsePriceFeedUpdatesInternal( + function parsePriceFeedUpdatesWithConfig( bytes[] calldata updateData, bytes32[] calldata priceIds, - uint64 minPublishTime, - uint64 maxPublishTime, - bool unique + uint64 minAllowedPublishTime, + uint64 maxAllowedPublishTime, + bool checkUniqueness, + bool checkUpdateDataIsMinimal, + bool storeUpdatesIfFresh ) - internal + public + payable returns (PythStructs.PriceFeed[] memory feeds, uint64[] memory slots) { uint requiredFee = getUpdateFee(updateData); @@ -124,9 +127,10 @@ contract MockPyth is AbstractPyth { if (feeds[i].id == priceIds[i]) { if ( - minPublishTime <= publishTime && - publishTime <= maxPublishTime && - (!unique || prevPublishTime < minPublishTime) + minAllowedPublishTime <= publishTime && + publishTime <= maxAllowedPublishTime && + (!checkUniqueness || + prevPublishTime < minAllowedPublishTime) ) { break; } else { @@ -146,11 +150,13 @@ contract MockPyth is AbstractPyth { uint64 minPublishTime, uint64 maxPublishTime ) external payable override returns (PythStructs.PriceFeed[] memory feeds) { - (feeds, ) = parsePriceFeedUpdatesInternal( + (feeds, ) = parsePriceFeedUpdatesWithConfig( updateData, priceIds, minPublishTime, maxPublishTime, + false, + true, false ); } @@ -161,36 +167,17 @@ contract MockPyth is AbstractPyth { uint64 minPublishTime, uint64 maxPublishTime ) external payable override returns (PythStructs.PriceFeed[] memory feeds) { - (feeds, ) = parsePriceFeedUpdatesInternal( + (feeds, ) = parsePriceFeedUpdatesWithConfig( updateData, priceIds, minPublishTime, maxPublishTime, - true + false, + true, + false ); } - function parsePriceFeedUpdatesWithSlotsStrict( - bytes[] calldata updateData, - bytes32[] calldata priceIds, - uint64 minPublishTime, - uint64 maxPublishTime - ) - external - payable - override - returns (PythStructs.PriceFeed[] memory feeds, uint64[] memory slots) - { - return - parsePriceFeedUpdatesInternal( - updateData, - priceIds, - minPublishTime, - maxPublishTime, - false - ); - } - function parseTwapPriceFeedUpdates( bytes[] calldata updateData, bytes32[] calldata priceIds diff --git a/target_chains/ethereum/sdk/solidity/abis/AbstractPyth.json b/target_chains/ethereum/sdk/solidity/abis/AbstractPyth.json index b0c8f504d0..8cd0d39878 100644 --- a/target_chains/ethereum/sdk/solidity/abis/AbstractPyth.json +++ b/target_chains/ethereum/sdk/solidity/abis/AbstractPyth.json @@ -599,16 +599,31 @@ }, { "internalType": "uint64", - "name": "minPublishTime", + "name": "minAllowedPublishTime", "type": "uint64" }, { "internalType": "uint64", - "name": "maxPublishTime", + "name": "maxAllowedPublishTime", "type": "uint64" + }, + { + "internalType": "bool", + "name": "checkUniqueness", + "type": "bool" + }, + { + "internalType": "bool", + "name": "checkUpdateDataIsMinimal", + "type": "bool" + }, + { + "internalType": "bool", + "name": "storeUpdatesIfFresh", + "type": "bool" } ], - "name": "parsePriceFeedUpdatesWithSlotsStrict", + "name": "parsePriceFeedUpdatesWithConfig", "outputs": [ { "components": [ diff --git a/target_chains/ethereum/sdk/solidity/abis/IPyth.json b/target_chains/ethereum/sdk/solidity/abis/IPyth.json index cbb0717797..260ad3ccaf 100644 --- a/target_chains/ethereum/sdk/solidity/abis/IPyth.json +++ b/target_chains/ethereum/sdk/solidity/abis/IPyth.json @@ -489,16 +489,31 @@ }, { "internalType": "uint64", - "name": "minPublishTime", + "name": "minAllowedPublishTime", "type": "uint64" }, { "internalType": "uint64", - "name": "maxPublishTime", + "name": "maxAllowedPublishTime", "type": "uint64" + }, + { + "internalType": "bool", + "name": "checkUniqueness", + "type": "bool" + }, + { + "internalType": "bool", + "name": "checkUpdateDataIsMinimal", + "type": "bool" + }, + { + "internalType": "bool", + "name": "storeUpdatesIfFresh", + "type": "bool" } ], - "name": "parsePriceFeedUpdatesWithSlotsStrict", + "name": "parsePriceFeedUpdatesWithConfig", "outputs": [ { "components": [ diff --git a/target_chains/ethereum/sdk/solidity/abis/MockPyth.json b/target_chains/ethereum/sdk/solidity/abis/MockPyth.json index ed4a5b0854..a4d1a7b991 100644 --- a/target_chains/ethereum/sdk/solidity/abis/MockPyth.json +++ b/target_chains/ethereum/sdk/solidity/abis/MockPyth.json @@ -738,16 +738,31 @@ }, { "internalType": "uint64", - "name": "minPublishTime", + "name": "minAllowedPublishTime", "type": "uint64" }, { "internalType": "uint64", - "name": "maxPublishTime", + "name": "maxAllowedPublishTime", "type": "uint64" + }, + { + "internalType": "bool", + "name": "checkUniqueness", + "type": "bool" + }, + { + "internalType": "bool", + "name": "checkUpdateDataIsMinimal", + "type": "bool" + }, + { + "internalType": "bool", + "name": "storeUpdatesIfFresh", + "type": "bool" } ], - "name": "parsePriceFeedUpdatesWithSlotsStrict", + "name": "parsePriceFeedUpdatesWithConfig", "outputs": [ { "components": [