Skip to content

feat(target_chains/ethereum/pyth): strict minimal updateData parsing #2637

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
May 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ abstract contract Scheduler is IScheduler, SchedulerState {
(
PythStructs.PriceFeed[] memory priceFeeds,
uint64[] memory slots
) = pyth.parsePriceFeedUpdatesWithSlots{value: pythFee}(
) = pyth.parsePriceFeedUpdatesWithSlotsStrict{value: pythFee}(
updateData,
params.priceIds,
0, // We enforce the past max validity ourselves in _validateShouldUpdatePrices
Expand Down
70 changes: 45 additions & 25 deletions target_chains/ethereum/contracts/contracts/pyth/Pyth.sol
Original file line number Diff line number Diff line change
Expand Up @@ -239,10 +239,10 @@ abstract contract Pyth is
if (k < context.priceIds.length && context.priceFeeds[k].id == 0) {
uint publishTime = uint(priceInfo.publishTime);
if (
publishTime >= context.config.minPublishTime &&
publishTime <= context.config.maxPublishTime &&
(!context.config.checkUniqueness ||
context.config.minPublishTime > prevPublishTime)
publishTime >= context.minAllowedPublishTime &&
publishTime <= context.maxAllowedPublishTime &&
(!context.checkUniqueness ||
context.minAllowedPublishTime > prevPublishTime)
) {
context.priceFeeds[k].id = priceId;
context.priceFeeds[k].price.price = priceInfo.price;
Expand All @@ -262,7 +262,7 @@ abstract contract Pyth is
function _processSingleUpdateDataBlob(
bytes calldata singleUpdateData,
PythInternalStructs.UpdateParseContext memory context
) internal view {
) internal view returns (uint64 numUpdates) {
// Check magic number and length first
if (
singleUpdateData.length <= 4 ||
Expand Down Expand Up @@ -312,12 +312,18 @@ abstract contract Pyth is
if (offset != encoded.length) {
revert PythErrors.InvalidUpdateData();
}

// Return the number of updates in this blob for tracking
return merkleData.numUpdates;
}

function parsePriceFeedUpdatesInternal(
bytes[] calldata updateData,
bytes32[] calldata priceIds,
PythInternalStructs.ParseConfig memory config
uint64 minAllowedPublishTime,
uint64 maxAllowedPublishTime,
bool checkUniqueness,
bool checkUpdateDataIsMinimal
)
internal
returns (
Expand All @@ -333,18 +339,35 @@ abstract contract Pyth is
// Create the context struct that holds all shared parameters
PythInternalStructs.UpdateParseContext memory context;
context.priceIds = priceIds;
context.config = config;
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);

// Track total updates for minimal update data check
uint64 totalUpdatesAcrossBlobs = 0;

unchecked {
// Process each update, passing the context struct
// Parsed results will be filled in context.priceFeeds and context.slots
for (uint i = 0; i < updateData.length; i++) {
_processSingleUpdateDataBlob(updateData[i], context);
totalUpdatesAcrossBlobs += _processSingleUpdateDataBlob(
updateData[i],
context
);
}
}

// In minimal update data mode, revert if we have more or less updates than price IDs
if (
checkUpdateDataIsMinimal &&
totalUpdatesAcrossBlobs != priceIds.length
) {
revert PythErrors.InvalidArgument();
}

// Check all price feeds were found
for (uint k = 0; k < priceIds.length; k++) {
if (context.priceFeeds[k].id == 0) {
Expand All @@ -369,15 +392,14 @@ abstract contract Pyth is
(priceFeeds, ) = parsePriceFeedUpdatesInternal(
updateData,
priceIds,
PythInternalStructs.ParseConfig(
minPublishTime,
maxPublishTime,
false
)
minPublishTime,
maxPublishTime,
false,
false
);
}

function parsePriceFeedUpdatesWithSlots(
function parsePriceFeedUpdatesWithSlotsStrict(
bytes[] calldata updateData,
bytes32[] calldata priceIds,
uint64 minPublishTime,
Expand All @@ -395,11 +417,10 @@ abstract contract Pyth is
parsePriceFeedUpdatesInternal(
updateData,
priceIds,
PythInternalStructs.ParseConfig(
minPublishTime,
maxPublishTime,
false
)
minPublishTime,
maxPublishTime,
false,
true
);
}

Expand Down Expand Up @@ -606,11 +627,10 @@ abstract contract Pyth is
(priceFeeds, ) = parsePriceFeedUpdatesInternal(
updateData,
priceIds,
PythInternalStructs.ParseConfig(
minPublishTime,
maxPublishTime,
true
)
minPublishTime,
maxPublishTime,
true,
false
);
}

Expand Down Expand Up @@ -683,7 +703,7 @@ abstract contract Pyth is
}

function version() public pure returns (string memory) {
return "1.4.4";
return "1.4.5-alpha.1";
}

/// @notice Calculates TWAP from two price points
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,18 @@ import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";
contract PythInternalStructs {
using BytesLib for bytes;

struct ParseConfig {
uint64 minPublishTime;
uint64 maxPublishTime;
bool checkUniqueness;
}

/// Internal struct to hold parameters for update processing
/// @dev Storing these variable in a struct rather than local variables
/// helps reduce stack depth when passing arguments to functions.
struct UpdateParseContext {
bytes32[] priceIds;
ParseConfig config;
uint64 minAllowedPublishTime;
uint64 maxAllowedPublishTime;
bool checkUniqueness;
/// When checkUpdateDataIsMinimal is true, parsing will revert
/// if the number of passed in updates exceeds or is less than
/// the length of priceIds.
bool checkUpdateDataIsMinimal;
PythStructs.PriceFeed[] priceFeeds;
uint64[] slots;
}
Expand Down
44 changes: 24 additions & 20 deletions target_chains/ethereum/contracts/forge-test/PulseScheduler.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,11 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
numInitialFeeds
);

mockParsePriceFeedUpdatesWithSlots(pyth, initialPriceFeeds, slots);
mockParsePriceFeedUpdatesWithSlotsStrict(
pyth,
initialPriceFeeds,
slots
);
bytes[] memory updateData = createMockUpdateData(initialPriceFeeds);

vm.prank(pusher);
Expand Down Expand Up @@ -830,7 +834,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
priceIds.length
);

mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds1, slots);
mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds1, slots);
bytes[] memory updateData1 = createMockUpdateData(priceFeeds1);

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

mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds2, slots); // Mock for the second call
mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds2, slots); // Mock for the second call
bytes[] memory updateData2 = createMockUpdateData(priceFeeds2);

// Perform second update
Expand Down Expand Up @@ -942,7 +946,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
);

uint256 mockPythFee = MOCK_PYTH_FEE_PER_FEED * params.priceIds.length;
mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds, slots);
mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
bytes[] memory updateData = createMockUpdateData(priceFeeds);

// Get state before
Expand Down Expand Up @@ -1027,7 +1031,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
priceIds.length
);
uint256 mockPythFee = MOCK_PYTH_FEE_PER_FEED * priceIds.length;
mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds, slots);
mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
bytes[] memory updateData = createMockUpdateData(priceFeeds);

// Calculate minimum keeper fee (overhead + feed-specific fee)
Expand Down Expand Up @@ -1085,7 +1089,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
PythStructs.PriceFeed[] memory priceFeeds1;
uint64[] memory slots1;
(priceFeeds1, slots1) = createMockPriceFeedsWithSlots(publishTime1, 2);
mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds1, slots1);
mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds1, slots1);
bytes[] memory updateData1 = createMockUpdateData(priceFeeds1);
vm.prank(pusher);
scheduler.updatePriceFeeds(subscriptionId, updateData1);
Expand All @@ -1096,7 +1100,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
PythStructs.PriceFeed[] memory priceFeeds2;
uint64[] memory slots2;
(priceFeeds2, slots2) = createMockPriceFeedsWithSlots(publishTime2, 2);
mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds2, slots2);
mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds2, slots2);
bytes[] memory updateData2 = createMockUpdateData(priceFeeds2);

// Expect revert because heartbeat condition is not met
Expand Down Expand Up @@ -1132,7 +1136,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
PythStructs.PriceFeed[] memory priceFeeds1;
uint64[] memory slots;
(priceFeeds1, slots) = createMockPriceFeedsWithSlots(publishTime1, 2);
mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds1, slots);
mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds1, slots);
bytes[] memory updateData1 = createMockUpdateData(priceFeeds1);
vm.prank(pusher);
scheduler.updatePriceFeeds(subscriptionId, updateData1);
Expand All @@ -1158,7 +1162,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
priceFeeds2[i].price.publishTime = publishTime2;
}

mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds2, slots);
mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds2, slots);
bytes[] memory updateData2 = createMockUpdateData(priceFeeds2);

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

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

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

// Mock Pyth response to return these feeds with mismatched slots
mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds, slots);
mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
bytes[] memory updateData = createMockUpdateData(priceFeeds);

// Expect revert with PriceSlotMismatch error
Expand Down Expand Up @@ -1350,7 +1354,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
PythStructs.PriceFeed[] memory priceFeeds_reduce,
uint64[] memory slots_reduce
) = createMockPriceFeedsWithSlots(publishTime + (i * 60), 2);
mockParsePriceFeedUpdatesWithSlots(
mockParsePriceFeedUpdatesWithSlotsStrict(
pyth,
priceFeeds_reduce,
slots_reduce
Expand Down Expand Up @@ -1422,7 +1426,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
PythStructs.PriceFeed[] memory priceFeeds;
uint64[] memory slots;
(priceFeeds, slots) = createMockPriceFeedsWithSlots(publishTime, 2);
mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds, slots);
mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
bytes[] memory updateData = createMockUpdateData(priceFeeds);

vm.prank(pusher);
Expand Down Expand Up @@ -1464,7 +1468,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
PythStructs.PriceFeed[] memory priceFeeds;
uint64[] memory slots;
(priceFeeds, slots) = createMockPriceFeedsWithSlots(publishTime, 3);
mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds, slots);
mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
bytes[] memory updateData = createMockUpdateData(priceFeeds);

vm.prank(pusher);
Expand Down Expand Up @@ -1519,7 +1523,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
PythStructs.PriceFeed[] memory priceFeeds;
uint64[] memory slots;
(priceFeeds, slots) = createMockPriceFeedsWithSlots(publishTime, 2);
mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds, slots);
mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
bytes[] memory updateData = createMockUpdateData(priceFeeds);

vm.prank(pusher);
Expand Down Expand Up @@ -1563,7 +1567,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
publishTime,
priceIds.length
);
mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds, slots);
mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
bytes[] memory updateData = createMockUpdateData(priceFeeds);

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

mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds, slots);
mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
bytes[] memory updateData = createMockUpdateData(priceFeeds);

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

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

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

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

// Expect revert with TimestampTooOld (checked in _validateShouldUpdatePrices)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ contract PulseSchedulerGasBenchmark is Test, PulseSchedulerTestUtils {
);

// Mock Pyth response for the benchmark
mockParsePriceFeedUpdatesWithSlots(pyth, newPriceFeeds, newSlots);
mockParsePriceFeedUpdatesWithSlotsStrict(pyth, newPriceFeeds, newSlots);

// Actual benchmark: Measure gas for updating price feeds
uint256 startGas = gasleft();
Expand Down Expand Up @@ -124,7 +124,7 @@ contract PulseSchedulerGasBenchmark is Test, PulseSchedulerTestUtils {
numFeeds
);

mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds, slots);
mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
bytes[] memory updateData = createMockUpdateData(priceFeeds);

// Update the price feeds. We should have enough balance to cover the update
Expand Down
Loading
Loading