Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -293,7 +293,7 @@ abstract contract Scheduler is IScheduler, SchedulerState {
(
PythStructs.PriceFeed[] memory priceFeeds,
uint64[] memory slots
) = pyth.parsePriceFeedUpdatesWithSlots{value: pythFee}(
) = pyth.parsePriceFeedUpdatesWithSlotsStrict{value: pythFee}(
updateData,
priceIds,
curTime > PAST_TIMESTAMP_MAX_VALIDITY_PERIOD
Expand Down
29 changes: 25 additions & 4 deletions target_chains/ethereum/contracts/contracts/pyth/Pyth.sol
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,24 @@ abstract contract Pyth is
if (msg.value < requiredFee) revert PythErrors.InsufficientFee();
}

// In minimal update data mode, revert if we have more or less updates than price IDs
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you try out doing it on the fly instead of parsing again to save gas (and hopefully contract size)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will try 🙏

Copy link
Contributor Author

@tejasbadadare tejasbadadare Apr 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We calculate on the fly now and inlined ParseConfig to chop a struct to save space (see commits after your review,) but we are still 372 bytes short 😭

Any idea if there's something unused that we can remove? Otherwise i'll abandon this approach and just use a heuristic check in Pulse.updatePriceFeeds to check that the updatedata isn't too large

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i recommend you reduce the optimizer runs a bit to match it. the optimizer inlines things to reduce jump costs.

if (config.checkUpdateDataIsMinimal) {
uint64 totalUpdatesAcrossBlobs = 0;
for (uint i = 0; i < updateData.length; i++) {
(uint offset, ) = extractUpdateTypeFromAccumulatorHeader(
updateData[i]
);

totalUpdatesAcrossBlobs += parseWormholeMerkleHeaderNumUpdates(
updateData[i],
offset
);
}
if (totalUpdatesAcrossBlobs != priceIds.length) {
revert PythErrors.InvalidArgument();
}
}

// Create the context struct that holds all shared parameters
PythInternalStructs.UpdateParseContext memory context;
context.priceIds = priceIds;
Expand Down Expand Up @@ -341,12 +359,13 @@ abstract contract Pyth is
PythInternalStructs.ParseConfig(
minPublishTime,
maxPublishTime,
false,
false
)
);
}

function parsePriceFeedUpdatesWithSlots(
function parsePriceFeedUpdatesWithSlotsStrict(
bytes[] calldata updateData,
bytes32[] calldata priceIds,
uint64 minPublishTime,
Expand All @@ -367,7 +386,8 @@ abstract contract Pyth is
PythInternalStructs.ParseConfig(
minPublishTime,
maxPublishTime,
false
false,
true
)
);
}
Expand Down Expand Up @@ -550,7 +570,8 @@ abstract contract Pyth is
PythInternalStructs.ParseConfig(
minPublishTime,
maxPublishTime,
true
true,
false
)
);
}
Expand Down Expand Up @@ -624,7 +645,7 @@ abstract contract Pyth is
}

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

/// @notice Calculates TWAP from two price points
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ contract PythInternalStructs {
uint64 minPublishTime;
uint64 maxPublishTime;
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;
}

/// Internal struct to hold parameters for update processing
Expand Down
34 changes: 17 additions & 17 deletions target_chains/ethereum/contracts/forge-test/PulseScheduler.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ 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 @@ -822,7 +822,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 @@ -873,7 +873,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 @@ -934,7 +934,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 @@ -1019,7 +1019,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 @@ -1078,7 +1078,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, priceIds);
Expand All @@ -1089,7 +1089,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 @@ -1126,7 +1126,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, priceIds);
Expand All @@ -1152,7 +1152,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 @@ -1178,7 +1178,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 @@ -1190,7 +1190,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 @@ -1231,7 +1231,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 @@ -1346,7 +1346,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 @@ -1388,7 +1388,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 @@ -1443,7 +1443,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);
bytes32[] memory priceIds = params.priceIds;

Expand Down Expand Up @@ -1488,7 +1488,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 @@ -1555,7 +1555,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
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,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 @@ -128,7 +128,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
74 changes: 72 additions & 2 deletions target_chains/ethereum/contracts/forge-test/Pyth.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils {
}
}

function testParsePriceFeedUpdatesWithSlotsWorks(uint seed) public {
function testParsePriceFeedUpdatesWithSlotsStrictWorks(uint seed) public {
setRandSeed(seed);
uint numMessages = 1 + (getRandUint() % 10);
(
Expand All @@ -251,7 +251,7 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils {
(
PythStructs.PriceFeed[] memory priceFeeds,
uint64[] memory slots
) = pyth.parsePriceFeedUpdatesWithSlots{value: updateFee}(
) = pyth.parsePriceFeedUpdatesWithSlotsStrict{value: updateFee}(
updateData,
priceIds,
0,
Expand Down Expand Up @@ -392,6 +392,76 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils {
);
}

function testParsePriceFeedUpdatesWithSlotsStrictRevertsWithExcessUpdateData()
public
{
// Create a price update with more price updates than requested price IDs
uint numPriceIds = 2;
uint numMessages = numPriceIds + 1; // One more than the number of price IDs

(
bytes32[] memory priceIds,
PriceFeedMessage[] memory messages
) = generateRandomPriceMessages(numMessages);

// Only use a subset of the price IDs to trigger the strict check
bytes32[] memory requestedPriceIds = new bytes32[](numPriceIds);
for (uint i = 0; i < numPriceIds; i++) {
requestedPriceIds[i] = priceIds[i];
}

(
bytes[] memory updateData,
uint updateFee
) = createBatchedUpdateDataFromMessages(messages);

// Should revert in strict mode
vm.expectRevert(PythErrors.InvalidArgument.selector);
pyth.parsePriceFeedUpdatesWithSlotsStrict{value: updateFee}(
updateData,
requestedPriceIds,
0,
MAX_UINT64
);
}

function testParsePriceFeedUpdatesWithSlotsStrictRevertsWithFewerUpdateData()
public
{
// Create a price update with fewer price updates than requested price IDs
uint numPriceIds = 3;
uint numMessages = numPriceIds - 1; // One less than the number of price IDs

(
bytes32[] memory priceIds,
PriceFeedMessage[] memory messages
) = generateRandomPriceMessages(numMessages);

// Create a larger array of requested price IDs to trigger the strict check
bytes32[] memory requestedPriceIds = new bytes32[](numPriceIds);
for (uint i = 0; i < numMessages; i++) {
requestedPriceIds[i] = priceIds[i];
}
// Add an extra price ID that won't be in the updates
requestedPriceIds[numMessages] = bytes32(
uint256(keccak256(abi.encodePacked("extra_id")))
);

(
bytes[] memory updateData,
uint updateFee
) = createBatchedUpdateDataFromMessages(messages);

// Should revert in strict mode because we have fewer updates than price IDs
vm.expectRevert(PythErrors.InvalidArgument.selector);
pyth.parsePriceFeedUpdatesWithSlotsStrict{value: updateFee}(
updateData,
requestedPriceIds,
0,
MAX_UINT64
);
}

function testParsePriceFeedUpdatesRevertsIfUpdateSourceChainIsInvalid()
public
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ abstract contract MockPriceFeedTestUtils is Test {
}

// Helper function to mock Pyth response with slots
function mockParsePriceFeedUpdatesWithSlots(
function mockParsePriceFeedUpdatesWithSlotsStrict(
address pyth,
PythStructs.PriceFeed[] memory priceFeeds,
uint64[] memory slots
Expand All @@ -187,7 +187,7 @@ abstract contract MockPriceFeedTestUtils is Test {
pyth,
expectedFee,
abi.encodeWithSelector(
IPyth.parsePriceFeedUpdatesWithSlots.selector
IPyth.parsePriceFeedUpdatesWithSlotsStrict.selector
),
abi.encode(priceFeeds, slots)
);
Expand Down
2 changes: 1 addition & 1 deletion target_chains/ethereum/sdk/solidity/AbstractPyth.sol
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ abstract contract AbstractPyth is IPyth {
override
returns (PythStructs.PriceFeed[] memory priceFeeds);

function parsePriceFeedUpdatesWithSlots(
function parsePriceFeedUpdatesWithSlotsStrict(
bytes[] calldata updateData,
bytes32[] calldata priceIds,
uint64 minPublishTime,
Expand Down
5 changes: 2 additions & 3 deletions target_chains/ethereum/sdk/solidity/IPyth.sol
Original file line number Diff line number Diff line change
Expand Up @@ -165,15 +165,14 @@ interface IPyth is IPythEvents {
uint64 maxPublishTime
) external payable returns (PythStructs.PriceFeed[] memory priceFeeds);

/// @dev Same as `parsePriceFeedUpdates`, but also returns the Pythnet slot
/// associated with each price update.
/// @dev Same as `parsePriceFeedUpdates`, but checks that the updateData is minimal and also returns the Pythnet slots.
/// @param updateData Array of price update data.
/// @param priceIds Array of price ids.
/// @param minPublishTime minimum acceptable publishTime for the given `priceIds`.
/// @param maxPublishTime maximum acceptable publishTime for the given `priceIds`.
/// @return priceFeeds Array of the price feeds corresponding to the given `priceIds` (with the same order).
/// @return slots Array of the Pythnet slot corresponding to the given `priceIds` (with the same order).
function parsePriceFeedUpdatesWithSlots(
function parsePriceFeedUpdatesWithSlotsStrict(
bytes[] calldata updateData,
bytes32[] calldata priceIds,
uint64 minPublishTime,
Expand Down
Loading