@@ -179,111 +179,174 @@ abstract contract Pyth is
179179 if (price.publishTime == 0 ) revert PythErrors.PriceFeedNotFound ();
180180 }
181181
182+ /// Internal struct to hold parameters for update processing
183+ /// @dev Storing these variable in a struct rather than local variables
184+ /// helps reduce stack depth.
185+ struct UpdateProcessingContext {
186+ bytes32 [] priceIds;
187+ uint64 minPublishTime;
188+ uint64 maxPublishTime;
189+ bool checkUniqueness;
190+ PythStructs.PriceFeed[] priceFeeds;
191+ uint64 [] slots;
192+ }
193+
194+ /// The initial Merkle header data in an updateData. The encoded bytes
195+ /// are kept in calldata for gas efficiency.
196+ /// @dev Storing these variable in a struct rather than local variables
197+ /// helps reduce stack depth.
198+ struct MerkleData {
199+ bytes20 digest;
200+ uint8 numUpdates;
201+ uint64 slot;
202+ }
203+
204+ /// @dev Helper function to process a single price update within a Merkle proof.
205+ function _processSingleMerkleUpdate (
206+ MerkleData memory merkleData ,
207+ bytes calldata encoded ,
208+ uint offset ,
209+ UpdateProcessingContext memory context
210+ ) internal pure returns (uint newOffset ) {
211+ PythInternalStructs.PriceInfo memory priceInfo;
212+ bytes32 priceId;
213+ uint64 prevPublishTime;
214+
215+ (
216+ newOffset,
217+ priceInfo,
218+ priceId,
219+ prevPublishTime
220+ ) = extractPriceInfoFromMerkleProof (merkleData.digest, encoded, offset);
221+
222+ uint k = 0 ;
223+ for (; k < context.priceIds.length ; k++ ) {
224+ if (context.priceIds[k] == priceId) {
225+ break ;
226+ }
227+ }
228+
229+ // Check if the priceId was requested and not already filled
230+ if (k < context.priceIds.length && context.priceFeeds[k].id == 0 ) {
231+ uint publishTime = uint (priceInfo.publishTime);
232+ if (
233+ publishTime >= context.minPublishTime &&
234+ publishTime <= context.maxPublishTime &&
235+ (! context.checkUniqueness ||
236+ context.minPublishTime > prevPublishTime)
237+ ) {
238+ context.priceFeeds[k].id = priceId;
239+ context.priceFeeds[k].price.price = priceInfo.price;
240+ context.priceFeeds[k].price.conf = priceInfo.conf;
241+ context.priceFeeds[k].price.expo = priceInfo.expo;
242+ context.priceFeeds[k].price.publishTime = publishTime;
243+ context.priceFeeds[k].emaPrice.price = priceInfo.emaPrice;
244+ context.priceFeeds[k].emaPrice.conf = priceInfo.emaConf;
245+ context.priceFeeds[k].emaPrice.expo = priceInfo.expo;
246+ context.priceFeeds[k].emaPrice.publishTime = publishTime;
247+ context.slots[k] = merkleData.slot;
248+ }
249+ }
250+ }
251+
252+ /// @dev Processes a single entry from the updateData array.
253+ function _processSingleUpdateDataBlob (
254+ bytes calldata singleUpdateData ,
255+ UpdateProcessingContext memory context
256+ ) internal view {
257+ // Check magic number and length first
258+ if (
259+ singleUpdateData.length <= 4 ||
260+ UnsafeCalldataBytesLib.toUint32 (singleUpdateData, 0 ) !=
261+ ACCUMULATOR_MAGIC
262+ ) {
263+ revert PythErrors.InvalidUpdateData ();
264+ }
265+
266+ uint offset;
267+ {
268+ UpdateType updateType;
269+ (offset, updateType) = extractUpdateTypeFromAccumulatorHeader (
270+ singleUpdateData
271+ );
272+
273+ if (updateType != UpdateType.WormholeMerkle) {
274+ revert PythErrors.InvalidUpdateData ();
275+ }
276+ }
277+
278+ // Extract Merkle data
279+ MerkleData memory merkleData;
280+ bytes calldata encoded;
281+ (
282+ offset,
283+ merkleData.digest,
284+ merkleData.numUpdates,
285+ encoded,
286+ merkleData.slot
287+ ) = extractWormholeMerkleHeaderDigestAndNumUpdatesAndEncodedAndSlotFromAccumulatorUpdate (
288+ singleUpdateData,
289+ offset
290+ );
291+
292+ // Process each update within the Merkle proof
293+ for (uint j = 0 ; j < merkleData.numUpdates; j++ ) {
294+ offset = _processSingleMerkleUpdate (
295+ merkleData,
296+ encoded,
297+ offset,
298+ context
299+ );
300+ }
301+
302+ // Check final offset
303+ if (offset != encoded.length ) {
304+ revert PythErrors.InvalidUpdateData ();
305+ }
306+ }
307+
182308 function parsePriceFeedUpdatesInternal (
183309 bytes [] calldata updateData ,
184310 bytes32 [] calldata priceIds ,
185311 PythInternalStructs.ParseConfig memory config
186- ) internal returns (PythStructs.PriceFeed[] memory priceFeeds ) {
312+ )
313+ internal
314+ returns (
315+ PythStructs.PriceFeed[] memory priceFeeds ,
316+ uint64 [] memory slots
317+ )
318+ {
187319 {
188320 uint requiredFee = getUpdateFee (updateData);
189321 if (msg .value < requiredFee) revert PythErrors.InsufficientFee ();
190322 }
191- unchecked {
192- priceFeeds = new PythStructs.PriceFeed [](priceIds.length );
193- for (uint i = 0 ; i < updateData.length ; i++ ) {
194- if (
195- updateData[i].length > 4 &&
196- UnsafeCalldataBytesLib.toUint32 (updateData[i], 0 ) ==
197- ACCUMULATOR_MAGIC
198- ) {
199- uint offset;
200- {
201- UpdateType updateType;
202- (
203- offset,
204- updateType
205- ) = extractUpdateTypeFromAccumulatorHeader (
206- updateData[i]
207- );
208-
209- if (updateType != UpdateType.WormholeMerkle) {
210- revert PythErrors.InvalidUpdateData ();
211- }
212- }
213323
214- bytes20 digest;
215- uint8 numUpdates;
216- bytes calldata encoded;
217- (
218- offset,
219- digest,
220- numUpdates,
221- encoded
222- ) = extractWormholeMerkleHeaderDigestAndNumUpdatesAndEncodedFromAccumulatorUpdate (
223- updateData[i],
224- offset
225- );
324+ // Create the context struct that holds all shared parameters
325+ UpdateProcessingContext memory context;
326+ context.priceIds = priceIds;
327+ context.minPublishTime = config.minPublishTime;
328+ context.maxPublishTime = config.maxPublishTime;
329+ context.checkUniqueness = config.checkUniqueness;
330+ context.priceFeeds = new PythStructs.PriceFeed [](priceIds.length );
331+ context.slots = new uint64 [](priceIds.length );
226332
227- for (uint j = 0 ; j < numUpdates; j++ ) {
228- PythInternalStructs.PriceInfo memory priceInfo;
229- bytes32 priceId;
230- uint64 prevPublishTime;
231- (
232- offset,
233- priceInfo,
234- priceId,
235- prevPublishTime
236- ) = extractPriceInfoFromMerkleProof (
237- digest,
238- encoded,
239- offset
240- );
241- {
242- // check whether caller requested for this data
243- uint k = findIndexOfPriceId (priceIds, priceId);
244-
245- // If priceFeed[k].id != 0 then it means that there was a valid
246- // update for priceIds[k] and we don't need to process this one.
247- if (k == priceIds.length || priceFeeds[k].id != 0 ) {
248- continue ;
249- }
250-
251- uint publishTime = uint (priceInfo.publishTime);
252- // Check the publish time of the price is within the given range
253- // and only fill the priceFeedsInfo if it is.
254- // If is not, default id value of 0 will still be set and
255- // this will allow other updates for this price id to be processed.
256- if (
257- publishTime >= config.minPublishTime &&
258- publishTime <= config.maxPublishTime &&
259- (! config.checkUniqueness ||
260- config.minPublishTime > prevPublishTime)
261- ) {
262- fillPriceFeedFromPriceInfo (
263- priceFeeds,
264- k,
265- priceId,
266- priceInfo,
267- publishTime
268- );
269- }
270- }
271- }
272- if (offset != encoded.length )
273- revert PythErrors.InvalidUpdateData ();
274- } else {
275- revert PythErrors.InvalidUpdateData ();
276- }
333+ unchecked {
334+ // Process each update, passing the context struct
335+ for (uint i = 0 ; i < updateData.length ; i++ ) {
336+ _processSingleUpdateDataBlob (updateData[i], context);
277337 }
338+ }
278339
279- for ( uint k = 0 ; k < priceIds. length ; k ++ ) {
280- if (priceFeeds[k].id == 0 ) {
281- revert PythErrors. PriceFeedNotFoundWithinRange ();
282- }
340+ // Check all price feeds were found
341+ for ( uint k = 0 ; k < priceIds. length ; k ++ ) {
342+ if (context.priceFeeds[k].id == 0 ) {
343+ revert PythErrors. PriceFeedNotFoundWithinRange ();
283344 }
284345 }
285- }
286346
347+ // Return results
348+ return (context.priceFeeds, context.slots);
349+ }
287350 function parsePriceFeedUpdates (
288351 bytes [] calldata updateData ,
289352 bytes32 [] calldata priceIds ,
@@ -294,6 +357,33 @@ abstract contract Pyth is
294357 payable
295358 override
296359 returns (PythStructs.PriceFeed[] memory priceFeeds )
360+ {
361+ (priceFeeds, ) = parsePriceFeedUpdatesInternal (
362+ updateData,
363+ priceIds,
364+ PythInternalStructs.ParseConfig (
365+ minPublishTime,
366+ maxPublishTime,
367+ false
368+ )
369+ );
370+ }
371+
372+ /// @dev Same as `parsePriceFeedUpdates`, but also returns the Pythnet slot
373+ /// associated with each price update.
374+ function parsePriceFeedUpdatesWithSlots (
375+ bytes [] calldata updateData ,
376+ bytes32 [] calldata priceIds ,
377+ uint64 minPublishTime ,
378+ uint64 maxPublishTime
379+ )
380+ external
381+ payable
382+ override
383+ returns (
384+ PythStructs.PriceFeed[] memory priceFeeds ,
385+ uint64 [] memory slots
386+ )
297387 {
298388 return
299389 parsePriceFeedUpdatesInternal (
@@ -339,8 +429,10 @@ abstract contract Pyth is
339429 offset,
340430 digest,
341431 numUpdates,
342- encoded
343- ) = extractWormholeMerkleHeaderDigestAndNumUpdatesAndEncodedFromAccumulatorUpdate (
432+ encoded,
433+ // slot ignored
434+
435+ ) = extractWormholeMerkleHeaderDigestAndNumUpdatesAndEncodedAndSlotFromAccumulatorUpdate (
344436 updateData,
345437 offset
346438 );
@@ -477,16 +569,15 @@ abstract contract Pyth is
477569 override
478570 returns (PythStructs.PriceFeed[] memory priceFeeds )
479571 {
480- return
481- parsePriceFeedUpdatesInternal (
482- updateData,
483- priceIds,
484- PythInternalStructs.ParseConfig (
485- minPublishTime,
486- maxPublishTime,
487- true
488- )
489- );
572+ (priceFeeds, ) = parsePriceFeedUpdatesInternal (
573+ updateData,
574+ priceIds,
575+ PythInternalStructs.ParseConfig (
576+ minPublishTime,
577+ maxPublishTime,
578+ true
579+ )
580+ );
490581 }
491582
492583 function getTotalFee (
@@ -514,7 +605,9 @@ abstract contract Pyth is
514605 uint k ,
515606 bytes32 priceId ,
516607 PythInternalStructs.PriceInfo memory info ,
517- uint publishTime
608+ uint publishTime ,
609+ uint64 [] memory slots ,
610+ uint64 slot
518611 ) private pure {
519612 priceFeeds[k].id = priceId;
520613 priceFeeds[k].price.price = info.price;
@@ -525,6 +618,7 @@ abstract contract Pyth is
525618 priceFeeds[k].emaPrice.conf = info.emaConf;
526619 priceFeeds[k].emaPrice.expo = info.expo;
527620 priceFeeds[k].emaPrice.publishTime = publishTime;
621+ slots[k] = slot;
528622 }
529623
530624 function queryPriceFeed (
@@ -555,7 +649,7 @@ abstract contract Pyth is
555649 }
556650
557651 function version () public pure returns (string memory ) {
558- return "1.4.4-alpha.4 " ;
652+ return "1.4.4-alpha.5 " ;
559653 }
560654
561655 function calculateTwap (
0 commit comments