Skip to content

Commit f01d4bd

Browse files
feat(target_chains/ethereum/pyth): strict minimal updateData parsing (#2637)
* feat: strict updatedata parsing * fix: abis, interfaces * fix: merge, optimizer_runs * fix: reset optimizer_runs to 200
1 parent 348b1af commit f01d4bd

File tree

15 files changed

+166
-73
lines changed

15 files changed

+166
-73
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ abstract contract Scheduler is IScheduler, SchedulerState {
273273
(
274274
PythStructs.PriceFeed[] memory priceFeeds,
275275
uint64[] memory slots
276-
) = pyth.parsePriceFeedUpdatesWithSlots{value: pythFee}(
276+
) = pyth.parsePriceFeedUpdatesWithSlotsStrict{value: pythFee}(
277277
updateData,
278278
params.priceIds,
279279
0, // We enforce the past max validity ourselves in _validateShouldUpdatePrices

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

Lines changed: 45 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -239,10 +239,10 @@ abstract contract Pyth is
239239
if (k < context.priceIds.length && context.priceFeeds[k].id == 0) {
240240
uint publishTime = uint(priceInfo.publishTime);
241241
if (
242-
publishTime >= context.config.minPublishTime &&
243-
publishTime <= context.config.maxPublishTime &&
244-
(!context.config.checkUniqueness ||
245-
context.config.minPublishTime > prevPublishTime)
242+
publishTime >= context.minAllowedPublishTime &&
243+
publishTime <= context.maxAllowedPublishTime &&
244+
(!context.checkUniqueness ||
245+
context.minAllowedPublishTime > prevPublishTime)
246246
) {
247247
context.priceFeeds[k].id = priceId;
248248
context.priceFeeds[k].price.price = priceInfo.price;
@@ -262,7 +262,7 @@ abstract contract Pyth is
262262
function _processSingleUpdateDataBlob(
263263
bytes calldata singleUpdateData,
264264
PythInternalStructs.UpdateParseContext memory context
265-
) internal view {
265+
) internal view returns (uint64 numUpdates) {
266266
// Check magic number and length first
267267
if (
268268
singleUpdateData.length <= 4 ||
@@ -312,12 +312,18 @@ abstract contract Pyth is
312312
if (offset != encoded.length) {
313313
revert PythErrors.InvalidUpdateData();
314314
}
315+
316+
// Return the number of updates in this blob for tracking
317+
return merkleData.numUpdates;
315318
}
316319

317320
function parsePriceFeedUpdatesInternal(
318321
bytes[] calldata updateData,
319322
bytes32[] calldata priceIds,
320-
PythInternalStructs.ParseConfig memory config
323+
uint64 minAllowedPublishTime,
324+
uint64 maxAllowedPublishTime,
325+
bool checkUniqueness,
326+
bool checkUpdateDataIsMinimal
321327
)
322328
internal
323329
returns (
@@ -333,18 +339,35 @@ abstract contract Pyth is
333339
// Create the context struct that holds all shared parameters
334340
PythInternalStructs.UpdateParseContext memory context;
335341
context.priceIds = priceIds;
336-
context.config = config;
342+
context.minAllowedPublishTime = minAllowedPublishTime;
343+
context.maxAllowedPublishTime = maxAllowedPublishTime;
344+
context.checkUniqueness = checkUniqueness;
345+
context.checkUpdateDataIsMinimal = checkUpdateDataIsMinimal;
337346
context.priceFeeds = new PythStructs.PriceFeed[](priceIds.length);
338347
context.slots = new uint64[](priceIds.length);
339348

349+
// Track total updates for minimal update data check
350+
uint64 totalUpdatesAcrossBlobs = 0;
351+
340352
unchecked {
341353
// Process each update, passing the context struct
342354
// Parsed results will be filled in context.priceFeeds and context.slots
343355
for (uint i = 0; i < updateData.length; i++) {
344-
_processSingleUpdateDataBlob(updateData[i], context);
356+
totalUpdatesAcrossBlobs += _processSingleUpdateDataBlob(
357+
updateData[i],
358+
context
359+
);
345360
}
346361
}
347362

363+
// In minimal update data mode, revert if we have more or less updates than price IDs
364+
if (
365+
checkUpdateDataIsMinimal &&
366+
totalUpdatesAcrossBlobs != priceIds.length
367+
) {
368+
revert PythErrors.InvalidArgument();
369+
}
370+
348371
// Check all price feeds were found
349372
for (uint k = 0; k < priceIds.length; k++) {
350373
if (context.priceFeeds[k].id == 0) {
@@ -369,15 +392,14 @@ abstract contract Pyth is
369392
(priceFeeds, ) = parsePriceFeedUpdatesInternal(
370393
updateData,
371394
priceIds,
372-
PythInternalStructs.ParseConfig(
373-
minPublishTime,
374-
maxPublishTime,
375-
false
376-
)
395+
minPublishTime,
396+
maxPublishTime,
397+
false,
398+
false
377399
);
378400
}
379401

380-
function parsePriceFeedUpdatesWithSlots(
402+
function parsePriceFeedUpdatesWithSlotsStrict(
381403
bytes[] calldata updateData,
382404
bytes32[] calldata priceIds,
383405
uint64 minPublishTime,
@@ -395,11 +417,10 @@ abstract contract Pyth is
395417
parsePriceFeedUpdatesInternal(
396418
updateData,
397419
priceIds,
398-
PythInternalStructs.ParseConfig(
399-
minPublishTime,
400-
maxPublishTime,
401-
false
402-
)
420+
minPublishTime,
421+
maxPublishTime,
422+
false,
423+
true
403424
);
404425
}
405426

@@ -606,11 +627,10 @@ abstract contract Pyth is
606627
(priceFeeds, ) = parsePriceFeedUpdatesInternal(
607628
updateData,
608629
priceIds,
609-
PythInternalStructs.ParseConfig(
610-
minPublishTime,
611-
maxPublishTime,
612-
true
613-
)
630+
minPublishTime,
631+
maxPublishTime,
632+
true,
633+
false
614634
);
615635
}
616636

@@ -683,7 +703,7 @@ abstract contract Pyth is
683703
}
684704

685705
function version() public pure returns (string memory) {
686-
return "1.4.4";
706+
return "1.4.5-alpha.1";
687707
}
688708

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

target_chains/ethereum/contracts/contracts/pyth/PythInternalStructs.sol

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,18 @@ import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";
99
contract PythInternalStructs {
1010
using BytesLib for bytes;
1111

12-
struct ParseConfig {
13-
uint64 minPublishTime;
14-
uint64 maxPublishTime;
15-
bool checkUniqueness;
16-
}
17-
1812
/// Internal struct to hold parameters for update processing
1913
/// @dev Storing these variable in a struct rather than local variables
2014
/// helps reduce stack depth when passing arguments to functions.
2115
struct UpdateParseContext {
2216
bytes32[] priceIds;
23-
ParseConfig config;
17+
uint64 minAllowedPublishTime;
18+
uint64 maxAllowedPublishTime;
19+
bool checkUniqueness;
20+
/// When checkUpdateDataIsMinimal is true, parsing will revert
21+
/// if the number of passed in updates exceeds or is less than
22+
/// the length of priceIds.
23+
bool checkUpdateDataIsMinimal;
2424
PythStructs.PriceFeed[] priceFeeds;
2525
uint64[] slots;
2626
}

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

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,11 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
256256
numInitialFeeds
257257
);
258258

259-
mockParsePriceFeedUpdatesWithSlots(pyth, initialPriceFeeds, slots);
259+
mockParsePriceFeedUpdatesWithSlotsStrict(
260+
pyth,
261+
initialPriceFeeds,
262+
slots
263+
);
260264
bytes[] memory updateData = createMockUpdateData(initialPriceFeeds);
261265

262266
vm.prank(pusher);
@@ -830,7 +834,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
830834
priceIds.length
831835
);
832836

833-
mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds1, slots);
837+
mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds1, slots);
834838
bytes[] memory updateData1 = createMockUpdateData(priceFeeds1);
835839

836840
// Perform first update
@@ -881,7 +885,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
881885
priceFeeds2[i].emaPrice.publishTime = publishTime2;
882886
}
883887

884-
mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds2, slots); // Mock for the second call
888+
mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds2, slots); // Mock for the second call
885889
bytes[] memory updateData2 = createMockUpdateData(priceFeeds2);
886890

887891
// Perform second update
@@ -942,7 +946,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
942946
);
943947

944948
uint256 mockPythFee = MOCK_PYTH_FEE_PER_FEED * params.priceIds.length;
945-
mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds, slots);
949+
mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
946950
bytes[] memory updateData = createMockUpdateData(priceFeeds);
947951

948952
// Get state before
@@ -1027,7 +1031,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
10271031
priceIds.length
10281032
);
10291033
uint256 mockPythFee = MOCK_PYTH_FEE_PER_FEED * priceIds.length;
1030-
mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds, slots);
1034+
mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
10311035
bytes[] memory updateData = createMockUpdateData(priceFeeds);
10321036

10331037
// Calculate minimum keeper fee (overhead + feed-specific fee)
@@ -1085,7 +1089,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
10851089
PythStructs.PriceFeed[] memory priceFeeds1;
10861090
uint64[] memory slots1;
10871091
(priceFeeds1, slots1) = createMockPriceFeedsWithSlots(publishTime1, 2);
1088-
mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds1, slots1);
1092+
mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds1, slots1);
10891093
bytes[] memory updateData1 = createMockUpdateData(priceFeeds1);
10901094
vm.prank(pusher);
10911095
scheduler.updatePriceFeeds(subscriptionId, updateData1);
@@ -1096,7 +1100,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
10961100
PythStructs.PriceFeed[] memory priceFeeds2;
10971101
uint64[] memory slots2;
10981102
(priceFeeds2, slots2) = createMockPriceFeedsWithSlots(publishTime2, 2);
1099-
mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds2, slots2);
1103+
mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds2, slots2);
11001104
bytes[] memory updateData2 = createMockUpdateData(priceFeeds2);
11011105

11021106
// Expect revert because heartbeat condition is not met
@@ -1132,7 +1136,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
11321136
PythStructs.PriceFeed[] memory priceFeeds1;
11331137
uint64[] memory slots;
11341138
(priceFeeds1, slots) = createMockPriceFeedsWithSlots(publishTime1, 2);
1135-
mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds1, slots);
1139+
mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds1, slots);
11361140
bytes[] memory updateData1 = createMockUpdateData(priceFeeds1);
11371141
vm.prank(pusher);
11381142
scheduler.updatePriceFeeds(subscriptionId, updateData1);
@@ -1158,7 +1162,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
11581162
priceFeeds2[i].price.publishTime = publishTime2;
11591163
}
11601164

1161-
mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds2, slots);
1165+
mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds2, slots);
11621166
bytes[] memory updateData2 = createMockUpdateData(priceFeeds2);
11631167

11641168
// Expect revert because deviation condition is not met
@@ -1183,7 +1187,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
11831187
PythStructs.PriceFeed[] memory priceFeeds1;
11841188
uint64[] memory slots1;
11851189
(priceFeeds1, slots1) = createMockPriceFeedsWithSlots(publishTime1, 2);
1186-
mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds1, slots1);
1190+
mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds1, slots1);
11871191
bytes[] memory updateData1 = createMockUpdateData(priceFeeds1);
11881192

11891193
vm.prank(pusher);
@@ -1195,7 +1199,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
11951199
uint64[] memory slots2;
11961200
(priceFeeds2, slots2) = createMockPriceFeedsWithSlots(publishTime2, 2);
11971201
// Mock Pyth response to return feeds with the older timestamp
1198-
mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds2, slots2);
1202+
mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds2, slots2);
11991203
bytes[] memory updateData2 = createMockUpdateData(priceFeeds2);
12001204

12011205
// Expect revert with TimestampOlderThanLastUpdate (checked in _validateShouldUpdatePrices)
@@ -1235,7 +1239,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
12351239
slots[1] = 200; // Different slot
12361240

12371241
// Mock Pyth response to return these feeds with mismatched slots
1238-
mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds, slots);
1242+
mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
12391243
bytes[] memory updateData = createMockUpdateData(priceFeeds);
12401244

12411245
// Expect revert with PriceSlotMismatch error
@@ -1350,7 +1354,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
13501354
PythStructs.PriceFeed[] memory priceFeeds_reduce,
13511355
uint64[] memory slots_reduce
13521356
) = createMockPriceFeedsWithSlots(publishTime + (i * 60), 2);
1353-
mockParsePriceFeedUpdatesWithSlots(
1357+
mockParsePriceFeedUpdatesWithSlotsStrict(
13541358
pyth,
13551359
priceFeeds_reduce,
13561360
slots_reduce
@@ -1422,7 +1426,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
14221426
PythStructs.PriceFeed[] memory priceFeeds;
14231427
uint64[] memory slots;
14241428
(priceFeeds, slots) = createMockPriceFeedsWithSlots(publishTime, 2);
1425-
mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds, slots);
1429+
mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
14261430
bytes[] memory updateData = createMockUpdateData(priceFeeds);
14271431

14281432
vm.prank(pusher);
@@ -1464,7 +1468,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
14641468
PythStructs.PriceFeed[] memory priceFeeds;
14651469
uint64[] memory slots;
14661470
(priceFeeds, slots) = createMockPriceFeedsWithSlots(publishTime, 3);
1467-
mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds, slots);
1471+
mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
14681472
bytes[] memory updateData = createMockUpdateData(priceFeeds);
14691473

14701474
vm.prank(pusher);
@@ -1519,7 +1523,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
15191523
PythStructs.PriceFeed[] memory priceFeeds;
15201524
uint64[] memory slots;
15211525
(priceFeeds, slots) = createMockPriceFeedsWithSlots(publishTime, 2);
1522-
mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds, slots);
1526+
mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
15231527
bytes[] memory updateData = createMockUpdateData(priceFeeds);
15241528

15251529
vm.prank(pusher);
@@ -1563,7 +1567,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
15631567
publishTime,
15641568
priceIds.length
15651569
);
1566-
mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds, slots);
1570+
mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
15671571
bytes[] memory updateData = createMockUpdateData(priceFeeds);
15681572

15691573
vm.prank(pusher);
@@ -1630,7 +1634,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
16301634
priceFeeds[i].emaPrice.expo = priceFeeds[i].price.expo;
16311635
}
16321636

1633-
mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds, slots);
1637+
mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
16341638
bytes[] memory updateData = createMockUpdateData(priceFeeds);
16351639

16361640
vm.prank(pusher);
@@ -1935,7 +1939,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
19351939
slots[1] = 100; // Same slot
19361940

19371941
// Mock Pyth response (should succeed in the real world as minValidTime is 0)
1938-
mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds, slots);
1942+
mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
19391943
bytes[] memory updateData = createMockUpdateData(priceFeeds);
19401944

19411945
// Expect PricesUpdated event with the latest valid timestamp
@@ -1988,7 +1992,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
19881992
slots[1] = 100; // Same slot
19891993

19901994
// Mock Pyth response (should succeed in the real world as minValidTime is 0)
1991-
mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds, slots);
1995+
mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
19921996
bytes[] memory updateData = createMockUpdateData(priceFeeds);
19931997

19941998
// Expect revert with TimestampTooOld (checked in _validateShouldUpdatePrices)

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ contract PulseSchedulerGasBenchmark is Test, PulseSchedulerTestUtils {
7171
);
7272

7373
// Mock Pyth response for the benchmark
74-
mockParsePriceFeedUpdatesWithSlots(pyth, newPriceFeeds, newSlots);
74+
mockParsePriceFeedUpdatesWithSlotsStrict(pyth, newPriceFeeds, newSlots);
7575

7676
// Actual benchmark: Measure gas for updating price feeds
7777
uint256 startGas = gasleft();
@@ -124,7 +124,7 @@ contract PulseSchedulerGasBenchmark is Test, PulseSchedulerTestUtils {
124124
numFeeds
125125
);
126126

127-
mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds, slots);
127+
mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
128128
bytes[] memory updateData = createMockUpdateData(priceFeeds);
129129

130130
// Update the price feeds. We should have enough balance to cover the update

0 commit comments

Comments
 (0)