|
|
@@ -0,0 +1,475 @@
|
|
|
+// SPDX-License-Identifier: Apache 2
|
|
|
+
|
|
|
+pragma solidity ^0.8.0;
|
|
|
+
|
|
|
+import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
|
|
|
+import "forge-std/Test.sol";
|
|
|
+
|
|
|
+import "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
|
|
|
+import "@pythnetwork/pyth-sdk-solidity/PythErrors.sol";
|
|
|
+import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";
|
|
|
+import "../contracts/pyth/PythInternalStructs.sol";
|
|
|
+import "../contracts/pyth/PythGovernanceInstructions.sol";
|
|
|
+import "../contracts/pyth/PythUpgradable.sol";
|
|
|
+import "../contracts/pyth/PythGetters.sol";
|
|
|
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
|
|
|
+import "../contracts/wormhole/interfaces/IWormhole.sol";
|
|
|
+import "../contracts/wormhole/Implementation.sol";
|
|
|
+import "../contracts/wormhole/Setup.sol";
|
|
|
+import "../contracts/wormhole/Wormhole.sol";
|
|
|
+import "../contracts/wormhole-receiver/WormholeReceiver.sol";
|
|
|
+import "../contracts/wormhole-receiver/ReceiverImplementation.sol";
|
|
|
+import "../contracts/wormhole-receiver/ReceiverSetup.sol";
|
|
|
+import "../contracts/wormhole-receiver/ReceiverGovernanceStructs.sol";
|
|
|
+import "../contracts/wormhole-receiver/ReceiverStructs.sol";
|
|
|
+import "../contracts/wormhole-receiver/ReceiverGovernance.sol";
|
|
|
+import "../contracts/libraries/external/BytesLib.sol";
|
|
|
+import "./utils/WormholeTestUtils.t.sol";
|
|
|
+import "./utils/PythTestUtils.t.sol";
|
|
|
+import "./utils/RandTestUtils.t.sol";
|
|
|
+
|
|
|
+contract PythGovernanceTest is
|
|
|
+ Test,
|
|
|
+ WormholeTestUtils,
|
|
|
+ PythTestUtils,
|
|
|
+ PythGovernanceInstructions
|
|
|
+{
|
|
|
+ using BytesLib for bytes;
|
|
|
+
|
|
|
+ IPyth public pyth;
|
|
|
+ address constant TEST_SIGNER1 = 0xbeFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe;
|
|
|
+ address constant TEST_SIGNER2 = 0x4ba0C2db9A26208b3bB1a50B01b16941c10D76db;
|
|
|
+ uint16 constant TEST_GOVERNANCE_CHAIN_ID = 1;
|
|
|
+ bytes32 constant TEST_GOVERNANCE_EMITTER =
|
|
|
+ 0x0000000000000000000000000000000000000000000000000000000000000011;
|
|
|
+ uint16 constant TEST_PYTH2_WORMHOLE_CHAIN_ID = 1;
|
|
|
+ bytes32 constant TEST_PYTH2_WORMHOLE_EMITTER =
|
|
|
+ 0x71f8dcb863d176e2c420ad6610cf687359612b6fb392e0642b0ca6b1f186aa3b;
|
|
|
+ uint16 constant TARGET_CHAIN_ID = 2;
|
|
|
+
|
|
|
+ function setUp() public {
|
|
|
+ pyth = IPyth(setUpPyth(setUpWormholeReceiver(1)));
|
|
|
+ }
|
|
|
+
|
|
|
+ function testNoOwner() public {
|
|
|
+ // Check that the ownership is renounced
|
|
|
+ assertEq(OwnableUpgradeable(address(pyth)).owner(), address(0));
|
|
|
+ }
|
|
|
+
|
|
|
+ function testValidDataSources() public {
|
|
|
+ assertTrue(
|
|
|
+ PythGetters(address(pyth)).isValidDataSource(
|
|
|
+ TEST_PYTH2_WORMHOLE_CHAIN_ID,
|
|
|
+ TEST_PYTH2_WORMHOLE_EMITTER
|
|
|
+ )
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ function testSetFee() public {
|
|
|
+ // Set fee to 5000 (5000 = 5 * 10^3)
|
|
|
+ bytes memory setFeeMessage = abi.encodePacked(
|
|
|
+ MAGIC,
|
|
|
+ uint8(GovernanceModule.Target),
|
|
|
+ uint8(GovernanceAction.SetFee),
|
|
|
+ TARGET_CHAIN_ID,
|
|
|
+ uint64(5), // value
|
|
|
+ uint64(3) // exponent
|
|
|
+ );
|
|
|
+
|
|
|
+ bytes memory vaa = encodeAndSignMessage(
|
|
|
+ setFeeMessage,
|
|
|
+ TEST_GOVERNANCE_CHAIN_ID,
|
|
|
+ TEST_GOVERNANCE_EMITTER,
|
|
|
+ 1
|
|
|
+ );
|
|
|
+
|
|
|
+ uint oldFee = PythGetters(address(pyth)).singleUpdateFeeInWei();
|
|
|
+ vm.expectEmit(true, true, true, true);
|
|
|
+ emit FeeSet(oldFee, 5000);
|
|
|
+
|
|
|
+ PythGovernance(address(pyth)).executeGovernanceInstruction(vaa);
|
|
|
+ assertEq(PythGetters(address(pyth)).singleUpdateFeeInWei(), 5000);
|
|
|
+ }
|
|
|
+
|
|
|
+ function testSetValidPeriod() public {
|
|
|
+ // Create governance VAA to set valid period to 0
|
|
|
+ bytes memory data = abi.encodePacked(
|
|
|
+ MAGIC,
|
|
|
+ uint8(GovernanceModule.Target),
|
|
|
+ uint8(GovernanceAction.SetValidPeriod),
|
|
|
+ TARGET_CHAIN_ID, // Target chain ID
|
|
|
+ uint64(0) // New valid period
|
|
|
+ );
|
|
|
+
|
|
|
+ bytes memory vaa = encodeAndSignMessage(
|
|
|
+ data,
|
|
|
+ TEST_GOVERNANCE_CHAIN_ID,
|
|
|
+ TEST_GOVERNANCE_EMITTER,
|
|
|
+ 1
|
|
|
+ );
|
|
|
+
|
|
|
+ uint oldValidPeriod = PythGetters(address(pyth))
|
|
|
+ .validTimePeriodSeconds();
|
|
|
+ vm.expectEmit(true, true, true, true);
|
|
|
+ emit ValidPeriodSet(oldValidPeriod, 0);
|
|
|
+
|
|
|
+ PythGovernance(address(pyth)).executeGovernanceInstruction(vaa);
|
|
|
+ assertEq(PythGetters(address(pyth)).validTimePeriodSeconds(), 0);
|
|
|
+ }
|
|
|
+
|
|
|
+ function testInvalidGovernanceMessage() public {
|
|
|
+ // Test with wrong magic number
|
|
|
+ bytes memory data = abi.encodePacked(
|
|
|
+ bytes4(0x12345678), // Wrong magic
|
|
|
+ uint8(GovernanceModule.Target),
|
|
|
+ uint8(GovernanceAction.SetValidPeriod),
|
|
|
+ uint16(1), // Target chain ID
|
|
|
+ uint64(0)
|
|
|
+ );
|
|
|
+
|
|
|
+ bytes memory vaa = encodeAndSignMessage(
|
|
|
+ data,
|
|
|
+ TEST_GOVERNANCE_CHAIN_ID,
|
|
|
+ TEST_GOVERNANCE_EMITTER,
|
|
|
+ 1
|
|
|
+ );
|
|
|
+
|
|
|
+ vm.expectRevert(PythErrors.InvalidGovernanceMessage.selector);
|
|
|
+ PythGovernance(address(pyth)).executeGovernanceInstruction(vaa);
|
|
|
+ }
|
|
|
+
|
|
|
+ function testInvalidGovernanceTarget() public {
|
|
|
+ // Test with wrong chain target
|
|
|
+ bytes memory data = abi.encodePacked(
|
|
|
+ MAGIC,
|
|
|
+ uint8(GovernanceModule.Target),
|
|
|
+ uint8(GovernanceAction.SetValidPeriod),
|
|
|
+ uint16(3), // Different chain ID for testing invalid target
|
|
|
+ uint64(0)
|
|
|
+ );
|
|
|
+
|
|
|
+ bytes memory vaa = encodeAndSignMessage(
|
|
|
+ data,
|
|
|
+ TEST_GOVERNANCE_CHAIN_ID,
|
|
|
+ TEST_GOVERNANCE_EMITTER,
|
|
|
+ 1
|
|
|
+ );
|
|
|
+
|
|
|
+ vm.expectRevert(PythErrors.InvalidGovernanceTarget.selector);
|
|
|
+ PythGovernance(address(pyth)).executeGovernanceInstruction(vaa);
|
|
|
+ }
|
|
|
+
|
|
|
+ function testInvalidGovernanceDataSource() public {
|
|
|
+ // Test with wrong emitter
|
|
|
+ bytes memory data = abi.encodePacked(
|
|
|
+ MAGIC,
|
|
|
+ uint8(GovernanceModule.Target),
|
|
|
+ uint8(GovernanceAction.SetValidPeriod),
|
|
|
+ bytes32("ethereum"),
|
|
|
+ uint64(0)
|
|
|
+ );
|
|
|
+
|
|
|
+ bytes memory vaa = encodeAndSignMessage(
|
|
|
+ data,
|
|
|
+ TEST_GOVERNANCE_CHAIN_ID,
|
|
|
+ bytes32(uint256(0x1111)), // Wrong emitter
|
|
|
+ 1
|
|
|
+ );
|
|
|
+
|
|
|
+ vm.expectRevert(PythErrors.InvalidGovernanceDataSource.selector);
|
|
|
+ PythGovernance(address(pyth)).executeGovernanceInstruction(vaa);
|
|
|
+ }
|
|
|
+
|
|
|
+ function testSetDataSources() public {
|
|
|
+ // Create governance VAA to set new data sources
|
|
|
+ bytes memory data = abi.encodePacked(
|
|
|
+ MAGIC,
|
|
|
+ uint8(GovernanceModule.Target),
|
|
|
+ uint8(GovernanceAction.SetDataSources),
|
|
|
+ TARGET_CHAIN_ID, // Target chain ID
|
|
|
+ uint8(1), // Number of data sources
|
|
|
+ uint16(1), // Chain ID
|
|
|
+ bytes32(
|
|
|
+ 0x0000000000000000000000000000000000000000000000000000000000001111
|
|
|
+ ) // Emitter
|
|
|
+ );
|
|
|
+
|
|
|
+ bytes memory vaa = encodeAndSignMessage(
|
|
|
+ data,
|
|
|
+ TEST_GOVERNANCE_CHAIN_ID,
|
|
|
+ TEST_GOVERNANCE_EMITTER,
|
|
|
+ 1
|
|
|
+ );
|
|
|
+
|
|
|
+ PythInternalStructs.DataSource[] memory oldDataSources = PythGetters(
|
|
|
+ address(pyth)
|
|
|
+ ).validDataSources();
|
|
|
+
|
|
|
+ PythInternalStructs.DataSource[]
|
|
|
+ memory newDataSources = new PythInternalStructs.DataSource[](1);
|
|
|
+ newDataSources[0] = PythInternalStructs.DataSource(
|
|
|
+ 1,
|
|
|
+ 0x0000000000000000000000000000000000000000000000000000000000001111
|
|
|
+ );
|
|
|
+
|
|
|
+ vm.expectEmit(true, true, true, true);
|
|
|
+ emit DataSourcesSet(oldDataSources, newDataSources);
|
|
|
+
|
|
|
+ PythGovernance(address(pyth)).executeGovernanceInstruction(vaa);
|
|
|
+
|
|
|
+ // Verify old data source is no longer valid
|
|
|
+ assertFalse(
|
|
|
+ PythGetters(address(pyth)).isValidDataSource(
|
|
|
+ TEST_PYTH2_WORMHOLE_CHAIN_ID,
|
|
|
+ TEST_PYTH2_WORMHOLE_EMITTER
|
|
|
+ )
|
|
|
+ );
|
|
|
+
|
|
|
+ // Verify new data source is valid
|
|
|
+ assertTrue(
|
|
|
+ PythGetters(address(pyth)).isValidDataSource(
|
|
|
+ 1,
|
|
|
+ 0x0000000000000000000000000000000000000000000000000000000000001111
|
|
|
+ )
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ function testSetWormholeAddress() public {
|
|
|
+ // Deploy a new wormhole contract
|
|
|
+ address newWormhole = address(setUpWormholeReceiver(1));
|
|
|
+
|
|
|
+ // Create governance VAA to set new wormhole address
|
|
|
+ bytes memory data = abi.encodePacked(
|
|
|
+ MAGIC,
|
|
|
+ uint8(GovernanceModule.Target),
|
|
|
+ uint8(GovernanceAction.SetWormholeAddress),
|
|
|
+ TARGET_CHAIN_ID, // Target chain ID
|
|
|
+ newWormhole // New wormhole address
|
|
|
+ );
|
|
|
+
|
|
|
+ bytes memory vaa = encodeAndSignMessage(
|
|
|
+ data,
|
|
|
+ TEST_GOVERNANCE_CHAIN_ID,
|
|
|
+ TEST_GOVERNANCE_EMITTER,
|
|
|
+ 1
|
|
|
+ );
|
|
|
+
|
|
|
+ address oldWormhole = address(PythGetters(address(pyth)).wormhole());
|
|
|
+ vm.expectEmit(true, true, true, true);
|
|
|
+ emit WormholeAddressSet(oldWormhole, newWormhole);
|
|
|
+
|
|
|
+ PythGovernance(address(pyth)).executeGovernanceInstruction(vaa);
|
|
|
+ assertEq(address(PythGetters(address(pyth)).wormhole()), newWormhole);
|
|
|
+ }
|
|
|
+
|
|
|
+ function testTransferGovernanceDataSource() public {
|
|
|
+ uint16 newEmitterChain = 2;
|
|
|
+ bytes32 newEmitterAddress = 0x0000000000000000000000000000000000000000000000000000000000001111;
|
|
|
+
|
|
|
+ // Create claim VAA from new governance
|
|
|
+ bytes memory claimData = abi.encodePacked(
|
|
|
+ MAGIC,
|
|
|
+ uint8(GovernanceModule.Target),
|
|
|
+ uint8(GovernanceAction.RequestGovernanceDataSourceTransfer),
|
|
|
+ TARGET_CHAIN_ID, // Target chain ID
|
|
|
+ uint32(1) // New governance index
|
|
|
+ );
|
|
|
+
|
|
|
+ bytes memory claimVaa = encodeAndSignMessage(
|
|
|
+ claimData,
|
|
|
+ newEmitterChain,
|
|
|
+ newEmitterAddress,
|
|
|
+ 1
|
|
|
+ );
|
|
|
+
|
|
|
+ // Create authorize VAA from current governance
|
|
|
+ bytes memory authData = abi.encodePacked(
|
|
|
+ MAGIC,
|
|
|
+ uint8(GovernanceModule.Target),
|
|
|
+ uint8(GovernanceAction.AuthorizeGovernanceDataSourceTransfer),
|
|
|
+ TARGET_CHAIN_ID, // Target chain ID
|
|
|
+ claimVaa
|
|
|
+ );
|
|
|
+
|
|
|
+ bytes memory authVaa = encodeAndSignMessage(
|
|
|
+ authData,
|
|
|
+ TEST_GOVERNANCE_CHAIN_ID,
|
|
|
+ TEST_GOVERNANCE_EMITTER,
|
|
|
+ 1
|
|
|
+ );
|
|
|
+
|
|
|
+ PythInternalStructs.DataSource
|
|
|
+ memory oldDataSource = PythInternalStructs.DataSource(
|
|
|
+ TEST_GOVERNANCE_CHAIN_ID,
|
|
|
+ TEST_GOVERNANCE_EMITTER
|
|
|
+ );
|
|
|
+ PythInternalStructs.DataSource
|
|
|
+ memory newDataSource = PythInternalStructs.DataSource(
|
|
|
+ newEmitterChain,
|
|
|
+ newEmitterAddress
|
|
|
+ );
|
|
|
+
|
|
|
+ vm.expectEmit(true, true, true, true);
|
|
|
+ emit GovernanceDataSourceSet(oldDataSource, newDataSource, 1);
|
|
|
+
|
|
|
+ PythGovernance(address(pyth)).executeGovernanceInstruction(authVaa);
|
|
|
+
|
|
|
+ // Verify old governance can't execute instructions anymore
|
|
|
+ bytes memory invalidData = abi.encodePacked(
|
|
|
+ MAGIC,
|
|
|
+ uint8(GovernanceModule.Target),
|
|
|
+ uint8(GovernanceAction.SetValidPeriod),
|
|
|
+ uint16(1), // Wrong chain ID for testing invalid target
|
|
|
+ uint64(0)
|
|
|
+ );
|
|
|
+
|
|
|
+ bytes memory invalidVaa = encodeAndSignMessage(
|
|
|
+ invalidData,
|
|
|
+ TEST_GOVERNANCE_CHAIN_ID,
|
|
|
+ TEST_GOVERNANCE_EMITTER,
|
|
|
+ 2
|
|
|
+ );
|
|
|
+
|
|
|
+ vm.expectRevert(PythErrors.InvalidGovernanceDataSource.selector);
|
|
|
+ PythGovernance(address(pyth)).executeGovernanceInstruction(invalidVaa);
|
|
|
+ }
|
|
|
+
|
|
|
+ function testSequentialGovernanceMessages() public {
|
|
|
+ // First governance message
|
|
|
+ bytes memory data1 = abi.encodePacked(
|
|
|
+ MAGIC,
|
|
|
+ uint8(GovernanceModule.Target),
|
|
|
+ uint8(GovernanceAction.SetValidPeriod),
|
|
|
+ TARGET_CHAIN_ID, // Target chain ID
|
|
|
+ uint64(10)
|
|
|
+ );
|
|
|
+
|
|
|
+ bytes memory vaa1 = encodeAndSignMessage(
|
|
|
+ data1,
|
|
|
+ TEST_GOVERNANCE_CHAIN_ID,
|
|
|
+ TEST_GOVERNANCE_EMITTER,
|
|
|
+ 1
|
|
|
+ );
|
|
|
+
|
|
|
+ PythGovernance(address(pyth)).executeGovernanceInstruction(vaa1);
|
|
|
+
|
|
|
+ // Second governance message
|
|
|
+ bytes memory data2 = abi.encodePacked(
|
|
|
+ MAGIC,
|
|
|
+ uint8(GovernanceModule.Target),
|
|
|
+ uint8(GovernanceAction.SetValidPeriod),
|
|
|
+ TARGET_CHAIN_ID, // Target chain ID
|
|
|
+ uint64(20)
|
|
|
+ );
|
|
|
+
|
|
|
+ bytes memory vaa2 = encodeAndSignMessage(
|
|
|
+ data2,
|
|
|
+ TEST_GOVERNANCE_CHAIN_ID,
|
|
|
+ TEST_GOVERNANCE_EMITTER,
|
|
|
+ 2
|
|
|
+ );
|
|
|
+
|
|
|
+ PythGovernance(address(pyth)).executeGovernanceInstruction(vaa2);
|
|
|
+
|
|
|
+ // Try to replay first message
|
|
|
+ vm.expectRevert(PythErrors.OldGovernanceMessage.selector);
|
|
|
+ PythGovernance(address(pyth)).executeGovernanceInstruction(vaa1);
|
|
|
+
|
|
|
+ // Try to replay second message
|
|
|
+ vm.expectRevert(PythErrors.OldGovernanceMessage.selector);
|
|
|
+ PythGovernance(address(pyth)).executeGovernanceInstruction(vaa2);
|
|
|
+ }
|
|
|
+
|
|
|
+ function testSetTransactionFee() public {
|
|
|
+ // Set transaction fee to 1000 (1000 = 1 * 10^3)
|
|
|
+ bytes memory setTransactionFeeMessage = abi.encodePacked(
|
|
|
+ MAGIC,
|
|
|
+ uint8(GovernanceModule.Target),
|
|
|
+ uint8(GovernanceAction.SetTransactionFee),
|
|
|
+ TARGET_CHAIN_ID,
|
|
|
+ uint64(1), // value
|
|
|
+ uint64(3) // exponent
|
|
|
+ );
|
|
|
+
|
|
|
+ bytes memory vaa = encodeAndSignMessage(
|
|
|
+ setTransactionFeeMessage,
|
|
|
+ TEST_GOVERNANCE_CHAIN_ID,
|
|
|
+ TEST_GOVERNANCE_EMITTER,
|
|
|
+ 1
|
|
|
+ );
|
|
|
+
|
|
|
+ uint oldFee = PythGetters(address(pyth)).transactionFeeInWei();
|
|
|
+ vm.expectEmit(true, true, true, true);
|
|
|
+ emit TransactionFeeSet(oldFee, 1000);
|
|
|
+
|
|
|
+ PythGovernance(address(pyth)).executeGovernanceInstruction(vaa);
|
|
|
+ assertEq(PythGetters(address(pyth)).transactionFeeInWei(), 1000);
|
|
|
+
|
|
|
+ // Test that update fee includes transaction fee
|
|
|
+ bytes[] memory updateData = new bytes[](0);
|
|
|
+ assertEq(pyth.getUpdateFee(updateData), 1000);
|
|
|
+
|
|
|
+ // Test that insufficient fee reverts
|
|
|
+ vm.expectRevert(PythErrors.InsufficientFee.selector);
|
|
|
+ pyth.updatePriceFeeds{value: 999}(updateData);
|
|
|
+
|
|
|
+ // Test that sufficient fee works
|
|
|
+ pyth.updatePriceFeeds{value: 1000}(updateData);
|
|
|
+ }
|
|
|
+
|
|
|
+ function encodeAndSignWormholeMessage(
|
|
|
+ bytes memory data,
|
|
|
+ uint16 emitterChainId,
|
|
|
+ bytes32 emitterAddress,
|
|
|
+ uint64 sequence,
|
|
|
+ uint8 numGuardians
|
|
|
+ ) internal view returns (bytes memory) {
|
|
|
+ return
|
|
|
+ generateVaa(
|
|
|
+ uint32(block.timestamp),
|
|
|
+ emitterChainId,
|
|
|
+ emitterAddress,
|
|
|
+ sequence,
|
|
|
+ data,
|
|
|
+ numGuardians
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ function encodeAndSignMessage(
|
|
|
+ bytes memory data,
|
|
|
+ uint16 emitterChainId,
|
|
|
+ bytes32 emitterAddress,
|
|
|
+ uint64 sequence
|
|
|
+ ) internal view returns (bytes memory) {
|
|
|
+ return
|
|
|
+ encodeAndSignWormholeMessage(
|
|
|
+ data,
|
|
|
+ emitterChainId,
|
|
|
+ emitterAddress,
|
|
|
+ sequence,
|
|
|
+ 1 // Number of guardians
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ // Events
|
|
|
+ event ContractUpgraded(
|
|
|
+ address oldImplementation,
|
|
|
+ address newImplementation
|
|
|
+ );
|
|
|
+ event GovernanceDataSourceSet(
|
|
|
+ PythInternalStructs.DataSource oldDataSource,
|
|
|
+ PythInternalStructs.DataSource newDataSource,
|
|
|
+ uint64 initialSequence
|
|
|
+ );
|
|
|
+ event DataSourcesSet(
|
|
|
+ PythInternalStructs.DataSource[] oldDataSources,
|
|
|
+ PythInternalStructs.DataSource[] newDataSources
|
|
|
+ );
|
|
|
+ event FeeSet(uint oldFee, uint newFee);
|
|
|
+ event ValidPeriodSet(uint oldValidPeriod, uint newValidPeriod);
|
|
|
+ event WormholeAddressSet(
|
|
|
+ address oldWormholeAddress,
|
|
|
+ address newWormholeAddress
|
|
|
+ );
|
|
|
+ event TransactionFeeSet(uint oldFee, uint newFee);
|
|
|
+}
|