Skip to content

Commit 98ab843

Browse files
committed
feat: add parseTwapPriceFeedUpdates function to IPyth and MockPyth
1 parent a87ff74 commit 98ab843

File tree

8 files changed

+639
-3
lines changed

8 files changed

+639
-3
lines changed

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -478,13 +478,13 @@ abstract contract Pyth is
478478
PythInternalStructs.TwapPriceInfo memory twapPriceInfoStart,
479479
PythInternalStructs.TwapPriceInfo memory twapPriceInfoEnd
480480
) private pure {
481-
// First validate each individual data point's internal consistency
481+
// First validate each individual price's uniqueness
482482
if (
483-
twapPriceInfoStart.prevPublishTime > twapPriceInfoStart.publishTime
483+
twapPriceInfoStart.prevPublishTime >= twapPriceInfoStart.publishTime
484484
) {
485485
revert PythErrors.InvalidTwapUpdateData();
486486
}
487-
if (twapPriceInfoEnd.prevPublishTime > twapPriceInfoEnd.publishTime) {
487+
if (twapPriceInfoEnd.prevPublishTime >= twapPriceInfoEnd.publishTime) {
488488
revert PythErrors.InvalidTwapUpdateData();
489489
}
490490

target_chains/ethereum/sdk/solidity/IPyth.sol

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,32 @@ interface IPyth is IPythEvents {
119119
uint64 maxPublishTime
120120
) external payable returns (PythStructs.PriceFeed[] memory priceFeeds);
121121

122+
/// @notice Parse time-weighted average price (TWAP) from two sets of price update data for the given `priceIds`.
123+
///
124+
/// This method calculates TWAP between two data points by processing the difference in cumulative price values
125+
/// divided by the time period. It requires two sets of update data that contain valid price information
126+
/// for all the requested price IDs.
127+
///
128+
/// This method requires the caller to pay a fee in wei; the required fee can be computed by calling
129+
/// `getUpdateFee` with the length of either updateData array (both arrays must have the same length).
130+
///
131+
/// @dev Reverts if:
132+
/// - The transferred fee is not sufficient
133+
/// - The updateData is invalid or malformed
134+
/// - There is no update for any of the given `priceIds`
135+
/// - The two update datasets are not comparable (different number of updates or mismatched price IDs)
136+
/// - The time ordering between data points is invalid (start time must be before end time)
137+
/// @param updateData Array containing two arrays of price update data (start and end points for TWAP calculation)
138+
/// @param priceIds Array of price ids to calculate TWAP for
139+
/// @return twapPriceFeeds Array of TWAP price feeds corresponding to the given `priceIds` (with the same order)
140+
function parseTwapPriceFeedUpdates(
141+
bytes[][] calldata updateData,
142+
bytes32[] calldata priceIds
143+
)
144+
external
145+
payable
146+
returns (PythStructs.TwapPriceFeed[] memory twapPriceFeeds);
147+
122148
/// @notice Similar to `parsePriceFeedUpdates` but ensures the updates returned are
123149
/// the first updates published in minPublishTime. That is, if there are multiple updates for a given timestamp,
124150
/// this method will return the first update. This method may store the price updates on-chain, if they

target_chains/ethereum/sdk/solidity/IPythEvents.sol

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,20 @@ interface IPythEvents {
1515
int64 price,
1616
uint64 conf
1717
);
18+
19+
/// @dev Emitted when the TWAP price feed with `id` has received a fresh update.
20+
/// @param id The Pyth Price Feed ID.
21+
/// @param startTime Start time of the TWAP.
22+
/// @param endTime End time of the TWAP.
23+
/// @param twapPrice Price of the TWAP.
24+
/// @param twapConf Confidence interval of the TWAP.
25+
/// @param downSlotRatio Down slot ratio of the TWAP.
26+
event TwapPriceFeedUpdate(
27+
bytes32 indexed id,
28+
uint64 startTime,
29+
uint64 endTime,
30+
int64 twapPrice,
31+
uint64 twapConf,
32+
uint32 downSlotRatio
33+
);
1834
}

target_chains/ethereum/sdk/solidity/MockPyth.sol

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,19 @@ contract MockPyth is AbstractPyth {
1111
uint singleUpdateFeeInWei;
1212
uint validTimePeriod;
1313

14+
// Mock structure for TWAP price information
15+
struct MockTwapPriceInfo {
16+
int32 expo;
17+
int64 price;
18+
uint64 conf;
19+
uint64 publishTime;
20+
uint64 prevPublishTime;
21+
uint64 publishSlot;
22+
int128 cumulativePrice;
23+
uint128 cumulativeConf;
24+
uint64 numDownSlots;
25+
}
26+
1427
constructor(uint _validTimePeriod, uint _singleUpdateFeeInWei) {
1528
singleUpdateFeeInWei = _singleUpdateFeeInWei;
1629
validTimePeriod = _validTimePeriod;
@@ -160,6 +173,186 @@ contract MockPyth is AbstractPyth {
160173
);
161174
}
162175

176+
function parseTwapPriceFeedUpdates(
177+
bytes[][] calldata updateData,
178+
bytes32[] calldata priceIds
179+
)
180+
external
181+
payable
182+
returns (PythStructs.TwapPriceFeed[] memory twapPriceFeeds)
183+
{
184+
// Validate inputs and fee
185+
if (updateData.length != 2) revert PythErrors.InvalidUpdateData();
186+
187+
uint requiredFee = getUpdateFee(updateData[0]);
188+
if (requiredFee != getUpdateFee(updateData[1]))
189+
revert PythErrors.InvalidUpdateData();
190+
if (msg.value < requiredFee) revert PythErrors.InsufficientFee();
191+
192+
twapPriceFeeds = new PythStructs.TwapPriceFeed[](priceIds.length);
193+
194+
// Process each price ID
195+
for (uint i = 0; i < priceIds.length; i++) {
196+
processTwapPriceFeed(updateData, priceIds[i], i, twapPriceFeeds);
197+
}
198+
}
199+
200+
function processTwapPriceFeed(
201+
bytes[][] calldata updateData,
202+
bytes32 priceId,
203+
uint index,
204+
PythStructs.TwapPriceFeed[] memory twapPriceFeeds
205+
) private {
206+
// Find start price feed
207+
PythStructs.PriceFeed memory startFeed;
208+
uint64 startPrevPublishTime;
209+
bool foundStart = false;
210+
211+
for (uint j = 0; j < updateData[0].length; j++) {
212+
(startFeed, startPrevPublishTime) = abi.decode(
213+
updateData[0][j],
214+
(PythStructs.PriceFeed, uint64)
215+
);
216+
217+
if (startFeed.id == priceId) {
218+
foundStart = true;
219+
break;
220+
}
221+
}
222+
223+
if (!foundStart) revert PythErrors.PriceFeedNotFoundWithinRange();
224+
225+
// Find end price feed
226+
PythStructs.PriceFeed memory endFeed;
227+
uint64 endPrevPublishTime;
228+
bool foundEnd = false;
229+
230+
for (uint j = 0; j < updateData[1].length; j++) {
231+
(endFeed, endPrevPublishTime) = abi.decode(
232+
updateData[1][j],
233+
(PythStructs.PriceFeed, uint64)
234+
);
235+
236+
if (endFeed.id == priceId) {
237+
foundEnd = true;
238+
break;
239+
}
240+
}
241+
242+
if (!foundEnd) revert PythErrors.PriceFeedNotFoundWithinRange();
243+
244+
// Validate time ordering
245+
if (startFeed.price.publishTime >= endFeed.price.publishTime) {
246+
revert PythErrors.InvalidTwapUpdateDataSet();
247+
}
248+
249+
// Convert to MockTwapPriceInfo
250+
MockTwapPriceInfo memory startInfo = createMockTwapInfo(
251+
startFeed,
252+
startPrevPublishTime
253+
);
254+
MockTwapPriceInfo memory endInfo = createMockTwapInfo(
255+
endFeed,
256+
endPrevPublishTime
257+
);
258+
259+
if (startInfo.publishSlot >= endInfo.publishSlot) {
260+
revert PythErrors.InvalidTwapUpdateDataSet();
261+
}
262+
263+
// Calculate and store TWAP
264+
twapPriceFeeds[index] = calculateTwap(priceId, startInfo, endInfo);
265+
266+
// Emit event in a separate function to reduce stack depth
267+
emitTwapUpdate(
268+
priceId,
269+
startInfo.publishTime,
270+
endInfo.publishTime,
271+
twapPriceFeeds[index]
272+
);
273+
}
274+
275+
function emitTwapUpdate(
276+
bytes32 priceId,
277+
uint64 startTime,
278+
uint64 endTime,
279+
PythStructs.TwapPriceFeed memory twapFeed
280+
) private {
281+
emit TwapPriceFeedUpdate(
282+
priceId,
283+
startTime,
284+
endTime,
285+
twapFeed.twap.price,
286+
twapFeed.twap.conf,
287+
twapFeed.downSlotRatio
288+
);
289+
}
290+
291+
function createMockTwapInfo(
292+
PythStructs.PriceFeed memory feed,
293+
uint64 prevPublishTime
294+
) internal pure returns (MockTwapPriceInfo memory mockInfo) {
295+
mockInfo.expo = feed.price.expo;
296+
mockInfo.price = feed.price.price;
297+
mockInfo.conf = feed.price.conf;
298+
mockInfo.publishTime = uint64(feed.price.publishTime);
299+
mockInfo.prevPublishTime = prevPublishTime;
300+
301+
// Use publishTime as publishSlot in mock implementation
302+
mockInfo.publishSlot = uint64(feed.price.publishTime);
303+
304+
// Create mock cumulative values for demonstration
305+
// In a real implementation, these would accumulate over time
306+
mockInfo.cumulativePrice =
307+
int128(feed.price.price) *
308+
int128(uint128(mockInfo.publishSlot));
309+
mockInfo.cumulativeConf =
310+
uint128(feed.price.conf) *
311+
uint128(mockInfo.publishSlot);
312+
313+
// Default to 0 down slots for mock
314+
mockInfo.numDownSlots = 0;
315+
316+
return mockInfo;
317+
}
318+
319+
function calculateTwap(
320+
bytes32 priceId,
321+
MockTwapPriceInfo memory twapPriceInfoStart,
322+
MockTwapPriceInfo memory twapPriceInfoEnd
323+
) internal pure returns (PythStructs.TwapPriceFeed memory twapPriceFeed) {
324+
twapPriceFeed.id = priceId;
325+
twapPriceFeed.startTime = twapPriceInfoStart.publishTime;
326+
twapPriceFeed.endTime = twapPriceInfoEnd.publishTime;
327+
328+
// Calculate differences between start and end points for slots and cumulative values
329+
uint64 slotDiff = twapPriceInfoEnd.publishSlot -
330+
twapPriceInfoStart.publishSlot;
331+
int128 priceDiff = twapPriceInfoEnd.cumulativePrice -
332+
twapPriceInfoStart.cumulativePrice;
333+
uint128 confDiff = twapPriceInfoEnd.cumulativeConf -
334+
twapPriceInfoStart.cumulativeConf;
335+
336+
// Calculate time-weighted average price (TWAP) and confidence
337+
int128 twapPrice = priceDiff / int128(uint128(slotDiff));
338+
uint128 twapConf = confDiff / uint128(slotDiff);
339+
340+
twapPriceFeed.twap.price = int64(twapPrice);
341+
twapPriceFeed.twap.conf = uint64(twapConf);
342+
twapPriceFeed.twap.expo = twapPriceInfoStart.expo;
343+
twapPriceFeed.twap.publishTime = twapPriceInfoEnd.publishTime;
344+
345+
// Calculate downSlotRatio as a value between 0 and 1,000,000
346+
uint64 totalDownSlots = twapPriceInfoEnd.numDownSlots -
347+
twapPriceInfoStart.numDownSlots;
348+
uint64 downSlotsRatio = (totalDownSlots * 1_000_000) / slotDiff;
349+
350+
// Safely downcast to uint32 (sufficient for value range 0-1,000,000)
351+
twapPriceFeed.downSlotRatio = uint32(downSlotsRatio);
352+
353+
return twapPriceFeed;
354+
}
355+
163356
function createPriceFeedUpdateData(
164357
bytes32 id,
165358
int64 price,

0 commit comments

Comments
 (0)