Skip to content

Commit dac3e31

Browse files
committed
feat: refactor TWAP price handling by moving TwapPriceInfo struct to PythStructs and implementing calculateTwap function in PythUtils
1 parent 46eb28d commit dac3e31

File tree

6 files changed

+106
-141
lines changed

6 files changed

+106
-141
lines changed

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

Lines changed: 15 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import "@pythnetwork/pyth-sdk-solidity/AbstractPyth.sol";
77
import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";
88

99
import "@pythnetwork/pyth-sdk-solidity/PythErrors.sol";
10+
import "@pythnetwork/pyth-sdk-solidity/PythUtils.sol";
1011
import "./PythAccumulator.sol";
1112
import "./PythGetters.sol";
1213
import "./PythSetters.sol";
@@ -314,7 +315,7 @@ abstract contract Pyth is
314315
view
315316
returns (
316317
uint newOffset,
317-
PythInternalStructs.TwapPriceInfo memory twapPriceInfo,
318+
PythStructs.TwapPriceInfo memory twapPriceInfo,
318319
bytes32 priceId
319320
)
320321
{
@@ -396,8 +397,8 @@ abstract contract Pyth is
396397
uint offsetEnd;
397398
bytes32 priceIdStart;
398399
bytes32 priceIdEnd;
399-
PythInternalStructs.TwapPriceInfo memory twapPriceInfoStart;
400-
PythInternalStructs.TwapPriceInfo memory twapPriceInfoEnd;
400+
PythStructs.TwapPriceInfo memory twapPriceInfoStart;
401+
PythStructs.TwapPriceInfo memory twapPriceInfoEnd;
401402
(
402403
offsetStart,
403404
twapPriceInfoStart,
@@ -444,8 +445,8 @@ abstract contract Pyth is
444445
}
445446

446447
function validateTwapPriceInfo(
447-
PythInternalStructs.TwapPriceInfo memory twapPriceInfoStart,
448-
PythInternalStructs.TwapPriceInfo memory twapPriceInfoEnd
448+
PythStructs.TwapPriceInfo memory twapPriceInfoStart,
449+
PythStructs.TwapPriceInfo memory twapPriceInfoEnd
449450
) private pure {
450451
// First validate each individual price's uniqueness
451452
if (
@@ -563,47 +564,14 @@ abstract contract Pyth is
563564

564565
function calculateTwap(
565566
bytes32 priceId,
566-
PythInternalStructs.TwapPriceInfo memory twapPriceInfoStart,
567-
PythInternalStructs.TwapPriceInfo memory twapPriceInfoEnd
568-
) private pure returns (PythStructs.TwapPriceFeed memory twapPriceFeed) {
569-
// Calculate differences between start and end points for slots and cumulative values
570-
// These differences represent the changes that occurred over the time window
571-
uint64 slotDiff = twapPriceInfoEnd.publishSlot -
572-
twapPriceInfoStart.publishSlot;
573-
int128 priceDiff = twapPriceInfoEnd.cumulativePrice -
574-
twapPriceInfoStart.cumulativePrice;
575-
uint128 confDiff = twapPriceInfoEnd.cumulativeConf -
576-
twapPriceInfoStart.cumulativeConf;
577-
578-
// Calculate time-weighted average price (TWAP) and confidence by dividing
579-
// the difference in cumulative values by the number of slots between data points
580-
int128 twapPrice = priceDiff / int128(uint128(slotDiff));
581-
uint128 twapConf = confDiff / uint128(slotDiff);
582-
583-
// Initialize the TWAP price feed structure
584-
twapPriceFeed.id = priceId;
585-
586-
// The conversion from int128 to int64 is safe because:
587-
// 1. Individual prices fit within int64 by protocol design
588-
// 2. TWAP is essentially an average price over time (cumulativePrice₂-cumulativePrice₁)/slotDiff
589-
// 3. This average must be within the range of individual prices that went into the calculation
590-
// We use int128 only as an intermediate type to safely handle cumulative sums
591-
twapPriceFeed.twap.price = int64(twapPrice);
592-
twapPriceFeed.twap.conf = uint64(twapConf);
593-
twapPriceFeed.twap.expo = twapPriceInfoStart.expo;
594-
twapPriceFeed.twap.publishTime = twapPriceInfoEnd.publishTime;
595-
twapPriceFeed.startTime = twapPriceInfoStart.publishTime;
596-
twapPriceFeed.endTime = twapPriceInfoEnd.publishTime;
597-
598-
// Calculate downSlotsRatio as a value between 0 and 1,000,000
599-
// 0 means no slots were missed, 1,000,000 means all slots were missed
600-
uint64 totalDownSlots = twapPriceInfoEnd.numDownSlots -
601-
twapPriceInfoStart.numDownSlots;
602-
uint64 downSlotsRatio = (totalDownSlots * 1_000_000) / slotDiff;
603-
604-
// Safely downcast to uint32 (sufficient for value range 0-1,000,000)
605-
twapPriceFeed.downSlotsRatio = uint32(downSlotsRatio);
606-
607-
return twapPriceFeed;
567+
PythStructs.TwapPriceInfo memory twapPriceInfoStart,
568+
PythStructs.TwapPriceInfo memory twapPriceInfoEnd
569+
) private pure returns (PythStructs.TwapPriceFeed memory) {
570+
return
571+
PythUtils.calculateTwap(
572+
priceId,
573+
twapPriceInfoStart,
574+
twapPriceInfoEnd
575+
);
608576
}
609577
}

target_chains/ethereum/contracts/contracts/pyth/PythAccumulator.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ abstract contract PythAccumulator is PythGetters, PythSetters, AbstractPyth {
256256
pure
257257
returns (
258258
uint endOffset,
259-
PythInternalStructs.TwapPriceInfo memory twapPriceInfo,
259+
PythStructs.TwapPriceInfo memory twapPriceInfo,
260260
bytes32 priceId
261261
)
262262
{
@@ -396,7 +396,7 @@ abstract contract PythAccumulator is PythGetters, PythSetters, AbstractPyth {
396396
private
397397
pure
398398
returns (
399-
PythInternalStructs.TwapPriceInfo memory twapPriceInfo,
399+
PythStructs.TwapPriceInfo memory twapPriceInfo,
400400
bytes32 priceId
401401
)
402402
{

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

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,6 @@ contract PythInternalStructs {
2626
uint64 emaConf;
2727
}
2828

29-
struct TwapPriceInfo {
30-
// slot 1
31-
int128 cumulativePrice;
32-
uint128 cumulativeConf;
33-
// slot 2
34-
uint64 numDownSlots;
35-
uint64 publishSlot;
36-
uint64 publishTime;
37-
uint64 prevPublishTime;
38-
// slot 3
39-
40-
int32 expo;
41-
}
42-
4329
struct DataSource {
4430
uint16 chainId;
4531
bytes32 emitterAddress;

target_chains/ethereum/sdk/solidity/MockPyth.sol

Lines changed: 23 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,14 @@ pragma solidity ^0.8.0;
44
import "./AbstractPyth.sol";
55
import "./PythStructs.sol";
66
import "./PythErrors.sol";
7+
import "./PythUtils.sol";
78

89
contract MockPyth is AbstractPyth {
910
mapping(bytes32 => PythStructs.PriceFeed) priceFeeds;
1011

1112
uint singleUpdateFeeInWei;
1213
uint validTimePeriod;
1314

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-
2715
constructor(uint _validTimePeriod, uint _singleUpdateFeeInWei) {
2816
singleUpdateFeeInWei = _singleUpdateFeeInWei;
2917
validTimePeriod = _validTimePeriod;
@@ -246,12 +234,12 @@ contract MockPyth is AbstractPyth {
246234
revert PythErrors.InvalidTwapUpdateDataSet();
247235
}
248236

249-
// Convert to MockTwapPriceInfo
250-
MockTwapPriceInfo memory startInfo = createMockTwapInfo(
237+
// Convert to TwapPriceInfo
238+
PythStructs.TwapPriceInfo memory startInfo = createMockTwapInfo(
251239
startFeed,
252240
startPrevPublishTime
253241
);
254-
MockTwapPriceInfo memory endInfo = createMockTwapInfo(
242+
PythStructs.TwapPriceInfo memory endInfo = createMockTwapInfo(
255243
endFeed,
256244
endPrevPublishTime
257245
);
@@ -261,98 +249,55 @@ contract MockPyth is AbstractPyth {
261249
}
262250

263251
// 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(
252+
twapPriceFeeds[index] = PythUtils.calculateTwap(
268253
priceId,
269-
startInfo.publishTime,
270-
endInfo.publishTime,
271-
twapPriceFeeds[index]
254+
startInfo,
255+
endInfo
272256
);
273-
}
274257

275-
function emitTwapUpdate(
276-
bytes32 priceId,
277-
uint64 startTime,
278-
uint64 endTime,
279-
PythStructs.TwapPriceFeed memory twapFeed
280-
) private {
258+
// Emit event
281259
emit TwapPriceFeedUpdate(
282260
priceId,
283-
startTime,
284-
endTime,
285-
twapFeed.twap.price,
286-
twapFeed.twap.conf,
287-
twapFeed.downSlotsRatio
261+
startInfo.publishTime,
262+
endInfo.publishTime,
263+
twapPriceFeeds[index].twap.price,
264+
twapPriceFeeds[index].twap.conf,
265+
twapPriceFeeds[index].downSlotsRatio
288266
);
289267
}
290268

291269
function createMockTwapInfo(
292270
PythStructs.PriceFeed memory feed,
293271
uint64 prevPublishTime
294-
) internal pure returns (MockTwapPriceInfo memory mockInfo) {
272+
) internal pure returns (PythStructs.TwapPriceInfo memory mockInfo) {
273+
// Set basic fields
295274
mockInfo.expo = feed.price.expo;
296-
mockInfo.price = feed.price.price;
297-
mockInfo.conf = feed.price.conf;
298275
mockInfo.publishTime = uint64(feed.price.publishTime);
299276
mockInfo.prevPublishTime = prevPublishTime;
300277

301278
// Use publishTime as publishSlot in mock implementation
279+
// In real implementation, this would be actual slot number
302280
mockInfo.publishSlot = uint64(feed.price.publishTime);
303281

304-
// Create mock cumulative values for demonstration
305-
// In a real implementation, these would accumulate over time
282+
// Calculate cumulative values
283+
// In mock implementation, we simulate cumulative values by multiplying current values by slot
284+
// This creates a linear accumulation which is sufficient for testing
285+
// In real implementation, these would be actual accumulated values over time
306286
mockInfo.cumulativePrice =
307287
int128(feed.price.price) *
308288
int128(uint128(mockInfo.publishSlot));
309289
mockInfo.cumulativeConf =
310290
uint128(feed.price.conf) *
311291
uint128(mockInfo.publishSlot);
312292

313-
// Default to 0 down slots for mock
293+
// Set number of down slots
294+
// In mock implementation we default to 0 down slots
295+
// In real implementation this would track actual network downtime
314296
mockInfo.numDownSlots = 0;
315297

316298
return mockInfo;
317299
}
318300

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 downSlotsRatio 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.downSlotsRatio = uint32(downSlotsRatio);
352-
353-
return twapPriceFeed;
354-
}
355-
356301
function createPriceFeedUpdateData(
357302
bytes32 id,
358303
int64 price,

target_chains/ethereum/sdk/solidity/PythStructs.sol

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,18 @@ contract PythStructs {
4343
// Down slot ratio
4444
uint32 downSlotsRatio;
4545
}
46+
47+
// Information used to calculate time-weighted average prices (TWAP)
48+
struct TwapPriceInfo {
49+
// slot 1
50+
int128 cumulativePrice;
51+
uint128 cumulativeConf;
52+
// slot 2
53+
uint64 numDownSlots;
54+
uint64 publishSlot;
55+
uint64 publishTime;
56+
uint64 prevPublishTime;
57+
// slot 3
58+
int32 expo;
59+
}
4660
}

target_chains/ethereum/sdk/solidity/PythUtils.sol

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// SPDX-License-Identifier: Apache-2.0
22
pragma solidity ^0.8.0;
33

4+
import "./PythStructs.sol";
5+
46
library PythUtils {
57
/// @notice Converts a Pyth price to a uint256 with a target number of decimals
68
/// @param price The Pyth price
@@ -31,4 +33,54 @@ library PythUtils {
3133
10 ** uint32(priceDecimals - targetDecimals);
3234
}
3335
}
36+
37+
/// @notice Calculates TWAP from two price points
38+
/// @dev The calculation is done by taking the difference of cumulative values and dividing by the time difference
39+
/// @param priceId The price feed ID
40+
/// @param twapPriceInfoStart The starting price point
41+
/// @param twapPriceInfoEnd The ending price point
42+
/// @return twapPriceFeed The calculated TWAP price feed
43+
function calculateTwap(
44+
bytes32 priceId,
45+
PythStructs.TwapPriceInfo memory twapPriceInfoStart,
46+
PythStructs.TwapPriceInfo memory twapPriceInfoEnd
47+
) public pure returns (PythStructs.TwapPriceFeed memory twapPriceFeed) {
48+
twapPriceFeed.id = priceId;
49+
twapPriceFeed.startTime = twapPriceInfoStart.publishTime;
50+
twapPriceFeed.endTime = twapPriceInfoEnd.publishTime;
51+
52+
// Calculate differences between start and end points for slots and cumulative values
53+
uint64 slotDiff = twapPriceInfoEnd.publishSlot -
54+
twapPriceInfoStart.publishSlot;
55+
int128 priceDiff = twapPriceInfoEnd.cumulativePrice -
56+
twapPriceInfoStart.cumulativePrice;
57+
uint128 confDiff = twapPriceInfoEnd.cumulativeConf -
58+
twapPriceInfoStart.cumulativeConf;
59+
60+
// Calculate time-weighted average price (TWAP) and confidence by dividing
61+
// the difference in cumulative values by the number of slots between data points
62+
int128 twapPrice = priceDiff / int128(uint128(slotDiff));
63+
uint128 twapConf = confDiff / uint128(slotDiff);
64+
65+
// The conversion from int128 to int64 is safe because:
66+
// 1. Individual prices fit within int64 by protocol design
67+
// 2. TWAP is essentially an average price over time (cumulativePrice₂-cumulativePrice₁)/slotDiff
68+
// 3. This average must be within the range of individual prices that went into the calculation
69+
// We use int128 only as an intermediate type to safely handle cumulative sums
70+
twapPriceFeed.twap.price = int64(twapPrice);
71+
twapPriceFeed.twap.conf = uint64(twapConf);
72+
twapPriceFeed.twap.expo = twapPriceInfoStart.expo;
73+
twapPriceFeed.twap.publishTime = twapPriceInfoEnd.publishTime;
74+
75+
// Calculate downSlotsRatio as a value between 0 and 1,000,000
76+
// 0 means no slots were missed, 1,000,000 means all slots were missed
77+
uint64 totalDownSlots = twapPriceInfoEnd.numDownSlots -
78+
twapPriceInfoStart.numDownSlots;
79+
uint64 downSlotsRatio = (totalDownSlots * 1_000_000) / slotDiff;
80+
81+
// Safely downcast to uint32 (sufficient for value range 0-1,000,000)
82+
twapPriceFeed.downSlotsRatio = uint32(downSlotsRatio);
83+
84+
return twapPriceFeed;
85+
}
3486
}

0 commit comments

Comments
 (0)