Skip to content

Commit 9867275

Browse files
authored
Merge pull request #2727 from pyth-network/parse-price-feed-update-optional-storage
refactor: Optional storage flag when parsing price feed updates
2 parents e0dd09a + 02fe840 commit 9867275

File tree

11 files changed

+289
-131
lines changed

11 files changed

+289
-131
lines changed

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -308,11 +308,14 @@ abstract contract Scheduler is IScheduler, SchedulerState, SchedulerConstants {
308308
(
309309
PythStructs.PriceFeed[] memory priceFeeds,
310310
uint64[] memory slots
311-
) = pyth.parsePriceFeedUpdatesWithSlotsStrict{value: pythFee}(
311+
) = pyth.parsePriceFeedUpdatesWithConfig{value: pythFee}(
312312
updateData,
313313
params.priceIds,
314314
0, // We enforce the past max validity ourselves in _validateShouldUpdatePrices
315-
curTime + FUTURE_TIMESTAMP_MAX_VALIDITY_PERIOD
315+
curTime + FUTURE_TIMESTAMP_MAX_VALIDITY_PERIOD,
316+
false,
317+
true,
318+
false
316319
);
317320

318321
// Verify all price feeds have the same Pythnet slot.

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

Lines changed: 40 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -317,34 +317,36 @@ abstract contract Pyth is
317317
return merkleData.numUpdates;
318318
}
319319

320-
function parsePriceFeedUpdatesInternal(
320+
function parsePriceFeedUpdatesWithConfig(
321321
bytes[] calldata updateData,
322322
bytes32[] calldata priceIds,
323323
uint64 minAllowedPublishTime,
324324
uint64 maxAllowedPublishTime,
325325
bool checkUniqueness,
326-
bool checkUpdateDataIsMinimal
326+
bool checkUpdateDataIsMinimal,
327+
bool storeUpdatesIfFresh
327328
)
328-
internal
329+
public
330+
payable
329331
returns (
330332
PythStructs.PriceFeed[] memory priceFeeds,
331333
uint64[] memory slots
332334
)
333335
{
334-
{
335-
uint requiredFee = getUpdateFee(updateData);
336-
if (msg.value < requiredFee) revert PythErrors.InsufficientFee();
337-
}
336+
if (msg.value < getUpdateFee(updateData))
337+
revert PythErrors.InsufficientFee();
338338

339339
// Create the context struct that holds all shared parameters
340-
PythInternalStructs.UpdateParseContext memory context;
341-
context.priceIds = priceIds;
342-
context.minAllowedPublishTime = minAllowedPublishTime;
343-
context.maxAllowedPublishTime = maxAllowedPublishTime;
344-
context.checkUniqueness = checkUniqueness;
345-
context.checkUpdateDataIsMinimal = checkUpdateDataIsMinimal;
346-
context.priceFeeds = new PythStructs.PriceFeed[](priceIds.length);
347-
context.slots = new uint64[](priceIds.length);
340+
PythInternalStructs.UpdateParseContext
341+
memory context = PythInternalStructs.UpdateParseContext({
342+
priceIds: priceIds,
343+
minAllowedPublishTime: minAllowedPublishTime,
344+
maxAllowedPublishTime: maxAllowedPublishTime,
345+
checkUniqueness: checkUniqueness,
346+
checkUpdateDataIsMinimal: checkUpdateDataIsMinimal,
347+
priceFeeds: new PythStructs.PriceFeed[](priceIds.length),
348+
slots: new uint64[](priceIds.length)
349+
});
348350

349351
// Track total updates for minimal update data check
350352
uint64 totalUpdatesAcrossBlobs = 0;
@@ -358,6 +360,23 @@ abstract contract Pyth is
358360
context
359361
);
360362
}
363+
364+
for (uint j = 0; j < priceIds.length; j++) {
365+
PythStructs.PriceFeed memory pf = context.priceFeeds[j];
366+
if (storeUpdatesIfFresh && pf.id != 0) {
367+
updateLatestPriceIfNecessary(
368+
priceIds[j],
369+
PythInternalStructs.PriceInfo({
370+
publishTime: uint64(pf.price.publishTime),
371+
expo: pf.price.expo,
372+
price: pf.price.price,
373+
conf: pf.price.conf,
374+
emaPrice: pf.emaPrice.price,
375+
emaConf: pf.emaPrice.conf
376+
})
377+
);
378+
}
379+
}
361380
}
362381

363382
// In minimal update data mode, revert if we have more or less updates than price IDs
@@ -378,6 +397,7 @@ abstract contract Pyth is
378397
// Return results
379398
return (context.priceFeeds, context.slots);
380399
}
400+
381401
function parsePriceFeedUpdates(
382402
bytes[] calldata updateData,
383403
bytes32[] calldata priceIds,
@@ -389,41 +409,17 @@ abstract contract Pyth is
389409
override
390410
returns (PythStructs.PriceFeed[] memory priceFeeds)
391411
{
392-
(priceFeeds, ) = parsePriceFeedUpdatesInternal(
412+
(priceFeeds, ) = parsePriceFeedUpdatesWithConfig(
393413
updateData,
394414
priceIds,
395415
minPublishTime,
396416
maxPublishTime,
397417
false,
418+
false,
398419
false
399420
);
400421
}
401422

402-
function parsePriceFeedUpdatesWithSlotsStrict(
403-
bytes[] calldata updateData,
404-
bytes32[] calldata priceIds,
405-
uint64 minPublishTime,
406-
uint64 maxPublishTime
407-
)
408-
external
409-
payable
410-
override
411-
returns (
412-
PythStructs.PriceFeed[] memory priceFeeds,
413-
uint64[] memory slots
414-
)
415-
{
416-
return
417-
parsePriceFeedUpdatesInternal(
418-
updateData,
419-
priceIds,
420-
minPublishTime,
421-
maxPublishTime,
422-
false,
423-
true
424-
);
425-
}
426-
427423
function extractTwapPriceInfos(
428424
bytes calldata updateData
429425
)
@@ -624,12 +620,13 @@ abstract contract Pyth is
624620
override
625621
returns (PythStructs.PriceFeed[] memory priceFeeds)
626622
{
627-
(priceFeeds, ) = parsePriceFeedUpdatesInternal(
623+
(priceFeeds, ) = parsePriceFeedUpdatesWithConfig(
628624
updateData,
629625
priceIds,
630626
minPublishTime,
631627
maxPublishTime,
632628
true,
629+
false,
633630
false
634631
);
635632
}
@@ -703,7 +700,7 @@ abstract contract Pyth is
703700
}
704701

705702
function version() public pure returns (string memory) {
706-
return "1.4.5";
703+
return "1.4.5-alpha.1";
707704
}
708705

709706
/// @notice Calculates TWAP from two price points

target_chains/ethereum/contracts/forge-test/GasBenchmark.t.sol

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,36 @@ contract GasBenchmark is Test, WormholeTestUtils, PythTestUtils {
240240
);
241241
}
242242

243+
function testBenchmarkParsePriceFeedUpdatesWithConfigTrue() public {
244+
bytes32[] memory ids = new bytes32[](1);
245+
ids[0] = priceIds[0];
246+
247+
pyth.parsePriceFeedUpdatesWithConfig{value: freshPricesUpdateFee[0]}(
248+
freshPricesUpdateData[0],
249+
ids,
250+
0,
251+
50,
252+
false,
253+
true, // check minimal
254+
false
255+
);
256+
}
257+
258+
function testBenchmarkParsePriceFeedUpdatesWithConfigFalse() public {
259+
bytes32[] memory ids = new bytes32[](1);
260+
ids[0] = priceIds[0];
261+
262+
pyth.parsePriceFeedUpdatesWithConfig{value: freshPricesUpdateFee[0]}(
263+
freshPricesUpdateData[0], // contains only priceIds[0]
264+
ids,
265+
0,
266+
50,
267+
false,
268+
true, // check minimal
269+
false
270+
);
271+
}
272+
243273
function testBenchmarkParsePriceFeedUpdatesUniqueFor() public {
244274
bytes32[] memory ids = new bytes32[](1);
245275
ids[0] = priceIds[0];

target_chains/ethereum/contracts/forge-test/Pyth.t.sol

Lines changed: 103 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,92 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils {
278278
}
279279
}
280280

281-
function testParsePriceFeedUpdatesWithSlotsStrictWorks(uint seed) public {
281+
function testParsePriceFeedUpdatesWithConfigIfStorageTrue(
282+
uint seed
283+
) public {
284+
setRandSeed(seed);
285+
uint numMessages = 1 + (getRandUint() % 10);
286+
(
287+
bytes32[] memory priceIds,
288+
PriceFeedMessage[] memory messages
289+
) = generateRandomPriceMessages(numMessages);
290+
291+
(
292+
bytes[] memory updateData,
293+
uint updateFee
294+
) = createBatchedUpdateDataFromMessages(messages);
295+
296+
(PythStructs.PriceFeed[] memory priceFeeds, ) = pyth
297+
.parsePriceFeedUpdatesWithConfig{value: updateFee}(
298+
updateData,
299+
priceIds,
300+
0,
301+
MAX_UINT64,
302+
false,
303+
true,
304+
true
305+
);
306+
307+
for (uint i = 0; i < numMessages; i++) {
308+
// Validating that returned priceIds are equal
309+
assertEq(priceFeeds[i].id, priceIds[i]);
310+
assertEq(priceFeeds[i].price.price, messages[i].price);
311+
assertEq(priceFeeds[i].price.conf, messages[i].conf);
312+
assertEq(priceFeeds[i].price.expo, messages[i].expo);
313+
assertEq(priceFeeds[i].price.publishTime, messages[i].publishTime);
314+
assertEq(priceFeeds[i].emaPrice.price, messages[i].emaPrice);
315+
assertEq(priceFeeds[i].emaPrice.conf, messages[i].emaConf);
316+
assertEq(priceFeeds[i].emaPrice.expo, messages[i].expo);
317+
assertEq(
318+
priceFeeds[i].emaPrice.publishTime,
319+
messages[i].publishTime
320+
);
321+
322+
// Validating that prices are stored on chain
323+
PythStructs.Price memory curPrice = pyth.getPriceUnsafe(
324+
messages[i].priceId
325+
);
326+
327+
assertEq(priceFeeds[i].price.price, curPrice.price);
328+
assertEq(priceFeeds[i].price.conf, curPrice.conf);
329+
assertEq(priceFeeds[i].price.expo, curPrice.expo);
330+
assertEq(priceFeeds[i].price.publishTime, curPrice.publishTime);
331+
}
332+
}
333+
334+
function testParsePriceFeedUpdatesWithConfigIfStorageFalse(
335+
uint seed
336+
) public {
337+
setRandSeed(seed);
338+
uint numMessages = 1 + (getRandUint() % 10);
339+
(
340+
bytes32[] memory priceIds,
341+
PriceFeedMessage[] memory messages
342+
) = generateRandomPriceMessages(numMessages);
343+
344+
(
345+
bytes[] memory updateData,
346+
uint updateFee
347+
) = createBatchedUpdateDataFromMessages(messages);
348+
349+
pyth.parsePriceFeedUpdatesWithConfig{value: updateFee}(
350+
updateData,
351+
priceIds,
352+
0,
353+
MAX_UINT64,
354+
false,
355+
true,
356+
false
357+
);
358+
359+
// validate that stored prices of each priceId are still unpopulated
360+
for (uint i = 0; i < numMessages; i++) {
361+
vm.expectRevert(PythErrors.PriceFeedNotFound.selector);
362+
pyth.getPriceUnsafe(priceIds[i]);
363+
}
364+
}
365+
366+
function testParsePriceFeedUpdatesWithConfigWorks(uint seed) public {
282367
setRandSeed(seed);
283368
uint numMessages = 1 + (getRandUint() % 10);
284369
(
@@ -293,11 +378,14 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils {
293378
(
294379
PythStructs.PriceFeed[] memory priceFeeds,
295380
uint64[] memory slots
296-
) = pyth.parsePriceFeedUpdatesWithSlotsStrict{value: updateFee}(
381+
) = pyth.parsePriceFeedUpdatesWithConfig{value: updateFee}(
297382
updateData,
298383
priceIds,
299384
0,
300-
MAX_UINT64
385+
MAX_UINT64,
386+
false,
387+
true,
388+
false
301389
);
302390

303391
assertEq(priceFeeds.length, numMessages);
@@ -434,7 +522,7 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils {
434522
);
435523
}
436524

437-
function testParsePriceFeedUpdatesWithSlotsStrictRevertsWithExcessUpdateData()
525+
function testParsePriceFeedUpdatesWithConfigRevertsWithExcessUpdateData()
438526
public
439527
{
440528
// Create a price update with more price updates than requested price IDs
@@ -459,15 +547,18 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils {
459547

460548
// Should revert in strict mode
461549
vm.expectRevert(PythErrors.InvalidArgument.selector);
462-
pyth.parsePriceFeedUpdatesWithSlotsStrict{value: updateFee}(
550+
pyth.parsePriceFeedUpdatesWithConfig{value: updateFee}(
463551
updateData,
464552
requestedPriceIds,
465553
0,
466-
MAX_UINT64
554+
MAX_UINT64,
555+
false,
556+
true,
557+
false
467558
);
468559
}
469560

470-
function testParsePriceFeedUpdatesWithSlotsStrictRevertsWithFewerUpdateData()
561+
function testParsePriceFeedUpdatesWithConfigRevertsWithFewerUpdateData()
471562
public
472563
{
473564
// Create a price update with fewer price updates than requested price IDs
@@ -496,11 +587,14 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils {
496587

497588
// Should revert in strict mode because we have fewer updates than price IDs
498589
vm.expectRevert(PythErrors.InvalidArgument.selector);
499-
pyth.parsePriceFeedUpdatesWithSlotsStrict{value: updateFee}(
590+
pyth.parsePriceFeedUpdatesWithConfig{value: updateFee}(
500591
updateData,
501592
requestedPriceIds,
502593
0,
503-
MAX_UINT64
594+
MAX_UINT64,
595+
false,
596+
true,
597+
false
504598
);
505599
}
506600

target_chains/ethereum/contracts/forge-test/utils/MockPriceFeedTestUtils.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ abstract contract MockPriceFeedTestUtils is Test {
187187
pyth,
188188
expectedFee,
189189
abi.encodeWithSelector(
190-
IPyth.parsePriceFeedUpdatesWithSlotsStrict.selector
190+
IPyth.parsePriceFeedUpdatesWithConfig.selector
191191
),
192192
abi.encode(priceFeeds, slots)
193193
);

target_chains/ethereum/sdk/solidity/AbstractPyth.sol

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -136,21 +136,6 @@ abstract contract AbstractPyth is IPyth {
136136
override
137137
returns (PythStructs.PriceFeed[] memory priceFeeds);
138138

139-
function parsePriceFeedUpdatesWithSlotsStrict(
140-
bytes[] calldata updateData,
141-
bytes32[] calldata priceIds,
142-
uint64 minPublishTime,
143-
uint64 maxPublishTime
144-
)
145-
external
146-
payable
147-
virtual
148-
override
149-
returns (
150-
PythStructs.PriceFeed[] memory priceFeeds,
151-
uint64[] memory slots
152-
);
153-
154139
function parseTwapPriceFeedUpdates(
155140
bytes[] calldata updateData,
156141
bytes32[] calldata priceIds

0 commit comments

Comments
 (0)