Skip to content

Commit 74217f9

Browse files
committed
feat: implement TwapPriceFeedMessage struct and related encoding functions for TWAP updates
1 parent 766ecde commit 74217f9

File tree

4 files changed

+207
-106
lines changed

4 files changed

+207
-106
lines changed

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

Lines changed: 66 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,59 @@ abstract contract Pyth is
308308
);
309309
}
310310

311+
function processSingleTwapUpdate(
312+
bytes calldata updateData
313+
)
314+
private
315+
view
316+
returns (
317+
uint newOffset,
318+
PythInternalStructs.TwapPriceInfo memory twapPriceInfo,
319+
bytes32 priceId
320+
)
321+
{
322+
UpdateType updateType;
323+
uint offset;
324+
bytes20 digest;
325+
uint8 numUpdates;
326+
bytes calldata encoded;
327+
// Extract and validate the header for start data
328+
(offset, updateType) = extractUpdateTypeFromAccumulatorHeader(
329+
updateData
330+
);
331+
332+
if (updateType != UpdateType.WormholeMerkle) {
333+
revert PythErrors.InvalidUpdateData();
334+
}
335+
336+
(
337+
offset,
338+
digest,
339+
numUpdates,
340+
encoded
341+
) = extractWormholeMerkleHeaderDigestAndNumUpdatesAndEncodedFromAccumulatorUpdate(
342+
updateData,
343+
offset
344+
);
345+
346+
// Add additional validation before extracting TWAP price info
347+
if (offset >= updateData.length) {
348+
revert PythErrors.InvalidUpdateData();
349+
}
350+
351+
// Extract start TWAP data with robust error checking
352+
(offset, twapPriceInfo, priceId) = extractTwapPriceInfoFromMerkleProof(
353+
digest,
354+
encoded,
355+
offset
356+
);
357+
358+
if (offset != encoded.length) {
359+
revert PythErrors.InvalidTwapUpdateData();
360+
}
361+
newOffset = offset;
362+
}
363+
311364
function parseTwapPriceFeedUpdates(
312365
bytes[][] calldata updateData,
313366
bytes32[] calldata priceIds
@@ -338,77 +391,31 @@ abstract contract Pyth is
338391
UnsafeCalldataBytesLib.toUint32(updateData[1][i], 0) ==
339392
ACCUMULATOR_MAGIC)
340393
) {
341-
PythInternalStructs.TwapUpdateData memory twapData;
342-
UpdateType updateType;
343-
(
344-
twapData.offsetStart,
345-
updateType
346-
) = extractUpdateTypeFromAccumulatorHeader(
347-
updateData[0][i]
348-
);
349-
if (updateType != UpdateType.WormholeMerkle) {
350-
revert PythErrors.InvalidUpdateData();
351-
}
352-
(
353-
twapData.offsetEnd,
354-
updateType
355-
) = extractUpdateTypeFromAccumulatorHeader(
356-
updateData[1][i]
357-
);
358-
if (updateType != UpdateType.WormholeMerkle) {
359-
revert PythErrors.InvalidUpdateData();
360-
}
361-
362-
(
363-
twapData.offsetStart,
364-
twapData.digestStart,
365-
twapData.numUpdatesStart,
366-
twapData.encodedStart
367-
) = extractWormholeMerkleHeaderDigestAndNumUpdatesAndEncodedFromAccumulatorUpdate(
368-
updateData[0][i],
369-
twapData.offsetStart
370-
);
371-
(
372-
twapData.offsetEnd,
373-
twapData.digestEnd,
374-
twapData.numUpdatesEnd,
375-
twapData.encodedEnd
376-
) = extractWormholeMerkleHeaderDigestAndNumUpdatesAndEncodedFromAccumulatorUpdate(
377-
updateData[1][i],
378-
twapData.offsetEnd
379-
);
380-
381-
PythInternalStructs.TwapPriceInfo memory twapPriceInfoStart;
382-
PythInternalStructs.TwapPriceInfo memory twapPriceInfoEnd;
394+
uint offsetStart;
395+
uint offsetEnd;
383396
bytes32 priceIdStart;
384397
bytes32 priceIdEnd;
385-
386-
// Use original calldata directly in function calls
398+
PythInternalStructs.TwapPriceInfo memory twapPriceInfoStart;
399+
PythInternalStructs.TwapPriceInfo memory twapPriceInfoEnd;
387400
(
388-
twapData.offsetStart,
401+
offsetStart,
389402
twapPriceInfoStart,
390403
priceIdStart
391-
) = extractTwapPriceInfoFromMerkleProof(
392-
twapData.digestStart,
393-
updateData[0][i],
394-
twapData.offsetStart
395-
);
404+
) = processSingleTwapUpdate(updateData[0][i]);
396405
(
397-
twapData.offsetEnd,
406+
offsetEnd,
398407
twapPriceInfoEnd,
399408
priceIdEnd
400-
) = extractTwapPriceInfoFromMerkleProof(
401-
twapData.digestEnd,
402-
updateData[1][i],
403-
twapData.offsetEnd
404-
);
409+
) = processSingleTwapUpdate(updateData[1][i]);
405410

406411
if (priceIdStart != priceIdEnd)
407412
revert PythErrors.InvalidTwapUpdateDataSet();
408413

409-
// Unlike parsePriceFeedUpdatesInternal, we don't call updateLatestPriceIfNecessary here.
410-
// TWAP calculations are read-only operations that compute time-weighted averages
411-
// without updating the contract's state, returning calculated values directly to the caller.
414+
// Perform additional validation checks on the TWAP price data
415+
// to ensure proper time ordering, consistent exponents, and timestamp integrity
416+
// before using the data for calculations
417+
validateTwapPriceInfo(twapPriceInfoStart, twapPriceInfoEnd);
418+
412419
uint k = findIndexOfPriceId(priceIds, priceIdStart);
413420

414421
// If priceFeed[k].id != 0 then it means that there was a valid
@@ -417,25 +424,11 @@ abstract contract Pyth is
417424
continue;
418425
}
419426

420-
// Perform additional validation checks on the TWAP price data
421-
// to ensure proper time ordering, consistent exponents, and timestamp integrity
422-
// before using the data for calculations
423-
validateTwapPriceInfo(twapPriceInfoStart, twapPriceInfoEnd);
424-
425427
twapPriceFeeds[k] = calculateTwap(
426428
priceIdStart,
427429
twapPriceInfoStart,
428430
twapPriceInfoEnd
429431
);
430-
if (twapData.offsetStart != twapData.encodedStart.length) {
431-
revert PythErrors.InvalidTwapUpdateData();
432-
}
433-
if (twapData.offsetEnd != twapData.encodedEnd.length) {
434-
revert PythErrors.InvalidTwapUpdateData();
435-
}
436-
if (twapData.offsetStart != twapData.offsetEnd) {
437-
revert PythErrors.InvalidTwapUpdateData();
438-
}
439432
} else {
440433
revert PythErrors.InvalidUpdateData();
441434
}

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

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,4 @@ contract PythInternalStructs {
4444
uint16 chainId;
4545
bytes32 emitterAddress;
4646
}
47-
48-
// Define a struct that encapsulates these related variables. This will reduce the number of individual variables on the stack.
49-
struct TwapUpdateData {
50-
uint offsetStart;
51-
uint offsetEnd;
52-
bytes20 digestStart;
53-
bytes20 digestEnd;
54-
uint8 numUpdatesStart;
55-
uint8 numUpdatesEnd;
56-
bytes encodedStart;
57-
bytes encodedEnd;
58-
}
5947
}

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

Lines changed: 49 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -108,23 +108,48 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils {
108108
) public returns (bytes[][] memory updateData, uint updateFee) {
109109
require(messages.length >= 2, "At least 2 messages required for TWAP");
110110

111-
// Select first two messages for TWAP calculation
112-
PriceFeedMessage[] memory startMessages = new PriceFeedMessage[](1);
113-
startMessages[0] = messages[0];
114-
115-
PriceFeedMessage[] memory endMessages = new PriceFeedMessage[](1);
116-
endMessages[0] = messages[1];
117-
118-
// Generate the update data for start and end
111+
// Create TWAP messages from regular price feed messages
112+
// For TWAP calculation, we need cumulative values that increase over time
113+
TwapPriceFeedMessage[]
114+
memory startTwapMessages = new TwapPriceFeedMessage[](1);
115+
startTwapMessages[0].priceId = messages[0].priceId;
116+
// For test purposes, we'll set cumulative values for start message
117+
startTwapMessages[0].cumulativePrice = int128(messages[0].price) * 1000;
118+
startTwapMessages[0].cumulativeConf = uint128(messages[0].conf) * 1000;
119+
startTwapMessages[0].numDownSlots = 0; // No down slots for testing
120+
startTwapMessages[0].expo = messages[0].expo;
121+
startTwapMessages[0].publishTime = messages[0].publishTime;
122+
startTwapMessages[0].prevPublishTime = messages[0].prevPublishTime;
123+
startTwapMessages[0].publishSlot = 1000; // Start slot
124+
125+
TwapPriceFeedMessage[]
126+
memory endTwapMessages = new TwapPriceFeedMessage[](1);
127+
endTwapMessages[0].priceId = messages[1].priceId;
128+
// For end message, make sure cumulative values are higher than start
129+
endTwapMessages[0].cumulativePrice =
130+
int128(messages[1].price) *
131+
1000 +
132+
startTwapMessages[0].cumulativePrice;
133+
endTwapMessages[0].cumulativeConf =
134+
uint128(messages[1].conf) *
135+
1000 +
136+
startTwapMessages[0].cumulativeConf;
137+
endTwapMessages[0].numDownSlots = 0; // No down slots for testing
138+
endTwapMessages[0].expo = messages[1].expo;
139+
endTwapMessages[0].publishTime = messages[1].publishTime;
140+
endTwapMessages[0].prevPublishTime = messages[1].prevPublishTime;
141+
endTwapMessages[0].publishSlot = 1100; // End slot (100 slots after start)
142+
143+
// Generate the update data for start and end using the TWAP-specific function
119144
bytes[] memory startUpdateData = new bytes[](1);
120-
startUpdateData[0] = generateWhMerkleUpdateWithSource(
121-
startMessages,
145+
startUpdateData[0] = generateWhMerkleTwapUpdateWithSource(
146+
startTwapMessages,
122147
config
123148
);
124149

125150
bytes[] memory endUpdateData = new bytes[](1);
126-
endUpdateData[0] = generateWhMerkleUpdateWithSource(
127-
endMessages,
151+
endUpdateData[0] = generateWhMerkleTwapUpdateWithSource(
152+
endTwapMessages,
128153
config
129154
);
130155

@@ -400,10 +425,6 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils {
400425
uint updateFee
401426
) = createBatchedTwapUpdateDataFromMessages(messages);
402427

403-
// log the updateData
404-
console.logBytes(updateData[0][0]);
405-
console.logBytes(updateData[1][0]);
406-
407428
// Parse the TWAP updates
408429
PythStructs.TwapPriceFeed[] memory twapPriceFeeds = pyth
409430
.parseTwapPriceFeedUpdates{value: updateFee}(updateData, priceIds);
@@ -414,11 +435,18 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils {
414435
assertEq(twapPriceFeeds[0].endTime, uint64(1100)); // publishTime end
415436
assertEq(twapPriceFeeds[0].twap.expo, int32(-8)); // expo
416437

417-
// The TWAP price should be the difference in cumulative price divided by the slot difference
418-
assertEq(twapPriceFeeds[0].twap.price, int64(105));
419-
420-
// The TWAP conf should be the difference in cumulative conf divided by the slot difference
421-
assertEq(twapPriceFeeds[0].twap.conf, uint64(9));
438+
// Expected TWAP price calculation:
439+
// (endCumulativePrice - startCumulativePrice) / (endSlot - startSlot)
440+
// ((110 * 1000 + 100 * 1000) - (100 * 1000)) / (1100 - 1000)
441+
// = (210000 - 100000) / 100 = 1100
442+
// The smart contract will convert this to int64, and our calculation simplified for clarity
443+
assertEq(twapPriceFeeds[0].twap.price, int64(1100));
444+
445+
// Expected TWAP conf calculation:
446+
// (endCumulativeConf - startCumulativeConf) / (endSlot - startSlot)
447+
// ((8 * 1000 + 10 * 1000) - (10 * 1000)) / (1100 - 1000)
448+
// = (18000 - 10000) / 100 = 80
449+
assertEq(twapPriceFeeds[0].twap.conf, uint64(80));
422450

423451
// Validate the downSlotsRatio is 0 in our test implementation
424452
assertEq(twapPriceFeeds[0].downSlotsRatio, uint32(0));

0 commit comments

Comments
 (0)