Преглед на файлове

Merge pull request #2526 from pyth-network/pyth-tx-fee

feat(target_chains/ethereum): add tx fee to evm contract
Daniel Chew преди 7 месеца
родител
ревизия
b8f388e625

+ 7 - 0
governance/xc_admin/packages/xc_admin_common/src/__tests__/GovernancePayload.test.ts

@@ -32,6 +32,7 @@ import {
   DataSource,
   SetDataSources,
 } from "../governance_payload/SetDataSources";
+import { SetTransactionFee } from "../governance_payload/SetTransactionFee";
 
 test("GovernancePayload ser/de", (done) => {
   jest.setTimeout(60000);
@@ -424,6 +425,12 @@ function governanceActionArb(): Arbitrary<PythGovernanceAction> {
             Buffer.from(token),
           );
         });
+    } else if (header.action === "SetTransactionFee") {
+      return fc
+        .record({ v: fc.bigUintN(64), e: fc.bigUintN(64) })
+        .map(({ v, e }) => {
+          return new SetTransactionFee(header.targetChainId, v, e);
+        });
     } else {
       throw new Error("Unsupported action type");
     }

+ 3 - 0
governance/xc_admin/packages/xc_admin_common/src/governance_payload/PythGovernanceAction.ts

@@ -16,6 +16,7 @@ export const TargetAction = {
   RequestGovernanceDataSourceTransfer: 5,
   SetWormholeAddress: 6,
   SetFeeInToken: 7,
+  SetTransactionFee: 8,
 } as const;
 
 export const EvmExecutorAction = {
@@ -46,6 +47,8 @@ export function toActionName(
         return "SetWormholeAddress";
       case 7:
         return "SetFeeInToken";
+      case 8:
+        return "SetTransactionFee";
     }
   } else if (
     deserialized.moduleId == MODULE_EVM_EXECUTOR &&

+ 44 - 0
governance/xc_admin/packages/xc_admin_common/src/governance_payload/SetTransactionFee.ts

@@ -0,0 +1,44 @@
+import { PythGovernanceActionImpl } from "./PythGovernanceAction";
+import * as BufferLayout from "@solana/buffer-layout";
+import * as BufferLayoutExt from "./BufferLayoutExt";
+import { ChainName } from "../chains";
+
+/** Set the transaction fee on the target chain to newFeeValue * 10^newFeeExpo */
+export class SetTransactionFee extends PythGovernanceActionImpl {
+  static layout: BufferLayout.Structure<
+    Readonly<{ newFeeValue: bigint; newFeeExpo: bigint }>
+  > = BufferLayout.struct([
+    BufferLayoutExt.u64be("newFeeValue"),
+    BufferLayoutExt.u64be("newFeeExpo"),
+  ]);
+
+  constructor(
+    targetChainId: ChainName,
+    readonly newFeeValue: bigint,
+    readonly newFeeExpo: bigint,
+  ) {
+    super(targetChainId, "SetTransactionFee");
+  }
+
+  static decode(data: Buffer): SetTransactionFee | undefined {
+    const decoded = PythGovernanceActionImpl.decodeWithPayload(
+      data,
+      "SetTransactionFee",
+      SetTransactionFee.layout,
+    );
+    if (!decoded) return undefined;
+
+    return new SetTransactionFee(
+      decoded[0].targetChainId,
+      decoded[1].newFeeValue,
+      decoded[1].newFeeExpo,
+    );
+  }
+
+  encode(): Buffer {
+    return super.encodeWithPayload(SetTransactionFee.layout, {
+      newFeeValue: this.newFeeValue,
+      newFeeExpo: this.newFeeExpo,
+    });
+  }
+}

+ 4 - 0
governance/xc_admin/packages/xc_admin_common/src/governance_payload/index.ts

@@ -20,6 +20,7 @@ import {
   StarknetSetWormholeAddress,
 } from "./SetWormholeAddress";
 import { EvmExecute } from "./ExecuteAction";
+import { SetTransactionFee } from "./SetTransactionFee";
 
 /** Decode a governance payload */
 export function decodeGovernancePayload(
@@ -73,6 +74,8 @@ export function decodeGovernancePayload(
     }
     case "Execute":
       return EvmExecute.decode(data);
+    case "SetTransactionFee":
+      return SetTransactionFee.decode(data);
     default:
       return undefined;
   }
@@ -86,5 +89,6 @@ export * from "./GovernanceDataSourceTransfer";
 export * from "./SetDataSources";
 export * from "./SetValidPeriod";
 export * from "./SetFee";
+export * from "./SetTransactionFee";
 export * from "./SetWormholeAddress";
 export * from "./ExecuteAction";

+ 8 - 3
target_chains/ethereum/contracts/contracts/pyth/Pyth.sol

@@ -86,7 +86,11 @@ abstract contract Pyth is
         // In the accumulator update data a single update can contain
         // up to 255 messages and we charge a singleUpdateFee per each
         // message
-        return 255 * singleUpdateFeeInWei() * updateDataSize;
+        return
+            255 *
+            singleUpdateFeeInWei() *
+            updateDataSize +
+            transactionFeeInWei();
     }
 
     function getUpdateFee(
@@ -330,7 +334,8 @@ abstract contract Pyth is
     function getTotalFee(
         uint totalNumUpdates
     ) private view returns (uint requiredFee) {
-        return totalNumUpdates * singleUpdateFeeInWei();
+        return
+            (totalNumUpdates * singleUpdateFeeInWei()) + transactionFeeInWei();
     }
 
     function findIndexOfPriceId(
@@ -392,6 +397,6 @@ abstract contract Pyth is
     }
 
     function version() public pure returns (string memory) {
-        return "1.4.3";
+        return "1.4.4-alpha.1";
     }
 }

+ 4 - 0
target_chains/ethereum/contracts/contracts/pyth/PythGetters.sol

@@ -91,4 +91,8 @@ contract PythGetters is PythState {
     function governanceDataSourceIndex() public view returns (uint32) {
         return _state.governanceDataSourceIndex;
     }
+
+    function transactionFeeInWei() public view returns (uint) {
+        return _state.transactionFeeInWei;
+    }
 }

+ 12 - 0
target_chains/ethereum/contracts/contracts/pyth/PythGovernance.sol

@@ -38,6 +38,7 @@ abstract contract PythGovernance is
         address oldWormholeAddress,
         address newWormholeAddress
     );
+    event TransactionFeeSet(uint oldFee, uint newFee);
 
     function verifyGovernanceVM(
         bytes memory encodedVM
@@ -97,6 +98,8 @@ abstract contract PythGovernance is
                 parseSetWormholeAddressPayload(gi.payload),
                 encodedVM
             );
+        } else if (gi.action == GovernanceAction.SetTransactionFee) {
+            setTransactionFee(parseSetTransactionFeePayload(gi.payload));
         } else {
             revert PythErrors.InvalidGovernanceMessage();
         }
@@ -243,4 +246,13 @@ abstract contract PythGovernance is
 
         emit WormholeAddressSet(oldWormholeAddress, address(wormhole()));
     }
+
+    function setTransactionFee(
+        SetTransactionFeePayload memory payload
+    ) internal {
+        uint oldFee = transactionFeeInWei();
+        setTransactionFeeInWei(payload.newFee);
+
+        emit TransactionFeeSet(oldFee, transactionFeeInWei());
+    }
 }

+ 24 - 1
target_chains/ethereum/contracts/contracts/pyth/PythGovernanceInstructions.sol

@@ -34,7 +34,8 @@ contract PythGovernanceInstructions {
         SetFee, // 3
         SetValidPeriod, // 4
         RequestGovernanceDataSourceTransfer, // 5
-        SetWormholeAddress // 6
+        SetWormholeAddress, // 6
+        SetTransactionFee // 7
     }
 
     struct GovernanceInstruction {
@@ -77,6 +78,10 @@ contract PythGovernanceInstructions {
         address newWormholeAddress;
     }
 
+    struct SetTransactionFeePayload {
+        uint newFee;
+    }
+
     /// @dev Parse a GovernanceInstruction
     function parseGovernanceInstruction(
         bytes memory encodedInstruction
@@ -220,4 +225,22 @@ contract PythGovernanceInstructions {
         if (encodedPayload.length != index)
             revert PythErrors.InvalidGovernanceMessage();
     }
+
+    /// @dev Parse a SetTransactionFeePayload (action 7) with minimal validation
+    function parseSetTransactionFeePayload(
+        bytes memory encodedPayload
+    ) public pure returns (SetTransactionFeePayload memory stf) {
+        uint index = 0;
+
+        uint64 val = encodedPayload.toUint64(index);
+        index += 8;
+
+        uint64 expo = encodedPayload.toUint64(index);
+        index += 8;
+
+        stf.newFee = uint256(val) * uint256(10) ** uint256(expo);
+
+        if (encodedPayload.length != index)
+            revert PythErrors.InvalidGovernanceMessage();
+    }
 }

+ 4 - 0
target_chains/ethereum/contracts/contracts/pyth/PythSetters.sol

@@ -48,4 +48,8 @@ contract PythSetters is PythState, IPythEvents {
     function setGovernanceDataSourceIndex(uint32 newIndex) internal {
         _state.governanceDataSourceIndex = newIndex;
     }
+
+    function setTransactionFeeInWei(uint fee) internal {
+        _state.transactionFeeInWei = fee;
+    }
 }

+ 2 - 0
target_chains/ethereum/contracts/contracts/pyth/PythState.sol

@@ -38,6 +38,8 @@ contract PythStorage {
         // Mapping of cached price information
         // priceId => PriceInfo
         mapping(bytes32 => PythInternalStructs.PriceInfo) latestPriceInfo;
+        // Fee charged per transaction, in addition to per-update fees
+        uint transactionFeeInWei;
     }
 }
 

+ 614 - 0
target_chains/ethereum/contracts/forge-test/PythGovernance.t.sol

@@ -0,0 +1,614 @@
+// SPDX-License-Identifier: Apache 2
+
+// NOTE: These tests were migrated from target_chains/ethereum/contracts/test/pyth.js but exclude the Wormhole-specific tests,
+// which remain in the original JavaScript test file.
+
+pragma solidity ^0.8.0;
+
+import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.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 "../contracts/pyth/mock/MockUpgradeableProxy.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),
+            TARGET_CHAIN_ID,
+            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 testUpgradeContractWithChainIdZeroIsInvalid() public {
+        // Deploy a new PythUpgradable contract
+        PythUpgradable newImplementation = new PythUpgradable();
+
+        // Create governance VAA with chain ID 0 (unset)
+        bytes memory data = abi.encodePacked(
+            MAGIC,
+            uint8(GovernanceModule.Target),
+            uint8(GovernanceAction.UpgradeContract),
+            uint16(0), // Chain ID 0 (unset)
+            address(newImplementation) // New implementation address
+        );
+
+        bytes memory vaa = encodeAndSignMessage(
+            data,
+            TEST_GOVERNANCE_CHAIN_ID,
+            TEST_GOVERNANCE_EMITTER,
+            1
+        );
+
+        // Should revert with InvalidGovernanceTarget
+        vm.expectRevert(PythErrors.InvalidGovernanceTarget.selector);
+        PythGovernance(address(pyth)).executeGovernanceInstruction(vaa);
+    }
+
+    // Helper function to get the second address from event data
+    function getSecondAddressFromEventData(
+        bytes memory data
+    ) internal pure returns (address) {
+        (, address secondAddr) = abi.decode(data, (address, address));
+        return secondAddr;
+    }
+
+    function testUpgradeContractShouldWork() public {
+        // Deploy a new PythUpgradable contract
+        PythUpgradable newImplementation = new PythUpgradable();
+
+        // Create governance VAA to upgrade the contract
+        bytes memory data = abi.encodePacked(
+            MAGIC,
+            uint8(GovernanceModule.Target),
+            uint8(GovernanceAction.UpgradeContract),
+            TARGET_CHAIN_ID, // Valid target chain ID
+            address(newImplementation) // New implementation address
+        );
+
+        bytes memory vaa = encodeAndSignMessage(
+            data,
+            TEST_GOVERNANCE_CHAIN_ID,
+            TEST_GOVERNANCE_EMITTER,
+            1
+        );
+
+        // Create a custom event checker for ContractUpgraded event
+        // Since we only care about the newImplementation parameter
+        vm.recordLogs();
+
+        // Execute the governance instruction
+        PythGovernance(address(pyth)).executeGovernanceInstruction(vaa);
+
+        // Get emitted logs and check the event parameters
+        Vm.Log[] memory entries = vm.getRecordedLogs();
+        bool foundUpgradeEvent = false;
+
+        for (uint i = 0; i < entries.length; i++) {
+            // The event signature for ContractUpgraded
+            bytes32 eventSignature = keccak256(
+                "ContractUpgraded(address,address)"
+            );
+
+            if (entries[i].topics[0] == eventSignature) {
+                // This is a ContractUpgraded event
+                // Get just the new implementation address using our helper
+                address recordedNewImplementation = getSecondAddressFromEventData(
+                        entries[i].data
+                    );
+
+                // Check newImplementation
+                assertEq(recordedNewImplementation, address(newImplementation));
+                foundUpgradeEvent = true;
+                break;
+            }
+        }
+
+        // Make sure we found the event
+        assertTrue(foundUpgradeEvent, "ContractUpgraded event not found");
+
+        // Verify the upgrade worked by checking the magic number
+        assertEq(
+            PythUpgradable(address(pyth)).pythUpgradableMagic(),
+            0x97a6f304
+        );
+
+        // Verify the implementation was upgraded to our new implementation
+        // Access implementation using the ERC1967 storage slot
+        address implAddr = address(
+            uint160(
+                uint256(
+                    vm.load(
+                        address(pyth),
+                        0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc // ERC1967 implementation slot
+                    )
+                )
+            )
+        );
+        assertEq(implAddr, address(newImplementation));
+    }
+
+    function testUpgradeContractToNonPythContractWontWork() public {
+        // Deploy a mock upgradeable proxy that isn't a proper Pyth implementation
+        MockUpgradeableProxy newImplementation = new MockUpgradeableProxy();
+
+        // Create governance VAA to upgrade to an invalid implementation
+        bytes memory data = abi.encodePacked(
+            MAGIC,
+            uint8(GovernanceModule.Target),
+            uint8(GovernanceAction.UpgradeContract),
+            TARGET_CHAIN_ID, // Valid target chain ID
+            address(newImplementation) // Invalid implementation address
+        );
+
+        bytes memory vaa = encodeAndSignMessage(
+            data,
+            TEST_GOVERNANCE_CHAIN_ID,
+            TEST_GOVERNANCE_EMITTER,
+            1
+        );
+
+        // Should revert with no specific error message because the mock implementation
+        // doesn't have the pythUpgradableMagic method
+        vm.expectRevert();
+        PythGovernance(address(pyth)).executeGovernanceInstruction(vaa);
+    }
+
+    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);
+}

+ 1 - 1
target_chains/ethereum/contracts/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@pythnetwork/pyth-evm-contract",
-  "version": "1.4.3",
+  "version": "1.4.4-alpha.1",
   "description": "",
   "private": "true",
   "devDependencies": {