Skip to content

Commit fee5fd1

Browse files
committed
feat: add parsePriceFeedUpdatesWithSlots to Pyth contract
1 parent 59ea3c8 commit fee5fd1

File tree

12 files changed

+638
-129
lines changed

12 files changed

+638
-129
lines changed

target_chains/ethereum/contracts/contracts/pulse/scheduler/Scheduler.sol

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -173,16 +173,19 @@ abstract contract Scheduler is IScheduler, SchedulerState {
173173
// Parse price feed updates with an expected timestamp range of [-10s, now]
174174
// We will validate the trigger conditions and timestamps ourselves
175175
// using the returned PriceFeeds.
176-
uint64 maxPublishTime = SafeCast.toUint64(block.timestamp);
177-
uint64 minPublishTime = maxPublishTime - 10 seconds;
178-
PythStructs.PriceFeed[] memory priceFeeds = pyth.parsePriceFeedUpdates{
176+
uint64 maxPublishTime = SafeCast.toUint64(block.timestamp) +
177+
FUTURE_TIMESTAMP_GRACE_PERIOD;
178+
uint64 minPublishTime = maxPublishTime - PAST_TIMESTAMP_GRACE_PERIOD;
179+
PythStructs.PriceFeed[] memory priceFeeds;
180+
uint64[] memory slots;
181+
(priceFeeds, slots) = pyth.parsePriceFeedUpdatesWithSlots{
179182
value: pythFee
180183
}(updateData, priceIds, minPublishTime, maxPublishTime);
181184

182-
// Verify all price feeds have the same timestamp
183-
uint256 timestamp = priceFeeds[0].price.publishTime;
184-
for (uint8 i = 1; i < priceFeeds.length; i++) {
185-
if (priceFeeds[i].price.publishTime != timestamp) {
185+
// Verify all price feeds have the same Pythnet slot
186+
uint64 slot = slots[0];
187+
for (uint8 i = 1; i < slots.length; i++) {
188+
if (slots[i] != slot) {
186189
revert PriceTimestampMismatch();
187190
}
188191
}

target_chains/ethereum/contracts/contracts/pyth/Pyth.sol

Lines changed: 198 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)