Skip to content

Commit 21b35d4

Browse files
committed
feat(twap): update TWAP price feed logic to handle multiple price feeds and improve validation
1 parent 7cd8977 commit 21b35d4

File tree

2 files changed

+192
-8
lines changed

2 files changed

+192
-8
lines changed

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -439,9 +439,9 @@ abstract contract Pyth is
439439
override
440440
returns (PythStructs.TwapPriceFeed[] memory twapPriceFeeds)
441441
{
442-
// TWAP requires exactly 2 updates - one for the start point and one for the end point
443-
// to calculate the time-weighted average price between those two points
444-
if (updateData.length != 2) {
442+
// TWAP requires pairs of updates (start and end points) for each price feed
443+
// So updateData length must be exactly 2 * number of price feeds
444+
if (updateData.length != priceIds.length * 2) {
445445
revert PythErrors.InvalidUpdateData();
446446
}
447447

@@ -450,7 +450,8 @@ abstract contract Pyth is
450450

451451
unchecked {
452452
twapPriceFeeds = new PythStructs.TwapPriceFeed[](priceIds.length);
453-
for (uint i = 0; i < updateData.length - 1; i++) {
453+
// Iterate over pairs of updates
454+
for (uint i = 0; i < updateData.length; i += 2) {
454455
if (
455456
(updateData[i].length > 4 &&
456457
UnsafeCalldataBytesLib.toUint32(updateData[i], 0) ==

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

Lines changed: 187 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,18 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils {
2727
uint8 constant MERKLE_TREE_DEPTH = 9;
2828

2929
// Base TWAP messages that will be used as templates for tests
30-
TwapPriceFeedMessage[1] baseTwapStartMessages;
31-
TwapPriceFeedMessage[1] baseTwapEndMessages;
32-
bytes32[1] basePriceIds;
30+
TwapPriceFeedMessage[2] baseTwapStartMessages;
31+
TwapPriceFeedMessage[2] baseTwapEndMessages;
32+
bytes32[2] basePriceIds;
3333

3434
function setUp() public {
3535
pyth = IPyth(setUpPyth(setUpWormholeReceiver(NUM_GUARDIAN_SIGNERS)));
3636

37-
// Initialize base TWAP messages
37+
// Initialize base TWAP messages for two price feeds
3838
basePriceIds[0] = bytes32(uint256(1));
39+
basePriceIds[1] = bytes32(uint256(2));
3940

41+
// First price feed TWAP messages
4042
baseTwapStartMessages[0] = TwapPriceFeedMessage({
4143
priceId: basePriceIds[0],
4244
cumulativePrice: 100_000, // Base cumulative value
@@ -58,6 +60,29 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils {
5860
prevPublishTime: 1000,
5961
expo: -8
6062
});
63+
64+
// Second price feed TWAP messages
65+
baseTwapStartMessages[1] = TwapPriceFeedMessage({
66+
priceId: basePriceIds[1],
67+
cumulativePrice: 500_000, // Different base cumulative value
68+
cumulativeConf: 20_000, // Different base cumulative conf
69+
numDownSlots: 0,
70+
publishSlot: 1000,
71+
publishTime: 1000,
72+
prevPublishTime: 900,
73+
expo: -8
74+
});
75+
76+
baseTwapEndMessages[1] = TwapPriceFeedMessage({
77+
priceId: basePriceIds[1],
78+
cumulativePrice: 800_000, // Increased by 300_000
79+
cumulativeConf: 40_000, // Increased by 20_000
80+
numDownSlots: 0,
81+
publishSlot: 1100,
82+
publishTime: 1100,
83+
prevPublishTime: 1000,
84+
expo: -8
85+
});
6186
}
6287

6388
function generateRandomPriceMessages(
@@ -766,4 +791,162 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils {
766791
priceIds
767792
);
768793
}
794+
795+
function testParseTwapPriceFeedUpdatesMultipleFeeds() public {
796+
bytes32[] memory priceIds = new bytes32[](2);
797+
priceIds[0] = basePriceIds[0];
798+
priceIds[1] = basePriceIds[1];
799+
800+
// Create update data for both price feeds
801+
bytes[] memory updateData = new bytes[](4); // 2 updates (start/end) for each price feed
802+
803+
// First price feed updates
804+
TwapPriceFeedMessage[]
805+
memory startMessages1 = new TwapPriceFeedMessage[](1);
806+
TwapPriceFeedMessage[] memory endMessages1 = new TwapPriceFeedMessage[](
807+
1
808+
);
809+
startMessages1[0] = baseTwapStartMessages[0];
810+
endMessages1[0] = baseTwapEndMessages[0];
811+
812+
// Second price feed updates
813+
TwapPriceFeedMessage[]
814+
memory startMessages2 = new TwapPriceFeedMessage[](1);
815+
TwapPriceFeedMessage[] memory endMessages2 = new TwapPriceFeedMessage[](
816+
1
817+
);
818+
startMessages2[0] = baseTwapStartMessages[1];
819+
endMessages2[0] = baseTwapEndMessages[1];
820+
821+
// Generate Merkle updates for both price feeds
822+
MerkleUpdateConfig memory config = MerkleUpdateConfig(
823+
MERKLE_TREE_DEPTH,
824+
NUM_GUARDIAN_SIGNERS,
825+
SOURCE_EMITTER_CHAIN_ID,
826+
SOURCE_EMITTER_ADDRESS,
827+
false
828+
);
829+
830+
updateData[0] = generateWhMerkleTwapUpdateWithSource(
831+
startMessages1,
832+
config
833+
);
834+
updateData[1] = generateWhMerkleTwapUpdateWithSource(
835+
endMessages1,
836+
config
837+
);
838+
updateData[2] = generateWhMerkleTwapUpdateWithSource(
839+
startMessages2,
840+
config
841+
);
842+
updateData[3] = generateWhMerkleTwapUpdateWithSource(
843+
endMessages2,
844+
config
845+
);
846+
847+
uint updateFee = pyth.getUpdateFee(updateData);
848+
849+
// Parse the TWAP updates
850+
PythStructs.TwapPriceFeed[] memory twapPriceFeeds = pyth
851+
.parseTwapPriceFeedUpdates{value: updateFee}(updateData, priceIds);
852+
853+
// Validate results for first price feed
854+
assertEq(twapPriceFeeds[0].id, basePriceIds[0]);
855+
assertEq(
856+
twapPriceFeeds[0].startTime,
857+
baseTwapStartMessages[0].publishTime
858+
);
859+
assertEq(twapPriceFeeds[0].endTime, baseTwapEndMessages[0].publishTime);
860+
assertEq(twapPriceFeeds[0].twap.expo, baseTwapStartMessages[0].expo);
861+
// Expected TWAP price: (210_000 - 100_000) / (1100 - 1000) = 1100
862+
assertEq(twapPriceFeeds[0].twap.price, int64(1100));
863+
// Expected TWAP conf: (18_000 - 10_000) / (1100 - 1000) = 80
864+
assertEq(twapPriceFeeds[0].twap.conf, uint64(80));
865+
assertEq(twapPriceFeeds[0].downSlotsRatio, uint32(0));
866+
867+
// Validate results for second price feed
868+
assertEq(twapPriceFeeds[1].id, basePriceIds[1]);
869+
assertEq(
870+
twapPriceFeeds[1].startTime,
871+
baseTwapStartMessages[1].publishTime
872+
);
873+
assertEq(twapPriceFeeds[1].endTime, baseTwapEndMessages[1].publishTime);
874+
assertEq(twapPriceFeeds[1].twap.expo, baseTwapStartMessages[1].expo);
875+
// Expected TWAP price: (800_000 - 500_000) / (1100 - 1000) = 3000
876+
assertEq(twapPriceFeeds[1].twap.price, int64(3000));
877+
// Expected TWAP conf: (40_000 - 20_000) / (1100 - 1000) = 200
878+
assertEq(twapPriceFeeds[1].twap.conf, uint64(200));
879+
assertEq(twapPriceFeeds[1].downSlotsRatio, uint32(0));
880+
}
881+
882+
function testParseTwapPriceFeedUpdatesRevertsWithMismatchedArrayLengths()
883+
public
884+
{
885+
// Case 1: More updates than needed for price feeds
886+
bytes32[] memory priceIds = new bytes32[](1); // One price feed
887+
priceIds[0] = basePriceIds[0];
888+
889+
// Create 4 updates (should only be 2 for one price feed)
890+
bytes[] memory updateData = new bytes[](4);
891+
892+
TwapPriceFeedMessage[]
893+
memory startMessages = new TwapPriceFeedMessage[](1);
894+
TwapPriceFeedMessage[] memory endMessages = new TwapPriceFeedMessage[](
895+
1
896+
);
897+
startMessages[0] = baseTwapStartMessages[0];
898+
endMessages[0] = baseTwapEndMessages[0];
899+
900+
MerkleUpdateConfig memory config = MerkleUpdateConfig(
901+
MERKLE_TREE_DEPTH,
902+
NUM_GUARDIAN_SIGNERS,
903+
SOURCE_EMITTER_CHAIN_ID,
904+
SOURCE_EMITTER_ADDRESS,
905+
false
906+
);
907+
908+
// Fill with valid updates, but too many of them
909+
updateData[0] = generateWhMerkleTwapUpdateWithSource(
910+
startMessages,
911+
config
912+
);
913+
updateData[1] = generateWhMerkleTwapUpdateWithSource(
914+
endMessages,
915+
config
916+
);
917+
updateData[2] = generateWhMerkleTwapUpdateWithSource(
918+
startMessages,
919+
config
920+
);
921+
updateData[3] = generateWhMerkleTwapUpdateWithSource(
922+
endMessages,
923+
config
924+
);
925+
926+
uint updateFee = pyth.getUpdateFee(updateData);
927+
928+
vm.expectRevert(PythErrors.InvalidUpdateData.selector);
929+
pyth.parseTwapPriceFeedUpdates{value: updateFee}(updateData, priceIds);
930+
931+
// Case 2: Fewer updates than needed for price feeds
932+
priceIds = new bytes32[](2); // Two price feeds
933+
priceIds[0] = basePriceIds[0];
934+
priceIds[1] = basePriceIds[1];
935+
936+
// Create only 2 updates (should be 4 for two price feeds)
937+
updateData = new bytes[](2);
938+
updateData[0] = generateWhMerkleTwapUpdateWithSource(
939+
startMessages,
940+
config
941+
);
942+
updateData[1] = generateWhMerkleTwapUpdateWithSource(
943+
endMessages,
944+
config
945+
);
946+
947+
updateFee = pyth.getUpdateFee(updateData);
948+
949+
vm.expectRevert(PythErrors.InvalidUpdateData.selector);
950+
pyth.parseTwapPriceFeedUpdates{value: updateFee}(updateData, priceIds);
951+
}
769952
}

0 commit comments

Comments
 (0)