Skip to content

Commit a5e15e5

Browse files
committed
test: add unit tests for TWAP price feed update validation and error handling
1 parent 671c6c3 commit a5e15e5

File tree

2 files changed

+162
-2
lines changed

2 files changed

+162
-2
lines changed

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import "./PythAccumulator.sol";
1111
import "./PythGetters.sol";
1212
import "./PythSetters.sol";
1313
import "./PythInternalStructs.sol";
14-
1514
abstract contract Pyth is
1615
PythGetters,
1716
PythSetters,
@@ -380,7 +379,6 @@ abstract contract Pyth is
380379
if (requiredFee != getUpdateFee(updateData[1])) {
381380
revert PythErrors.InvalidUpdateData();
382381
}
383-
384382
if (msg.value < requiredFee) revert PythErrors.InsufficientFee();
385383

386384
unchecked {

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

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,8 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils {
159159
updateData[1] = endUpdateData;
160160

161161
// Calculate the update fee
162+
// We only charge fee for 1 update even though we need 2 updates to derive TWAP.
163+
// This is for better UX since user's intention is to get a single TWAP price.
162164
updateFee = pyth.getUpdateFee(updateData[0]);
163165
}
164166

@@ -451,4 +453,164 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils {
451453
// Validate the downSlotsRatio is 0 in our test implementation
452454
assertEq(twapPriceFeeds[0].downSlotsRatio, uint32(0));
453455
}
456+
457+
function testParseTwapPriceFeedUpdatesRevertsWithInvalidUpdateDataLength()
458+
public
459+
{
460+
bytes32[] memory priceIds = new bytes32[](1);
461+
priceIds[0] = bytes32(uint256(1));
462+
463+
// Create invalid update data with wrong length
464+
bytes[][] memory updateData = new bytes[][](1); // Should be 2
465+
updateData[0] = new bytes[](1);
466+
467+
vm.expectRevert(PythErrors.InvalidUpdateData.selector);
468+
pyth.parseTwapPriceFeedUpdates{value: 0}(updateData, priceIds);
469+
}
470+
471+
function testParseTwapPriceFeedUpdatesRevertsWithMismatchedPriceIds()
472+
public
473+
{
474+
bytes32[] memory priceIds = new bytes32[](1);
475+
priceIds[0] = bytes32(uint256(1));
476+
477+
PriceFeedMessage[] memory messages = new PriceFeedMessage[](2);
478+
479+
// Start message with priceId 1
480+
messages[0].priceId = bytes32(uint256(1));
481+
messages[0].price = 100;
482+
messages[0].publishTime = 1000;
483+
messages[0].prevPublishTime = 900;
484+
485+
// End message with different priceId 2
486+
messages[1].priceId = bytes32(uint256(2)); // Different priceId
487+
messages[1].price = 110;
488+
messages[1].publishTime = 1100;
489+
messages[1].prevPublishTime = 1000;
490+
491+
(
492+
bytes[][] memory updateData,
493+
uint updateFee
494+
) = createBatchedTwapUpdateDataFromMessages(messages);
495+
496+
vm.expectRevert(PythErrors.InvalidTwapUpdateDataSet.selector);
497+
pyth.parseTwapPriceFeedUpdates{value: updateFee}(updateData, priceIds);
498+
}
499+
500+
function testParseTwapPriceFeedUpdatesRevertsWithInvalidTimeOrdering()
501+
public
502+
{
503+
bytes32[] memory priceIds = new bytes32[](1);
504+
priceIds[0] = bytes32(uint256(1));
505+
506+
PriceFeedMessage[] memory messages = new PriceFeedMessage[](2);
507+
508+
// Start message with later time
509+
messages[0].priceId = priceIds[0];
510+
messages[0].price = 100;
511+
messages[0].publishTime = 1100; // Later time
512+
messages[0].prevPublishTime = 1000;
513+
514+
// End message with earlier time
515+
messages[1].priceId = priceIds[0];
516+
messages[1].price = 110;
517+
messages[1].publishTime = 1000; // Earlier time
518+
messages[1].prevPublishTime = 900;
519+
520+
(
521+
bytes[][] memory updateData,
522+
uint updateFee
523+
) = createBatchedTwapUpdateDataFromMessages(messages);
524+
525+
vm.expectRevert(PythErrors.InvalidTwapUpdateDataSet.selector);
526+
pyth.parseTwapPriceFeedUpdates{value: updateFee}(updateData, priceIds);
527+
}
528+
529+
function testParseTwapPriceFeedUpdatesRevertsWithMismatchedExponents()
530+
public
531+
{
532+
bytes32[] memory priceIds = new bytes32[](1);
533+
priceIds[0] = bytes32(uint256(1));
534+
535+
PriceFeedMessage[] memory messages = new PriceFeedMessage[](2);
536+
537+
// Start message with expo -8
538+
messages[0].priceId = priceIds[0];
539+
messages[0].price = 100;
540+
messages[0].expo = -8;
541+
messages[0].publishTime = 1000;
542+
messages[0].prevPublishTime = 900;
543+
544+
// End message with different expo -6
545+
messages[1].priceId = priceIds[0];
546+
messages[1].price = 110;
547+
messages[1].expo = -6; // Different exponent
548+
messages[1].publishTime = 1100;
549+
messages[1].prevPublishTime = 1000;
550+
551+
(
552+
bytes[][] memory updateData,
553+
uint updateFee
554+
) = createBatchedTwapUpdateDataFromMessages(messages);
555+
556+
vm.expectRevert(PythErrors.InvalidTwapUpdateDataSet.selector);
557+
pyth.parseTwapPriceFeedUpdates{value: updateFee}(updateData, priceIds);
558+
}
559+
560+
function testParseTwapPriceFeedUpdatesRevertsWithInvalidPrevPublishTime()
561+
public
562+
{
563+
bytes32[] memory priceIds = new bytes32[](1);
564+
priceIds[0] = bytes32(uint256(1));
565+
566+
PriceFeedMessage[] memory messages = new PriceFeedMessage[](2);
567+
568+
// Start message with invalid prevPublishTime
569+
messages[0].priceId = priceIds[0];
570+
messages[0].price = 100;
571+
messages[0].publishTime = 1000;
572+
messages[0].prevPublishTime = 1100; // Invalid: prevPublishTime > publishTime
573+
574+
// End message
575+
messages[1].priceId = priceIds[0];
576+
messages[1].price = 110;
577+
messages[1].publishTime = 1200;
578+
messages[1].prevPublishTime = 1000;
579+
580+
(
581+
bytes[][] memory updateData,
582+
uint updateFee
583+
) = createBatchedTwapUpdateDataFromMessages(messages);
584+
585+
vm.expectRevert(PythErrors.InvalidTwapUpdateData.selector);
586+
pyth.parseTwapPriceFeedUpdates{value: updateFee}(updateData, priceIds);
587+
}
588+
589+
function testParseTwapPriceFeedUpdatesRevertsWithInsufficientFee() public {
590+
bytes32[] memory priceIds = new bytes32[](1);
591+
priceIds[0] = bytes32(uint256(1));
592+
593+
PriceFeedMessage[] memory messages = new PriceFeedMessage[](2);
594+
595+
messages[0].priceId = priceIds[0];
596+
messages[0].price = 100;
597+
messages[0].publishTime = 1000;
598+
messages[0].prevPublishTime = 900;
599+
600+
messages[1].priceId = priceIds[0];
601+
messages[1].price = 110;
602+
messages[1].publishTime = 1100;
603+
messages[1].prevPublishTime = 1000;
604+
605+
(
606+
bytes[][] memory updateData,
607+
uint updateFee
608+
) = createBatchedTwapUpdateDataFromMessages(messages);
609+
610+
vm.expectRevert(PythErrors.InsufficientFee.selector);
611+
pyth.parseTwapPriceFeedUpdates{value: updateFee - 1}(
612+
updateData,
613+
priceIds
614+
); // Send insufficient fee
615+
}
454616
}

0 commit comments

Comments
 (0)